///|
/// Failures while resolving framework-owned application paths.
pub(all) suberror AppPathError {
  MissingApplicationIdentifier
  InvalidIdentifier(identifier~ : String)
  MissingHomeDirectory(platform~ : String)
  MissingEnvironmentPath(name~ : String, platform~ : String)
  MissingExecutablePath
  UnsupportedPath(kind~ : String, platform~ : String)
  InvalidPathOverride(kind~ : String, path~ : String, reason~ : String)
  SystemPath(kind~ : String, status~ : Int, detail~ : String)
  PlatformProbe(status~ : Int, detail~ : String)
} derive(Debug, Eq)

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

///|
pub extend AppPathError with Debug::{to_repr}

///|
pub fn AppPathError::message(self : AppPathError) -> String {
  match self {
    MissingApplicationIdentifier => "application identifier is not configured"
    InvalidIdentifier(identifier~) =>
      "invalid application identifier: " + identifier
    MissingHomeDirectory(platform~) =>
      "cannot resolve the home directory on " + platform
    MissingEnvironmentPath(name~, platform~) =>
      "cannot resolve " + name + " on " + platform
    MissingExecutablePath => "cannot resolve the application executable path"
    UnsupportedPath(kind~, platform~) =>
      "application path " + kind + " is not available on " + platform
    InvalidPathOverride(kind~, path~, reason~) =>
      "cannot override application path " +
      kind +
      " with " +
      path +
      ": " +
      reason
    SystemPath(kind~, status~, detail~) =>
      "cannot resolve application path " +
      kind +
      " (" +
      status.to_string() +
      "): " +
      detail
    PlatformProbe(status~, detail~) =>
      "cannot determine the Proton platform (" +
      status.to_string() +
      "): " +
      detail
  }
}

///|
/// Standard application paths corresponding to Electron's high-frequency
/// `app.getPath` names.
pub(all) enum AppPathKind {
  Home
  AppData
  UserData
  SessionData
  Temp
  Executable
  Logs
  Assets
  Module
  Desktop
  Documents
  Downloads
  Music
  Pictures
  Videos
  Recent
} derive(Debug, Eq)

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

///|
pub extend AppPathKind with Debug::{to_repr}

///|
fn AppPathKind::electron_name(self : AppPathKind) -> String {
  match self {
    Home => "home"
    AppData => "appData"
    UserData => "userData"
    SessionData => "sessionData"
    Temp => "temp"
    Executable => "exe"
    Logs => "logs"
    Assets => "assets"
    Module => "module"
    Desktop => "desktop"
    Documents => "documents"
    Downloads => "downloads"
    Music => "music"
    Pictures => "pictures"
    Videos => "videos"
    Recent => "recent"
  }
}

///|
fn AppPathKind::expects_directory(self : AppPathKind) -> Bool {
  match self {
    Executable | Module => false
    _ => true
  }
}

///|
/// Resolves the absolute application resource directory.
///
/// `proton_cli dev` supplies the project resource directory explicitly. A
/// packaged application resolves the package resource directory beside its
/// executable. Directly launched code uses the current working directory.
pub fn resource_dir() -> String {
  runtime_resource_dir()
}

///|
/// Returns the application resource root, corresponding to Electron's
/// `app.getAppPath()`.
pub fn app_path() -> String {
  runtime_resource_dir()
}

///|
/// Returns whether this process is running from a Proton package.
pub fn is_packaged() -> Bool {
  packaged_application_manifest_path() is Some(_)
}

///|
fn runtime_resource_dir() -> String {
  match @env.get_env_var("PROTON_RESOURCE_DIR") {
    Some(value) if value.trim().to_owned() != "" => {
      let path : @mbpath.Path = value.trim().to_owned()
      return path.resolve().to_string()
    }
    _ => ()
  }
  match packaged_resource_dir() {
    Some(path) => path
    None => {
      let current : @mbpath.Path = "."
      current.resolve().to_string()
    }
  }
}

///|
fn packaged_resource_dir() -> String? {
  packaged_application_manifest_path().map(path => {
    packaged_resource_dir_from_manifest(path, @mbpath.sep == '\\')
  })
}

///|
fn packaged_resource_dir_from_manifest(path : String, windows : Bool) -> String {
  let manifest : @mbpath.Path = path
  let root = manifest.dirname()
  if windows {
    root.join("Resources").normalize().to_string()
  } else {
    root.normalize().to_string()
  }
}

