///| Remote config parsing helpers

///|
pub(all) struct RemoteConfig {
  mut urls : Array[String]
  mut pushurls : Array[String]
  mut fetch : Array[String]
  mut push : Array[String]
  mut promisor : Bool
  mut partial_clone_filter : String?
  mut mirror : Bool
  mut tagopt : String?
}

///|
pub(all) struct BranchConfig {
  name : String
  mut remote : String?
  mut merges : Array[String]
  mut rebase : Bool?
}

///|
pub(all) struct FetchRefspec {
  negative : Bool
  src : String
  dst : String
}

///|
pub(all) struct PushRefspec {
  force : Bool
  src : String
  dst : String
}

///|
pub(all) struct ConfigBlock {
  mut header : String?
  mut section : String?
  mut name : String?
  mut lines : Array[String]
}

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

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

///|
pub fn config_strip_quotes(s : String) -> String {
  let t = s.trim().to_owned()
  if t.length() >= 2 && t.has_prefix("\"") && t.has_suffix("\"") {
    String::unsafe_substring(t, start=1, end=t.length() - 1)
  } else {
    t
  }
}

///|
pub fn parse_section_header(line : String) -> (String, String?) {
  let body = String::unsafe_substring(line, start=1, end=line.length() - 1)
  match body.find(" ") {
    Some(idx) => {
      let sec = String::unsafe_substring(body, start=0, end=idx).to_lower()
      let name_part = String::unsafe_substring(
        body,
        start=idx + 1,
        end=body.length(),
      )
      (sec, name_part |> config_strip_quotes |> Some)
    }
    None => (body.to_lower(), None)
  }
}

///|
pub fn parse_config_kv(line : String) -> (String, String)? {
  let trimmed = trim_string_internal(line)
  if trimmed.length() == 0 || trimmed.has_prefix("#") || trimmed.has_prefix(";") {
    return None
  }
  match trimmed.find("=") {
    Some(eq_idx) => {
      let key = trim_string_internal(
        String::unsafe_substring(trimmed, start=0, end=eq_idx),
      ).to_lower()
      let value = trim_string_internal(
        String::unsafe_substring(
          trimmed,
          start=eq_idx + 1,
          end=trimmed.length(),
        ),
      )
      Some((key, value))
    }
    None => None
  }
}

///|
pub fn parse_config_blocks(content : String) -> Array[ConfigBlock] {
  let blocks : Array[ConfigBlock] = []
  let current : ConfigBlock = {
    header: None,
    section: None,
    name: None,
    lines: [],
  }
  for line_view in content.split("\n") {
    let line = line_view.to_owned()
    let trimmed = trim_string_internal(line)
    if trimmed.has_prefix("[") && trimmed.has_suffix("]") {
      if current.header is Some(_) || current.lines.length() > 0 {
        let copied_lines : Array[String] = []
        for item in current.lines {
          copied_lines.push(item)
        }
        blocks.push({
          header: current.header,
          section: current.section,
          name: current.name,
          lines: copied_lines,
        })
      }
      let (section, name) = parse_section_header(trimmed)
      current.header = Some(trimmed)
      current.section = Some(section)
      current.name = name
      current.lines = []
      continue
    }
    current.lines.push(line)
  }
  if current.header is Some(_) || current.lines.length() > 0 {
    let copied_lines : Array[String] = []
    for item in current.lines {
      copied_lines.push(item)
    }
    blocks.push({
      header: current.header,
      section: current.section,
      name: current.name,
      lines: copied_lines,
    })
  }
  blocks
}

///|
pub fn render_config_blocks(blocks : Array[ConfigBlock]) -> String {
  let lines : Array[String] = []
  for block in blocks {
    if block.header is Some(h) {
      lines.push(h)
    }
    for line in block.lines {
      lines.push(line)
    }
  }
  let mut content = lines.join("\n")
  if content.length() > 0 && !content.has_suffix("\n") {
    content = content + "\n"
  }
  content
}

///|
pub fn config_has_remote_section(
  blocks : Array[ConfigBlock],
  name : String,
) -> Bool {
  for block in blocks {
    if block.section == Some("remote") && block.name == Some(name) {
      return true
    }
  }
  false
}

