///|
struct Windows {
  drive : BytesView
  root : BytesView
  anchor : BytesView
  parts : ArrayView[BytesView]
} derive(Eq, Hash)

///|
pub impl ToJson for Windows with to_json(self : Windows) -> Json {
  self.to_string().to_json()
}

///|
fn Windows::inspect(self : Windows) -> Json raise @utf8.Malformed {
  {
    "drive": @utf8.decode(self.drive),
    "root": @utf8.decode(self.root),
    "anchor": @utf8.decode(self.anchor),
    "parts": self.parts.map(@utf8.decode(_)),
  }
}

///|
pub impl @json.FromJson for Windows with from_json(
  json : Json,
  path : @json.JsonPath,
) -> Windows {
  let string : String = @json.FromJson::from_json(json, path)
  Windows::parse(@utf8.encode(string))
}

///|
pub fn Windows::parts(self : Windows) -> @builtin.Iter[BytesView] {
  self.parts.iter()
}

///|
pub fn Windows::drive(self : Windows) -> BytesView {
  self.drive
}

///|
pub fn Windows::root(self : Windows) -> BytesView {
  self.root
}

///|
pub fn Windows::anchor(self : Windows) -> BytesView {
  self.anchor
}

///|
pub impl Compare for Windows with compare(self : Windows, other : Windows) -> Int {
  if self.anchor != other.anchor {
    return self.anchor.compare(other.anchor)
  }
  return self.parts.compare(other.parts)
}

///|
fn Windows::parse(path : BytesView) -> Windows {
  enum UncState {
    Zero
    One
  }
  let drive = @buffer.new()
  let root = @buffer.new()
  fn make_path(parts : Array[BytesView]) -> Windows {
    let drive_string = drive.contents()
    let root = root.contents()
    drive.write_bytes(root)
    let anchor = drive.contents()
    return Windows::{ drive: drive_string, root, anchor, parts }
  }

  let parts : Array[BytesView] = []
  let part = @buffer.new()
  let path = match path {
    [drive_letter, ':', '/' | '\\', .. path] => {
      drive.write_byte(drive_letter)
      drive.write_byte(':')
      root.write_byte('\\')
      parts.push([drive_letter, ..b":\\"])
      skip~: loop path {
        ['/' | '\\', .. path] => continue skip~ path
        [char, .. path] => {
          part.write_byte(char)
          path
        }
        [] => return make_path(parts)
      }
    }
    [drive_letter, ':', char, .. path] => {
      drive.write_byte(drive_letter)
      drive.write_byte(':')
      parts.push([drive_letter, ':'])
      part.write_byte(char)
      path
    }
    [drive_letter, ':'] => {
      drive.write_byte(drive_letter)
      drive.write_byte(':')
      parts.push([drive_letter, ':'])
      return make_path(parts)
    }
    ['/' | '\\', '/' | '\\', .. path] => {
      drive.write_bytes("\\\\")
      unc~: loop (Zero, path) {
        (Zero, ['/' | '\\', .. path]) => {
          drive.write_byte('\\')
          continue unc~ (One, path)
        }
        (Zero, [char, .. path]) => {
          drive.write_byte(char)
          continue unc~ (Zero, path)
        }
        (Zero, []) => {
          parts.push(drive.contents())
          return make_path(parts)
        }
        (One, ['/' | '\\', .. path]) => {
          root.write_byte('\\')
          parts.push([..drive.contents(), '\\'])
          path
        }
        (One, [char, .. path]) => {
          drive.write_byte(char)
          continue unc~ (One, path)
        }
        (One, []) => {
          let drive = drive.contents()
          if drive is [.., '\\'] {
            parts.push(drive)
            return make_path(parts)
          } else {
            root.write_byte('\\')
            parts.push([..drive, '\\'])
            return make_path(parts)
          }
        }
      }
    }
    ['/' | '\\', char, .. path] => {
      root.write_byte('\\')
      parts.push("\\")
      part.write_byte(char)
      path
    }
    ['/' | '\\'] => {
      root.write_byte('\\')
      parts.push("\\")
      return make_path(parts)
    }
    [char, .. path] => {
      part.write_byte(char)
      path
    }
    [] => return make_path(parts)
  }
  fn push_part(part : BytesView) {
    if part is ("" | ".") {
      return
    }
    parts.push(part)
  }

  path~: loop path {
    ['/' | '\\', .. path] => {
      push_part(part.contents())
      skip~: loop path {
        ['/' | '\\', .. path] => continue skip~ path
        [char, .. path] => {
          part.reset()
          part.write_byte(char)
          continue path~ path
        }
        [] => break path~ make_path(parts)
      }
    }
    [char, .. path] => {
      part.write_byte(char)
      continue path~ path
    }
    [] => {
      push_part(part.contents())
      break path~ make_path(parts)
    }
  }
}

