///|
/// One decoded Source Map segment. Lines and columns are zero-based.
pub struct Mapping {
  generated_line : Int
  generated_column : Int
  source_index : Int?
  original_line : Int?
  original_column : Int?
  name_index : Int?
} derive(Eq, Debug)

///|
/// Construct one generated-only or original-source mapping segment.
pub fn Mapping::make(
  generated_line : Int,
  generated_column : Int,
  source_index? : Int? = None,
  original_line? : Int? = None,
  original_column? : Int? = None,
  name_index? : Int? = None,
) -> Mapping {
  {
    generated_line,
    generated_column,
    source_index,
    original_line,
    original_column,
    name_index,
  }
}

///|
/// A resolved mapping suitable for debugger and editor integrations.
pub struct ResolvedMapping {
  generated_line : Int
  generated_column : Int
  source : String?
  original_line : Int?
  original_column : Int?
  name : String?
} derive(Eq, Debug)

///|
/// Select which neighboring mapping a position query returns.
pub(all) enum MappingBias {
  GreatestLowerBound
  LeastUpperBound
} derive(Eq, Debug)

///|
/// A decoded Source Map v3 core model.
pub struct SourceMap {
  file : String?
  source_root : String?
  sources : Array[String]
  sources_content : Array[String?]
  names : Array[String]
  mappings : Array[Mapping]
}

