///| Deterministic text reports for command-line tools, CI logs, and bug

///| reports. The formatter contains no terminal control codes so snapshots are

///|
/// stable across operating systems.
fn append_line(text : String, line : String) -> String {
  text + line + "\n"
}

///|
fn span_text(span : Span) -> String {
  "[" + span.start.to_string() + ", " + span.end.to_string() + ")"
}

///|
pub fn render_structure(binary : Module) -> String {
  let mut text = "WebAssembly binary v" + binary.version().to_string() + "\n"
  text = append_line(text, binary.stats().description())
  text = append_line(text, "sections:")
  for section in binary.sections() {
    let name = match section.custom_name {
      Some(value) => "custom " + value
      None => section.kind.label()
    }
    text = append_line(
      text,
      "  - " +
      name +
      " id=" +
      section.id.to_string() +
      " payload=" +
      span_text(section.payload) +
      " bytes=" +
      section.payload_size().to_string(),
    )
  }
  text
}

///|
fn render_type_list(types : Array[FunctionType]) -> String {
  let mut text = "types: " + types.length().to_string() + "\n"
  let mut index = 0
  for type_ in types {
    text = append_line(
      text,
      "  [" + index.to_string() + "] " + type_.signature(),
    )
    index = index + 1
  }
  text
}

///|
fn render_import_list(imports : Array[Import]) -> String {
  let mut text = "imports: " + imports.length().to_string() + "\n"
  for item in imports {
    let type_suffix = match item.type_index {
      Some(index) => " type=" + index.to_string()
      None => ""
    }
    text = append_line(
      text,
      "  - " +
      item.module_name +
      "." +
      item.name +
      " : " +
      item.kind.label() +
      type_suffix,
    )
  }
  text
}

///|
fn render_export_list(exports : Array[Export]) -> String {
  let mut text = "exports: " + exports.length().to_string() + "\n"
  for item in exports {
    text = append_line(
      text,
      "  - " +
      item.name +
      " : " +
      item.kind.label() +
      " #" +
      item.index.to_string(),
    )
  }
  text
}

///|
fn render_memory_list(memories : Array[MemoryInfo]) -> String {
  let mut text = "memories: " + memories.length().to_string() + "\n"
  let mut index = 0
  for memory in memories {
    let mode = if memory.limits.shared { ", shared" } else { "" }
    let width = if memory.limits.memory64 { ", memory64" } else { "" }
    text = append_line(
      text,
      "  [" +
      index.to_string() +
      "] " +
      memory.limits.description() +
      mode +
      width,
    )
    index = index + 1
  }
  text
}

///|
fn render_table_list(tables : Array[TableInfo]) -> String {
  let mut text = "tables: " + tables.length().to_string() + "\n"
  let mut index = 0
  for table in tables {
    text = append_line(
      text,
      "  [" +
      index.to_string() +
      "] " +
      table.element_type.label() +
      " " +
      table.limits.description(),
    )
    index = index + 1
  }
  text
}

///|
fn render_global_list(globals : Array[GlobalInfo]) -> String {
  let mut text = "globals: " + globals.length().to_string() + "\n"
  let mut index = 0
  for global in globals {
    let mutability = if global.mutable { "mutable" } else { "immutable" }
    text = append_line(
      text,
      "  [" +
      index.to_string() +
      "] " +
      global.value_type.label() +
      " " +
      mutability +
      " init=" +
      span_text(global.init_span),
    )
    index = index + 1
  }
  text
}

///|
pub fn render_decoded(decoded : DecodedModule) -> String {
  let mut text = render_structure(decoded.binary())
  text = text + render_type_list(decoded.types())
  text = text + render_import_list(decoded.imports())
  text = text + render_table_list(decoded.tables)
  text = text + render_memory_list(decoded.memories)
  text = text + render_global_list(decoded.globals)
  text = append_line(
    text,
    "defined functions: " + decoded.defined_function_count().to_string(),
  )
  text = append_line(
    text,
    "code bodies: " + decoded.code_bodies.length().to_string(),
  )
  text = text + render_export_list(decoded.exports())
  match decoded.start_function {
    Some(index) =>
      text = append_line(text, "start function: " + index.to_string())
    None => ()
  }
  if decoded.warnings().length() > 0 {
    text = append_line(text, "warnings:")
    for warning in decoded.warnings() {
      text = append_line(text, "  - " + warning)
    }
  }
  text
}

///|
pub struct SectionDifference {
  label : String
  left_size : Int?
  right_size : Int?
}

///|
pub fn SectionDifference::description(self : SectionDifference) -> String {
  let left = match self.left_size {
    Some(value) => value.to_string()
    None => "missing"
  }
  let right = match self.right_size {
    Some(value) => value.to_string()
    None => "missing"
  }
  self.label + ": " + left + " -> " + right
}

///|
pub fn diff_structure(
  left : Module,
  right : Module,
) -> Array[SectionDifference] {
  let differences : Array[SectionDifference] = []
  for kind_id in 0..<14 {
    let kind = match section_kind_from_id(kind_id) {
      Some(value) => value
      None => continue
    }
    let left_size = match left.find_section(kind) {
      Some(section) => Some(section.payload_size())
      None => None
    }
    let right_size = match right.find_section(kind) {
      Some(section) => Some(section.payload_size())
      None => None
    }
    if left_size != right_size {
      differences.push({ label: kind.label(), left_size, right_size })
    }
  }
  differences
}

///|
pub fn render_structure_diff(left : Module, right : Module) -> String {
  let differences = diff_structure(left, right)
  if differences.length() == 0 {
    "No structural section differences.\n"
  } else {
    let mut text = "Structural section differences:\n"
    for difference in differences {
      text = append_line(text, "  - " + difference.description())
    }
    text
  }
}