///|
test "parse_windows_path/empty" {
  @json.inspect(Windows::parse("").inspect(), content={
    "drive": "",
    "root": "",
    "anchor": "",
    "parts": [],
  })
}

///|
test "parse_windows_path/drive" {
  @json.inspect(Windows::parse("C:").inspect(), content={
    "drive": "C:",
    "root": "",
    "anchor": "C:",
    "parts": ["C:"],
  })
  @json.inspect(Windows::parse("ABC:").inspect(), content={
    "drive": "",
    "root": "",
    "anchor": "",
    "parts": ["ABC:"],
  })
  @json.inspect(Windows::parse("C:\\").inspect(), content={
    "drive": "C:",
    "root": "\\",
    "anchor": "C:\\",
    "parts": ["C:\\"],
  })
  @json.inspect(Windows::parse("C:\\\\").inspect(), content={
    "drive": "C:",
    "root": "\\",
    "anchor": "C:\\",
    "parts": ["C:\\"],
  })
  @json.inspect(Windows::parse("C:\\Windows").inspect(), content={
    "drive": "C:",
    "root": "\\",
    "anchor": "C:\\",
    "parts": ["C:\\", "Windows"],
  })
  @json.inspect(Windows::parse("C:\\\\Windows").inspect(), content={
    "drive": "C:",
    "root": "\\",
    "anchor": "C:\\",
    "parts": ["C:\\", "Windows"],
  })
  @json.inspect(Windows::parse("C:/Windows").inspect(), content={
    "drive": "C:",
    "root": "\\",
    "anchor": "C:\\",
    "parts": ["C:\\", "Windows"],
  })
  @json.inspect(Windows::parse("C:/Windows/").inspect(), content={
    "drive": "C:",
    "root": "\\",
    "anchor": "C:\\",
    "parts": ["C:\\", "Windows"],
  })
  @json.inspect(Windows::parse("C:\\Windows\\").inspect(), content={
    "drive": "C:",
    "root": "\\",
    "anchor": "C:\\",
    "parts": ["C:\\", "Windows"],
  })
  @json.inspect(Windows::parse("C:Windows").inspect(), content={
    "drive": "C:",
    "root": "",
    "anchor": "C:",
    "parts": ["C:", "Windows"],
  })
  @json.inspect(Windows::parse("C:Windows/").inspect(), content={
    "drive": "C:",
    "root": "",
    "anchor": "C:",
    "parts": ["C:", "Windows"],
  })
}

///|
test "parse_windows_path/root" {
  @json.inspect(Windows::parse("/").inspect(), content={
    "drive": "",
    "root": "\\",
    "anchor": "\\",
    "parts": ["\\"],
  })
  @json.inspect(Windows::parse("\\").inspect(), content={
    "drive": "",
    "root": "\\",
    "anchor": "\\",
    "parts": ["\\"],
  })
  @json.inspect(Windows::parse("Windows").inspect(), content={
    "drive": "",
    "root": "",
    "anchor": "",
    "parts": ["Windows"],
  })
  @json.inspect(Windows::parse("/Windows").inspect(), content={
    "drive": "",
    "root": "\\",
    "anchor": "\\",
    "parts": ["\\", "Windows"],
  })
  @json.inspect(Windows::parse("\\Windows").inspect(), content={
    "drive": "",
    "root": "\\",
    "anchor": "\\",
    "parts": ["\\", "Windows"],
  })
}