///|
pub fn parse_remote_config(
  content : String,
) -> (Map[String, RemoteConfig], Map[String, BranchConfig]) {
  let remotes : Map[String, RemoteConfig] = Map([])
  let branches : Map[String, BranchConfig] = Map([])
  let mut current_section : String? = None
  let mut current_name : String? = None
  for line_view in content.split("\n") {
    let line = trim_string_internal(line_view.to_owned())
    if line.length() == 0 || line.has_prefix("#") || line.has_prefix(";") {
      continue
    }
    if line.has_prefix("[") && line.has_suffix("]") {
      let body = String::unsafe_substring(line, start=1, end=line.length() - 1)
      match body.find(" ") {
        Some(idx) => {
          let sec = String::unsafe_substring(body, start=0, end=idx).to_lower()
          let name_part = String::unsafe_substring(
            body,
            start=idx + 1,
            end=body.length(),
          )
          let name = config_strip_quotes(name_part)
          current_section = Some(sec)
          current_name = Some(name)
        }
        None => {
          current_section = Some(body.to_lower())
          current_name = None
        }
      }
      continue
    }
    match line.find("=") {
      Some(eq_idx) => {
        let key = trim_string_internal(
          String::unsafe_substring(line, start=0, end=eq_idx),
        ).to_lower()
        let value = trim_string_internal(
          String::unsafe_substring(line, start=eq_idx + 1, end=line.length()),
        )
        match (current_section, current_name) {
          (Some("remote"), Some(name)) => {
            let rc = remotes
              .get(name)
              .unwrap_or({
                urls: [],
                pushurls: [],
                fetch: [],
                push: [],
                promisor: false,
                partial_clone_filter: None,
                mirror: false,
                tagopt: None,
              })
            match key {
              "url" => rc.urls.push(value)
              "pushurl" => rc.pushurls.push(value)
              "fetch" => rc.fetch.push(value)
              "push" => rc.push.push(value)
              "promisor" => {
                let v = value.to_lower()
                rc.promisor = v == "true" || v == "1" || v == "yes"
              }
              "partialclonefilter" => rc.partial_clone_filter = Some(value)
              "mirror" => {
                let v = value.to_lower()
                rc.mirror = v == "true" || v == "1" || v == "yes"
              }
              "tagopt" => rc.tagopt = Some(value)
              _ => ()
            }
            remotes[name] = rc
          }
          (Some("branch"), Some(name)) => {
            let bc = branches
              .get(name)
              .unwrap_or({ name, remote: None, merges: [], rebase: None })
            match key {
              "remote" => bc.remote = Some(value)
              "merge" => {
                let parts = value.split(" ").collect()
                for p_view in parts {
                  let p = p_view.to_owned().trim().to_owned()
                  if p.length() > 0 {
                    bc.merges.push(p)
                  }
                }
              }
              "rebase" => {
                let v = value.to_lower()
                if v == "true" || v == "1" || v == "yes" {
                  bc.rebase = Some(true)
                } else if v == "false" || v == "0" || v == "no" {
                  bc.rebase = Some(false)
                }
              }
              _ => ()
            }
            branches[name] = bc
          }
          _ => ()
        }
      }
      None => ()
    }
  }
  (remotes, branches)
}

///|
pub fn split_config_key(
  full_key : String,
  section : String,
) -> (String, String)? {
  let prefix = section + "."
  if !full_key.has_prefix(prefix) {
    return None
  }
  let rest = String::unsafe_substring(
    full_key,
    start=prefix.length(),
    end=full_key.length(),
  )
  match rest.rev_find(".") {
    None => None
    Some(idx) => {
      let name = String::unsafe_substring(rest, start=0, end=idx)
      let key = String::unsafe_substring(rest, start=idx + 1, end=rest.length())
      Some((name, key))
    }
  }
}

///|
pub fn parse_config_override_pairs() -> Array[(String, String)] {
  let out : Array[(String, String)] = []
  let overrides = @bitio.env_get("GIT_CONFIG_OVERRIDES")
  guard overrides is Some(env_value) else { return out }
  for line in split_by_newline(env_value) {
    if line.length() == 0 {
      continue
    }
    match line.find("=") {
      Some(idx) => {
        let key = String::unsafe_substring(line, start=0, end=idx).to_lower()
        let value = String::unsafe_substring(
          line,
          start=idx + 1,
          end=line.length(),
        )
        out.push((key, value))
      }
      None => out.push((line.to_lower(), ""))
    }
  }
  out
}

