///|
/// Rendering entries into shell syntax, and syncing them into a startup file
/// inside a marked block.
///
/// Every managed file gets exactly one block:
///
/// ```sh
/// # >>> shrc >>>
/// export EDITOR='vim'
/// alias ll='ls -la'
/// # <<< shrc <<<
/// ```
///
/// Rewriting the block is idempotent, so `shrc set` can run on every change
/// without ever appending a duplicate line.

///|
/// The line that opens a managed block.
pub let block_begin : String = "# >>> shrc >>>"

///|
/// The line that closes a managed block.
pub let block_end : String = "# <<< shrc <<<"

///|
/// Single-quote a value for a POSIX shell (`'` becomes `'\''`).
pub fn quote_posix(value : String) -> String {
  "'" + value.replace_all(old="'", new="'\\''") + "'"
}

///|
/// Single-quote a value for fish. Inside fish single quotes only `\` and `'`
/// are special.
pub fn quote_fish(value : String) -> String {
  let escaped = value
    .replace_all(old="\\", new="\\\\")
    .replace_all(old="'", new="\\'")
  "'" + escaped + "'"
}

///|
/// Single-quote a value for csh/tcsh. csh expands `!` for history even inside
/// single quotes, so it is escaped separately; a `'` is closed and reopened
/// around an escaped one.
pub fn quote_csh(value : String) -> String {
  let bang = value.replace_all(old="!", new="\\!")
  "'" + bang.replace_all(old="'", new="'\\''") + "'"
}

///|
/// Double-quote a value for nushell. nushell's double-quoted strings do not
/// interpolate `$`, so only backslash, the quote itself and the control
/// characters need escaping.
pub fn quote_nu(value : String) -> String {
  let escaped = value
    .replace_all(old="\\", new="\\\\")
    .replace_all(old="\n", new="\\n")
    .replace_all(old="\t", new="\\t")
    .replace_all(old="\r", new="\\r")
    .replace_all(old="\"", new="\\\"")
  "\"" + escaped + "\""
}

///|
/// Single-quote a value for PowerShell. A single quote is doubled.
pub fn quote_ps(value : String) -> String {
  "'" + value.replace_all(old="'", new="''") + "'"
}

///|
/// Double-quote a value for a POSIX shell while leaving `$` and backticks
/// live, so an adopted `PATH` keeps expanding. Only `\` and `"` are escaped.
pub fn quote_posix_live(value : String) -> String {
  let escaped = value
    .replace_all(old="\\", new="\\\\")
    .replace_all(old="\"", new="\\\"")
  "\"" + escaped + "\""
}

///|
/// Double-quote a live value for fish. Interpolation works like a POSIX
/// shell, so the escaping is the same.
pub fn quote_fish_live(value : String) -> String {
  let escaped = value
    .replace_all(old="\\", new="\\\\")
    .replace_all(old="\"", new="\\\"")
  "\"" + escaped + "\""
}

///|
/// Double-quote a live value for csh/tcsh. `!` stays a history escape even
/// inside double quotes, so it is escaped as well.
pub fn quote_csh_live(value : String) -> String {
  let escaped = value
    .replace_all(old="\\", new="\\\\")
    .replace_all(old="\"", new="\\\"")
    .replace_all(old="!", new="\\!")
  "\"" + escaped + "\""
}

///|
/// Quote a live value for nushell: an interpolating `$"..."` string, where
/// `$VAR` expands at load time.
pub fn quote_nu_live(value : String) -> String {
  let escaped = value
    .replace_all(old="\\", new="\\\\")
    .replace_all(old="\"", new="\\\"")
  "$\"" + escaped + "\""
}

///|
/// Double-quote a live value for PowerShell, where `$VAR` expands inside
/// double quotes. The backtick is PowerShell's escape character.
pub fn quote_ps_live(value : String) -> String {
  let escaped = value
    .replace_all(old="`", new="``")
    .replace_all(old="\"", new="`\"")
  "\"" + escaped + "\""
}

///|
/// The literal quoting of a value in the syntax of `family`.
fn quote_literal(family : ShellFamily, value : String) -> String {
  match family {
    Posix => quote_posix(value)
    Fish => quote_fish(value)
    Csh => quote_csh(value)
    Nu => quote_nu(value)
    PowerShell => quote_ps(value)
  }
}

///|
/// The interpolating quoting of a live value in the syntax of `family`.
fn quote_live(family : ShellFamily, value : String) -> String {
  match family {
    Posix => quote_posix_live(value)
    Fish => quote_fish_live(value)
    Csh => quote_csh_live(value)
    Nu => quote_nu_live(value)
    PowerShell => quote_ps_live(value)
  }
}

///|
/// The quoted form of an entry's value in the syntax of `family`: single
/// quotes normally, interpolating quotes when the value stays live shell
/// syntax. Live values are canonical (`$NAME`), so their references are
/// rewritten for the family before quoting. A search path is stored with `:`
/// separators and rendered with the family's own separator.
fn quoted_value(family : ShellFamily, entry : Entry) -> String {
  let text = if entry.list && family_path_sep(family) != path_sep {
    entry.value.replace_all(old=path_sep, new=family_path_sep(family))
  } else {
    entry.value
  }
  if entry.raw {
    quote_live(family, translate_refs(family, text))
  } else {
    quote_literal(family, text)
  }
}

