// mbt-subtitle 命令行入口。

///|
const VERSION : String = "0.1.0"

///|
fn usage() -> String {
  let s = StringBuilder()
  s.write_string("mbt-subtitle \{VERSION} — SRT / WebVTT 字幕工具\n\n")
  s.write_string("用法:\n")
  s.write_string("  mbt-subtitle check <文件> [--json]\n")
  s.write_string(
    "  mbt-subtitle shift <文件> --by <1.5s|500ms|00:00:01.500> [--output <文件>]\n",
  )
  s.write_string(
    "  mbt-subtitle scale <文件> --percent <整数> [--output <文件>]\n",
  )
  s.write_string(
    "  mbt-subtitle fix <文件> [--gap 100ms] [--output <文件>]\n",
  )
  s.write_string(
    "  mbt-subtitle convert <文件> --to  [--output <文件>]\n\n",
  )
  s.write_string("选项:\n")
  s.write_string("  -v, --version  显示版本\n")
  s.write_string("  -h, --help     显示本帮助\n\n")
  s.write_string(
    "退出码:\n  0  成功\n  1  check 发现问题\n  2  用法或读写错误\n",
  )
  s.to_string()
}

///|
struct Options {
  mut command : String?
  mut file : String?
  mut by : String?
  mut percent : Int?
  mut gap : String?
  mut to : String?
  mut output : String?
  mut json : Bool
  mut help : Bool
  mut version : Bool
}

///|
fn Options::new() -> Options {
  {
    command: None,
    file: None,
    by: None,
    percent: None,
    gap: None,
    to: None,
    output: None,
    json: false,
    help: false,
    version: false,
  }
}

///|
fn output(text : String) -> Unit {
  let chars = text.iter().to_array()
  let mut n = chars.length()
  while n > 0 && (chars[n - 1] == '\n' || chars[n - 1] == '\r') {
    n = n - 1
  }
  if n == 0 {
    return
  }
  let buffer = StringBuilder()
  for i = 0; i < n; i = i + 1 {
    buffer.write_char(chars[i])
  }
  println(buffer.to_string())
}

///|
fn usage_error(message : String) -> Unit {
  println("错误: \{message}")
  println("")
  output(usage())
  @sys.exit(2)
}

///|
fn next_arg(args : Array[String], i : Int, name : String) -> String {
  if i + 1 >= args.length() {
    usage_error("\{name} 需要一个参数")
  }
  args[i + 1]
}

///|
fn parse_uint(raw : String) -> Int? {
  if raw.is_empty() {
    return None
  }
  let mut value = 0
  for c in raw {
    if !c.is_ascii_digit() {
      return None
    }
    value = value * 10 + (c.to_int() - 48)
  }
  Some(value)
}

///|
fn parse_args(args : Array[String]) -> Options {
  let options = Options::new()
  let positional : Array[String] = []
  let mut i = 1
  while i < args.length() {
    let arg = args[i]
    match arg {
      "--json" => options.json = true
      "--by" => {
        options.by = Some(next_arg(args, i, "--by"))
        i = i + 1
      }
      "--percent" => {
        options.percent = parse_uint(next_arg(args, i, "--percent"))
        i = i + 1
      }
      "--gap" => {
        options.gap = Some(next_arg(args, i, "--gap"))
        i = i + 1
      }
      "--to" => {
        options.to = Some(next_arg(args, i, "--to"))
        i = i + 1
      }
      "--output" => {
        options.output = Some(next_arg(args, i, "--output"))
        i = i + 1
      }
      "--help" | "-h" => options.help = true
      "--version" | "-v" => options.version = true
      _ =>
        if arg.has_prefix("-") {
          usage_error("未知选项「\{arg}」")
        } else {
          positional.push(arg)
        }
    }
    i = i + 1
  }
  if positional.length() > 0 {
    options.command = Some(positional[0])
  }
  if positional.length() > 1 {
    options.file = Some(positional[1])
  }
  if positional.length() > 2 {
    usage_error("参数过多")
  }
  options
}