///|
test "parse_windows_path/unc" {
  @json.inspect(Windows::parse("//").inspect(), content={
    "drive": "\\\\",
    "root": "",
    "anchor": "\\\\",
    "parts": ["\\\\"],
  })
  @json.inspect(Windows::parse("\\\\").inspect(), content={
    "drive": "\\\\",
    "root": "",
    "anchor": "\\\\",
    "parts": ["\\\\"],
  })
  @json.inspect(Windows::parse("/\\").inspect(), content={
    "drive": "\\\\",
    "root": "",
    "anchor": "\\\\",
    "parts": ["\\\\"],
  })
  @json.inspect(Windows::parse("///").inspect(), content={
    "drive": "\\\\\\",
    "root": "",
    "anchor": "\\\\\\",
    "parts": ["\\\\\\"],
  })
  @json.inspect(Windows::parse("////").inspect(), content={
    "drive": "\\\\\\",
    "root": "\\",
    "anchor": "\\\\\\\\",
    "parts": ["\\\\\\\\"],
  })
  @json.inspect(Windows::parse("/////").inspect(), content={
    "drive": "\\\\\\",
    "root": "\\",
    "anchor": "\\\\\\\\",
    "parts": ["\\\\\\\\"],
  })
  @json.inspect(Windows::parse("//unc").inspect(), content={
    "drive": "\\\\unc",
    "root": "",
    "anchor": "\\\\unc",
    "parts": ["\\\\unc"],
  })
  @json.inspect(Windows::parse("//unc/").inspect(), content={
    "drive": "\\\\unc\\",
    "root": "",
    "anchor": "\\\\unc\\",
    "parts": ["\\\\unc\\"],
  })
  @json.inspect(Windows::parse("//unc/share").inspect(), content={
    "drive": "\\\\unc\\share",
    "root": "\\",
    "anchor": "\\\\unc\\share\\",
    "parts": ["\\\\unc\\share\\"],
  })
  @json.inspect(Windows::parse("//unc/share/").inspect(), content={
    "drive": "\\\\unc\\share",
    "root": "\\",
    "anchor": "\\\\unc\\share\\",
    "parts": ["\\\\unc\\share\\"],
  })
  @json.inspect(Windows::parse("\\\\unc\\share"), content="\\\\unc\\share\\")
  @json.inspect(Windows::parse("\\\\unc\\share\\"), content="\\\\unc\\share\\")
  @json.inspect(
    Windows::parse("//unc/share/a/b"),
    content="\\\\unc\\share\\a\\b",
  )
  @json.inspect(
    Windows::parse("\\\\unc\\share\\a\\b"),
    content="\\\\unc\\share\\a\\b",
  )
}

///|
test "parse_windows_path/collapse_slashes" {
  @json.inspect(Windows::parse("a//b///c////d").inspect(), content={
    "drive": "",
    "root": "",
    "anchor": "",
    "parts": ["a", "b", "c", "d"],
  })
  @json.inspect(Windows::parse("a\\\\b\\\\\\c\\\\\\\\d").inspect(), content={
    "drive": "",
    "root": "",
    "anchor": "",
    "parts": ["a", "b", "c", "d"],
  })
  @json.inspect(Windows::parse("a/b/c/d").inspect(), content={
    "drive": "",
    "root": "",
    "anchor": "",
    "parts": ["a", "b", "c", "d"],
  })
}

