///|
suberror CliError {
  Usage(String)
}

///|
fn CliError::message(self : CliError) -> String {
  match self {
    Usage(reason) => reason
  }
}

///|
/// Prints a diagnostic and terminates with a non-zero exit status.
///
/// 打印诊断信息并以非零状态码退出。
fn[T] die(message : String) -> T {
  eprintln("moonqr: \{message}")
  @sys.exit(1)
  abort("unreachable")
}

///|
struct Options {
  payload : String
  ec_level : @lib.EcLevel
  min_version : Int32
  max_version : Int32
  mode : @lib.Mode?
  mask : @lib.Mask?
  quiet_zone : Int32
  scale : Int32
  module_size : Int32
  format : String
  output : String?
  dark : String
  light : String
}

///|
fn parse_ec(value : String) -> @lib.EcLevel raise CliError {
  match value {
    "L" | "l" => @lib.EcLevel::L
    "M" | "m" => @lib.EcLevel::M
    "Q" | "q" => @lib.EcLevel::Q
    "H" | "h" => @lib.EcLevel::H
    _ =>
      raise CliError::Usage(
        "invalid EC level '\{value}'; expected L, M, Q or H",
      )
  }
}

///|
fn parse_mode(value : String) -> @lib.Mode raise CliError {
  match value {
    "numeric" | "Numeric" | "N" => @lib.Mode::Numeric
    "alphanumeric" | "Alphanumeric" | "A" => @lib.Mode::Alphanumeric
    "byte" | "Byte" | "B" => @lib.Mode::Byte
    _ =>
      raise CliError::Usage(
        "invalid mode '\{value}'; expected numeric, alphanumeric or byte",
      )
  }
}

///|
fn parse_mask(value : String) -> @lib.Mask raise CliError {
  match value {
    "0" => @lib.Mask::M0
    "1" => @lib.Mask::M1
    "2" => @lib.Mask::M2
    "3" => @lib.Mask::M3
    "4" => @lib.Mask::M4
    "5" => @lib.Mask::M5
    "6" => @lib.Mask::M6
    "7" => @lib.Mask::M7
    _ => raise CliError::Usage("invalid mask '\{value}'; expected 0..7")
  }
}

///|
fn parse_int(value : String, what : String) -> Int32 raise CliError {
  if value.is_empty() {
    raise CliError::Usage("missing value for \{what}")
  }
  let mut result = 0
  for c in value {
    if c < '0' || c > '9' {
      raise CliError::Usage("invalid integer '\{value}' for \{what}")
    }
    result = result * 10 + (c.to_int() - '0'.to_int())
  }
  result
}

///|
fn take_value(
  argv : Array[String],
  index : Int32,
  flag : String,
) -> (String, Int32) raise CliError {
  if index + 1 >= argv.length() {
    raise CliError::Usage("missing value after \{flag}")
  }
  (argv[index + 1], index + 2)
}

///|
fn print_help() -> Unit {
  println(
    "moonqr - MoonBit QR Code 生成器 / QR Code Generator\n\n用法 / USAGE:\n  moonqr <载荷/payload> [选项/options]\n  moonqr - [选项/options] < 文件\n  moonqr [选项/options] -- <载荷/payload>\n\n选项 / OPTIONS:\n  -l, --level       纠错等级 / error correction level (default M)\n  -v, --version <1..40>      指定版本 / force an explicit version\n      --min-version       最小候选版本 / smallest version considered\n      --max-version       最大候选版本 / largest version considered\n  -m, --mode           numeric | alphanumeric | byte (default auto)\n      --mask <0..7>          指定掩码 / force a mask pattern\n  -q, -qz, --quiet-zone   静默区宽度 / quiet zone width in modules (default 4)\n  -s, --scale             ASCII 缩放倍数 / ASCII scale factor (default 1)\n      --module-size       SVG 模块像素大小 / SVG module size in pixels (default 4)\n  -f, --format         ascii | half | svg | json (default: by output extension)\n      --json                 JSON 元数据与模块矩阵 / JSON metadata and module rows\n  -o, --output         输出到文件 / write to a file instead of stdout\n      --svg                  格式快捷方式 / shorthand for --format svg\n      --half                 格式快捷方式 / shorthand for --format half\n      --dark      SVG 前景色 / SVG foreground color (default #000000)\n      --light     SVG 背景色 / SVG background color (default #ffffff)\n  -h, --help                 显示帮助 / show this help\n  -V, --cli-version          显示版本 / show the CLI version\n\n将载荷写成 - 时从标准输入读取,末尾换行会被去除。\nUse - as the payload to read from stdin; trailing newlines are stripped.\n使用 -- 可编码以 - 开头的载荷。\nUse -- before a payload that starts with -.\n\n示例 / EXAMPLES:\n  moonqr \"HELLO WORLD\" -l H -o hello.svg\n  moonqr \"https://moonbitlang.com\" --format half\n  moonqr \"你好世界\" -l M -o hello.svg\n  moonqr \"0123456789\" -v 2 -m numeric -s 2\n  echo -n \"HELLO\" | moonqr - -l H --svg\n  cat payload.txt | moonqr - -o code.svg\n  moonqr --json \"HELLO\" | jq .version\n  moonqr -- --payload-starting-with-dash",
  )
}

