///|
/// Policy for outer mappings that cannot be resolved through the inner map.
pub(all) enum MissingMappingPolicy {
  KeepGenerated
  Drop
} derive(Eq, Debug)

///|
/// Options controlling two-stage map composition.
pub(all) struct ComposeOptions {
  bias : LookupBias
  missing : MissingMappingPolicy
  preserve_outer_names : Bool
}

///|
/// Default composition options.
pub fn ComposeOptions::default() -> ComposeOptions {
  {
    bias: GreatestLowerBound,
    missing: KeepGenerated,
    preserve_outer_names: true,
  }
}

///|
/// Statistics returned by detailed composition.
pub(all) struct ComposeStats {
  outer_mappings : Int
  composed_mappings : Int
  generated_only : Int
  dropped : Int
}

///|
/// A composed map together with coverage statistics.
pub(all) struct ComposeResult {
  map : DecodedSourceMap
  stats : ComposeStats
}

///|
fn composed_name(
  outer : Mapping,
  inner : Mapping,
  preserve_outer_names : Bool,
) -> String? {
  if preserve_outer_names {
    match outer.name {
      Some(name) => Some(name)
      None => inner.name
    }
  } else {
    inner.name
  }
}

///|
fn compose_one(
  outer : Mapping,
  inner_map : DecodedSourceMap,
  options : ComposeOptions,
) -> Mapping? {
  match outer.original {
    None =>
      match options.missing {
        KeepGenerated =>
          Some(Mapping::generated_only(generated=outer.generated))
        Drop => None
      }
    Some(intermediate) => {
      let inner = original_position_for(
        inner_map,
        generated=Position::new(
          line=intermediate.line,
          column=intermediate.column,
        ),
        bias=options.bias,
      )
      match inner {
        Some(inner) =>
          match inner.original {
            Some(original) =>
              Some(
                Mapping::mapped(
                  generated=outer.generated,
                  original~,
                  name?=composed_name(
                    outer,
                    inner,
                    options.preserve_outer_names,
                  ),
                ),
              )
            None =>
              match options.missing {
                KeepGenerated =>
                  Some(Mapping::generated_only(generated=outer.generated))
                Drop => None
              }
          }
        None =>
          match options.missing {
            KeepGenerated =>
              Some(Mapping::generated_only(generated=outer.generated))
            Drop => None
          }
      }
    }
  }
}

///|
/// Compose two decoded source maps and return coverage statistics.
///
/// `outer` maps final generated code to an intermediate artifact. `inner` maps
/// that artifact to original sources. The composed source table is copied from
/// `inner`, and the generated file is copied from `outer`.
pub fn compose_with_options(
  outer : DecodedSourceMap,
  inner : DecodedSourceMap,
  options? : ComposeOptions = ComposeOptions::default(),
) -> ComposeResult {
  let mappings : Array[Mapping] = []
  let mut composed_mappings = 0
  let mut generated_only = 0
  let mut dropped = 0
  for mapping in outer.mappings {
    match compose_one(mapping, inner, options) {
      Some(composed) => {
        if composed.original is Some(_) {
          composed_mappings = composed_mappings + 1
        } else {
          generated_only = generated_only + 1
        }
        mappings.push(composed)
      }
      None => dropped = dropped + 1
    }
  }
  let map = DecodedSourceMap::new(
    file?=outer.file,
    sources=inner.sources.copy(),
    mappings~,
  )
  {
    map,
    stats: {
      outer_mappings: outer.mappings.length(),
      composed_mappings,
      generated_only,
      dropped,
    },
  }
}

///|
/// Compose final-to-intermediate and intermediate-to-original decoded maps.
pub fn compose(
  generated_to_intermediate : DecodedSourceMap,
  intermediate_to_original : DecodedSourceMap,
) -> DecodedSourceMap {
  compose_with_options(generated_to_intermediate, intermediate_to_original).map
}

///|
/// Parse, flatten and compose two source-map JSON documents.
pub fn compose_json(
  outer_json : StringView,
  inner_json : StringView,
  indent? : Int,
) -> String raise SourceMapError {
  let outer = decode_document(parse_document(outer_json))
  let inner = decode_document(parse_document(inner_json))
  let document = SourceMapDocument::Regular(
    encode_decoded(compose(outer, inner)),
  )
  encode_document(document, indent?)
}