///| Formatting options for INI and properties output.
pub struct FormatOptions {
  redact_secrets : Bool
  include_source_comments : Bool
  blank_line_between_sections : Bool
  quote_values : Bool
} derive(@debug.Debug, Eq)

pub fn default_format_options() -> FormatOptions {
  {
    redact_secrets: false,
    include_source_comments: true,
    blank_line_between_sections: true,
    quote_values: false,
  }
}

pub fn safe_format_options() -> FormatOptions {
  {
    redact_secrets: true,
    include_source_comments: false,
    blank_line_between_sections: true,
    quote_values: false,
  }
}

///| Renders a merged view as INI text.
pub fn render_ini(view : ConfigView, options? : FormatOptions = default_format_options()) -> String {
  let builder = StringBuilder()
  let sections = collect_sections(view)
  render_global_entries(builder, view, options)
  let mut wrote_any_section = has_global_entries(view)
  for section_name in sections {
    if section_name != "" {
      if wrote_any_section && options.blank_line_between_sections {
        builder.write_string("\n")
      }
      builder.write_string("[" + section_name + "]\n")
      for item in view.values {
        if item.section == section_name {
          render_entry_line(builder, item, options)
        }
      }
      wrote_any_section = true
    }
  }
  builder.to_string()
}

fn has_global_entries(view : ConfigView) -> Bool {
  for item in view.values {
    if item.section == "" {
      return true
    }
  }
  false
}

fn render_global_entries(builder : StringBuilder, view : ConfigView, options : FormatOptions) -> Unit {
  for item in view.values {
    if item.section == "" {
      render_entry_line(builder, item, options)
    }
  }
}

fn render_entry_line(builder : StringBuilder, item : ResolvedValue, options : FormatOptions) -> Unit {
  let path = entry_path(item.section, item.key)
  if options.include_source_comments {
    builder.write_string("; from " + item.source_layer + " " + item.source + ":" + item.span.line.to_string() + "\n")
  }
  let value = if options.redact_secrets { redact_value(path, item.value) } else { item.value }
  builder.write_string(item.key + " = " + format_value(value, options.quote_values) + "\n")
}

fn format_value(value : String, quote : Bool) -> String {
  if quote || value.contains("#") || value.contains(";") || value.has_prefix(" ") || value.has_suffix(" ") {
    "\"" + value.replace(old="\\", new="\\\\").replace(old="\"", new="\\\"") + "\""
  } else {
    value
  }
}

fn collect_sections(view : ConfigView) -> Array[String] {
  let sections : Array[String] = []
  for item in view.values {
    if item.section != "" && !contains_section(sections, item.section) {
      sections.push(item.section)
    }
  }
  sections
}

fn contains_section(sections : Array[String], name : String) -> Bool {
  for section_name in sections {
    if section_name == name {
      return true
    }
  }
  false
}

///| Renders a merged view as Java-style properties text.
pub fn render_properties(view : ConfigView, options? : FormatOptions = default_format_options()) -> String {
  let builder = StringBuilder()
  for item in view.values {
    let path = entry_path(item.section, item.key)
    if options.include_source_comments {
      builder.write_string("# from " + item.source_layer + " " + item.source + ":" + item.span.line.to_string() + "\n")
    }
    let value = if options.redact_secrets { redact_value(path, item.value) } else { item.value }
    builder.write_string(path + "=" + escape_properties_value(value) + "\n")
  }
  builder.to_string()
}

fn escape_properties_value(value : String) -> String {
  value
    .replace(old="\\", new="\\\\")
    .replace(old="\n", new="\\n")
    .replace(old="\t", new="\\t")
}

///| Returns a document preview with defaults from schema filled into missing keys.
pub fn render_defaults_preview(schema : ConfigSchema, view : ConfigView) -> String {
  let builder = StringBuilder()
  for schema_section in schema.sections {
    builder.write_string("[" + schema_section.name + "]\n")
    for schema_field in schema_section.fields {
      let key = field_key(schema_field.path)
      match view.get_path(schema_field.path) {
        Some(value) => builder.write_string(key + " = " + value + "\n")
        None => match schema_field.default_value {
          Some(default_value) => builder.write_string(key + " = " + default_value + " ; default\n")
          None => if schema_field.required {
            builder.write_string("; " + key + " = \n")
          }
        }
      }
    }
    builder.write_string("\n")
  }
  builder.to_string()
}

fn field_key(path : String) -> String {
  let parts = path.split(".").map(part => part.to_owned()).collect()
  if parts.length() == 0 {
    path
  } else {
    parts[parts.length() - 1]
  }
}

///| Produces a plain key inventory grouped by section.
pub fn render_inventory(view : ConfigView) -> String {
  let builder = StringBuilder()
  builder.write_string("Configuration Inventory\n")
  builder.write_string("=======================\n\n")
  for item in view.values {
    let path = entry_path(item.section, item.key)
    builder.write_string("- " + path + " (" + item.source_layer + ", " + item.source + ":" + item.span.line.to_string() + ")\n")
  }
  builder.to_string()
}