///|
fn ascii_lower(s : String) -> String {
  let buffer = StringBuilder()
  for c in s {
    buffer.write_char(c.to_ascii_lowercase())
  }
  buffer.to_string()
}

///|
fn format_of_path(path : String) -> @lib.SubtitleFormat {
  if ascii_lower(path).has_suffix(".vtt") {
    @lib.Vtt
  } else {
    @lib.Srt
  }
}

///|
fn format_of_name(name : String) -> @lib.SubtitleFormat? {
  match ascii_lower(name) {
    "srt" => Some(@lib.Srt)
    "vtt" => Some(@lib.Vtt)
    _ => None
  }
}

///|
fn read_text(path : String) -> Result[String, String] {
  Ok(@fs.read_file_to_string(path)) catch {
    _ => Err("读取文件失败")
  }
}

///|
fn load_subtitle(path : String) -> Result[@lib.Subtitle, String] {
  match read_text(path) {
    Err(message) => Err(message)
    Ok(text) => @lib.parse_subtitle(text, format_of_path(path))
  }
}

///|
fn write_result(path : String, content : String) -> Unit {
  @fs.write_string_to_file(path, content) catch {
    _ => {
      println("无法写入「\{path}」")
      @sys.exit(2)
    }
  }
}

///|
fn emit_result(options : Options, content : String) -> Unit {
  match options.output {
    Some(path) => write_result(path, content)
    None => output(content)
  }
}

///|
fn split_char(text : String, separator : Char) -> Array[String] {
  let parts : Array[String] = []
  let buffer = StringBuilder()
  for c in text {
    if c == separator {
      parts.push(buffer.to_string())
      buffer.reset()
    } else {
      buffer.write_char(c)
    }
  }
  parts.push(buffer.to_string())
  parts
}

///|
fn chars_from(text : String, start : Int) -> String {
  let chars = text.iter().to_array()
  let buffer = StringBuilder()
  let mut i = start
  while i < chars.length() {
    buffer.write_char(chars[i])
    i = i + 1
  }
  buffer.to_string()
}

///|
fn parse_seconds(raw : String) -> Int? {
  let parts = split_char(raw, '.')
  let whole = match parse_uint(parts[0]) {
    Some(value) => value
    None => return None
  }
  let millis = if parts.length() > 1 {
    let fraction = parts[1]
    if fraction.length() > 3 {
      return None
    }
    let padded = if fraction.length() == 1 {
      fraction + "00"
    } else if fraction.length() == 2 {
      fraction + "0"
    } else {
      fraction
    }
    match parse_uint(padded) {
      Some(value) => value
      None => return None
    }
  } else {
    0
  }
  Some(whole * 1000 + millis)
}

///|
fn parse_duration(raw : String) -> Int? {
  let negative = raw.has_prefix("-")
  let body = if negative { chars_from(raw, 1) } else { raw }
  let millis = if body.has_suffix("ms") {
    let chars = body.iter().to_array()
    parse_uint(chars_to_string(chars, 0, chars.length() - 2))
  } else if body.has_suffix("s") {
    let chars = body.iter().to_array()
    parse_seconds(chars_to_string(chars, 0, chars.length() - 1))
  } else {
    @lib.parse_timecode(body)
  }
  match millis {
    None => None
    Some(value) => if negative { Some(-value) } else { Some(value) }
  }
}

///|
fn chars_to_string(chars : Array[Char], start : Int, end : Int) -> String {
  let buffer = StringBuilder()
  let mut i = start
  while i < end && i < chars.length() {
    buffer.write_char(chars[i])
    i = i + 1
  }
  buffer.to_string()
}

///|
fn json_escape(s : String) -> String {
  let builder = StringBuilder()
  for c in s {
    match c {
      '"' => builder.write_string("\\\"")
      '\\' => builder.write_string("\\\\")
      '\n' => builder.write_string("\\n")
      '\r' => builder.write_string("\\r")
      '\t' => builder.write_string("\\t")
      _ => builder.write_char(c)
    }
  }
  builder.to_string()
}

