///|
/// Diagnostics.
///
/// This package exists to be the *only* one in the module that mentions
/// `error-report`. The kinds themselves live in `css/kind`, which has no
/// dependencies, so the AST can name a problem without linking a renderer; here
/// is where a named problem becomes a renderable report, and there is exactly
/// one function that does it.
///
/// The arrangement is the one `lib/error` already uses, and it is what makes
/// `error-report`'s eventual spin-out a rename rather than an untangling. If a
/// second call site into `@report` appears here, the question to ask is whether
/// the thing it needs belongs on `Diagnostic` instead.

///|
/// A problem, with the span it was found at.
pub(all) struct Diagnostic {
  kind : @kind.ErrorKind
  span : @span.Span
  severity : @report.Severity
  /// The second place worth looking: the `{` that was never closed, the
  /// declaration a stray `!important` belonged to. Most of what makes a
  /// complaint answerable rather than merely true.
  related : Array[(@span.Span, String)]
}

///|
pub fn Diagnostic::new(
  kind : @kind.ErrorKind,
  span : @span.Span,
  severity? : @report.Severity = Error,
  related? : Array[(@span.Span, String)] = [],
) -> Diagnostic {
  { kind, span, severity, related, }
}

///|
/// A diagnostic whose severity follows from its kind.
///
/// An unknown at-rule is a warning rather than an error, because CSS gains new
/// at-rules faster than any parser learns them; refusing one would make this
/// library wrong every time the language moves.
pub fn Diagnostic::of_kind(
  kind : @kind.ErrorKind,
  span : @span.Span,
) -> Diagnostic {
  let severity : @report.Severity = if kind.is_warning() {
    Warning
  } else {
    Error
  }
  { kind, span, severity, related: [], }
}

///|
/// A CSS source was rejected.
///
/// One error type for the whole module, raised only in strict mode. The
/// tolerant path never raises: it puts a `Bogus` in the tree and a `Diagnostic`
/// in the list, which is what lets a caller parse Bootstrap and still get a
/// stylesheet.
pub(all) suberror CssError {
  CssError(Diagnostic)
}

///|
pub fn[T] Diagnostic::raise_(self : Diagnostic) -> T raise CssError {
  raise CssError(self)
}

///|
pub fn CssError::diagnostic(self : CssError) -> Diagnostic {
  let CssError(d) = self
  d
}

///|
/// Turn a diagnostic into a renderable report.
///
/// **This is the only function in the module that mentions `error-report`.**
pub fn Diagnostic::to_report(
  self : Diagnostic,
  source : @report.SourceId,
) -> @report.Report {
  let r = @report.Report::new(self.severity, self.explain())
    .with_code(self.kind.code())
    .with_label(
      @report.Label::primary(source, span_of(self.span), message?=self.point()),
    )
  for rel in self.related {
    let (span, note) = rel
    let _ = r.with_label(
      @report.Label::secondary(source, span_of(span), message=note),
    )
  }
  match self.help() {
    Some(h) => r.with_help(h)
    None => r
  }
}

///|
/// The one place a `@span.Span` becomes an `@report.Span`.
///
/// Both are measured in UTF-16 code units, deliberately, so this is a change of
/// representation and not of unit -- which is why it can be a three-line
/// function instead of a conversion that has to be got right at every call
/// site.
fn span_of(s : @span.Span) -> @report.Span {
  @report.Span::of_range(s.start, s.end)
}

///|
/// The headline: what went wrong, in one sentence, in the reader's terms.
fn Diagnostic::explain(self : Diagnostic) -> String {
  match self.kind {
    UnexpectedEof => "the file ended in the middle of a rule"
    UnterminatedString => "this string is never closed"
    UnterminatedComment => "this comment is never closed"
    UnterminatedUrl => "this `url(` is never closed"
    BadUrl => "this unquoted url contains a character that must be quoted"
    UnexpectedCloser(c) =>
      "there is no `" + opener_of(c) + "` for this `" + c + "`"
    UnclosedBlock(c) => "this `" + c + "` is never closed"
    BadSelector => "this is not a selector"
    DanglingCombinator => "this combinator has nothing after it"
    BadAttributeSelector => "this is not an attribute selector"
    BadPseudoClass(n) => "`:" + n + "` was given arguments of the wrong shape"
    BadAnB => "this is not an `an+b` formula"
    ExpectedColon =>
      "a declaration needs a `:` between the property and the value"
    EmptyValue => "this declaration has no value"
    ImportantNotLast => "`!important` must be the last thing in a value"
    BadBang => "the only thing a `!` may introduce here is `important`"
    BadDeclaration => "this declaration could not be read"
    BadAtRuleShape(n) => "`@" + n + "` was written with the wrong shape"
    UnknownAtRule(n) => "`@" + n + "` is not an at-rule this library knows"
    BadAtRulePrelude(n) => "the prelude of `@" + n + "` could not be read"
    BadMediaQuery => "this is not a media query"
    BadSupportsCondition => "this is not a `@supports` condition"
    MixedLogicalOps => "`and` and `or` may not be mixed without parentheses"
    BadKeyframeSelector =>
      "a keyframe selector is `from`, `to`, or a percentage"
    DeclarationAtTopLevel => "a declaration must be inside a rule"
    RuleInDeclarationContext => "only declarations may appear here"
    Unexpected(what) => "expected " + what
  }
}

///|
/// The caret label. `None` when the headline has already said it, so that a
/// rendered report does not say the same thing twice at two indents.
fn Diagnostic::point(self : Diagnostic) -> String? {
  match self.kind {
    ExpectedColon => Some("this needs a `:`")
    ImportantNotLast => Some("this comes after `!important`")
    UnknownAtRule(_) => Some("kept as written")
    DanglingCombinator => Some("nothing follows this")
    _ => None
  }
}

///|
/// The rule, in one sentence. This is the part a person actually learns from,
/// so it states the rule rather than restating the failure.
fn Diagnostic::help(self : Diagnostic) -> String? {
  match self.kind {
    UnknownAtRule(_) =>
      Some("its prelude and block are kept unparsed, so nothing is lost")
    MixedLogicalOps => Some("group them: `(a and b) or c`")
    ImportantNotLast => Some("write `color: red !important`")
    BadUrl => Some("quote it: `url(\"a b.png\")`")
    BadKeyframeSelector => Some("for example `0%`, `100%`, `from`, `to`")
    EmptyValue => Some("either give it a value or remove the declaration")
    BadDeclaration =>
      Some(
        "the declaration was skipped up to the next `;` and parsing continued",
      )
    _ => None
  }
}

///|
fn opener_of(closer : String) -> String {
  match closer {
    ")" => "("
    "]" => "["
    "}" => "{"
    _ => closer
  }
}

///|
/// Whether this diagnostic is severe enough to fail a strict parse.
///
/// A predicate rather than a comparison at the call site, because
/// `@report.Severity` has no `Eq` and this is the only question anyone asks of
/// it -- so the match belongs here once rather than everywhere.
pub fn Diagnostic::is_error(self : Diagnostic) -> Bool {
  match self.severity {
    Error => true
    _ => false
  }
}