///|
async fn source(path : String) -> Bytes {
  if path == "-" {
    @stdio.stdin.read_all().binary()
  } else {
    @fs.read_file(path).binary()
  }
}

///|
async fn run(args : ArrayView[String]) -> Int {
  let normalized : Array[String] = []
  let mut options = true
  for arg in args {
    if arg == "--" {
      options = false
    }
    if options && arg.has_prefix("--unified=") {
      normalized.push("-U" + arg[10:].to_owned())
    } else if options && arg.has_prefix("--context=") {
      normalized.push("-C" + arg[10:].to_owned())
    } else {
      normalized.push(arg)
    }
  }
  let parsed = @cli.parse(normalized, [
    @cli.flag("brief", short='q'),
    @cli.flag("report-identical-files", short='s'),
    @cli.flag("text", short='a'),
    @cli.flag("unified", short='u'),
    @cli.option("unified-lines", short='U'),
    @cli.flag("context", short='c'),
    @cli.option("context-lines", short='C'),
    @cli.option("label"),
    @cli.flag("help"),
    @cli.flag("version"),
  ])
  if parsed.contains("help") {
    @stdio.stdout.write(
      "Usage: diff [-qsauc] [-U LINES | -C LINES] [--label LABEL] FILE1 FILE2\n      --help      display this help and exit\n      --version   display version and exit\nCompare regular files or one stdin operand. Normal, unified and context output.\n-q reports differences only; -s reports equality; -a treats binary input as text.\nTimestamp headers use TZ=UTC or public TZif files; label stdin explicitly.\nDirectory recursion and whitespace/locale options are unsupported.\n",
    )
    return 0
  }
  if parsed.contains("version") {
    @stdio.stdout.write("diff 0.1.2\n")
    return 0
  }
  guard parsed.operands.length() == 2 else {
    fail("expected two file operands")
  }
  let names = parsed.operands
  let unified_mode = parsed.contains("unified") ||
    parsed.contains("unified-lines")
  let context_mode = parsed.contains("context") ||
    parsed.contains("context-lines")
  if unified_mode && context_mode {
    fail("conflicting output style options")
  }
  let labels = parsed.values("label")
  if labels.length() > 2 {
    fail("too many labels")
  }
  let context = @string.parse_int(
    parsed
    .last_value(if context_mode { "context-lines" } else { "unified-lines" })
    .unwrap_or("3"),
  )
  guard context >= 0 && context <= 1000000 else {
    fail("context must be between 0 and 1000000")
  }
  let a = source(names[0])
  let b = if names[0] == names[1] { a } else { source(names[1]) }
  let display_left = labels.get(0).unwrap_or(names[0])
  let display_right = labels.get(1).unwrap_or(names[1])
  if a == b {
    if parsed.contains("report-identical-files") {
      @stdio.stdout.write(
        "Files \{display_left} and \{display_right} are identical\n",
      )
    }
    return 0
  }
  if parsed.contains("brief") {
    @stdio.stdout.write("Files \{display_left} and \{display_right} differ\n")
    return 1
  }
  if !parsed.contains("text") &&
    (a.iter().any(v => v == b'\x00') || b.iter().any(v => v == b'\x00')) {
    @stdio.stdout.write(
      "Binary files \{display_left} and \{display_right} differ\n",
    )
    return 1
  }
  let changes = edits(lines(a), lines(b))
  if unified_mode || context_mode {
    let left = if labels.length() > 0 {
      labels[0]
    } else {
      header(names[0], context_mode)
    }
    let right = if labels.length() > 1 {
      labels[1]
    } else {
      header(names[1], context_mode)
    }
    @stdio.stdout.write(
      unified(
        changes,
        context,
        @bytere.argument(left),
        @bytere.argument(right),
        context_format=context_mode,
      ),
    )
  } else {
    @stdio.stdout.write(normal(changes))
  }
  1
}

///|
fn padded(value : Int, width : Int) -> String {
  let text = value.to_string()
  "0".repeat(Int::max(0, width - text.length())) + text
}

///|
fn context_date(date : @time.ZonedDateTime) -> String {
  let weekday = match date.weekday() {
    Monday => "Mon"
    Tuesday => "Tue"
    Wednesday => "Wed"
    Thursday => "Thu"
    Friday => "Fri"
    Saturday => "Sat"
    Sunday => "Sun"
  }
  let month = [
      "Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov",
      "Dec",
    ][date.month() - 1]
  weekday +
  " " +
  month +
  " " +
  (if date.day() < 10 { " " } else { "" }) +
  date.day().to_string() +
  " " +
  padded(date.hour(), 2) +
  ":" +
  padded(date.minute(), 2) +
  ":" +
  padded(date.second(), 2) +
  " " +
  date.year().to_string()
}

///|
async fn header(path : String, context : Bool) -> String {
  if path == "-" {
    fail("unified stdin headers require an explicit --label")
  }
  let zone = @time_zone.load()
  let (seconds, nanos) = @fs.mtime(path)
  let date = zone.at_unix(seconds, nanosecond=nanos)
  if context {
    return path + "\t" + context_date(date)
  }
  let offset = date.offset().seconds()
  let magnitude = if offset < 0 { -offset } else { offset }
  path +
  "\t" +
  padded(date.year(), 4) +
  "-" +
  padded(date.month(), 2) +
  "-" +
  padded(date.day(), 2) +
  " " +
  padded(date.hour(), 2) +
  ":" +
  padded(date.minute(), 2) +
  ":" +
  padded(date.second(), 2) +
  "." +
  padded(nanos, 9) +
  " " +
  (if offset < 0 { "-" } else { "+" }) +
  padded(magnitude / 3600, 2) +
  padded(magnitude % 3600 / 60, 2)
}

///|
async fn main {
  let status = run(@env.args()[1:]) catch {
    @cli.CliError(message~, ..) => {
      @stdio.stderr.write("diff: \{message}\n")
      2
    }
    err => {
      @stdio.stderr.write("diff: \{err}\n")
      2
    }
  }
  if status != 0 {
    @sys.exit(status)
  }
}