///|
/// Diagnostics for the bridge.
///
/// The same arrangement as `@css.error` and `lib/error`: one function reaches
/// `error-report`, and everything else names a problem without linking a
/// renderer.
///
/// Spans come from `@basic.Span`, the shrubbery side's position type, because
/// what a bridge diagnostic points at is a place in a shrubbery file. The
/// conversion to `@report.Span` reads `.idx`, which is already a UTF-16 code
/// unit offset, so this is a change of representation and not of unit.

///|
pub(all) struct Diagnostic {
  kind : @kind.ErrorKind
  span : @basic.Span
  severity : @report.Severity
  related : Array[(@basic.Span, String)]
}

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

///|
pub fn Diagnostic::is_error(self : Diagnostic) -> Bool {
  match self.severity {
    Error => true
    _ => false
  }
}

///|
/// A shrubbery-CSS source was rejected.
pub(all) suberror ShrubCssError {
  ShrubCssError(Diagnostic)
}

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

///|
pub fn ShrubCssError::diagnostic(self : ShrubCssError) -> Diagnostic {
  let ShrubCssError(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
  }
}

///|
fn span_of(s : @basic.Span) -> @report.Span {
  @report.Span::of_range(s.start.idx, s.end.idx)
}

///|
fn Diagnostic::explain(self : Diagnostic) -> String {
  match self.kind {
    ExpectedBlock => "this needs a `:` and an indented block"
    HeadlessBlock => "this block has nothing before it"
    AltsUnsupported => "`|` has no meaning in Shrubbery CSS"
    BracesUnsupported => "`{ }` has no meaning in Shrubbery CSS"
    QuotesUnsupported => "`'...'` has no meaning in Shrubbery CSS"
    DeclarationAtTopLevel => "a declaration must be inside a rule"
    RuleInDeclarationContext => "only declarations may appear here"
    TypeSelectorNotFirst =>
      "two element names touch, with no relation between them"
    DanglingCombinator => "this relation has nothing on one side of it"
    StrayKeyword(k) => "`~" + k + "` is not a relation"
    SigilSelector(s) => "`" + s + "` is CSS's spelling, not this syntax's"
    OperatorCombinator(o) =>
      "`" + o + "` is CSS's spelling for a relation, not this syntax's"
    UnknownSelectorCall(n) =>
      "`" + n + "()` is not something a selector can contain"
    BadCallShape(n) => "`" + n + "` was given arguments of the wrong shape"
    HexDigitsLost => "this colour's leading zeros were lost before it got here"
    UnknownUnit(u) => "`" + u + "` is not a unit"
    BadSelector => "this is not a selector"
    BareTermInBlock(t) =>
      "`" + t + "` is a bare word, with no value and no block"
    ColonPseudo(p, v) =>
      if p == "" {
        "`" + v + "` is a bare word here, not the pseudo-class it looks like"
      } else {
        "`" + p + ":` reads as a declaration of the property `" + p + "`"
      }
    SemicolonDeclarations => "`;` does not separate declarations here"
    BadAnB => "this is not an `an+b` formula"
    Unsupported(what) => what + " has no meaning in Shrubbery CSS"
  }
}

///|
fn Diagnostic::point(self : Diagnostic) -> String? {
  match self.kind {
    ColonPseudo(p, v) =>
      if p == "" {
        None
      } else {
        Some("`" + v + "` was read as the value")
      }
    TypeSelectorNotFirst => Some("this one begins a second element")
    DanglingCombinator => Some("nothing on this side")
    SemicolonDeclarations => Some("this ended up inside the value before it")
    _ => None
  }
}

///|
/// The rule, in one sentence, and where possible the exact thing to type.
///
/// This is the part that teaches, and for a syntax nobody has seen before it is
/// most of the value of the diagnostic. So it names the replacement rather than
/// describing it.
fn Diagnostic::help(self : Diagnostic) -> String? {
  match self.kind {
    ExpectedBlock => Some("a rule or a declaration ends with `:` and its body")
    AltsUnsupported => Some("nothing in CSS branches this way; `|` is reserved")
    BracesUnsupported => Some("a block is opened with `:` and indentation")
    TypeSelectorNotFirst =>
      Some("a descendant is written `a ~in b`; the relation is never a space")
    SigilSelector(s) => Some(sigil_help(s))
    OperatorCombinator(o) =>
      Some(
        match o {
          ">" => "write `~child`"
          "+" => "write `~next`"
          "~" => "write `~sibling`"
          _ => "write the relation as a keyword"
        },
      )
    ColonPseudo(_, v) => Some("a pseudo-class is a call: write `" + v + "()`")
    SemicolonDeclarations =>
      Some("the value runs to the end of the line; one declaration per line")
    HexDigitsLost => Some("write it as a string: `hex(\"007700\")`")
    DeclarationAtTopLevel =>
      Some("put it in a rule, or use `root():` for the document element")
    BareTermInBlock(_) =>
      Some("a declaration needs a value, and a rule needs a block")
    UnknownUnit(_) =>
      Some("use `dim(n, \"unit\")` for a unit this library does not know")
    _ => None
  }
}

///|
fn sigil_help(s : String) -> String {
  match s {
    "." => "write `class(card)` for `.card`"
    "#" => "write `id(sidebar)` for `#sidebar`, or `hex(f7f7f7)` for a colour"
    "&" => "write `parent()`"
    "*" => "write `any()`"
    "@" => "an at-rule is a call: write `media(...)` for `@media`"
    _ => "write the thing's name instead of a symbol for it"
  }
}