///|
test "parse_windows_path/collapse_dots" {
  @json.inspect(Windows::parse("./a/b/c").inspect(), content={
    "drive": "",
    "root": "",
    "anchor": "",
    "parts": ["a", "b", "c"],
  })
  @json.inspect(Windows::parse("a/b/c/.").inspect(), content={
    "drive": "",
    "root": "",
    "anchor": "",
    "parts": ["a", "b", "c"],
  })
  @json.inspect(Windows::parse("a/b/c/./d").inspect(), content={
    "drive": "",
    "root": "",
    "anchor": "",
    "parts": ["a", "b", "c", "d"],
  })
  @json.inspect(Windows::parse("a/b/c/././d").inspect(), content={
    "drive": "",
    "root": "",
    "anchor": "",
    "parts": ["a", "b", "c", "d"],
  })
  @json.inspect(Windows::parse("a/b/c/./././d").inspect(), content={
    "drive": "",
    "root": "",
    "anchor": "",
    "parts": ["a", "b", "c", "d"],
  })
  @json.inspect(Windows::parse("//./").inspect(), content={
    "drive": "\\\\.\\",
    "root": "",
    "anchor": "\\\\.\\",
    "parts": ["\\\\.\\"],
  })
  @json.inspect(Windows::parse("//./.").inspect(), content={
    "drive": "\\\\.\\.",
    "root": "\\",
    "anchor": "\\\\.\\.\\",
    "parts": ["\\\\.\\.\\"],
  })
  @json.inspect(Windows::parse("//./././a/b/c").inspect(), content={
    "drive": "\\\\.\\.",
    "root": "\\",
    "anchor": "\\\\.\\.\\",
    "parts": ["\\\\.\\.\\", "a", "b", "c"],
  })
}

///|
pub impl Show for Windows with output(self : Windows, logger : &Logger) -> Unit {
  if self.anchor == "" && self.parts is [] {
    logger.write_char('.')
    return
  }
  if self.drive != "" || self.root != "" {
    logger.write_string(@utf8.decode_lossy(self.drive))
    logger.write_string(@utf8.decode_lossy(self.root))
    let parts = self.parts[1:]
    for i, part in parts {
      logger.write_string(@utf8.decode_lossy(part))
      if i < parts.length() - 1 {
        logger.write_char('\\')
      }
    }
  } else {
    for i, part in self.parts {
      logger.write_string(@utf8.decode_lossy(part))
      if i < self.parts.length() - 1 {
        logger.write_char('\\')
      }
    }
  }
}

///|
pub fn Windows::to_bytes(self : Windows) -> Bytes {
  if self.anchor == "" && self.parts is [] {
    return "."
  }
  let buffer = @buffer.new()
  if self.drive != "" || self.root != "" {
    buffer.write_bytesview(self.drive)
    buffer.write_bytesview(self.root)
    let parts = self.parts[1:]
    for i, part in parts {
      buffer.write_bytesview(part)
      if i < parts.length() - 1 {
        buffer.write_byte('\\')
      }
    }
  } else {
    for i, part in self.parts {
      buffer.write_bytesview(part)
      if i < self.parts.length() - 1 {
        buffer.write_byte('\\')
      }
    }
  }
  buffer.contents()
}

///|
pub fn Windows::parent(self : Windows) -> Windows? {
  if self.parts.length() == 0 {
    return None
  }
  if (self.drive != "" || self.root != "") && self.parts.length() == 1 {
    return None
  }
  Some(Windows::{ ..self, parts: self.parts[:self.parts.length() - 1] })
}

///|
test "BasicPath::parent" {
  let path = Windows::{
    drive: "C:",
    root: "\\",
    anchor: "C:\\",
    parts: ["C:\\", "Windows", "System32"],
  }
  @json.inspect(path.parent(), content=["C:\\Windows"])
  let path = Windows::{
    drive: "C:",
    root: "\\",
    anchor: "C:\\",
    parts: ["C:\\"],
  }
  @json.inspect(path.parent() is None, content=true)
  let path = Windows::{ drive: "", root: "", anchor: "", parts: [] }
  @json.inspect(path.parent() is None, content=true)
}

///|
pub impl ToPosix for Windows with to_posix(self : Windows) -> Posix {
  fn convert_to_slash(source : BytesView, target : @buffer.Buffer) -> Unit {
    loop source {
      ['\\', .. chars] => {
        target.write_byte('/')
        continue chars
      }
      [char, .. chars] => {
        target.write_byte(char)
        continue chars
      }
      [] => ()
    }
  }

  if self.drive != "" || self.root != "" {
    let anchor = @buffer.new()
    convert_to_slash(self.drive, anchor)
    convert_to_slash(self.root, anchor)
    let anchor = Posix::parse(anchor.to_bytes())
    Posix::{ ..anchor, parts: [..anchor.parts, ..self.parts[1:]] }
  } else {
    Posix::{ root: self.root, anchor: self.anchor, parts: [..self.parts] }
  }
}