///|
fn packaged_application_manifest_path() -> String? {
  let args = @env.args()
  guard args.length() > 0 else { return None }
  let executable : @mbpath.Path = args[0]
  let executable = executable.resolve()
  let executable_dir = executable.dirname()
  let candidates = if @mbpath.sep == '\\' {
    [executable_dir.join("proton-package.json")]
  } else {
    [
      executable_dir.join("../Resources/proton-package.json"),
      executable_dir
      .join("../share")
      .join(executable.basename().to_owned())
      .join("proton-package.json"),
    ]
  }
  for candidate in candidates {
    let candidate = candidate.normalize()
    let packaged = @mbfs.is_file(candidate.to_string()) catch { _ => false }
    if packaged {
      return Some(candidate.to_string())
    }
  }
  None
}

///|
/// Resolves the stable directory for this application's persistent native data.
/// This function resolves the path but does not create the directory.
pub fn App::data_dir(self : App) -> String raise AppPathError {
  self.path(AppPathKind::UserData)
}

///|
/// Overrides a standard application path before the application runtime starts.
///
/// The path must be absolute and already exist, matching Electron's `setPath`
/// validation. Runtime-owned paths such as `SessionData` and `Logs` are
/// consumed by Proton startup in addition to being returned by `App::path`.
pub fn App::set_path(
  self : App,
  kind : AppPathKind,
  path : String,
) -> App raise AppPathError {
  let platform = runtime_platform()
  ensure_path_available(kind, platform)
  let normalized = normalize_path_override(kind, path, true)
  self.path_overrides[kind.electron_name()] = normalized
  if kind == AppPathKind::Logs {
    self.electron_default_logs_path = false
    self.create_logs_path = false
  }
  self
}

///|
/// Selects the application log directory before runtime startup.
///
/// A custom absolute path is created when the application starts. Omitting the
/// path selects Electron's default: the platform log directory on macOS and a
/// `logs` directory inside `UserData` on Windows and Linux.
pub fn App::set_app_logs_path(
  self : App,
  path? : String,
) -> App raise AppPathError {
  match path {
    Some(path) => {
      self.path_overrides[AppPathKind::Logs.electron_name()] = normalize_path_override(
        AppPathKind::Logs,
        path,
        false,
      )
      self.electron_default_logs_path = false
    }
    None => self.electron_default_logs_path = true
  }
  self.create_logs_path = true
  self
}

///|
/// Resolves a standard application path.
///
/// `UserData`, `SessionData`, and `Logs` use the application's stable
/// reverse-DNS identifier instead of its display name so package renames do not
/// move persisted state.
pub fn App::path(self : App, kind : AppPathKind) -> String raise AppPathError {
  let platform = runtime_platform()
  ensure_path_available(kind, platform)
  if kind == AppPathKind::Logs && self.electron_default_logs_path {
    return self.electron_default_log_dir(platform)
  }
  match self.path_overrides.get(kind.electron_name()) {
    Some(path) => return path
    None => ()
  }
  match kind {
    Home =>
      home_dir_for_platform(
        platform,
        @env.get_env_var("HOME"),
        @env.get_env_var("USERPROFILE"),
      )
    AppData =>
      application_data_root_for_platform(
        platform,
        @env.get_env_var("HOME"),
        @env.get_env_var("APPDATA"),
        @env.get_env_var("XDG_CONFIG_HOME"),
      )
    UserData =>
      app_data_dir_for_platform(
        platform,
        self.application_identifier(),
        @env.get_env_var("HOME"),
        @env.get_env_var("LOCALAPPDATA"),
        @env.get_env_var("XDG_DATA_HOME"),
      )
    SessionData => self.session_data_path(self.application_identifier())
    Temp =>
      temp_dir_for_platform(
        platform,
        @env.get_env_var("TMPDIR"),
        @env.get_env_var("TEMP"),
        @env.get_env_var("TMP"),
      )
    Executable => executable_path()
    Logs =>
      app_log_dir_for_platform(
        platform,
        self.application_identifier(),
        @env.get_env_var("HOME"),
        @env.get_env_var("LOCALAPPDATA"),
        @env.get_env_var("XDG_STATE_HOME"),
      )
    Assets => executable_dir()
    Module => executable_path()
    Desktop => resolve_system_path(@native.SystemPathKind::Desktop, "desktop")
    Documents =>
      resolve_system_path(@native.SystemPathKind::Documents, "documents")
    Downloads =>
      resolve_system_path(@native.SystemPathKind::Downloads, "downloads")
    Music => resolve_system_path(@native.SystemPathKind::Music, "music")
    Pictures =>
      resolve_system_path(@native.SystemPathKind::Pictures, "pictures")
    Videos => resolve_system_path(@native.SystemPathKind::Videos, "videos")
    Recent => resolve_system_path(@native.SystemPathKind::Recent, "recent")
  }
}

