///|
/// Filepath utilities for ZIP archives
/// 
/// Handles path normalization, sanitization, and conversion between formats

///|
/// File path in ZIP archive
pub(all) struct Fpath(String) derive(Eq, Compare, FromJson, Hash)

///|
pub extend Fpath with Eq::{not_equal, equal}

///|
pub extend Fpath with Compare::{op_lt, op_le, op_ge, compare, op_gt}

///|
pub extend Fpath with @json.FromJson::{from_json}

///|
pub extend Fpath with Hash::{hash, hash_combine}

///|
/// Get the length of the path string
pub fn Fpath::length(self : Fpath) -> Int {
  self.0.length()
}

///|
/// Get character at index
#alias("_[_]")
pub fn Fpath::at(self : Fpath, index : Int) -> Int {
  // FIXME(upstream): ("_[_]") + op_get 
  // would cause weird errors
  self.0[index].to_int()
}

///|
/// Check if path is empty
pub fn Fpath::is_empty(self : Fpath) -> Bool {
  self.0.is_empty()
}

///|
/// String concatenation for paths
/// FIXME(upstream): derive Add
pub impl Add for Fpath with fn add(x, other) {
  Fpath(x.0 + other.0)
}

///|
pub extend Fpath with Add::{add}

///|
/// Convert to string for display
pub impl Show for Fpath with fn output(x, logger) {
  Show::output(x.0, logger)
}

///|
/// FIXME: this is actually needed,
/// otherwise it will quote the string
pub impl Show for Fpath with fn to_string(x) {
  x.0
}

///|
pub extend Fpath with Show::{to_string, output}

///|
/// JSON serialization - serializes as plain string
pub impl ToJson for Fpath with fn to_json(x) {
  Json::string(x.0)
}

///|
pub extend Fpath with ToJson::{to_json}

///|
/// Convert backslashes to forward slashes (for Windows paths)
/// Normalize path separators to '/' (Windows->Unix style), preserving relative structure.
pub fn ensure_unix(path : Fpath) -> Fpath {
  let chars : Array[Char] = []
  for i = 0; i < path.length(); i = i + 1 {
    let code = path[i]
    let c = code.unsafe_to_char()
    if c == '\\' {
      let _ = chars.push('/')
    } else {
      let _ = chars.push(c)
    }
  }
  Fpath(String::from_array(chars))
}

///|
/// Ensure path ends with '/' (for directories)
/// Ensure trailing '/' for directory semantics (adds if missing; empty -> "./").
pub fn ensure_directoryness(path : Fpath) -> Fpath {
  if path.is_empty() {
    Fpath("./")
  } else if path[path.length() - 1] == '/' {
    path
  } else {
    path + "/"
  }
}

///|
/// Sanitize a file path by removing dangerous segments
/// Removes: empty segments, ".", "..", and absolute path markers
/// Remove dangerous segments ("", ".", "..") and collapse separators (“/” and “\”).
pub fn sanitize(path : Fpath) -> Fpath {
  fn keep_segment(seg : String) -> Bool {
    seg != "" && seg != "." && seg != ".."
  }

  // Split on both / and \
  let segments : Array[String] = []
  let current_chars : Array[Char] = []
  for i = 0; i < path.length(); i = i + 1 {
    let code = path[i]
    let c = code.unsafe_to_char()
    if c == '/' || c == '\\' {
      if current_chars.length() > 0 {
        let current = String::from_array(current_chars)
        if keep_segment(current) {
          let _ = segments.push(current)
        }
        current_chars.clear()
      }
    } else {
      let _ = current_chars.push(c)
    }
  }

  // Don't forget the last segment
  if current_chars.length() > 0 {
    let current = String::from_array(current_chars)
    if keep_segment(current) {
      let _ = segments.push(current)
    }
  }

  // Join with /
  if segments.is_empty() {
    Fpath("")
  } else {
    let mut result = segments[0]
    for i = 1; i < segments.length(); i = i + 1 {
      result = result + "/" + segments[i]
    }
    Fpath(result)
  }
}