///|
pub fn[ToWindows : ToWindows] Windows::relative_to(
  self : Windows,
  other : ToWindows,
) -> Windows {
  let other = other.to_windows()
  let self_parts_length = self.parts.length()
  let other_parts_length = other.parts.length()
  let minimum_parts_length = @cmp.minimum(self_parts_length, other_parts_length)
  let start_index = if self.anchor != "" { 1 } else { 0 }
  let common_parts_length = for i in start_index.. Windows {
  if other.drive != "" {
    return other
  }
  if other.root != "" {
    return Windows::{
      drive: self.drive,
      root: other.root,
      anchor: [..self.drive, ..other.root],
      parts: other.parts,
    }
  }
  Windows::{ ..self, parts: [..self.parts, ..other.parts] }
}

///|
test "Path::join" {
  let a = Windows::{
    drive: "C:",
    root: "\\",
    anchor: "C:\\",
    parts: ["C:\\", "Windows"],
  }
  let b = Windows::{ drive: "", root: "", anchor: "", parts: ["System32"] }
  let c = a.join(b)
  @json.inspect(c.inspect(), content={
    "drive": "C:",
    "root": "\\",
    "anchor": "C:\\",
    "parts": ["C:\\", "Windows", "System32"],
  })
  let a = Windows::{
    drive: "C:",
    root: "\\",
    anchor: "C:\\",
    parts: ["C:\\", "Windows"],
  }
  let b = Windows::{
    drive: "D:",
    root: "\\",
    anchor: "D:\\",
    parts: ["D:\\", "System32"],
  }
  let c = a.join(b)
  @json.inspect(c.inspect(), content={
    "drive": "D:",
    "root": "\\",
    "anchor": "D:\\",
    "parts": ["D:\\", "System32"],
  })
  let a = Windows::{
    drive: "C:",
    root: "\\",
    anchor: "C:\\",
    parts: ["C:\\", "Windows"],
  }
  let b = Windows::{
    drive: "",
    root: "\\",
    anchor: "\\",
    parts: ["\\", "System32"],
  }
  let c = a.join(b)
  @json.inspect(c.inspect(), content={
    "drive": "C:",
    "root": "\\",
    "anchor": "C:\\",
    "parts": ["\\", "System32"],
  })
}

///|
pub fn Windows::suffix(self : Windows) -> BytesView? {
  guard self.name() is Some(name) else { None }
  loop name {
    [] => None
    [.., '.'] as view => Some(view.data()[view.start_offset():])
    [.. rest, _] => continue rest
  }
}

///|
pub fn Windows::name(self : Windows) -> BytesView? {
  let parts_length = self.parts.length()
  if self.anchor != "" && self.parts.length() == 1 {
    return None
  }
  Some(self.parts[parts_length - 1])
}

///|
pub trait ToWindows {
  to_windows(Self) -> Windows
}

///|
pub impl ToWindows for String with to_windows(self : String) -> Windows {
  ToWindows::to_windows(self.view())
}

///|
pub impl ToWindows for @string.View with to_windows(self : @string.View) -> Windows {
  Windows::parse(@utf8.encode(self))
}

///|
pub impl ToWindows for Bytes with to_windows(self : Bytes) -> Windows {
  Windows::parse(self)
}

///|
pub impl ToWindows for BytesView with to_windows(self : BytesView) -> Windows {
  Windows::parse(self)
}

///|
pub impl ToWindows for Windows with to_windows(self : Windows) -> Windows {
  self
}

///|
pub impl[T : ToWindows] ToWindows for Array[T] with to_windows(self : Array[T]) -> Windows {
  ToWindows::to_windows(self[:])
}

///|
pub impl[T : ToWindows] ToWindows for ArrayView[T] with to_windows(
  self : ArrayView[T],
) -> Windows {
  let mut path = Windows::{ drive: "", root: "", anchor: "", parts: [] }
  for item in self {
    path = path.join(item.to_windows())
  }
  path
}

///|
pub fn[ToWindows : ToWindows] windows(path : ToWindows) -> Windows {
  path.to_windows()
}