///|
/// One compiler or linker argument together with its origin.
pub(all) struct ResolvedFlag {
  value : String
  package_id : String
  field : String
  location : Location
} derive(Eq, @debug.Debug)

///|
pub(all) struct FlagResult {
  flags : Array[ResolvedFlag]
  diagnostics : Array[Diagnostic]
} derive(Eq, @debug.Debug)

///|
/// Render collected arguments as one POSIX shell command fragment.
///
/// The underlying `flags` array remains the portable, lossless interface.
/// This text form is intended for copying into sh-compatible shells.
pub fn FlagResult::shell_text(self : FlagResult) -> String {
  self.flags.map(flag => shell_quote(flag.value)).join(" ")
}

///|
fn shell_quote(value : String) -> String {
  if !value.is_empty() && value.iter().all(shell_safe_char) {
    return value
  }
  let output = StringBuilder()
  output.write_char('\'')
  for char in value {
    if char == '\'' {
      output.write_string("'\\''")
    } else {
      output.write_char(char)
    }
  }
  output.write_char('\'')
  output.to_string()
}

///|
fn shell_safe_char(char : Char) -> Bool {
  (char >= 'a' && char <= 'z') ||
  (char >= 'A' && char <= 'Z') ||
  (char >= '0' && char <= '9') ||
  char == '_' ||
  char == '@' ||
  char == '%' ||
  char == '+' ||
  char == '=' ||
  char == ':' ||
  char == ',' ||
  char == '.' ||
  char == '/' ||
  char == '-'
}

///|
/// Return a copy with repeated compiler and linker search paths removed.
///
/// Both attached (`-I/path`, `-L/path`) and split (`-I /path`, `-L /path`)
/// forms are recognized. Other flags, including repeated libraries, are kept
/// because their order and repetition can affect linking.
pub fn FlagResult::deduplicate_search_paths(self : FlagResult) -> FlagResult {
  let flags : Array[ResolvedFlag] = []
  let seen : Map[String, Bool] = Map([])
  let mut i = 0
  while i < self.flags.length() {
    let flag = self.flags[i]
    let split_search_path = (flag.value == "-I" || flag.value == "-L") &&
      i + 1 < self.flags.length() &&
      same_flag_origin(flag, self.flags[i + 1])
    if split_search_path {
      let next = self.flags[i + 1]
      let key = flag.value + "\n" + next.value
      if !seen.contains(key) {
        seen[key] = true
        flags.push(flag)
        flags.push(next)
      }
      i = i + 2
    } else {
      let is_search_path = (
          flag.value.has_prefix("-I") || flag.value.has_prefix("-L")
        ) &&
        flag.value.length() > 2
      let key = if is_search_path {
        flag.value[0:2].to_owned() + "\n" + flag.value[2:].to_owned()
      } else {
        flag.value
      }
      if !is_search_path || !seen.contains(key) {
        if is_search_path {
          seen[key] = true
        }
        flags.push(flag)
      }
      i = i + 1
    }
  }
  { flags, diagnostics: self.diagnostics.copy(), }
}

///|
/// Prefix absolute compiler and linker search paths with a sysroot.
///
/// Attached and split `-I`, `-L` and `-isystem` forms are supported. Relative
/// paths and unrelated arguments are unchanged, and every rewritten flag keeps
/// its original package, field and source location.
pub fn FlagResult::with_sysroot(
  self : FlagResult,
  sysroot : String,
) -> FlagResult {
  if sysroot.is_empty() || sysroot == "/" {
    return { flags: self.flags.copy(), diagnostics: self.diagnostics.copy(), }
  }
  let root_chars = sysroot.to_array()
  let root = if root_chars[root_chars.length() - 1] == '/' {
    sysroot[:sysroot.length() - 1].to_owned()
  } else {
    sysroot
  }
  let flags : Array[ResolvedFlag] = []
  let mut i = 0
  while i < self.flags.length() {
    let flag = self.flags[i]
    let split = (
        flag.value == "-I" || flag.value == "-L" || flag.value == "-isystem"
      ) &&
      i + 1 < self.flags.length() &&
      same_flag_origin(flag, self.flags[i + 1])
    if split {
      let operand = self.flags[i + 1]
      flags.push(flag)
      flags.push(flag_with_value(operand, sysroot_path(root, operand.value)))
      i = i + 2
    } else {
      let prefix_length = if flag.value.has_prefix("-isystem") &&
        flag.value.length() > 8 {
        8
      } else if (flag.value.has_prefix("-I") || flag.value.has_prefix("-L")) &&
        flag.value.length() > 2 {
        2
      } else {
        0
      }
      if prefix_length > 0 {
        let path = flag.value[prefix_length:].to_owned()
        flags.push(
          flag_with_value(
            flag,
            flag.value[:prefix_length].to_owned() + sysroot_path(root, path),
          ),
        )
      } else {
        flags.push(flag)
      }
      i = i + 1
    }
  }
  { flags, diagnostics: self.diagnostics.copy(), }
}

