///|
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
  color_output : 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 {
  let help =
    #|Usage: jq [OPTION]... [FILTER] [FILE...]
    #|      --help      display this help and exit
    #|      --version   display version and exit
    #|  -c, --compact-output    print compact JSON
    #|  -r, --raw-output        print strings without JSON quotes
    #|  -j, --join-output       raw output without result separators
    #|  -n, --null-input        evaluate once with null; do not read inputs
    #|  -s, --slurp             collect input values in an array
    #|  -R, --raw-input         read text lines; -Rs reads all text
    #|  -l, --logs              skip non-JSON lines (project extension)
    #|  -S, --sort-keys         sort object keys recursively
    #|  -e, --exit-status       return 1 for false/null, 4 for no result
    #|  -f, --from-file FILE    read the filter from FILE
    #|      --arg NAME VALUE   bind a string variable
    #|      --argjson NAME JSON bind a JSON variable
    #|      --indent N         indentation -1 (tabs) through 7
    #|      --tab              indent with tabs
    #|  -C, --color-output      emit ANSI colors (JQ_COLORS customizes the palette)
    #|  -M, --monochrome-output disable colors, including when -C is present
  help
}

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

///|
fn compile_query(
  query : String,
  arguments : Map[String, String],
) -> @ast.Expr raise {
  let mut expr = @jq_filter.parse(query)
  for name, value in arguments {
    expr = @ast.Expr::As(@moonjq.parse(value).expr, name, expr)
  }
  expr
}

///|
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 prepare_args(args : ArrayView[String]) -> 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 mut indent = 2
  let mut tab = false
  let mut compact_output = false
  let indent_values = parsed.values("indent")
  let mut indent_index = 0
  for option in parsed.order {
    match option {
      "compact-output" => {
        compact_output = true
        tab = false
      }
      "tab" => {
        compact_output = false
        tab = true
      }
      "indent" => {
        let value = indent_values[indent_index]
        indent_index += 1
        let spaces = @string.parse_int(value) catch {
          _ => return Error("jq: indentation must be between -1 and 7")
        }
        if spaces < -1 || spaces > 7 || value.trim() != value {
          return Error("jq: indentation must be between -1 and 7")
        }
        // jq 1.8.2 uses tabs for -1 and keeps line breaks for zero spaces.
        compact_output = false
        indent = spaces.max(0)
        tab = spaces == -1
      }
      _ => ()
    }
  }
  let arguments : Map[String, String] = Map([])
  let string_args = parsed.values("arg")
  let json_args = parsed.values("argjson")
  let mut string_index = 0
  let mut json_index = 0
  for option in parsed.order {
    if option != "arg" && option != "argjson" {
      continue
    }
    let value = if option == "arg" {
      let value = string_args[string_index]
      string_index += 1
      value
    } else {
      let value = json_args[json_index]
      json_index += 1
      value
    }
    let parts : Array[String] = value
      .split("\u{0}")
      .map(item => item.to_owned())
      .collect()
    if parts.length() != 2 || parts[0] == "" {
      return Error("jq: --\{option} requires NAME and VALUE")
    }
    if option == "arg" {
      arguments[parts[0]] = json_string_literal(parts[1])
    } else {
      ignore(@json.parse(parts[1])) catch {
        _ =>
          return Error(
            "jq: invalid JSON text passed to --argjson\nUse jq --help for help with command-line options,\nor 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 => QuerySource::Inline(positionals.get(0).unwrap_or("."))
    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 {
  match source {
    Inline(text) => text
    File(path) => @fs.read_file(path).text()
  }
}

///|
fn render_result(
  value : Json,
  raw_output : Bool,
  compact_output : Bool,
  sort_keys : Bool,
  indent : Int?,
  tab : Bool,
  colors : Array[String]?,
) -> 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
      _ => render_json(value, !compact_output, spacing, tab, colors)
    }
  } else {
    render_json(value, !compact_output, spacing, tab, colors)
  }
}

///|
async fn write_results(
  results : Iter[Json],
  raw_output : Bool,
  compact_output : Bool,
  join_output : Bool,
  sort_keys : Bool,
  indent : Int?,
  tab : Bool,
  colors : Array[String]?,
) -> Json? {
  let mut last : Json? = None
  for value in results {
    @stdio.stdout.write(
      render_result(
        value, raw_output, compact_output, sort_keys, indent, tab, colors,
      ),
    )
    if !join_output {
      @stdio.stdout.write("\n")
    }
    last = Some(value)
  }
  last
}

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

///|
async fn main {
  let metadata_args = @env.args()[1:]
  if metadata_args.length() == 1 && metadata_args[0] == "--version" {
    @stdio.stdout.write("jq 0.2.2\n")
    return
  }
  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 (palette, valid_colors) = parse_colors(@env.get_env_var("JQ_COLORS"))
      if !valid_colors {
        @stdio.stderr.write("Failed to set $JQ_COLORS\n")
      }
      let query_text = read_query(options.query) catch {
        err => {
          @stdio.stderr.write("moonjq: \{err}\n")
          @sys.exit(2)
          return
        }
      }
      let query = compile_query(query_text, options.arguments) catch {
        err => {
          @stdio.stderr.write("moonjq: \{err}\n")
          @sys.exit(3)
          return
        }
      }
      try {
        let state : Evaluation = {
          query,
          options,
          colors: if options.color_output {
            Some(palette)
          } else {
            None
          },
          last: None,
          values: [],
          raw: [],
        }
        if options.null_input {
          state.emit(Json::null())
        } else {
          for source in options.inputs {
            match source {
              Stdin => state.read(@stdio.stdin)
              File(path) => {
                let file = @fs.open(path, mode=ReadOnly)
                defer file.close()
                state.read(file)
              }
            }
          }
          if options.slurp {
            state.emit(
              if options.raw_input {
                Json::string(@utf8.decode(Bytes::from_array(state.raw)))
              } else {
                Json::array(state.values)
              },
            )
          }
        }
        if options.exit_status {
          @sys.exit(
            status_for_results(
              match state.last {
                None => []
                Some(value) => [value]
              },
            ),
          )
        }
      } catch {
        err => {
          @stdio.stderr.write("moonjq: \{err}\n")
          @sys.exit(
            match err {
              @json.InvalidChar(_, _)
              | @json.InvalidEof
              | @json.InvalidNumber(_, _)
              | @json.InvalidIdentEscape(_)
              | @json.DepthLimitExceeded
              | @utf8.Malformed(_) => 4
              _ => 5
            },
          )
          return
        }
      }
    }
  }
}