///|
/// Build and validate a decoded Source Map.
pub fn SourceMap::make(
  sources : Array[String],
  names : Array[String],
  mappings : Array[Mapping],
  file? : String? = None,
  source_root? : String? = None,
  sources_content? : Array[String?] = [],
) -> SourceMap raise SourceMapError {
  if sources_content.length() != 0 &&
    sources_content.length() != sources.length() {
    raise SourceMapError::At(
      0, "sources_content length must be zero or match sources length",
    )
  }
  let mut previous_line = 0
  let mut previous_column = -1
  for i in 0.. Unit raise SourceMapError {
  match (mapping.source_index, mapping.original_line, mapping.original_column) {
    (None, None, None) => ()
    (Some(source), Some(line), Some(column)) => {
      if source < 0 || source >= source_count {
        raise SourceMapError::At(offset, "source index is out of bounds")
      }
      if line < 0 || column < 0 {
        raise SourceMapError::At(
          offset, "original positions must be non-negative",
        )
      }
    }
    _ =>
      raise SourceMapError::At(
        offset, "source, original line and original column must appear together",
      )
  }
  match mapping.name_index {
    Some(_) if mapping.source_index is None =>
      raise SourceMapError::At(offset, "name requires an original position")
    Some(name) if name < 0 || name >= name_count =>
      raise SourceMapError::At(offset, "name index is out of bounds")
    _ => ()
  }
}

///|
/// Parse a Source Map v3 `mappings` string using the supplied tables.
pub fn parse_mappings(
  encoded : String,
  sources : Array[String],
  names : Array[String],
) -> SourceMap raise SourceMapError {
  let mappings : Array[Mapping] = Array::new()
  let mut generated_line = 0
  let mut previous_generated_column = 0
  let mut previous_source = 0
  let mut previous_original_line = 0
  let mut previous_original_column = 0
  let mut previous_name = 0
  let mut start = 0
  for offset in 0..<=encoded.length() {
    let delimiter = if offset == encoded.length() {
      ';'
    } else {
      encoded.unsafe_get(offset).to_int().unsafe_to_char()
    }
    if delimiter == ',' || delimiter == ';' {
      if offset > start {
        let values = decode_vlq_values(encoded[start:offset].to_owned()) catch {
          error => raise shift_error(error, start)
        }
        if values.length() != 1 && values.length() != 4 && values.length() != 5 {
          raise SourceMapError::At(
            start, "a segment must contain 1, 4, or 5 fields",
          )
        }
        previous_generated_column += values[0]
        if previous_generated_column < 0 {
          raise SourceMapError::At(start, "generated column became negative")
        }
        let mut source : Int? = None
        let mut original_line : Int? = None
        let mut original_column : Int? = None
        let mut name : Int? = None
        if values.length() >= 4 {
          previous_source += values[1]
          previous_original_line += values[2]
          previous_original_column += values[3]
          source = Some(previous_source)
          original_line = Some(previous_original_line)
          original_column = Some(previous_original_column)
          if values.length() == 5 {
            previous_name += values[4]
            name = Some(previous_name)
          }
        }
        mappings.push({
          generated_line,
          generated_column: previous_generated_column,
          source_index: source,
          original_line,
          original_column,
          name_index: name,
        })
      }
      start = offset + 1
      if delimiter == ';' && offset < encoded.length() {
        generated_line += 1
        previous_generated_column = 0
      }
    }
  }
  SourceMap::make(sources, names, mappings)
}

///|
fn shift_error(error : SourceMapError, base : Int) -> SourceMapError {
  match error {
    At(offset, message) => At(base + offset, message)
  }
}

///|
/// Encode decoded mappings into a canonical Source Map v3 mappings string.
pub fn SourceMap::encode_mappings(self : SourceMap) -> String {
  let mut out = ""
  let mut generated_line = 0
  let mut previous_generated_column = 0
  let mut previous_source = 0
  let mut previous_original_line = 0
  let mut previous_original_column = 0
  let mut previous_name = 0
  let mut first_in_line = true
  for mapping in self.mappings {
    while generated_line < mapping.generated_line {
      out = out + ";"
      generated_line += 1
      previous_generated_column = 0
      first_in_line = true
    }
    if !first_in_line {
      out = out + ","
    }
    out = out + encode_vlq(mapping.generated_column - previous_generated_column)
    previous_generated_column = mapping.generated_column
    match
      (mapping.source_index, mapping.original_line, mapping.original_column) {
      (Some(source), Some(line), Some(column)) => {
        out = out + encode_vlq(source - previous_source)
        out = out + encode_vlq(line - previous_original_line)
        out = out + encode_vlq(column - previous_original_column)
        previous_source = source
        previous_original_line = line
        previous_original_column = column
        match mapping.name_index {
          Some(name) => {
            out = out + encode_vlq(name - previous_name)
            previous_name = name
          }
          None => ()
        }
      }
      _ => ()
    }
    first_in_line = false
  }
  out
}

///|
fn SourceMap::resolve(self : SourceMap, mapping : Mapping) -> ResolvedMapping {
  let source = match mapping.source_index {
    Some(index) => Some(self.sources[index])
    None => None
  }
  let name = match mapping.name_index {
    Some(index) => Some(self.names[index])
    None => None
  }
  {
    generated_line: mapping.generated_line,
    generated_column: mapping.generated_column,
    source,
    original_line: mapping.original_line,
    original_column: mapping.original_column,
    name,
  }
}

///|
/// Find a mapping neighboring a generated position on the same line.
pub fn SourceMap::original_position_for_with_bias(
  self : SourceMap,
  line : Int,
  column : Int,
  bias : MappingBias,
) -> ResolvedMapping? {
  let mut low = 0
  let mut high = self.mappings.length()
  while low < high {
    let mid = low + (high - low) / 2
    let mapping = self.mappings[mid]
    let column_precedes = match bias {
      GreatestLowerBound => mapping.generated_column <= column
      LeastUpperBound => mapping.generated_column < column
    }
    if mapping.generated_line < line ||
      (mapping.generated_line == line && column_precedes) {
      low = mid + 1
    } else {
      high = mid
    }
  }
  let index = match bias {
    GreatestLowerBound => low - 1
    LeastUpperBound => low
  }
  if index < 0 ||
    index >= self.mappings.length() ||
    self.mappings[index].generated_line != line {
    return None
  }
  Some(self.resolve(self.mappings[index]))
}

///|
/// Find the greatest mapping at or before a generated position on the same line.
pub fn SourceMap::original_position_for(
  self : SourceMap,
  line : Int,
  column : Int,
) -> ResolvedMapping? {
  self.original_position_for_with_bias(line, column, GreatestLowerBound)
}

///|
/// Find a mapping neighboring an original position on the same source line.
pub fn SourceMap::generated_position_for_with_bias(
  self : SourceMap,
  source : String,
  line : Int,
  column : Int,
  bias : MappingBias,
) -> ResolvedMapping? {
  let mut best : Mapping? = None
  for mapping in self.mappings {
    match
      (mapping.source_index, mapping.original_line, mapping.original_column) {
      (Some(source_index), Some(original_line), Some(original_column)) => {
        let column_matches = match bias {
          GreatestLowerBound => original_column <= column
          LeastUpperBound => original_column >= column
        }
        if self.sources[source_index] == source &&
          original_line == line &&
          column_matches {
          match best {
            Some(current) =>
              match current.original_column {
                Some(current_column) => {
                  let current_is_better = match bias {
                    GreatestLowerBound => current_column >= original_column
                    LeastUpperBound => current_column <= original_column
                  }
                  if !current_is_better {
                    best = Some(mapping)
                  }
                }
                None => best = Some(mapping)
              }
            None => best = Some(mapping)
          }
        }
      }
      _ => ()
    }
  }
  match best {
    Some(mapping) => Some(self.resolve(mapping))
    None => None
  }
}

///|
/// Find the closest mapping at or before an original column on the same source line.
pub fn SourceMap::generated_position_for(
  self : SourceMap,
  source : String,
  line : Int,
  column : Int,
) -> ResolvedMapping? {
  self.generated_position_for_with_bias(
    source,
    line,
    column,
    GreatestLowerBound,
  )
}

///|
/// Find every generated position that maps to an exact original position.
/// This preserves generated mapping order and supports one-to-many mappings.
pub fn SourceMap::generated_positions_for(
  self : SourceMap,
  source : String,
  line : Int,
  column : Int,
) -> Array[ResolvedMapping] {
  let results : Array[ResolvedMapping] = Array::new()
  if line < 0 || column < 0 {
    return results
  }
  for mapping in self.mappings {
    match
      (mapping.source_index, mapping.original_line, mapping.original_column) {
      (Some(source_index), Some(original_line), Some(original_column)) =>
        if self.sources[source_index] == source &&
          original_line == line &&
          original_column == column {
          results.push(self.resolve(mapping))
        }
      _ => ()
    }
  }
  results
}