///|
pub struct PruneSummary {
  sources_before : Int
  sources_after : Int
  names_before : Int
  names_after : Int
  mappings_before : Int
  mappings_after : Int
} derive(Eq, Debug, ToJson)

///|
fn source_content_at(map : SourceMap, index : Int) -> String? {
  if index >= 0 && index < map.sources_content.length() {
    map.sources_content[index]
  } else {
    None
  }
}

///|
fn source_index_used(map : SourceMap, index : Int) -> Bool {
  for segment in map.mappings {
    if segment.source_index == Some(index) {
      return true
    }
  }
  map.ignore_list.contains(index)
}

///|
fn name_index_used(map : SourceMap, index : Int) -> Bool {
  for segment in map.mappings {
    if segment.name_index == Some(index) {
      return true
    }
  }
  false
}

///|
pub fn SourceMap::unused_sources(self : SourceMap) -> Array[String] {
  let unused : Array[String] = []
  for index, source in self.sources {
    if !source_index_used(self, index) {
      unused.push(source)
    }
  }
  unused
}

///|
pub fn SourceMap::unused_names(self : SourceMap) -> Array[String] {
  let unused : Array[String] = []
  for index, name in self.names {
    if !name_index_used(self, index) {
      unused.push(name)
    }
  }
  unused
}

///|
pub fn SourceMap::sorted(self : SourceMap) -> SourceMap {
  { ..self, mappings: sorted_mapping_copy(self.mappings) }
}

///|
pub fn SourceMap::prune_unused_sources(self : SourceMap) -> SourceMap {
  let remap : Array[Int?] = []
  let sources : Array[String] = []
  let sources_content : Array[String?] = []
  let keep_content = self.sources_content.length() > 0
  for index, source in self.sources {
    if source_index_used(self, index) {
      remap.push(Some(sources.length()))
      sources.push(source)
      if keep_content {
        sources_content.push(source_content_at(self, index))
      }
    } else {
      remap.push(None)
    }
  }
  let mappings = self.mappings.map(segment => {
    let source_index = match segment.source_index {
      Some(index) =>
        if index >= 0 && index < remap.length() {
          remap[index]
        } else {
          None
        }
      None => None
    }
    {
      ..segment,
      source_index,
      original_line: if source_index is Some(_) {
        segment.original_line
      } else {
        None
      },
      original_column: if source_index is Some(_) {
        segment.original_column
      } else {
        None
      },
    }
  })
  let ignore_list : Array[Int] = []
  for old_index in self.ignore_list {
    if old_index >= 0 && old_index < remap.length() {
      if remap[old_index] is Some(new_index) {
        if !ignore_list.contains(new_index) {
          ignore_list.push(new_index)
        }
      }
    }
  }
  { ..self, sources, sources_content, mappings, ignore_list }
}

///|
pub fn SourceMap::prune_unused_names(self : SourceMap) -> SourceMap {
  let remap : Array[Int?] = []
  let names : Array[String] = []
  for index, name in self.names {
    if name_index_used(self, index) {
      remap.push(Some(names.length()))
      names.push(name)
    } else {
      remap.push(None)
    }
  }
  let mappings = self.mappings.map(segment => {
    let name_index = match segment.name_index {
      Some(index) =>
        if index >= 0 && index < remap.length() {
          remap[index]
        } else {
          None
        }
      None => None
    }
    { ..segment, name_index, }
  })
  { ..self, names, mappings }
}

///|
pub fn SourceMap::prune_unused(self : SourceMap) -> SourceMap {
  self.prune_unused_sources().prune_unused_names()
}

///|
fn merge_content(left : String?, right : String?) -> String? {
  match left {
    Some(_) => left
    None => right
  }
}

///|
pub fn SourceMap::deduplicate_sources(self : SourceMap) -> SourceMap {
  let remap : Array[Int] = []
  let sources : Array[String] = []
  let sources_content : Array[String?] = []
  let keep_content = self.sources_content.length() > 0
  for index, source in self.sources {
    match find_string(sources, source) {
      Some(existing) => {
        remap.push(existing)
        if keep_content {
          sources_content[existing] = merge_content(
            sources_content[existing],
            source_content_at(self, index),
          )
        }
      }
      None => {
        let next = sources.length()
        remap.push(next)
        sources.push(source)
        if keep_content {
          sources_content.push(source_content_at(self, index))
        }
      }
    }
  }
  let mappings = self.mappings.map(segment => {
    let source_index = match segment.source_index {
      Some(index) =>
        if index >= 0 && index < remap.length() {
          Some(remap[index])
        } else {
          None
        }
      None => None
    }
    {
      ..segment,
      source_index,
      original_line: if source_index is Some(_) {
        segment.original_line
      } else {
        None
      },
      original_column: if source_index is Some(_) {
        segment.original_column
      } else {
        None
      },
    }
  })
  let ignore_list : Array[Int] = []
  for old_index in self.ignore_list {
    if old_index >= 0 && old_index < remap.length() {
      let new_index = remap[old_index]
      if !ignore_list.contains(new_index) {
        ignore_list.push(new_index)
      }
    }
  }
  { ..self, sources, sources_content, mappings, ignore_list }
}

///|
pub fn SourceMap::deduplicate_names(self : SourceMap) -> SourceMap {
  let remap : Array[Int] = []
  let names : Array[String] = []
  for name in self.names {
    match find_string(names, name) {
      Some(existing) => remap.push(existing)
      None => {
        let next = names.length()
        remap.push(next)
        names.push(name)
      }
    }
  }
  let mappings = self.mappings.map(segment => {
    let name_index = match segment.name_index {
      Some(index) =>
        if index >= 0 && index < remap.length() {
          Some(remap[index])
        } else {
          None
        }
      None => None
    }
    { ..segment, name_index, }
  })
  { ..self, names, mappings }
}

///|
pub fn SourceMap::canonical(self : SourceMap) -> SourceMap {
  self.deduplicate_sources().deduplicate_names().prune_unused().sorted()
}

///|
pub fn SourceMap::prune_summary(self : SourceMap) -> PruneSummary {
  let pruned = self.prune_unused()
  {
    sources_before: self.sources.length(),
    sources_after: pruned.sources.length(),
    names_before: self.names.length(),
    names_after: pruned.names.length(),
    mappings_before: self.mappings.length(),
    mappings_after: pruned.mappings.length(),
  }
}

///|
pub fn PruneSummary::summary(self : PruneSummary) -> String {
  "sources \{self.sources_before}->\{self.sources_after}, names \{self.names_before}->\{self.names_after}, mappings \{self.mappings_before}->\{self.mappings_after}"
}