///|
pub fn apply_config_overrides(
  remotes : Map[String, RemoteConfig],
  branches : Map[String, BranchConfig],
  overrides : Array[(String, String)],
) -> Unit {
  for pair in overrides {
    let (raw_key, value) = pair
    let mut handled = false
    match split_config_key(raw_key, "remote") {
      Some((name, key)) => {
        let rc = remotes
          .get(name)
          .unwrap_or({
            urls: [],
            pushurls: [],
            fetch: [],
            push: [],
            promisor: false,
            partial_clone_filter: None,
            mirror: false,
            tagopt: None,
          })
        match key {
          "url" =>
            if value.length() == 0 {
              rc.urls = []
            } else {
              rc.urls.push(value)
            }
          "pushurl" =>
            if value.length() == 0 {
              rc.pushurls = []
            } else {
              rc.pushurls.push(value)
            }
          "fetch" =>
            if value.length() == 0 {
              rc.fetch = []
            } else {
              rc.fetch.push(value)
            }
          "push" =>
            if value.length() == 0 {
              rc.push = []
            } else {
              rc.push.push(value)
            }
          "promisor" =>
            if value.length() > 0 {
              let v = value.to_lower()
              rc.promisor = v == "true" || v == "1" || v == "yes"
            }
          "partialclonefilter" =>
            rc.partial_clone_filter = if value.length() == 0 {
              None
            } else {
              Some(value)
            }
          "mirror" =>
            if value.length() > 0 {
              let v = value.to_lower()
              rc.mirror = v == "true" || v == "1" || v == "yes"
            }
          "tagopt" =>
            rc.tagopt = if value.length() == 0 { None } else { Some(value) }
          _ => ()
        }
        remotes[name] = rc
        handled = true
      }
      None => ()
    }
    if handled {
      continue
    }
    match split_config_key(raw_key, "branch") {
      Some((name, key)) => {
        let bc = branches
          .get(name)
          .unwrap_or({ name, remote: None, merges: [], rebase: None })
        match key {
          "remote" =>
            bc.remote = if value.length() == 0 { None } else { Some(value) }
          "merge" =>
            if value.length() == 0 {
              bc.merges = []
            } else {
              bc.merges.push(value)
            }
          "rebase" =>
            if value.length() > 0 {
              let v = value.to_lower()
              if v == "true" || v == "1" || v == "yes" {
                bc.rebase = Some(true)
              } else if v == "false" || v == "0" || v == "no" {
                bc.rebase = Some(false)
              }
            }
          _ => ()
        }
        branches[name] = bc
      }
      None => ()
    }
  }
}

///|
pub fn read_repo_config(
  fs : &@bit.RepoFileSystem,
  git_dir : String,
) -> (Map[String, RemoteConfig], Map[String, BranchConfig]) {
  let config_path = git_dir + "/config"
  if !fs.is_file(config_path) {
    return (Map([]), Map([]))
  }
  let content = decode_bytes_lossy(
    fs.read_file(config_path) catch {
      _ => return (Map([]), Map([]))
    },
  )
  let (remotes, branches) = parse_remote_config(content)
  let overrides = parse_config_override_pairs()
  if overrides.length() > 0 {
    apply_config_overrides(remotes, branches, overrides)
  }
  (remotes, branches)
}

///|
pub fn list_remotes(
  fs : &@bit.RepoFileSystem,
  git_dir : String,
) -> Array[String] {
  let (remotes, _) = read_repo_config(fs, git_dir)
  let out : Array[String] = []
  for name, _ in remotes {
    out.push(name)
  }
  out.sort()
  out
}

///|
pub fn list_remotes_verbose(
  fs : &@bit.RepoFileSystem,
  git_dir : String,
) -> Array[(String, String)] {
  let (remotes, _) = read_repo_config(fs, git_dir)
  let out : Array[(String, String)] = []
  let names = list_remotes(fs, git_dir)
  for name in names {
    match remotes.get(name) {
      Some(rc) => {
        if rc.urls.length() == 0 {
          continue
        }
        let url = rc.urls[0]
        let filter = if rc.promisor && rc.partial_clone_filter is Some(f) {
          " [" + f + "]"
        } else {
          ""
        }
        out.push((name, url + " (fetch)" + filter))
        let push_urls = if rc.pushurls.length() > 0 {
          rc.pushurls
        } else {
          rc.urls
        }
        for pu in push_urls {
          out.push((name, pu + " (push)"))
        }
      }
      None => ()
    }
  }
  out
}

///|
pub fn get_remote_url(
  fs : &@bit.RepoFileSystem,
  git_dir : String,
  name : String,
) -> String? {
  let config_path = git_dir + "/config"
  if !fs.is_file(config_path) {
    return None
  }
  let content = decode_bytes_lossy(
    fs.read_file(config_path) catch {
      _ => return None
    },
  )
  let target_section = "[remote \"\{name}\"]"
  let mut in_section = false
  for line_view in content.split("\n") {
    let line = trim_string_internal(line_view.to_owned())
    if line == target_section {
      in_section = true
      continue
    }
    if line.has_prefix("[") {
      in_section = false
      continue
    }
    if in_section && line.has_prefix("url = ") {
      return Some(String::unsafe_substring(line, start=6, end=line.length()))
    }
  }
  None
}