///|
/// Remove exact system `-I` and `-L` search paths.
///
/// The caller supplies the system paths explicitly, keeping library behavior
/// deterministic across hosts. `-isystem` and path prefixes are intentionally
/// retained, matching pkgconf's system path filtering behavior.
pub fn FlagResult::filter_system_search_paths(
  self : FlagResult,
  include_paths : Array[String],
  library_paths : Array[String],
) -> FlagResult {
  let includes : Map[String, Bool] = Map([])
  let libraries : Map[String, Bool] = Map([])
  for path in include_paths {
    includes[path] = true
  }
  for path in library_paths {
    libraries[path] = true
  }
  let flags : Array[ResolvedFlag] = []
  let mut i = 0
  while i < self.flags.length() {
    let flag = self.flags[i]
    let split = (flag.value == "-I" || flag.value == "-L") &&
      i + 1 < self.flags.length() &&
      same_flag_origin(flag, self.flags[i + 1])
    if split {
      let operand = self.flags[i + 1]
      let paths = if flag.value == "-I" { includes } else { libraries }
      if !paths.contains(operand.value) {
        flags.push(flag)
        flags.push(operand)
      }
      i = i + 2
    } else {
      let removable = if flag.value.has_prefix("-I") && flag.value.length() > 2 {
        includes.contains(flag.value[2:].to_owned())
      } else if flag.value.has_prefix("-L") && flag.value.length() > 2 {
        libraries.contains(flag.value[2:].to_owned())
      } else {
        false
      }
      if !removable {
        flags.push(flag)
      }
      i = i + 1
    }
  }
  { flags, diagnostics: self.diagnostics.copy(), }
}

///|
fn flag_with_value(flag : ResolvedFlag, value : String) -> ResolvedFlag {
  {
    value,
    package_id: flag.package_id,
    field: flag.field,
    location: flag.location,
  }
}

///|
fn sysroot_path(sysroot : String, path : String) -> String {
  if is_absolute_search_path(path) {
    sysroot + path
  } else {
    path
  }
}

///|
fn is_absolute_search_path(path : String) -> Bool {
  path.has_prefix("/")
}

///|
fn same_flag_origin(left : ResolvedFlag, right : ResolvedFlag) -> Bool {
  left.package_id == right.package_id &&
  left.field == right.field &&
  left.location == right.location
}

///|
/// Collect public compiler flags in dependant-first order.
pub fn PackageSet::collect_cflags(
  self : PackageSet,
  root : String,
) -> FlagResult {
  // Private dependencies still contribute headers and compile definitions.
  // Their linker flags are restricted to static linking in collect_libs.
  let resolution = self.resolve(root, include_private=true)
  let flags : Array[ResolvedFlag] = []
  let diagnostics = resolution.diagnostics.copy()
  for id in self.flag_order(root, true) {
    append_flag_field(self, id, "Cflags", flags, diagnostics)
  }
  { flags, diagnostics, }
}

///|
/// Collect linker flags in dependant-first order. Static queries also include
/// private dependency edges and each package's `Libs.private` field.
pub fn PackageSet::collect_libs(
  self : PackageSet,
  root : String,
  static_linking? : Bool = false,
) -> FlagResult {
  let resolution = self.resolve(root, include_private=static_linking)
  let flags : Array[ResolvedFlag] = []
  let diagnostics = resolution.diagnostics.copy()
  for id in self.flag_order(root, static_linking) {
    append_flag_field(self, id, "Libs", flags, diagnostics)
    if static_linking {
      append_flag_field(self, id, "Libs.private", flags, diagnostics)
    }
  }
  { flags, diagnostics, }
}

