///|
priv enum InputSource {
  Stdin
  File(String)
}

///|
priv enum QuerySource {
  Inline(String)
  File(String)
}

///|
priv struct CliOptions {
  query : QuerySource
  inputs : Array[InputSource]
  logs : Bool
  raw_output : Bool
  compact_output : Bool
  null_input : Bool
  sort_keys : Bool
  join_output : Bool
  slurp : Bool
  raw_input : Bool
  exit_status : Bool
  indent : Int?
  tab : Bool
  arguments : Map[String, String]
}

///|
priv enum CliParseResult {
  Help
  Error(String)
  Run(CliOptions)
}

///|
priv suberror ArgError {
  ArgError(String)
}

///|
fn help_message() -> String {
  "Usage: jq [-CjcnrRsM] [-f FILTER_FILE] [--arg NAME VALUE] [--argjson NAME JSON] [FILTER] [FILE...]"
}

///|
fn json_string_literal(value : String) -> String {
  Json::string(value).stringify()
}

///|
fn substitute_arguments(
  query : String,
  arguments : Map[String, String],
) -> String {
  if arguments.is_empty() {
    return query
  }
  let chars : Array[Char] = query.iter().collect()
  let output = StringBuilder()
  let mut index = 0
  let mut quoted = false
  while index < chars.length() {
    let char = chars[index]
    if char == '"' && (index == 0 || chars[index - 1] != '\\') {
      quoted = !quoted
      output.write_char(char)
      index += 1
      continue
    }
    if !quoted && char == '$' && index + 1 < chars.length() {
      let mut end = index + 1
      while end < chars.length() &&
            chars[end] is ('a'..='z' | 'A'..='Z' | '_' | '0'..='9') {
        end += 1
      }
      if end > index + 1 {
        let name = String::from_array(chars[index + 1:end])
        if arguments.get(name) is Some(value) {
          output.write_string(value)
          index = end
          continue
        }
      }
    }
    output.write_char(char)
    index += 1
  }
  output.to_string()
}

///|
fn sort_json(value : Json) -> Json {
  match value {
    Object(fields) => {
      let entries = fields.to_array()
      entries.sort_by((left, right) => left.0.lexical_compare(right.0))
      let sorted : Map[String, Json] = Map([])
      for entry in entries {
        let key = entry.0
        sorted[key] = sort_json(entry.1)
      }
      Json::Json(sorted)
    }
    Array(items) => Json::Json(items.map(sort_json))
    other => other
  }
}