///|
pub fn parse_refspec_pair(spec : String) -> (String, String) {
  match spec.find(":") {
    Some(idx) => {
      let src = if idx == 0 {
        ""
      } else {
        String::unsafe_substring(spec, start=0, end=idx)
      }
      let dst = if idx + 1 >= spec.length() {
        ""
      } else {
        String::unsafe_substring(spec, start=idx + 1, end=spec.length())
      }
      (src, dst)
    }
    None => (spec, spec)
  }
}

///|
pub fn parse_fetch_refspecs(specs : Array[String]) -> Array[FetchRefspec] {
  let out : Array[FetchRefspec] = []
  for raw in specs {
    let mut negative = false
    let mut s = raw
    if s.has_prefix("^") {
      negative = true
      s = String::unsafe_substring(s, start=1, end=s.length())
    }
    if s.has_prefix("+") {
      s = String::unsafe_substring(s, start=1, end=s.length())
    }
    let (src, dst) = parse_refspec_pair(s)
    out.push({ negative, src, dst })
  }
  out
}

///|
pub fn parse_push_refspecs(specs : Array[String]) -> Array[PushRefspec] {
  let out : Array[PushRefspec] = []
  for raw in specs {
    let mut force = false
    let mut s = raw
    if s.has_prefix("+") {
      force = true
      s = String::unsafe_substring(s, start=1, end=s.length())
    }
    let (src, dst) = parse_refspec_pair(s)
    out.push({ force, src, dst })
  }
  out
}

///|
pub fn normalize_track_ref(track : String) -> String {
  if track.has_prefix("refs/") {
    return track
  }
  if track.has_prefix("heads/") {
    return "refs/heads/" +
      String::unsafe_substring(track, start=6, end=track.length())
  }
  "refs/heads/" + track
}

///|
pub fn refspec_capture(pattern : String, refname : String) -> String? {
  match pattern.find("*") {
    None => if pattern == refname { Some("") } else { None }
    Some(idx) => {
      let prefix = String::unsafe_substring(pattern, start=0, end=idx)
      let suffix = String::unsafe_substring(
        pattern,
        start=idx + 1,
        end=pattern.length(),
      )
      if refname.has_prefix(prefix) &&
        refname.has_suffix(suffix) &&
        refname.length() >= prefix.length() + suffix.length() {
        let mid = String::unsafe_substring(
          refname,
          start=prefix.length(),
          end=refname.length() - suffix.length(),
        )
        Some(mid)
      } else {
        None
      }
    }
  }
}

///|
pub fn refspec_map_dst(
  src_pat : String,
  dst_pat : String,
  refname : String,
) -> String? {
  match refspec_capture(src_pat, refname) {
    Some(mid) =>
      if dst_pat.find("*") is Some(idx) {
        let prefix = String::unsafe_substring(dst_pat, start=0, end=idx)
        let suffix = String::unsafe_substring(
          dst_pat,
          start=idx + 1,
          end=dst_pat.length(),
        )
        Some(prefix + mid + suffix)
      } else {
        Some(dst_pat)
      }
    None => None
  }
}

///|
pub fn update_fetch_refspec(
  spec : String,
  old_name : String,
  new_name : String,
) -> String {
  let mut prefix = ""
  let mut body = spec
  if body.has_prefix("^") {
    prefix = prefix + "^"
    body = String::unsafe_substring(body, start=1, end=body.length())
  }
  if body.has_prefix("+") {
    prefix = prefix + "+"
    body = String::unsafe_substring(body, start=1, end=body.length())
  }
  let has_colon = body.find(":") is Some(_)
  let (src, dst) = parse_refspec_pair(body)
  let old_prefix = "refs/remotes/" + old_name + "/"
  if dst.has_prefix(old_prefix) {
    let suffix = String::unsafe_substring(
      dst,
      start=old_prefix.length(),
      end=dst.length(),
    )
    let new_dst = "refs/remotes/" + new_name + "/" + suffix
    let new_body = if has_colon { src + ":" + new_dst } else { new_dst }
    prefix + new_body
  } else {
    spec
  }
}

///|
pub fn normalize_remote_branch_name(refname : String) -> String {
  if refname.has_prefix("refs/heads/") {
    String::unsafe_substring(refname, start=11, end=refname.length())
  } else {
    refname
  }
}