///|
fn parse_args(argv : Array[String]) -> Options raise CliError {
  let mut payload : String? = None
  let mut ec_level = @lib.EcLevel::M
  let mut min_version = 1
  let mut max_version = 40
  let mut mode : @lib.Mode? = None
  let mut mask : @lib.Mask? = None
  let mut quiet_zone = 4
  let mut scale = 1
  let mut module_size = 4
  let mut format = "auto"
  let mut output : String? = None
  let mut dark = "#000000"
  let mut light = "#ffffff"
  let mut i = 1
  let mut options_done = false
  while i < argv.length() {
    let arg = argv[i]
    if !options_done && arg == "--" {
      options_done = true
      i = i + 1
      continue
    }
    if options_done || arg == "-" || !arg.has_prefix("-") {
      match payload {
        None => payload = Some(arg)
        Some(_) => raise CliError::Usage("unexpected extra argument '\{arg}'")
      }
      i = i + 1
      continue
    }
    match arg {
      "-h" | "--help" => {
        print_help()
        @sys.exit(0)
        abort("unreachable")
      }
      "-V" | "--cli-version" => {
        println("moonqr 0.2.0")
        @sys.exit(0)
        abort("unreachable")
      }
      "--json" => {
        format = "json"
        i = i + 1
      }
      "--svg" => {
        format = "svg"
        i = i + 1
      }
      "--half" => {
        format = "half"
        i = i + 1
      }
      "-l" | "--level" => {
        let (value, next) = take_value(argv, i, arg)
        ec_level = parse_ec(value)
        i = next
      }
      "-v" | "--version" => {
        let (value, next) = take_value(argv, i, arg)
        let v = parse_int(value, arg)
        min_version = v
        max_version = v
        i = next
      }
      "--min-version" => {
        let (value, next) = take_value(argv, i, arg)
        min_version = parse_int(value, arg)
        i = next
      }
      "--max-version" => {
        let (value, next) = take_value(argv, i, arg)
        max_version = parse_int(value, arg)
        i = next
      }
      "-m" | "--mode" => {
        let (value, next) = take_value(argv, i, arg)
        mode = Some(parse_mode(value))
        i = next
      }
      "--mask" => {
        let (value, next) = take_value(argv, i, arg)
        mask = Some(parse_mask(value))
        i = next
      }
      "-q" | "-qz" | "--quiet-zone" => {
        let (value, next) = take_value(argv, i, arg)
        quiet_zone = parse_int(value, arg)
        i = next
      }
      "-s" | "--scale" => {
        let (value, next) = take_value(argv, i, arg)
        scale = parse_int(value, arg)
        i = next
      }
      "--module-size" => {
        let (value, next) = take_value(argv, i, arg)
        module_size = parse_int(value, arg)
        i = next
      }
      "-f" | "--format" => {
        let (value, next) = take_value(argv, i, arg)
        match value {
          "ascii" | "half" | "svg" | "json" => format = value
          _ =>
            raise CliError::Usage(
              "invalid format '\{value}'; expected ascii, half, svg or json",
            )
        }
        i = next
      }
      "-o" | "--output" => {
        let (value, next) = take_value(argv, i, arg)
        output = Some(value)
        i = next
      }
      "--dark" => {
        let (value, next) = take_value(argv, i, arg)
        dark = value
        i = next
      }
      "--light" => {
        let (value, next) = take_value(argv, i, arg)
        light = value
        i = next
      }
      _ => raise CliError::Usage("unknown option '\{arg}' (try --help)")
    }
  }
  let payload = match payload {
    Some("-") => read_stdin_text()
    Some(text) => text
    None => raise CliError::Usage("missing payload (try --help)")
  }
  if min_version < 1 || min_version > 40 {
    raise CliError::Usage("min version must be within 1..40")
  }
  if max_version < 1 || max_version > 40 {
    raise CliError::Usage("max version must be within 1..40")
  }
  if min_version > max_version {
    raise CliError::Usage("min version must not exceed max version")
  }
  if quiet_zone < 0 {
    raise CliError::Usage("quiet zone must be non-negative")
  }
  if scale < 1 {
    raise CliError::Usage("scale must be at least 1")
  }
  if module_size < 1 {
    raise CliError::Usage("module size must be at least 1")
  }
  {
    payload,
    ec_level,
    min_version,
    max_version,
    mode,
    mask,
    quiet_zone,
    scale,
    module_size,
    format,
    output,
    dark,
    light,
  }
}

