///|
/// Incremental builder for normalized regular source maps.
///
/// Sources and names are interned in first-seen order. Mappings must be added
/// in generated order, which catches a common source-map producer bug at the
/// point where it is introduced.
pub(all) struct SourceMapBuilder {
  file : String?
  sources : Array[SourceEntry]
  names : Array[String]
  mappings : Array[Mapping]
}

///|
/// Create an empty source map builder.
pub fn SourceMapBuilder::new(file? : String) -> SourceMapBuilder {
  { file, sources: [], names: [], mappings: [] }
}

///|
fn source_entry_index(
  sources : ArrayView[SourceEntry],
  target : SourceEntry,
) -> Int? {
  for index, source in sources {
    if source == target {
      return Some(index)
    }
  }
  None
}

///|
fn string_index(values : ArrayView[String], target : String) -> Int? {
  for index, value in values {
    if value == target {
      return Some(index)
    }
  }
  None
}

///|
/// Intern a source entry and return its stable index.
pub fn SourceMapBuilder::add_source(
  self : SourceMapBuilder,
  url? : String,
  content? : String,
  ignored? : Bool = false,
) -> Int {
  let entry = SourceEntry::new(url?, content?, ignored~)
  match source_entry_index(self.sources, entry) {
    Some(index) => index
    None => {
      let index = self.sources.length()
      self.sources.push(entry)
      index
    }
  }
}

///|
/// Intern a symbol name and return its stable index.
pub fn SourceMapBuilder::add_name(
  self : SourceMapBuilder,
  name : String,
) -> Int {
  match string_index(self.names, name) {
    Some(index) => index
    None => {
      let index = self.names.length()
      self.names.push(name)
      index
    }
  }
}

///|
fn check_builder_order(
  mappings : ArrayView[Mapping],
  generated : Position,
) -> Unit raise SourceMapError {
  if !generated.is_valid() {
    raise InvalidDocument(message="generated position must be non-negative")
  }
  if !mappings.is_empty() {
    let previous = mappings[mappings.length() - 1].generated
    if previous.compare(generated) > 0 {
      raise InvalidDocument(
        message="builder mappings must be added in generated order",
      )
    }
  }
}

///|
/// Add a generated-only segment.
pub fn SourceMapBuilder::add_generated(
  self : SourceMapBuilder,
  generated~ : Position,
) -> Unit raise SourceMapError {
  check_builder_order(self.mappings, generated)
  self.mappings.push(Mapping::generated_only(generated~))
}

///|
/// Add a segment mapped to an interned source.
pub fn SourceMapBuilder::add_mapping(
  self : SourceMapBuilder,
  generated~ : Position,
  source_index~ : Int,
  original_line~ : Int,
  original_column~ : Int,
  name? : String,
) -> Unit raise SourceMapError {
  check_builder_order(self.mappings, generated)
  if source_index < 0 || source_index >= self.sources.length() {
    raise InvalidDocument(message="builder source index is outside its table")
  }
  let original = OriginalPosition::new(
    source_index~,
    line=original_line,
    column=original_column,
  )
  if !original.is_valid() {
    raise InvalidDocument(message="original position must be non-negative")
  }
  match name {
    Some(name) => self.add_name(name) |> ignore
    None => ()
  }
  self.mappings.push(Mapping::mapped(generated~, original~, name?))
}

///|
/// Return an independent decoded snapshot.
pub fn SourceMapBuilder::build_decoded(
  self : SourceMapBuilder,
) -> DecodedSourceMap {
  DecodedSourceMap::new(
    file?=self.file,
    sources=self.sources.copy(),
    mappings=self.mappings.copy(),
  )
}

///|
/// Build a normalized version-3 regular source map.
pub fn SourceMapBuilder::build(
  self : SourceMapBuilder,
) -> RegularSourceMap raise SourceMapError {
  encode_decoded(self.build_decoded())
}

///|
/// Number of interned sources.
pub fn SourceMapBuilder::source_count(self : SourceMapBuilder) -> Int {
  self.sources.length()
}

///|
/// Number of emitted mappings.
pub fn SourceMapBuilder::mapping_count(self : SourceMapBuilder) -> Int {
  self.mappings.length()
}

///|
/// Remove every mapping while retaining source and name tables.
pub fn SourceMapBuilder::clear_mappings(self : SourceMapBuilder) -> Unit {
  self.mappings.clear()
}