///|
/// Render one entry in the syntax of `family`.
pub fn render_entry(family : ShellFamily, entry : Entry) -> String {
  match family {
    Posix => {
      let value = quoted_value(family, entry)
      match entry.kind {
        Env => "export \{entry.name}=\{value}"
        Alias => "alias \{entry.name}=\{value}"
      }
    }
    Fish => {
      let value = quoted_value(family, entry)
      match entry.kind {
        Env => "set -gx \{entry.name} \{value}"
        Alias => "alias \{entry.name} \{value}"
      }
    }
    Csh => {
      let value = quoted_value(family, entry)
      match entry.kind {
        Env => "setenv \{entry.name} \{value}"
        Alias => "alias \{entry.name} \{value}"
      }
    }
    Nu =>
      match entry.kind {
        Env => "$env.\{entry.name} = \{quoted_value(family, entry)}"
        // nushell refuses `alias NAME = "..."` (an alias cannot expand to an
        // expression), so the value is the pipeline as typed.
        Alias => "alias \{entry.name} = \{entry.value}"
      }
    PowerShell =>
      match entry.kind {
        Env => "$env:\{entry.name} = \{quoted_value(family, entry)}"
        // PowerShell has no alias that carries arguments, so an alias becomes
        // a function whose body is the value as typed.
        Alias => "function \{entry.name} { \{entry.value} }"
      }
  }
}

///|
/// The statement that applies `entry` to the shell that evaluates it.
pub fn render_apply(family : ShellFamily, entry : Entry) -> String {
  render_entry(family, entry)
}

///|
/// A block that defines a `shrc` shell function which applies `set`/`rm`
/// to the current shell automatically. Returns an empty string for families
/// where no integration is provided.
pub fn shell_integration_block(family : ShellFamily) -> String {
  match family {
    Posix =>
      [
        "# >>> shrc shell integration >>>", "shrc() {", "  case \"$1\" in", "    set|rm|remove|unset|delete)",
        "      eval \"$(command shrc \"$@\")\"", "      ;;", "    *)", "      command shrc \"$@\"",
        "      ;;", "  esac", "}", "# <<< shrc shell integration <<<", "",
      ].join("\n")
    Fish =>
      [
        "# >>> shrc shell integration >>>", "function shrc", "  switch $argv[1]",
        "    case set rm remove unset delete", "      eval (command shrc $argv)",
        "    case '*'", "      command shrc $argv", "  end", "end", "# <<< shrc shell integration <<<",
        "",
      ].join("\n")
    _ => ""
  }
}

///|
/// The statement that removes `entry` from the shell that evaluates it.
pub fn render_remove(family : ShellFamily, entry : Entry) -> String {
  match family {
    Posix =>
      match entry.kind {
        Env => "unset \{entry.name}"
        Alias => "unalias \{entry.name} 2>/dev/null || true"
      }
    Fish =>
      match entry.kind {
        Env => "set -e \{entry.name}"
        Alias => "functions -e \{entry.name} 2>/dev/null; true"
      }
    Csh =>
      match entry.kind {
        Env => "unsetenv \{entry.name}"
        Alias => "unalias \{entry.name}"
      }
    Nu =>
      match entry.kind {
        Env => "hide-env \{entry.name}"
        Alias => "hide \{entry.name}"
      }
    PowerShell =>
      match entry.kind {
        Env => "Remove-Item Env:\{entry.name} -ErrorAction SilentlyContinue"
        Alias =>
          "Remove-Item Function:\{entry.name} -ErrorAction SilentlyContinue"
      }
  }
}

///|
/// Split a file into lines, dropping the empty element a trailing newline
/// leaves behind.
fn split_lines(text : String) -> Array[String] {
  let raw = text.split("\n").map(v => v.to_owned()).to_array()
  if raw.length() > 0 && raw[raw.length() - 1] == "" {
    raw.exact_view(end=raw.length() - 1).to_owned()
  } else {
    raw
  }
}

///|
/// The lines of a managed block, markers included.
fn block_lines(family : ShellFamily, entries : Array[Entry]) -> Array[String] {
  let lines = [block_begin]
  for entry in entries {
    lines.push(render_entry(family, entry))
  }
  lines.push(block_end)
  lines
}

///|
/// Return `text` with its managed block replaced by the one for `entries`.
///
/// A file without a block gets one appended; an empty entry list removes the
/// block entirely, so `shrc rm` leaves no trace. Duplicate or unbalanced
/// markers — what a hand edit can leave behind — are collapsed too: the first
/// complete block is rewritten in place and any other marker or block is
/// dropped, so a file can never accumulate two shrc blocks, and a stray
/// opening marker can never swallow the lines around it.
pub fn sync_block(
  text : String,
  family : ShellFamily,
  entries : Array[Entry],
) -> String {
  let lines = split_lines(text)
  let block = block_lines(family, entries)
  let out = []
  let mut replaced = false
  let mut i = 0
  while i < lines.length() {
    let line = lines[i]
    if line == block_begin {
      let mut end = i + 1
      let mut nested = false
      while end < lines.length() && lines[end] != block_end {
        if lines[end] == block_begin {
          nested = true
          break
        }
        end = end + 1
      }
      if nested {
        // A second opening marker before any closing one: this outer marker
        // is stray, and the inner pair is the real block.
        i = i + 1
      } else if end < lines.length() {
        if !replaced && entries.length() > 0 {
          for block_line in block {
            out.push(block_line)
          }
        }
        replaced = true
        i = end + 1
      } else {
        // An opening marker with no closing one: drop the stray marker and
        // keep everything after it.
        i = i + 1
      }
    } else if line == block_end {
      // A closing marker with no opening one.
      i = i + 1
    } else {
      out.push(line)
      i = i + 1
    }
  }
  if !replaced && entries.length() > 0 {
    if out.length() > 0 {
      out.push("")
    }
    for block_line in block {
      out.push(block_line)
    }
  }
  if out.length() == 0 {
    ""
  } else {
    out.join("\n") + "\n"
  }
}