///|
fn encode_qr(options : Options) -> @lib.QrCode {
  try
    @lib.encode(
      options.payload,
      ec_level=options.ec_level,
      min_version=options.min_version,
      max_version=options.max_version,
      mode?=options.mode,
      mask?=options.mask,
    )
  catch {
    e => die(e.message())
  } noraise {
    qr => qr
  }
}

///|
fn inferred_format(options : Options) -> String {
  if options.format != "auto" {
    return options.format
  }
  match options.output {
    Some(path) => {
      let lower = path.to_lower()
      if lower.has_suffix(".txt") {
        "ascii"
      } else if lower.has_suffix(".json") {
        "json"
      } else {
        "svg"
      }
    }
    None => "ascii"
  }
}

///|
fn mask_number(mask : @lib.Mask) -> Int32 {
  match mask {
    @lib.Mask::M0 => 0
    @lib.Mask::M1 => 1
    @lib.Mask::M2 => 2
    @lib.Mask::M3 => 3
    @lib.Mask::M4 => 4
    @lib.Mask::M5 => 5
    @lib.Mask::M6 => 6
    @lib.Mask::M7 => 7
  }
}

///|
/// JSON metadata plus row-major module rows (`1` dark, `0` light), without
/// the quiet zone.
///
/// JSON 元数据与行优先模块行(`1` 深色,`0` 浅色),不含静默区。
fn render_json(qr : @lib.QrCode) -> String {
  let sb = StringBuilder()
  sb <+
    "{\"version\":\{qr.version().value()},\"ecLevel\":\"\{qr.ec_level()}\",\"mask\":\{mask_number(qr.mask())},\"size\":\{qr.size()},\"modules\":["
  for y in 0.. 0 {
      sb <+ ","
    }
    sb <+ "\""
    for x in 0.. String {
  let format = inferred_format(options)
  match format {
    "json" => render_json(qr)
    "svg" =>
      try
        @lib.to_svg(
          qr,
          quiet_zone=options.quiet_zone,
          module_size=options.module_size,
          dark=options.dark,
          light=options.light,
        )
      catch {
        e => die(e.message())
      } noraise {
        text => text
      }
    "half" => @lib.to_ascii_half(qr, quiet_zone=options.quiet_zone)
    _ => @lib.to_ascii(qr, quiet_zone=options.quiet_zone, scale=options.scale)
  }
}

///|
fn write_output(path : String, text : String) -> Unit {
  try @fs.write_string_to_file(path, text) catch {
    _ => die("failed to write output file '\{path}'")
  } noraise {
    _ => ()
  }
}

///|
fn main {
  let argv = @env.args()
  let options = try parse_args(argv) catch {
    e => die(e.message())
  } noraise {
    options => options
  }
  let qr = encode_qr(options)
  let text = render(qr, options)
  match options.output {
    Some(path) => write_output(path, text)
    None => println(text.trim(chars="\n"))
  }
}