// Parsing of OpenCC conversion configurations.
//
// A configuration names its input dictionaries as `.ocd2` files. This module
// turns those references into the binding names used by the generated `dict`
// package (so `STPhrases.ocd2` becomes `st_phrases`) and keeps the nesting of
// `match_policy` groups intact, because the nesting is semantics.

///|
/// Read a field of a JSON object.
fn field(json : Json, key : String) -> Json? {
  if json is Object(object) {
    object.get(key)
  } else {
    None
  }
}

///|
/// Read a JSON string.
fn as_text(json : Json) -> String? {
  if json is String(text) {
    Some(text)
  } else {
    None
  }
}

///|
/// Read a JSON array.
fn as_list(json : Json) -> Array[Json]? {
  if json is Array(items) {
    Some(items)
  } else {
    None
  }
}

///|
/// Policy of a dictionary group, as declared in a configuration.
pub(all) enum SpecPolicy {
  Union
  ShortCircuit
}

///|
/// A conversion pass: a dictionary, or a group of passes.
pub(all) enum Spec {
  Leaf(String)
  Group(SpecPolicy, Array[Spec])
}

///|
/// One conversion configuration.
pub struct ChainSpec {
  name : String
  title : String
  normalization : Array[Spec]
  segmentation : Spec?
  conversion : Array[Spec]
}

///|
/// Every leaf dictionary name of a pass, in order.
pub fn Spec::leaves(self : Spec) -> Array[String] {
  match self {
    Leaf(name) => [name]
    Group(_, children) => {
      let out : Array[String] = []
      for child in children {
        for name in child.leaves() {
          out.push(name)
        }
      }
      out
    }
  }
}

///|
/// `STPhrases.ocd2` -> `st_phrases`.
fn binding_of(file : String) -> String {
  let stem = match file.strip_suffix(".ocd2") {
    Some(stem) => stem
    None => file.to_string_view()
  }
  module_name(stem.to_owned() + ".txt")
}

///|
/// Read a `{ "dict":  }` wrapper.
fn spec_of_holder(json : Json) -> Spec? {
  match field(json, "dict") {
    Some(dict) => spec_of(dict)
    None => None
  }
}

///|
/// Read a leaf dictionary or a nested group.
pub fn spec_of(json : Json) -> Spec? {
  match field(json, "type") {
    Some(kind) =>
      match as_text(kind) {
        Some("group") => {
          let policy = match field(json, "match_policy") {
            Some(node) =>
              match as_text(node) {
                Some("union") => Union
                _ => ShortCircuit
              }
            None => ShortCircuit
          }
          let children : Array[Spec] = []
          match field(json, "dicts") {
            Some(node) =>
              match as_list(node) {
                Some(items) =>
                  for item in items {
                    match spec_of(item) {
                      Some(child) => children.push(child)
                      None => ()
                    }
                  }
                None => ()
              }
            None => ()
          }
          Some(Group(policy, children))
        }
        Some(_) => leaf_of(json)
        None => leaf_of(json)
      }
    None => leaf_of(json)
  }
}

///|
/// Read `{ "file": "X.ocd2" }` as a leaf.
fn leaf_of(json : Json) -> Spec? {
  match field(json, "file") {
    Some(node) =>
      match as_text(node) {
        Some(name) => Some(Leaf(binding_of(name)))
        None => None
      }
    None => None
  }
}

///|
/// Parse one configuration file.
///
/// Returns the spec and how many parts of it could not be understood.
pub fn parse_config(name : String, text : String) -> (ChainSpec?, Int) {
  try {
    let json = @json.parse(text)
    let mut problems = 0
    let title = match field(json, "name") {
      Some(node) =>
        match as_text(node) {
          Some(title) => title
          None => ""
        }
      None => ""
    }
    let normalization : Array[Spec] = []
    match field(json, "normalization") {
      Some(node) =>
        match as_list(node) {
          Some(items) =>
            for item in items {
              match spec_of_holder(item) {
                Some(spec) => normalization.push(spec)
                None => problems = problems + 1
              }
            }
          None => problems = problems + 1
        }
      None => ()
    }
    // Segmentation decides which spans may be converted independently.
    let segmentation = match field(json, "segmentation") {
      Some(node) =>
        match field(node, "dict") {
          Some(dict) => spec_of(dict)
          None => None
        }
      None => None
    }
    let conversion : Array[Spec] = []
    match field(json, "conversion_chain") {
      Some(node) =>
        match as_list(node) {
          Some(items) =>
            for item in items {
              match spec_of_holder(item) {
                Some(spec) => conversion.push(spec)
                None => problems = problems + 1
              }
            }
          None => problems = problems + 1
        }
      None => problems = problems + 1
    }
    (Some({ name, title, normalization, segmentation, conversion, }), problems)
  } catch {
    _ => (None, 1)
  }
}