///| Helper functions for git-bit native CLI

///|
fn bytes_from_array(arr : Array[Byte]) -> Bytes {
  Bytes::from_array(FixedArray::makei(arr.length(), i => arr[i]))
}

///|
fn str_to_bytes(s : String) -> Bytes {
  @utf8.encode(s)
}

///|
fn decode_bytes(data : Bytes) -> String {
  @string_utils.decode_bytes(data)
}

///|
fn trim_string(s : String) -> String {
  @string_utils.trim_string(s)
}

///|
async fn warn_unimplemented_arg(cmd : String, arg : String) -> Unit {
  if arg == "--" {
    return
  }
  let msg = "warning: " + cmd + ": unsupported option '" + arg + "'\n"
  @stdio.stderr.write(str_to_bytes(msg))
}

///|
async fn print_line(s : String) -> Unit {
  @stdio.stdout.write(str_to_bytes(s + "\n"))
}

///|
async fn eprint_line(s : String) -> Unit {
  @stdio.stderr.write(str_to_bytes(s + "\n"))
}

///|
async fn read_all_stdin() -> Bytes {
  let out : Array[Byte] = []
  while true {
    match @stdio.stdin.read_some(max_len=8192) {
      None => break
      Some(bytes) =>
        for b in bytes {
          out.push(b)
        }
    }
  }
  bytes_from_array(out)
}

///|
fn get_effective_remote_url(fs : OsFs, git_dir : String) -> String? {
  @bitlib.get_effective_remote_url(fs, git_dir)
}

///|
fn get_author_string() -> String {
  match @env.get_env_var("GIT_AUTHOR_NAME") {
    Some(name) => {
      let email = @env.get_env_var("GIT_AUTHOR_EMAIL").unwrap_or("unknown")
      name + " <" + email + ">"
    }
    None => get_identity_from_config()
  }
}

///|
fn get_identity_from_config() -> String {
  let root = get_work_root()
  let git_dir = root + "/.git"
  let config_path = git_dir + "/config"
  let content = @fs.read_file_to_string(config_path) catch { _ => "" }
  let name = if content.length() > 0 {
    @config_parse.get_config_value_from_content(
      content,
      "user.name",
      None,
      false,
    )
  } else {
    None
  }
  let email = if content.length() > 0 {
    @config_parse.get_config_value_from_content(
      content,
      "user.email",
      None,
      false,
    )
  } else {
    None
  }
  let home = @env.get_env_var("HOME").unwrap_or("")
  let global_content = if home.length() > 0 {
    @fs.read_file_to_string(home + "/.gitconfig") catch {
      _ => ""
    }
  } else {
    ""
  }
  let name = match name {
    Some(_) => name
    None =>
      if global_content.length() > 0 {
        @config_parse.get_config_value_from_content(
          global_content,
          "user.name",
          None,
          false,
        )
      } else {
        None
      }
  }
  let email = match email {
    Some(_) => email
    None =>
      if global_content.length() > 0 {
        @config_parse.get_config_value_from_content(
          global_content,
          "user.email",
          None,
          false,
        )
      } else {
        None
      }
  }
  match (name, email) {
    (Some(n), Some(e)) => n + " <" + e + ">"
    (Some(n), None) => n + " <" + n + "@localhost>"
    (None, Some(e)) => {
      let user = @env.get_env_var("USER").unwrap_or("Unknown")
      user + " <" + e + ">"
    }
    (None, None) =>
      match @env.get_env_var("USER") {
        Some(u) => u + " <" + u + "@localhost>"
        None => "Unknown "
      }
  }
}

///|
fn get_hub_signing_key() -> String? {
  match @env.get_env_var("BIT_COLLAB_SIGN_KEY") {
    Some(value) => if value.length() > 0 { Some(value) } else { None }
    None => None
  }
}

///|
fn get_hub_require_signed() -> Bool {
  match @env.get_env_var("BIT_COLLAB_REQUIRE_SIGNED") {
    Some(value) => {
      let normalized = value.to_lower()
      normalized == "1" || normalized == "true" || normalized == "yes"
    }
    None => false
  }
}

///|
fn format_timestamp_iso(ts : Int64, tz : String) -> String {
  let (year, month, day, hour, minute, second) = epoch_to_calendar(ts, tz)
  let y = year.to_string()
  let mo = pad2(month)
  let d = pad2(day)
  let h = pad2(hour)
  let mi = pad2(minute)
  let s = pad2(second)
  "\{y}-\{mo}-\{d} \{h}:\{mi}:\{s} \{tz}"
}

///|
fn get_commit_timestamp() -> Int64 {
  match @env.get_env_var("GIT_COMMITTER_DATE") {
    Some(s) =>
      match parse_git_env_timestamp(s) {
        Some(ts) => ts
        None => get_current_timestamp()
      }
    None => get_current_timestamp()
  }
}

///|
fn parse_git_env_timestamp(raw : String) -> Int64? {
  let mut token : String? = None
  for part_view in raw.split(" ") {
    let part = part_view.to_owned()
    if part.length() > 0 {
      token = Some(part)
      break
    }
  }
  match token {
    Some(part) =>
      if part.has_prefix("@") {
        let numeric = String::unsafe_substring(part, start=1, end=part.length())
        let parsed = @string.parse_int64(numeric) catch { _ => return None }
        Some(parsed)
      } else {
        let parsed = @string.parse_int64(part) catch { _ => -1L }
        if parsed >= 0L {
          Some(parsed)
        } else {
          parse_iso_date_timestamp(raw)
        }
      }
    None => None
  }
}