///|
fn report_json(
  subtitle : @lib.Subtitle,
  issues : Array[@lib.Issue],
) -> String {
  let builder = StringBuilder()
  builder.write_string("{\"cues\":")
  builder.write_string(subtitle.cues.length().to_string())
  builder.write_string(",\"issues\":[")
  let mut first = true
  for issue in issues {
    if !first {
      builder.write_string(",")
    }
    builder.write_string("{\"kind\":\"")
    builder.write_string(@lib.issue_kind_name(issue.kind))
    builder.write_string("\",\"cue\":")
    builder.write_string(issue.cue_index.to_string())
    builder.write_string(",\"message\":\"")
    builder.write_string(json_escape(issue.message))
    builder.write_string("\"}")
    first = false
  }
  builder.write_string("]}")
  builder.to_string()
}

///|
fn main {
  let args = @env.args()
  let options = parse_args(args)
  if options.help {
    output(usage())
    @sys.exit(0)
    return
  }
  if options.version {
    println("mbt-subtitle \{VERSION}")
    @sys.exit(0)
    return
  }
  let command = match options.command {
    Some(value) => value
    None => {
      usage_error("缺少命令")
      ""
    }
  }
  let path = match options.file {
    Some(value) => value
    None => {
      usage_error("缺少文件参数")
      ""
    }
  }
  let subtitle = match load_subtitle(path) {
    Ok(value) => value
    Err(message) => {
      println("无法解析「\{path}」: \{message}")
      @sys.exit(2)
      @lib.empty_subtitle()
    }
  }
  match command {
    "check" => {
      let issues = @lib.validate(subtitle, @lib.ValidateOptions::new())
      if options.json {
        let json = report_json(subtitle, issues)
        match options.output {
          Some(path) => write_result(path, json)
          None => output(json)
        }
      } else if issues.length() == 0 {
        println("check: \{subtitle.cues.length()} 条字幕,未发现问题")
      } else {
        println(
          "check: \{subtitle.cues.length()} 条字幕,发现 \{issues.length()} 个问题",
        )
        for issue in issues {
          println(
            "  cue \{issue.cue_index}: \{@lib.issue_kind_name(issue.kind)} — \{issue.message}",
          )
        }
      }
      if issues.length() > 0 {
        @sys.exit(1)
      } else {
        @sys.exit(0)
      }
    }
    "shift" => {
      let by = match options.by {
        Some(value) => value
        None => {
          usage_error("shift 需要 --by 参数")
          ""
        }
      }
      let delta = match parse_duration(by) {
        Some(value) => value
        None => {
          usage_error("无法解析 --by「\{by}」")
          0
        }
      }
      let moved = @lib.shift(subtitle, delta)
      emit_result(options, @lib.serialize_subtitle(moved, format_of_path(path)))
      @sys.exit(0)
    }
    "scale" => {
      let percent = match options.percent {
        Some(value) => value
        None => {
          usage_error("scale 需要 --percent 参数")
          0
        }
      }
      let scaled = @lib.scale(subtitle, percent)
      emit_result(
        options,
        @lib.serialize_subtitle(scaled, format_of_path(path)),
      )
      @sys.exit(0)
    }
    "fix" => {
      let gap = match options.gap {
        Some(value) =>
          match parse_duration(value) {
            Some(ms) => ms
            None => {
              usage_error("无法解析 --gap「\{value}」")
              100
            }
          }
        None => 100
      }
      let fixed = @lib.resolve_overlaps(subtitle, gap)
      emit_result(options, @lib.serialize_subtitle(fixed, format_of_path(path)))
      @sys.exit(0)
    }
    "convert" => {
      let target_name = match options.to {
        Some(value) => value
        None => {
          usage_error("convert 需要 --to 参数")
          ""
        }
      }
      let target = match format_of_name(target_name) {
        Some(value) => value
        None => {
          usage_error("--to 只支持 srt / vtt")
          @lib.Srt
        }
      }
      emit_result(options, @lib.serialize_subtitle(subtitle, target))
      @sys.exit(0)
    }
    _ => usage_error("未知命令「\{command}」")
  }
}