///|
fn normalize_path(path : String) -> String {
  let trimmed = path.trim(chars=" \n\r\t").to_owned()
  if trimmed.is_empty() {
    "."
  } else {
    trimmed.replace_all(old="\\", new="/")
  }
}

///|
fn dirname_string(path : String) -> String {
  let normalized = normalize_path(path)
  match normalized.rev_find("/") {
    Some(index) => if index == 0 { "/" } else { normalized[:index].to_owned() }
    None => "."
  }
}

///|
fn join_path(base : String, child : String) -> String {
  let left = normalize_path(base)
  let right = normalize_path(child)
  if left == "." || left.is_empty() {
    right
  } else if right == "." || right.is_empty() {
    left
  } else if left.has_suffix("/") {
    "\{left}\{right}"
  } else {
    "\{left}/\{right}"
  }
}

///|
fn join_strings(items : Array[String], sep : String) -> String {
  if items.is_empty() {
    ""
  } else {
    let buffer = StringBuilder::new()
    for index = 0; index < items.length(); index = index + 1 {
      if index > 0 {
        buffer.write_string(sep)
      }
      buffer.write_string(items[index])
    }
    buffer.to_string()
  }
}

///|
fn ensure_dir_all(path : String) -> Unit raise MoonforgeError {
  let normalized = normalize_path(path)
  if normalized == "." || normalized.is_empty() || @fs.path_exists(normalized) {
    ()
  } else {
    let parent = dirname_string(normalized)
    if parent != normalized {
      ensure_dir_all(parent)
    }
    @fs.create_dir(normalized) catch {
      @fs.IOError(message) =>
        config_error("failed to create directory '\{normalized}': \{message}")
    }
  }
}

///|
fn ensure_output_parents(
  root : String,
  outputs : Array[String],
) -> Unit raise MoonforgeError {
  for output in outputs {
    let resolved = resolve_project_path(root, output)
    ensure_dir_all(dirname_string(resolved))
  }
}

///|
fn hash_string(parts : Array[String]) -> String {
  let ctx = @crypto.SHA256::new()
  for part in parts {
    ctx.update(@utf8.encode(part))
    ctx.update(@utf8.encode("\n--moonforge--\n"))
  }
  @crypto.bytes_to_hex_string(ctx.finalize())
}

///|
fn hash_bytes(bytes : Bytes) -> String {
  @crypto.bytes_to_hex_string(@crypto.sha256(bytes))
}

///|
fn remove_path_recursive(path : String) -> Unit raise MoonforgeError {
  let normalized = normalize_path(path)
  if !@fs.path_exists(normalized) {
    ()
  } else {
    let is_dir = @fs.is_dir(normalized) catch {
      @fs.IOError(message) =>
        execution_error("failed to inspect '\{normalized}': \{message}")
    }
    if is_dir {
      let entries = @fs.read_dir(normalized) catch {
        @fs.IOError(message) =>
          execution_error(
            "failed to read directory '\{normalized}': \{message}",
          )
      }
      for entry in entries {
        remove_path_recursive(join_path(normalized, entry))
      }
      @fs.remove_dir(normalized) catch {
        @fs.IOError(message) =>
          execution_error(
            "failed to remove directory '\{normalized}': \{message}",
          )
      }
    } else {
      @fs.remove_file(normalized) catch {
        @fs.IOError(message) =>
          execution_error("failed to remove file '\{normalized}': \{message}")
      }
    }
  }
}

///|
fn read_text_file(path : String) -> String raise MoonforgeError {
  @fs.read_file_to_string(path) catch {
    @fs.IOError(message) => config_error("failed to read '\{path}': \{message}")
  }
}

///|
fn write_text_file(
  path : String,
  content : String,
) -> Unit raise MoonforgeError {
  ensure_dir_all(dirname_string(path))
  @fs.write_string_to_file(path, content) catch {
    @fs.IOError(message) => cache_error("failed to write '\{path}': \{message}")
  }
}