///|
fn parse_iso_date_timestamp(raw : String) -> Int64? {
  let s = raw.trim().to_owned()
  if s.length() < 16 {
    return None
  }
  if s[4] != '-' || s[7] != '-' {
    return None
  }
  let sep = s[10]
  if sep != ' ' && sep != 'T' {
    return None
  }
  let year = @string.parse_int64(String::unsafe_substring(s, start=0, end=4)) catch {
    _ => return None
  }
  let month = @string.parse_int64(String::unsafe_substring(s, start=5, end=7)) catch {
    _ => return None
  }
  let day = @string.parse_int64(String::unsafe_substring(s, start=8, end=10)) catch {
    _ => return None
  }
  let time_str = String::unsafe_substring(s, start=11, end=s.length())
  let mut time_end = time_str.length()
  for i in 0.. return None
  }
  let minute = @string.parse_int64(
    String::unsafe_substring(time_part, start=3, end=5),
  ) catch {
    _ => return None
  }
  let second = if time_part.length() >= 8 && time_part[5] == ':' {
    @string.parse_int64(String::unsafe_substring(time_part, start=6, end=8)) catch {
      _ => 0L
    }
  } else {
    0L
  }
  let mut ts = days_from_epoch(year, month, day) * 86400L +
    hour * 3600L +
    minute * 60L +
    second
  let after_time = String::unsafe_substring(
    s,
    start=11 + time_end,
    end=s.length(),
  )
  ts = ts - parse_iso_tz_offset_seconds(after_time)
  Some(ts)
}

///|
fn parse_iso_tz_offset_seconds(s : String) -> Int64 {
  let t = s.trim().to_owned()
  if t.length() < 5 {
    return 0L
  }
  let sign : Int64 = if t[0] == '-' {
    -1L
  } else if t[0] == '+' {
    1L
  } else {
    return 0L
  }
  let (hh_str, mm_str) = if t.length() >= 6 && t[3] == ':' {
    (
      String::unsafe_substring(t, start=1, end=3),
      String::unsafe_substring(t, start=4, end=6),
    )
  } else {
    (
      String::unsafe_substring(t, start=1, end=3),
      String::unsafe_substring(t, start=3, end=5),
    )
  }
  let hh = @string.parse_int64(hh_str) catch { _ => return 0L }
  let mm = @string.parse_int64(mm_str) catch { _ => return 0L }
  sign * (hh * 3600L + mm * 60L)
}

///|
fn days_from_epoch(year : Int64, month : Int64, day : Int64) -> Int64 {
  let month_days : Array[Int64] = [
    0, 31, 59, 90, 120, 151, 181, 212, 243, 273, 304, 334,
  ]
  let mut days = 365L * (year - 1970L)
  if year > 1970L {
    days = days +
      (year - 1969L) / 4L -
      (year - 1901L) / 100L +
      (year - 1601L) / 400L
  } else {
    days = days -
      (1970L - year) / 4L +
      (1970L - year) / 100L -
      (1970L - year) / 400L
  }
  days = days + month_days[(month - 1L).to_int()]
  if month > 2L {
    let is_leap = (year % 4L == 0L && year % 100L != 0L) || year % 400L == 0L
    if is_leap {
      days += 1L
    }
  }
  days + day - 1L
}

///|
fn pad2(n : Int) -> String {
  if n < 10 {
    "0" + n.to_string()
  } else {
    n.to_string()
  }
}

///|
fn epoch_to_calendar(ts : Int64, tz : String) -> (Int, Int, Int, Int, Int, Int) {
  let tz_offset_seconds = parse_tz_offset(tz)
  let adjusted = ts + tz_offset_seconds.to_int64()
  let mut days = (adjusted / 86400L).to_int()
  let mut remaining = (adjusted % 86400L).to_int()
  if remaining < 0 {
    days -= 1
    remaining += 86400
  }
  let hour = remaining / 3600
  remaining = remaining % 3600
  let minute = remaining / 60
  let second = remaining % 60
  let (year, month, day) = log_days_to_date(days)
  (year, month, day, hour, minute, second)
}

///|
fn parse_tz_offset(tz : String) -> Int {
  if tz.length() < 5 {
    return 0
  }
  let chars = tz.to_array()
  let sign = if chars[0] == '-' { -1 } else { 1 }
  let h = (chars[1].to_int() - '0'.to_int()) * 10 +
    (chars[2].to_int() - '0'.to_int())
  let m = (chars[3].to_int() - '0'.to_int()) * 10 +
    (chars[4].to_int() - '0'.to_int())
  sign * (h * 3600 + m * 60)
}

///|
fn log_days_to_date(days_since_epoch : Int) -> (Int, Int, Int) {
  let z = days_since_epoch + 719468
  let era = (if z >= 0 { z } else { z - 146096 }) / 146097
  let doe = z - era * 146097
  let yoe = (doe - doe / 1460 + doe / 36524 - doe / 146096) / 365
  let y = yoe + era * 400
  let doy = doe - (365 * yoe + yoe / 4 - yoe / 100)
  let mp = (5 * doy + 2) / 153
  let d = doy - (153 * mp + 2) / 5 + 1
  let m = mp + (if mp < 10 { 3 } else { -9 })
  let year = y + (if m <= 2 { 1 } else { 0 })
  (year, m, d)
}