///|
pub(all) enum Path {
  WPath(WinPath)
  UPath(UnixPath)
} derive(ToJson, Hash, Eq)

///|
/// [Microsoft Naming a Volume](https://learn.microsoft.com/en-us/windows/win32/fileio/naming-a-volume)
/// [Microsoft Naming Files, Paths, and Namespaces](https://learn.microsoft.com/en-us/windows/win32/fileio/naming-a-file)
/// [Microsoft Maximum Path Length Limitation](https://learn.microsoft.com/en-us/windows/win32/fileio/maximum-file-path-limitation)
pub struct WinPath {
  prefix : WinPrefix
  directory : WinDirectory
  file : WinFile?
} derive(ToJson, Hash, Eq)

///|
/// 
/// #### Verbatim Prefix
/// 
/// refer to [Microsoft Win32 File Namespaces](https://learn.microsoft.com/en-us/windows/win32/fileio/naming-a-file#win32-file-namespaces)
/// > For file I/O,
/// > the "\\?\" prefix to a path string tells the Windows APIs to disable all string parsing and to send the string that follows it straight to the file system.
/// 
/// #### Windows Encoding 
/// 
/// refer to [Microsoft Path Unicode Normalization](https://learn.microsoft.com/en-us/windows/win32/fileio/maximum-file-path-limitation)
/// 
/// > There is no need to perform any Unicode normalization on path and file name strings for use by the Windows file I/O API functions 
/// > because the file system treats path and file names as an opaque sequence of WCHARs.
/// > Any normalization that your application requires should be performed with this in mind, 
/// > external of any calls to related Windows file I/O API functions.
/// 
/// #### Relative Paths Cannot use Verbatim Prefix
/// 
/// refer to [Microsoft Verbatim restriction](https://learn.microsoft.com/en-us/windows/win32/fileio/maximum-file-path-limitation)
/// 
/// > Because you cannot use the "\\?\" prefix with a relative path, 
/// > relative paths are always limited to a total of MAX_PATH characters.
/// 
pub(all) enum WinPrefix {
  /// Syntax: `\`
  ///
  /// Example: `\Users\username\Documents\`
  Root
  /// Syntax: `\\?\\` 
  ///
  /// Example: `\\?\GLOBALROOT\`
  VerbatimSymlink(symlink~ : String)
  /// Syntax: `\\?\UNC\\\`
  ///
  /// Example: `\\?\UNC\server\share\`
  VerbatimUNC(hostname~ : String, shared_folder~ : String)
  /// Syntax: `\\?\:\`
  ///
  /// Example: `\\?\C:\`
  VerbatimVolumeLetter(Char)
  /// Syntax: `\\?\Volume{}`
  ///
  /// Example: `\\?\Volume{26a21bda-a627-11d7-9931-806e6f6e6963}`
  ///
  /// Warning: this letter is case-insensitive.
  ///
  /// [Microsoft Enumerating Volume GUID Paths](https://learn.microsoft.com/en-us/windows/win32/fileio/enumerating-unique-volume-names)
  VerbatimVolumeGUID(String)
  /// Syntax: `\\.\\`
  ///
  /// Example: `\\.\COM56\`
  ///
  /// [Microsoft Win32 Device Namespaces](https://learn.microsoft.com/en-us/windows/win32/fileio/naming-a-file#win32-device-namespaces)
  DeviceNS(device~ : String)
  /// Syntax: `\\\\`
  ///
  /// Example: `\\server\share\`
  UNC(hostname~ : String, shared_folder~ : String)
  /// Syntax: `:\`
  ///
  /// Example: `C:\`
  ///
  /// Warning: this letter is case-insensitive.
  VolumeLetter(Char)
  None
} derive(ToJson, Hash, Eq)

///|
/// [Posix 4.13 Pathname Resolution](https://pubs.opengroup.org/onlinepubs/9699919799/basedefs/V1_chap04.html#tag_04_13)
/// 
/// 
pub struct UnixPath {
  is_absolute : Bool
  directory : UnixDirectory
  file : UnixFile?
} derive(ToJson, Hash, Eq)

///|
pub struct File[Component] {
  base : Component
  extension : Component?
} derive(ToJson, Hash, Eq)

///|
pub typealias Directory[WinPathComponent] as WinDirectory

///|
pub typealias Directory[UnixPathComponent] as UnixDirectory

///|
pub typealias File[WinPathComponent] as WinFile

///|
pub typealias File[UnixPathComponent] as UnixFile

///|
struct Directory[Component](FixedArray[Component]) derive(ToJson, Hash, Eq)

///|
struct WinPathComponent(String) derive(ToJson, Hash, Eq)

///|
struct UnixPathComponent(String) derive(ToJson, Hash, Eq)

///|
trait PlatformPath {
  from(s : StringView) -> Self raise PathError
}

///|
pub impl PlatformPath for WinPathComponent with from(s) {
  WinPathComponent::from(s, start=0)
}

///|
pub(all) suberror PathError {
  PathError(start~ : Int, message~ : String)
} derive(Show)

///|
let windows_reserved_names : FixedArray[String] = [
  ///
  "CON", "PRN", "AUX", "NUL",
  ///
   "COM1", "COM2", "COM3", "COM4", "COM5", "COM6", "COM7", "COM8", "COM9",
  ///
   "LPT1", "LPT2", "LPT3", "LPT4", "LPT5", "LPT6", "LPT7", "LPT8", "LPT9",
  ///
   "COM\u00b9", "COM\u00b2", "COM\u00b3",
  ///
   "LPT\u00b9", "LPT\u00b2", "LPT\u00b3",
]