///|
pub fn is_mirror_fetch_config(rc : RemoteConfig) -> Bool {
  for raw in rc.fetch {
    let mut spec = raw
    if spec.has_prefix("^") {
      spec = String::unsafe_substring(spec, start=1, end=spec.length())
    }
    if spec.has_prefix("+") {
      spec = String::unsafe_substring(spec, start=1, end=spec.length())
    }
    let (_src, dst) = parse_refspec_pair(spec)
    if dst == "refs/*" {
      return true
    }
    if dst.has_prefix("refs/") && !dst.has_prefix("refs/remotes/") {
      return true
    }
  }
  false
}

///|
pub fn remote_set_branches(
  fs : &@bit.FileSystem,
  rfs : &@bit.RepoFileSystem,
  git_dir : String,
  name : String,
  branches : Array[String],
  add : Bool,
) -> Unit raise @bit.GitError {
  let config_path = git_dir + "/config"
  if !rfs.is_file(config_path) {
    raise @bit.GitError::InvalidObject("No such remote '\{name}'")
  }
  let content = decode_bytes_lossy(rfs.read_file(config_path))
  let (remotes, _) = parse_remote_config(content)
  let rc = match remotes.get(name) {
    Some(cfg) => cfg
    None => raise @bit.GitError::InvalidObject("No such remote '\{name}'")
  }
  let mirror_fetch = is_mirror_fetch_config(rc)
  let updated_specs : Array[String] = []
  if add {
    for spec in rc.fetch {
      updated_specs.push(spec)
    }
  }
  for branch in branches {
    let src_ref = normalize_track_ref(branch)
    let short = normalize_remote_branch_name(src_ref)
    let dst_ref = if mirror_fetch {
      src_ref
    } else {
      "refs/remotes/" + name + "/" + short
    }
    let spec = "+" + src_ref + ":" + dst_ref
    let mut exists = false
    for existing in updated_specs {
      if existing == spec {
        exists = true
        break
      }
    }
    if !exists {
      updated_specs.push(spec)
    }
  }
  let blocks = parse_config_blocks(content)
  let new_blocks : Array[ConfigBlock] = []
  let mut found = false
  for block in blocks {
    if block.section == Some("remote") && block.name == Some(name) {
      found = true
      let updated = block
      let other_lines : Array[String] = []
      for line in updated.lines {
        match parse_config_kv(line) {
          Some((key, _)) if key == "fetch" => ()
          _ => other_lines.push(line)
        }
      }
      let final_lines : Array[String] = []
      for line in other_lines {
        final_lines.push(line)
      }
      for spec in updated_specs {
        final_lines.push("\tfetch = " + spec)
      }
      updated.lines = final_lines
      new_blocks.push(updated)
    } else {
      new_blocks.push(block)
    }
  }
  if !found {
    raise @bit.GitError::InvalidObject("No such remote '\{name}'")
  }
  fs.write_string(config_path, render_config_blocks(new_blocks))
}

///|
pub fn set_config_key(
  fs : &@bit.FileSystem,
  rfs : &@bit.RepoFileSystem,
  git_dir : String,
  section : String,
  name : String,
  key : String,
  value : String,
) -> Unit raise @bit.GitError {
  let config_path = git_dir + "/config"
  let target_section = "[\{section} \"\{name}\"]"
  let content = if rfs.is_file(config_path) {
    decode_bytes_lossy(rfs.read_file(config_path))
  } else {
    ""
  }
  let lines : Array[String] = []
  let mut in_section = false
  let mut found_section = false
  let mut key_set = false
  for line_view in content.split("\n") {
    let line = line_view.to_owned()
    let trimmed = trim_string_internal(line)
    if trimmed == target_section {
      in_section = true
      found_section = true
      lines.push(line)
      continue
    }
    if in_section && trimmed.has_prefix("[") {
      if !key_set {
        lines.push("\t\{key} = \{value}")
        key_set = true
      }
      in_section = false
    }
    if in_section && trimmed.has_prefix("\{key} = ") {
      lines.push("\t\{key} = \{value}")
      key_set = true
      continue
    }
    lines.push(line)
  }
  if found_section {
    if in_section && !key_set {
      lines.push("\t\{key} = \{value}")
    }
  } else {
    if lines.length() > 0 &&
      trim_string_internal(lines[lines.length() - 1]) != "" {
      lines.push("")
    }
    lines.push(target_section)
    lines.push("\t\{key} = \{value}")
  }
  let new_content = lines
    .iter()
    .fold(init="", fn(acc, line) {
      if acc == "" {
        line
      } else {
        acc + "\n" + line
      }
    })
  fs.write_string(config_path, new_content)
}