///|
/// The ONE place this module names `error-report`.
///
/// Everything below `error` produces a `Diagnostic`, which is this module's
/// own type and carries no rendering. This function is the whole bridge, so
/// that swapping the renderer is a one-function change and so that
/// `tools/boundary-check.sh` can assert that nothing else links it.
///
/// The rendering adds what the reference does not have and a person wants:
/// a second span for the statement that a message refers to but does not
/// point at, a help line where the fix is not obvious from the message, and
/// a stable code to search for.
pub fn Diagnostic::to_report(
  self : Diagnostic,
  source : @report.SourceId,
) -> @report.Report {
  let mut r = @report.Report::error(self.message())
  match self.code() {
    Some(c) => r = r.with_code(c)
    None => ()
  }
  match self.span {
    Some(s) =>
      r = r.with_label(
        @report.Label::primary(source, span_of(s), message?=self.anchor_note()),
      )
    None => ()
  }
  for extra in self.related {
    r = r.with_label(
      @report.Label::secondary(source, span_of(extra.0), message=extra.1),
    )
  }
  match self.help() {
    Some(h) => r = r.with_help(h)
    None => ()
  }
  r
}

///|
/// A `Span` of this module is two positions; `error-report`'s is an offset
/// and a length, both in UTF-16 code units.
fn span_of(s : @basic.Span) -> @report.Span {
  @report.Span::of_range(s.start.offset, s.end.offset)
}

///|
/// A short, stable code to search for.
pub fn Diagnostic::code(self : Diagnostic) -> String? {
  match self.kind {
    IllFormed(r) => Some(r.code())
    Prohibited(_) => Some("purepy::prohibited")
    NotYetSupported(..) => Some("purepy::not-yet-supported")
    PythonSyntax(_) => Some("purepy::syntax")
    Program(_) => Some("purepy::program")
  }
}

///|
/// What to write against the primary span, when the message alone does not
/// say what is wrong with THAT position.
fn Diagnostic::anchor_note(self : Diagnostic) -> String? {
  match self.kind {
    IllFormed(CapturedReassignment(..)) => Some("reassigned here")
    IllFormed(UnreachableStatement) => Some("never reached")
    IllFormed(UnreachableCase(..)) => Some("never reached")
    IllFormed(SubmoduleNameClash(..)) => Some("bound here")
    IllFormed(ImportAfterStatement) => Some("this import comes too late")
    IllFormed(NonTopLevelImport) => Some("an import inside a body")
    _ => None
  }
}

///|
/// What a person is likely to want to do about it.
///
/// Only where the message does not already say. A rule that is surprising --
/// the capture rules, and the two "not a first-class value" ones -- earns a
/// line; "duplicate field name 'x'" does not.
pub fn Diagnostic::help(self : Diagnostic) -> String? {
  match self.kind {
    IllFormed(UnassignedVariable(..)) =>
      Some("assign it in every branch, or before the if")
    IllFormed(UnassignedMember(..)) =>
      Some(
        "the module assigns it in some branches only; assign it in every branch",
      )
    IllFormed(CapturedReassignment(..)) =>
      Some(
        "a closure captured this name, and PurePy has no cell for it to see a later value; bind a new name instead",
      )
    IllFormed(SelfCaptureAssignment(..)) =>
      Some(
        "the right-hand side closes over the name being bound; give one of them another name",
      )
    IllFormed(CapturedGeneratorVariable(..)) =>
      Some(
        "the generator rebinds the variable on each step, so every lambda would see its last value; pass it as a parameter",
      )
    IllFormed(ModuleAsValue(..)) =>
      Some("name a member of it instead, or import the member directly")
    IllFormed(ClassAsValue(..)) =>
      Some("call it to construct one, or match on it in a pattern")
    IllFormed(SubmoduleNotImported(q~)) =>
      Some("add `import " + q + "` or `from ... import ...` for it")
    IllFormed(TopLevelReturn) =>
      Some("a module body runs to the end; there is nothing to return to")
    IllFormed(OwnDescendantImport(q~, ..)) =>
      Some("use `from " + parent_of(q) + " import " + last_of(q) + "`")
    NotYetSupported(issue~, ..) =>
      Some("tracked upstream as issue #" + issue.to_string())
    _ => None
  }
}

///|
fn parent_of(q : String) -> String {
  match q.rev_split_once(".") {
    Some((head, _)) => head.to_owned()
    None => q
  }
}

///|
fn last_of(q : String) -> String {
  match q.rev_split_once(".") {
    Some((_, tail)) => tail.to_owned()
    None => q
  }
}

///|
/// A diagnostic with a second place to look.
pub fn Diagnostic::with_related(
  self : Diagnostic,
  span : @basic.Span,
  message : String,
) -> Diagnostic {
  let related = self.related.copy()
  related.push((span, message))
  { ..self, related, }
}