///| A variable placeholder found in a config value, such as ${DATABASE_URL}.
pub struct Placeholder {
  path : String
  name : String
  raw : String
} derive(@debug.Debug, Eq)

///| A resolved placeholder value supplied by a caller.
pub struct PlaceholderValue {
  name : String
  value : String
} derive(@debug.Debug, Eq)

pub fn placeholder_value(name : String, value : String) -> PlaceholderValue {
  { name, value }
}

///| Finds ${NAME} placeholders in all effective values.
pub fn find_placeholders(view : ConfigView) -> Array[Placeholder] {
  let placeholders : Array[Placeholder] = []
  for item in view.values {
    let path = entry_path(item.section, item.key)
    append_placeholders(placeholders, path, item.value)
  }
  placeholders
}

fn append_placeholders(placeholders : Array[Placeholder], path : String, value : String) -> Unit {
  let mut index = 0
  while index < value.length() {
    if index + 2 <= value.length() && value[index:index + 2].to_owned() == "${" {
      let end = find_placeholder_end(value, index + 2)
      match end {
        None => index = index + 2
        Some(end_index) => {
          let raw = value[index:end_index + 1].to_owned()
          let name = value[index + 2:end_index].to_owned()
          placeholders.push({ path, name, raw })
          index = end_index + 1
        }
      }
    } else {
      index = index + 1
    }
  }
}

fn find_placeholder_end(value : String, start : Int) -> Int? {
  let mut index = start
  while index < value.length() {
    if value[index] == '}' {
      return Some(index)
    }
    index = index + 1
  }
  None
}

///| Reports placeholders without supplied values.
pub fn unresolved_placeholder_diagnostics(view : ConfigView, supplied : Array[PlaceholderValue]) -> Array[Diagnostic] {
  let diagnostics : Array[Diagnostic] = []
  for placeholder in find_placeholders(view) {
    if !has_placeholder_value(supplied, placeholder.name) {
      diagnostics.push(warning("unresolved-placeholder", "key '" + placeholder.path + "' references unresolved placeholder '" + placeholder.raw + "'"))
    }
  }
  diagnostics
}

fn has_placeholder_value(values : Array[PlaceholderValue], name : String) -> Bool {
  for item in values {
    if item.name == name {
      return true
    }
  }
  false
}

///| Replaces placeholders in a value.
pub fn expand_placeholders(value : String, supplied : Array[PlaceholderValue]) -> String {
  let builder = StringBuilder()
  let mut index = 0
  while index < value.length() {
    if index + 2 <= value.length() && value[index:index + 2].to_owned() == "${" {
      let end = find_placeholder_end(value, index + 2)
      match end {
        None => {
          builder.write_char(value[index].to_int().unsafe_to_char())
          index = index + 1
        }
        Some(end_index) => {
          let name = value[index + 2:end_index].to_owned()
          match lookup_placeholder_value(supplied, name) {
            None => builder.write_string(value[index:end_index + 1].to_owned())
            Some(replacement) => builder.write_string(replacement)
          }
          index = end_index + 1
        }
      }
    } else {
      builder.write_char(value[index].to_int().unsafe_to_char())
      index = index + 1
    }
  }
  builder.to_string()
}

fn lookup_placeholder_value(values : Array[PlaceholderValue], name : String) -> String? {
  for item in values {
    if item.name == name {
      return Some(item.value)
    }
  }
  None
}

///| Escapes a properties key.
pub fn escape_properties_key(key : String) -> String {
  let builder = StringBuilder()
  let mut index = 0
  while index < key.length() {
    let ch = key[index]
    if ch == ' ' || ch == ':' || ch == '=' || ch == '#' || ch == '!' || ch == '\\' {
      builder.write_char('\\')
    }
    builder.write_char(ch.to_int().unsafe_to_char())
    index = index + 1
  }
  builder.to_string()
}

///| Unescapes common properties escape sequences.
pub fn unescape_properties_value(value : String) -> String {
  let builder = StringBuilder()
  let mut index = 0
  while index < value.length() {
    if value[index] == '\\' && index + 1 < value.length() {
      let next = value[index + 1]
      if next == 'n' {
        builder.write_char('\n')
      } else if next == 't' {
        builder.write_char('\t')
      } else if next == 'r' {
        builder.write_char('\r')
      } else {
        builder.write_char(next.to_int().unsafe_to_char())
      }
      index = index + 2
    } else {
      builder.write_char(value[index].to_int().unsafe_to_char())
      index = index + 1
    }
  }
  builder.to_string()
}

///| Produces a placeholder inventory table.
pub fn render_placeholder_report(view : ConfigView, supplied : Array[PlaceholderValue]) -> String {
  let builder = StringBuilder()
  builder.write_string("| Path | Placeholder | Resolved |\n")
  builder.write_string("| --- | --- | --- |\n")
  for placeholder in find_placeholders(view) {
    builder.write_string("| " + placeholder.path + " | " + placeholder.raw + " | " + yes_no_placeholder(has_placeholder_value(supplied, placeholder.name)) + " |\n")
  }
  builder.to_string()
}

fn yes_no_placeholder(value : Bool) -> String {
  if value { "yes" } else { "no" }
}

///| Renders properties with placeholders expanded.
pub fn render_expanded_properties(view : ConfigView, supplied : Array[PlaceholderValue]) -> String {
  let builder = StringBuilder()
  for item in view.values {
    let path = entry_path(item.section, item.key)
    builder.write_string(escape_properties_key(path) + "=" + escape_properties_value(expand_placeholders(item.value, supplied)) + "\n")
  }
  builder.to_string()
}