///|
fn indent_json(value : Json, indent : Int, tab : Bool) -> String {
  let rendered = value.stringify(indent~)
  if !tab || indent == 0 {
    return rendered
  }
  let output = StringBuilder()
  for line in rendered.split("\n") {
    let mut index = 0
    while index < line.length() && line[index] == ' ' {
      index += 1
    }
    let tabs = index / indent
    for _ in 0.. Array[String] raise ArgError {
  let prepared : Array[String] = []
  let mut index = 0
  while index < args.length() {
    let arg = args[index]
    if arg == "--arg" || arg == "--argjson" {
      if index + 2 >= args.length() {
        raise ArgError("{arg} requires NAME and VALUE")
      }
      prepared.push(arg + "=" + args[index + 1] + "\u{0}" + args[index + 2])
      index += 3
    } else {
      prepared.push(arg)
      index += 1
    }
  }
  prepared
}

///|
fn parse_cli(args : ArrayView[String]) -> CliParseResult {
  let normalized = prepare_args(args) catch {
    ArgError(message) =>
      return Error("jq: argument preparation failed: \{message}")
  }
  let parsed = @cli.parse(normalized, [
    @cli.flag("compact-output", short='c'),
    @cli.flag("join-output", short='j'),
    @cli.flag("logs", short='l'),
    @cli.flag("null-input", short='n'),
    @cli.flag("slurp", short='s'),
    @cli.flag("raw-output", short='r'),
    @cli.flag("raw-input", short='R'),
    @cli.flag("sort-keys", short='S'),
    @cli.flag("exit-status", short='e'),
    @cli.flag("color-output", short='C'),
    @cli.flag("monochrome-output", short='M'),
    @cli.option("from-file", short='f'),
    @cli.option("indent"),
    @cli.flag("tab"),
    @cli.option("arg"),
    @cli.option("argjson"),
    @cli.flag("help"),
  ]) catch {
    @cli.CliError(option~, message~, ..) =>
      return Error("jq: \{message}: '\{option}'\n\{help_message()}")
  }
  if parsed.contains("help") {
    return Help
  }
  if parsed.contains("logs") && parsed.contains("null-input") {
    return Error(
      "jq: logs and null-input are mutually exclusive\n\{help_message()}",
    )
  }
  if parsed.contains("logs") && parsed.contains("raw-input") {
    return Error(
      "jq: logs and raw-input are mutually exclusive\n{help_message()}",
    )
  }
  let indent = match parsed.last_value("indent") {
    Some(value) => {
      let parsed_indent = @string.parse_int(value) catch {
        _ => return Error("jq: invalid indentation: '{value}'")
      }
      if parsed_indent < 0 {
        Some(2)
      } else if parsed_indent > 64 {
        return Error("jq: indentation must be between 0 and 64")
      } else {
        Some(parsed_indent)
      }
    }
    None => None
  }
  let arguments : Map[String, String] = Map([])
  for value in parsed.values("arg") {
    let parts : Array[String] = value
      .split("\u{0}")
      .map(item => item.to_owned())
      .collect()
    if parts.length() != 2 || parts[0] == "" {
      return Error("jq: --arg requires NAME and VALUE")
    }
    arguments[parts[0]] = json_string_literal(parts[1])
  }
  for value in parsed.values("argjson") {
    let parts : Array[String] = value
      .split("\u{0}")
      .map(item => item.to_owned())
      .collect()
    if parts.length() != 2 || parts[0] == "" {
      return Error("jq: --argjson requires NAME and JSON")
    }
    ignore(@json.parse(parts[1])) catch {
      _ =>
        return Error(
          "jq: invalid JSON text passed to --argjson\n" +
          "Use jq --help for help with command-line options,\n" +
          "or see the jq manpage, or online docs at https://jqlang.org",
        )
    }
    arguments[parts[0]] = parts[1]
  }
  let positionals = parsed.operands
  let filter_files = parsed.values("from-file")
  let query = match filter_files.length() {
    0 => {
      if positionals.is_empty() {
        return Error("error: missing filter\n\n" + help_message())
      }
      QuerySource::Inline(positionals[0])
    }
    1 => File(filter_files[0])
    _ => return Error("error: multiple filters provided\n\n" + help_message())
  }
  let input_start = if filter_files.is_empty() { 1 } else { 0 }
  let inputs : Array[InputSource] = []
  for index in input_start.. String raise {
  match source {
    Inline(text) => text
    File(path) => @fs.read_file_to_string(path)
  }
}

///|
async fn read_input(source : InputSource) -> String {
  match source {
    Stdin => @stdio.stdin.read_all().text()
    File(path) => @fs.read_file_to_string(path)
  }
}

///|
fn run_query(
  query : @moonjq.Query,
  input : String,
  logs : Bool,
) -> Iter[Json] raise {
  if logs {
    query.eval_logs(input)
  } else {
    let json = @json.parse(input)
    query.eval(json)
  }
}

///|
fn raw_values(input : String) -> Array[Json] {
  let values : Array[Json] = []
  let lines : Array[String] = input
    .split("\n")
    .map(line => line.to_owned())
    .collect()
  for index, line in lines {
    if index + 1 == lines.length() && line == "" {
      continue
    }
    values.push(Json::string(line))
  }
  values
}

///|
fn render_result(
  value : Json,
  raw_output : Bool,
  compact_output : Bool,
  sort_keys : Bool,
  indent : Int?,
  tab : Bool,
) -> String {
  let value = if sort_keys { sort_json(value) } else { value }
  let spacing = match indent {
    Some(value) => value
    None => if compact_output { 0 } else { 2 }
  }
  if raw_output {
    match value {
      String(text) => text
      _ => indent_json(value, spacing, tab)
    }
  } else {
    indent_json(value, spacing, tab)
  }
}

///|
async fn write_results(
  results : Iter[Json],
  raw_output : Bool,
  compact_output : Bool,
  join_output : Bool,
  sort_keys : Bool,
  indent : Int?,
  tab : Bool,
) -> Unit {
  let rendered : Array[String] = []
  for value in results {
    rendered.push(
      render_result(value, raw_output, compact_output, sort_keys, indent, tab),
    )
  }
  for index, value in rendered {
    @stdio.stdout.write(value)
    if !join_output || index + 1 < rendered.length() {
      @stdio.stdout.write(if join_output { "" } else { "\n" })
    }
  }
}

///|
fn status_for_results(values : Array[Json]) -> Int {
  match values.last() {
    None => 4
    Some(False | Null) => 1
    Some(_) => 0
  }
}

///|
async fn main {
  let args = @env.args()[1:]
  match parse_cli(args) {
    Help => {
      @stdio.stdout.write(help_message() + "\n")
      return
    }
    Error(msg) => {
      @stdio.stderr.write(msg)
      if !msg.has_suffix("\n") {
        @stdio.stderr.write("\n")
      }
      @sys.exit(2)
      return
    }
    Run(options) => {
      let query_text = read_query(options.query) catch {
        err => {
          @stdio.stderr.write("moonjq: \{err}\n")
          @sys.exit(2)
          return
        }
      }
      let query = @moonjq.parse(
        substitute_arguments(query_text, options.arguments),
      ) catch {
        err => {
          @stdio.stderr.write("moonjq: \{err}\n")
          @sys.exit(3)
          return
        }
      }
      try {
        if options.null_input {
          let results = query.eval(null).collect()
          write_results(
            results.iter(),
            options.raw_output,
            options.compact_output,
            options.join_output,
            options.sort_keys,
            options.indent,
            options.tab,
          )
          if options.exit_status {
            @sys.exit(status_for_results(results))
          }
        } else if options.slurp {
          let input_value = if options.raw_input {
            let all = StringBuilder()
            for input_source in options.inputs {
              all.write_string(read_input(input_source))
            }
            Json::Json(all.to_string())
          } else {
            let all : Array[Json] = []
            for input_source in options.inputs {
              let input = read_input(input_source)
              if options.logs {
                for value in query.eval_logs(input) {
                  all.push(value)
                }
              } else {
                all.push(@json.parse(input))
              }
            }
            Json::array(all)
          }
          let results = query.eval(input_value).collect()
          write_results(
            results.iter(),
            options.raw_output,
            options.compact_output,
            options.join_output,
            options.sort_keys,
            options.indent,
            options.tab,
          )
          if options.exit_status {
            @sys.exit(status_for_results(results))
          }
        } else if options.raw_input {
          let results : Array[Json] = []
          for input_source in options.inputs {
            let input = read_input(input_source)
            for value in raw_values(input) {
              for result in query.eval(value) {
                results.push(result)
              }
            }
          }
          write_results(
            results.iter(),
            options.raw_output,
            options.compact_output,
            options.join_output,
            options.sort_keys,
            options.indent,
            options.tab,
          )
          if options.exit_status {
            @sys.exit(status_for_results(results))
          }
        } else {
          let results : Array[Json] = []
          for input_source in options.inputs {
            let input = read_input(input_source)
            for result in run_query(query, input, options.logs) {
              results.push(result)
            }
          }
          write_results(
            results.iter(),
            options.raw_output,
            options.compact_output,
            options.join_output,
            options.sort_keys,
            options.indent,
            options.tab,
          )
          if options.exit_status {
            @sys.exit(status_for_results(results))
          }
        }
      } catch {
        err => {
          @stdio.stderr.write("moonjq: \{err}\n")
          @sys.exit(5)
          return
        }
      }
    }
  }
}