///|
/// A half-open UTF-16 range in the original input, with a one-based start
/// line and column. Markdown ranges cover the paragraph, including markup.
struct SourceSpan {
  start : Int32
  end : Int32
  line : Int32
  column : Int32
} derive(Eq, Debug)

///|
pub fn SourceSpan::start(self : SourceSpan) -> Int32 {
  self.start
}

///|
pub fn SourceSpan::end(self : SourceSpan) -> Int32 {
  self.end
}

///|
pub fn SourceSpan::line(self : SourceSpan) -> Int32 {
  self.line
}

///|
pub fn SourceSpan::column(self : SourceSpan) -> Int32 {
  self.column
}

///|
/// Markdown paths resolve against the retained MDC document. Locations are
/// absent if that document was parsed with `locs=false`.
pub enum Origin {
  Text(SourceSpan)
  Markdown(path~ : @mdc.NodePath, span~ : SourceSpan?)
} derive(Eq, Debug)

///|
struct Occurrence {
  requirement : Requirement
  origin : Origin
} derive(Eq, Debug)

///|
pub fn Occurrence::requirement(self : Occurrence) -> Requirement {
  self.requirement
}

///|
pub fn Occurrence::origin(self : Occurrence) -> Origin {
  self.origin
}

///|
struct Diagnostic {
  text : String
  error : ParseError
  origin : Origin
} derive(Eq, Debug)

///|
pub fn Diagnostic::text(self : Diagnostic) -> String {
  self.text
}

///|
pub fn Diagnostic::error(self : Diagnostic) -> ParseError {
  self.error
}

///|
pub fn Diagnostic::origin(self : Diagnostic) -> Origin {
  self.origin
}

///|
/// An immutable extraction result. Syntax errors do not discard valid siblings.
/// The Markdown constructor retains its MDC document and frontmatter.
struct Specification {
  requirements : ReadOnlyArray[Occurrence]
  diagnostics : ReadOnlyArray[Diagnostic]
  document : @mdc.Document?
} derive(Debug)

///|
pub fn Specification::requirements(
  self : Specification,
) -> ReadOnlyArray[Occurrence] {
  self.requirements
}

///|
pub fn Specification::diagnostics(
  self : Specification,
) -> ReadOnlyArray[Diagnostic] {
  self.diagnostics
}

///|
pub fn Specification::document(self : Specification) -> @mdc.Document? {
  self.document
}

///|
fn collect_candidate(
  text : String,
  origin : Origin,
  requirements : Array[Occurrence],
  diagnostics : Array[Diagnostic],
) -> Unit {
  let requirement = Requirement::Requirement(text) catch {
    error => {
      diagnostics.push({ text, error, origin, })
      return
    }
  }
  requirements.push({ requirement, origin, })
}

///|
/// Parse one requirement per nonblank line. Every nonblank line is checked.
/// Accept LF, CRLF, CR and a leading BOM. Spans exclude indentation/newlines.
pub fn parse_text(source : String) -> Specification {
  let requirements = []
  let diagnostics = []
  let mut offset = if source.has_prefix("\u{FEFF}") { 1 } else { 0 }
  let mut line = 1
  let mut line_start = 0
  while offset < source.length() {
    let mut end = offset
    while end < source.length() &&
          source.code_unit_at(end) != 10 &&
          source.code_unit_at(end) != 13 {
      end += 1
    }
    let mut first = offset
    let mut last = end
    while first < last && is_space(source.code_unit_at(first)) {
      first += 1
    }
    while last > first && is_space(source.code_unit_at(last - 1)) {
      last -= 1
    }
    if first < last {
      let span = SourceSpan::{
        start: first,
        end: last,
        line,
        column: first - line_start + 1,
      }
      collect_candidate(
        source[first:last].to_owned(),
        Text(span),
        requirements,
        diagnostics,
      )
    }
    offset = end
    if offset < source.length() {
      offset += 1
      if source.code_unit_at(end) == 13 &&
        offset < source.length() &&
        source.code_unit_at(offset) == 10 {
        offset += 1
      }
    }
    line += 1
    line_start = offset
  }
  {
    requirements: ReadOnlyArray::from_array(requirements[:]),
    diagnostics: ReadOnlyArray::from_array(diagnostics[:]),
    document: None,
  }
}

///|
pub extend SourceSpan with Eq::{equal, not_equal}

///|
pub extend SourceSpan with @debug.Debug::{to_repr}

///|
pub extend Origin with Eq::{equal, not_equal}

///|
pub extend Origin with @debug.Debug::{to_repr}

///|
pub extend Occurrence with Eq::{equal, not_equal}

///|
pub extend Occurrence with @debug.Debug::{to_repr}

///|
pub extend Diagnostic with Eq::{equal, not_equal}

///|
pub extend Diagnostic with @debug.Debug::{to_repr}

///|
pub extend Specification with @debug.Debug::{to_repr}