///|
fn App::session_data_path(
  self : App,
  identifier : String,
) -> String raise AppPathError {
  match self.path_overrides.get(AppPathKind::SessionData.electron_name()) {
    Some(path) => path
    None => {
      let user_data = match
        self.path_overrides.get(AppPathKind::UserData.electron_name()) {
        Some(path) => path
        None =>
          app_data_dir_for_platform(
            runtime_platform(),
            identifier,
            @env.get_env_var("HOME"),
            @env.get_env_var("LOCALAPPDATA"),
            @env.get_env_var("XDG_DATA_HOME"),
          )
      }
      @mbpath.Path(user_data).join("browser").normalize().to_string()
    }
  }
}

///|
fn runtime_platform() -> String raise AppPathError {
  let info = @native.runtime_info() catch {
    error => raise PlatformProbe(status=error.status(), detail=error.message())
  }
  info.platform
}

///|
fn ensure_path_available(
  kind : AppPathKind,
  platform : String,
) -> Unit raise AppPathError {
  match kind {
    Assets if platform != "windows" && platform != "linux" =>
      raise UnsupportedPath(kind="assets", platform~)
    Recent if platform != "windows" =>
      raise UnsupportedPath(kind="recent", platform~)
    _ => ()
  }
}

///|
fn normalize_path_override(
  kind : AppPathKind,
  path : String,
  must_exist : Bool,
) -> String raise AppPathError {
  let path = path.trim().to_owned()
  guard path != "" else {
    raise InvalidPathOverride(
      kind=kind.electron_name(),
      path~,
      reason="path must not be empty",
    )
  }
  let candidate : @mbpath.Path = path
  guard candidate.is_absolute() else {
    raise InvalidPathOverride(
      kind=kind.electron_name(),
      path~,
      reason="path must be absolute",
    )
  }
  let normalized = candidate.normalize().to_string()
  let exists = @mbfs.path_exists(normalized)
  if must_exist && !exists {
    raise InvalidPathOverride(
      kind=kind.electron_name(),
      path=normalized,
      reason="path does not exist",
    )
  }
  if exists && kind.expects_directory() && !path_is_directory(normalized) {
    raise InvalidPathOverride(
      kind=kind.electron_name(),
      path=normalized,
      reason="path is not a directory",
    )
  }
  if exists && !kind.expects_directory() && !path_is_file(normalized) {
    raise InvalidPathOverride(
      kind=kind.electron_name(),
      path=normalized,
      reason="path is not a file",
    )
  }
  normalized
}

///|
fn path_is_directory(path : String) -> Bool {
  @mbfs.is_dir(path) catch {
    _ => false
  }
}

///|
fn path_is_file(path : String) -> Bool {
  @mbfs.is_file(path) catch {
    _ => false
  }
}

///|
fn App::electron_default_log_dir(
  self : App,
  platform : String,
) -> String raise AppPathError {
  if platform == "macos" {
    app_log_dir_for_platform(
      platform,
      self.application_identifier(),
      @env.get_env_var("HOME"),
      @env.get_env_var("LOCALAPPDATA"),
      @env.get_env_var("XDG_STATE_HOME"),
    )
  } else {
    @mbpath.Path(self.path(AppPathKind::UserData))
    .join("logs")
    .normalize()
    .to_string()
  }
}

///|
fn executable_path() -> String raise AppPathError {
  guard @env.args().get(0) is Some(executable) else {
    raise MissingExecutablePath
  }
  let path : @mbpath.Path = executable
  path.resolve().to_string()
}

///|
fn executable_dir() -> String raise AppPathError {
  let executable : @mbpath.Path = executable_path()
  executable.dirname().normalize().to_string()
}

///|
fn resolve_system_path(
  kind : @native.SystemPathKind,
  label : String,
) -> String raise AppPathError {
  @native.system_path(kind) catch {
    error =>
      raise SystemPath(
        kind=label,
        status=error.status(),
        detail=error.message(),
      )
  }
}