///|
/// Return the shortest dependency path from `root` to `target`.
///
/// Public edges are always considered; private edges participate only when
/// `include_private` is true. An empty array means the target is not reachable.
pub fn PackageSet::dependency_path(
  self : PackageSet,
  root : String,
  target : String,
  include_private? : Bool = false,
) -> Array[String] {
  let root_requirement : Requirement = { name: root, op: Any, version: "", }
  let root_id = match self.packages.get(root) {
    Some(_) => Some(root)
    None => self.find_provider(root_requirement).map(pkg => pkg.id)
  }
  guard root_id is Some(start) else { return [] }
  let paths : Map[String, Array[String]] = Map([])
  let queue : Array[String] = [start]
  paths[start] = [start]
  let mut index = 0
  while index < queue.length() {
    let id = queue[index]
    if id == target {
      return paths.get(id).unwrap().copy()
    }
    let pkg = self.packages.get(id).unwrap()
    append_dependency_paths(self, pkg, "Requires", paths, queue)
    if include_private {
      append_dependency_paths(self, pkg, "Requires.private", paths, queue)
    }
    index = index + 1
  }
  []
}

///|
// pkgconf emits the requested package first, then walks dependencies in their
// declaration order. A separate traversal is used because `Resolution.order`
// is intentionally dependency-first for build planning.
fn PackageSet::flag_order(
  self : PackageSet,
  root : String,
  include_private : Bool,
) -> Array[String] {
  let order : Array[String] = []
  let seen : Map[String, Bool] = Map([])
  let root_requirement : Requirement = { name: root, op: Any, version: "", }
  let root_id = match self.packages.get(root) {
    Some(_) => Some(root)
    None => self.find_provider(root_requirement).map(pkg => pkg.id)
  }
  if root_id is Some(id) {
    seen[id] = true
    order.push(id)
    let mut index = 0
    while index < order.length() {
      let pkg = self.packages.get(order[index]).unwrap()
      append_flag_requirements(self, pkg, "Requires", seen, order)
      if include_private {
        append_flag_requirements(self, pkg, "Requires.private", seen, order)
      }
      index = index + 1
    }
  }
  order
}

///|
fn append_flag_requirements(
  packages : PackageSet,
  pkg : Package,
  field_name : String,
  seen : Map[String, Bool],
  order : Array[String],
) -> Unit {
  guard pkg.document.field(field_name) is Some(field) else { return }
  guard parse_requirements(field.value, source=field.location.source)
    is Ok(requirements) else {
    return
  }
  for requirement in requirements {
    let dependency_id = matching_dependency_id(packages, requirement)
    if dependency_id is Some(id) {
      if !seen.contains(id) {
        seen[id] = true
        order.push(id)
      }
    }
  }
}

///|
fn append_dependency_paths(
  packages : PackageSet,
  pkg : Package,
  field_name : String,
  paths : Map[String, Array[String]],
  queue : Array[String],
) -> Unit {
  guard pkg.document.field(field_name) is Some(field) else { return }
  guard parse_requirements(field.value, source=field.location.source)
    is Ok(requirements) else {
    return
  }
  for requirement in requirements {
    guard matching_dependency_id(packages, requirement) is Some(id) else {
      continue
    }
    if !paths.contains(id) {
      let path = paths.get(pkg.id).unwrap().copy()
      path.push(id)
      paths[id] = path
      queue.push(id)
    }
  }
}

///|
fn matching_dependency_id(
  packages : PackageSet,
  requirement : Requirement,
) -> String? {
  match packages.packages.get(requirement.name) {
    Some(dependency) => {
      let version = dependency.document.field("Version").unwrap().value
      if requirement.matches(version) {
        Some(requirement.name)
      } else {
        None
      }
    }
    None => packages.find_provider(requirement).map(provider => provider.id)
  }
}

///|
fn append_flag_field(
  packages : PackageSet,
  id : String,
  field_name : String,
  output : Array[ResolvedFlag],
  diagnostics : Array[Diagnostic],
) -> Unit {
  guard packages.packages.get(id) is Some(pkg) else { return }
  guard pkg.document.field(field_name) is Some(entry) else { return }
  match split_flags(entry.value, source=entry.location.source) {
    Ok(values) =>
      for value in values {
        output.push({
          value,
          package_id: id,
          field: field_name,
          location: entry.location,
        })
      }
    Err(problem) =>
      diagnostics.push(
        diag(problem.code, problem.message, {
          source: entry.value_location.source,
          line: entry.value_location.line,
          column: entry.value_location.column + problem.location.column - 1,
        }),
      )
  }
}