///|
let windows_reserved_names_set : Set[StringView] = Set::from_iter(
  windows_reserved_names.iter().map(x => x),
)

///|
fn is_windows_reserved_character(ch : Char) -> Bool {
  match ch {
    '<'
    | '>'
    | ':'
    | '\"'
    | '/'
    | '\\'
    | '|'
    | '?'
    | '*'
    | '\u0000'
    | '\u0001'..='\u001F' => true
    _ => false
  }
}

///|
struct PathBuilder {
  prefix : WinPrefix?
  is_absolute : Bool
  directory : Array[String]
  mut basename : String?
  mut extension : String?
}

///|
pub fn PathBuilder::from_path(path : Path) -> PathBuilder {
  match path {
    UPath(path) => {
      let prefix = Option::None
      let is_absolute = path.is_absolute()
      let directory = path.directory[:]
        .iter()
        .map(IsString::to_string)
        .collect()
      let mut basename : String? = None
      let mut extension : String? = None
      match path.file {
        Some({ base, extension: Some(UnixPathComponent(ext)) }) => {
          basename = Some(base.0)
          extension = Some(ext)
        }
        Some({ base, extension: None }) => basename = Some(base.0)
        _ => ()
      }
      PathBuilder::{ prefix, is_absolute, directory, basename, extension }
    }
    WPath(path) => {
      let prefix = Some(path.prefix)
      let is_absolute = path.is_absolute()
      let directory = path.directory[:]
        .iter()
        .map(IsString::to_string)
        .collect()
      let mut basename : String? = None
      let mut extension : String? = None
      match path.file {
        Some({ base, extension: Some(WinPathComponent(ext)) }) => {
          basename = Some(base.0)
          extension = Some(ext)
        }
        Some({ base, extension: None }) => basename = Some(base.0)
        _ => ()
      }
      PathBuilder::{ prefix, is_absolute, directory, basename, extension }
    }
  }
}

///|
pub fn PathBuilder::to_path(self : PathBuilder) -> Path {
  match self.prefix {
    Some(prefix) => {
      // windows path
      let components : Array[WinPathComponent] = self.directory[:]
        .iter()
        .map(x => WinPathComponent(x))
        .collect()
      let directory = Directory(FixedArray::from_array(components))
      let file : WinFile? = match self.basename {
        Some(b) => {
          let ext : WinPathComponent? = match self.extension {
            Some(e) => Some(WinPathComponent(e))
            None => None
          }
          Some(File::{ base: WinPathComponent(b), extension: ext })
        }
        None => None
      }
      WPath(WinPath::new(prefix~, directory~, file?))
    }
    None => {
      // unix path
      let components : Array[UnixPathComponent] = self.directory[:]
        .iter()
        .map(x => UnixPathComponent(x))
        .collect()
      let directory = Directory(FixedArray::from_array(components))
      let file : UnixFile? = match self.basename {
        Some(b) => {
          let ext : UnixPathComponent? = match self.extension {
            Some(e) => Some(UnixPathComponent(e))
            None => None
          }
          Some(File::{ base: UnixPathComponent(b), extension: ext })
        }
        None => None
      }
      UPath(UnixPath::new(is_absolute=self.is_absolute, directory~, file?))
    }
  }
}

///|
/// When PathBuilder contains filename add_directory would pop this filename, and then add directory in its parent directory
pub fn PathBuilder::add_directory(
  self : PathBuilder,
  directory : StringView,
) -> Unit raise PathError {
  match self.prefix {
    Some(_) => {
      // windows path
      // If there is a basename (filename) remove it, then push the new directory
      if self.basename is Some(_) {
        self.basename = None
        self.extension = None
      }
      let _comp = WinPathComponent::from(directory, start=0)
      self.directory.push(_comp.0)
    }
    None => {
      // unix path
      if self.basename is Some(_) {
        self.basename = None
        self.extension = None
      }
      let _comp = UnixPathComponent::from(directory, start=0)
      self.directory.push(_comp.0)
    }
  }
}

///|
pub fn PathBuilder::set_basename(
  self : PathBuilder,
  basename : StringView,
) -> Unit raise PathError {
  match self.prefix {
    Some(_) => {
      // windows path
      let comp = WinPathComponent::from(basename, start=0)
      self.basename = Some(comp.0)
    }
    None => {
      // unix path
      let comp = UnixPathComponent::from(basename, start=0)
      self.basename = Some(comp.0)
    }
  }
}

///|
pub fn PathBuilder::set_extension(
  self : PathBuilder,
  extension : StringView,
) -> Unit raise PathError {
  match self.prefix {
    Some(_) => {
      // windows path
      // validate extension (must not contain '.') using WinPathComponent rules
      for i, ch in extension {
        if ch == '.' {
          raise PathError(
            start=i,
            message="Extension must not contain '.' character.",
          )
        }
      }
      let comp = WinPathComponent::from(extension, start=0)
      self.extension = Some(comp.0)
    }
    None => {
      // unix path
      let comp = UnixPathComponent::from(extension, start=0)
      self.extension = Some(comp.0)
    }
  }
}

///|
pub fn PathBuilder::set_file(
  self : PathBuilder,
  base~ : StringView,
  extension~ : StringView,
) -> Unit raise PathError {
  self.set_basename(base)
  self.set_extension(extension)
}