///|
fn home_dir_for_platform(
  platform : String,
  home : String?,
  user_profile : String?,
) -> String raise AppPathError {
  let base = match platform {
    "windows" => user_profile
    "macos" | "linux" => home
    _ => None
  }
  match non_empty_path(base) {
    Some(path) => @mbpath.Path(path).normalize().to_string()
    None => raise MissingHomeDirectory(platform~)
  }
}

///|
fn application_data_root_for_platform(
  platform : String,
  home : String?,
  app_data : String?,
  xdg_config_home : String?,
) -> String raise AppPathError {
  let root = match platform {
    "macos" =>
      @mbpath.Path(require_path_base(home, platform)).join(
        "Library/Application Support",
      )
    "windows" =>
      @mbpath.Path(
        require_environment_path(app_data, "application data", platform),
      )
    "linux" =>
      match non_empty_path(xdg_config_home) {
        Some(path) => @mbpath.Path(path)
        None => @mbpath.Path(require_path_base(home, platform)).join(".config")
      }
    _ => raise MissingEnvironmentPath(name="application data", platform~)
  }
  root.normalize().to_string()
}

///|
fn temp_dir_for_platform(
  platform : String,
  tmpdir : String?,
  temp : String?,
  tmp : String?,
) -> String raise AppPathError {
  let candidates = match platform {
    "windows" => [temp, tmp]
    "macos" | "linux" => [tmpdir, Some("/tmp")]
    _ => []
  }
  for candidate in candidates {
    if non_empty_path(candidate) is Some(path) {
      return @mbpath.Path(path).normalize().to_string()
    }
  }
  raise MissingEnvironmentPath(name="temporary directory", platform~)
}

///|
fn app_log_dir_for_platform(
  platform : String,
  identifier : String,
  home : String?,
  local_app_data : String?,
  xdg_state_home : String?,
) -> String raise AppPathError {
  validate_app_identifier(identifier)
  match platform {
    "macos" =>
      @mbpath.Path(require_path_base(home, platform))
      .join("Library/Logs")
      .join(identifier)
      .normalize()
      .to_string()
    "windows" =>
      @mbpath.Path(require_path_base(local_app_data, platform))
      .join(identifier)
      .join("Logs")
      .normalize()
      .to_string()
    "linux" => {
      let base = match non_empty_path(xdg_state_home) {
        Some(path) => @mbpath.Path(path)
        None =>
          @mbpath.Path(require_path_base(home, platform)).join(".local/state")
      }
      base.join(identifier).join("logs").normalize().to_string()
    }
    _ => raise MissingHomeDirectory(platform~)
  }
}

///|
fn app_data_dir_for_platform(
  platform : String,
  identifier : String,
  home : String?,
  local_app_data : String?,
  xdg_data_home : String?,
) -> String raise AppPathError {
  validate_app_identifier(identifier)
  let base = match platform {
    "macos" =>
      @mbpath.Path(require_path_base(home, platform)).join(
        "Library/Application Support",
      )
    "windows" => @mbpath.Path(require_path_base(local_app_data, platform))
    "linux" =>
      match non_empty_path(xdg_data_home) {
        Some(path) => @mbpath.Path(path)
        None =>
          @mbpath.Path(require_path_base(home, platform)).join(".local/share")
      }
    _ => raise MissingHomeDirectory(platform~)
  }
  base.join(identifier).normalize().to_string()
}

///|
fn validate_app_identifier(identifier : String) -> Unit raise AppPathError {
  let value = identifier.trim().to_owned()
  if value == "" ||
    value == "." ||
    value == ".." ||
    value.contains("/") ||
    value.contains("\\") ||
    value.contains("\u0000") {
    raise InvalidIdentifier(identifier~)
  }
}

///|
fn non_empty_path(value : String?) -> String? {
  match value {
    Some(path) => {
      let path = path.trim().to_owned()
      if path == "" {
        None
      } else {
        Some(path)
      }
    }
    None => None
  }
}

///|
fn require_path_base(
  value : String?,
  platform : String,
) -> String raise AppPathError {
  match non_empty_path(value) {
    Some(path) => path
    None => raise MissingHomeDirectory(platform~)
  }
}

///|
fn require_environment_path(
  value : String?,
  name : String,
  platform : String,
) -> String raise AppPathError {
  match non_empty_path(value) {
    Some(path) => path
    None => raise MissingEnvironmentPath(name~, platform~)
  }
}