///|
pub(all) struct SteamShortcut {
  index : String
  app_id : UInt
  name : String
  exe : String
  start_dir : String
  launch_options : String
  tags : Array[String]
} derive(Eq, Debug)

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

///|
pub extend SteamShortcut with @debug.Debug::{to_repr}

///|
struct ShortcutFile {
  entries : Array[BinaryEntry]
}

///|
pub fn new_shortcut_file() -> ShortcutFile {
  { entries: [{ key: "shortcuts", value: ObjectValue([]), }], }
}

///|
pub fn read_shortcut_file(
  entries : Array[BinaryEntry],
) -> ShortcutFile raise SteamError {
  let result = ShortcutFile::{ entries, }
  result.shortcuts() |> ignore
  result
}

///|
fn binary_field(entries : Array[BinaryEntry], key : String) -> BinaryValue? {
  for entry in entries {
    if entry.key.equal_ignore_ascii_case(key) {
      return Some(entry.value)
    }
  }
  None
}

///|
fn shortcut_text(
  fields : Array[BinaryEntry],
  key : String,
  required : Bool,
) -> String raise SteamError {
  match binary_field(fields, key) {
    Some(StringValue(text)) => text
    None if !required => ""
    _ => raise InvalidManifest("Invalid shortcut text field: " + key)
  }
}

///|
fn ShortcutFile::root_index(self : ShortcutFile) -> Int raise SteamError {
  let mut result = -1
  for i in 0..= 0 {
        raise InvalidManifest("Duplicate shortcuts root")
      }
      result = i
    }
  }
  if result < 0 {
    raise InvalidManifest("Missing shortcuts root")
  }
  result
}

///|
fn ShortcutFile::children(
  self : ShortcutFile,
) -> Array[BinaryEntry] raise SteamError {
  guard self.entries[self.root_index()].value is ObjectValue(children) else {
    raise InvalidManifest("shortcuts root must be an object")
  }
  children
}

///|
pub fn ShortcutFile::shortcuts(
  self : ShortcutFile,
) -> Array[SteamShortcut] raise SteamError {
  let result : Array[SteamShortcut] = []
  let indices : Map[String, Bool] = Map([])
  for entry in self.children() {
    if indices.contains(entry.key) {
      raise InvalidManifest("Duplicate shortcut index")
    }
    indices[entry.key] = true
    guard entry.value is ObjectValue(fields) else {
      raise InvalidManifest("Shortcut entry must be an object")
    }
    let name = shortcut_text(fields, "AppName", true)
    let exe = shortcut_text(fields, "exe", true)
    let app_id = match binary_field(fields, "appid") {
      Some(Int32Value(n)) => n.reinterpret_as_uint()
      None => shortcut_app_id(name, exe)
      _ => raise InvalidManifest("Shortcut appid must be Int32")
    }
    let tags = match binary_field(fields, "tags") {
      None => []
      Some(ObjectValue(tags)) =>
        tags.map(tag => {
          match tag.value {
            StringValue(s) => s
            _ => raise InvalidManifest("Shortcut tag must be text")
          }
        })
      _ => raise InvalidManifest("Shortcut tags must be an object")
    }
    result.push({
      index: entry.key,
      app_id,
      name,
      exe,
      start_dir: shortcut_text(fields, "StartDir", false),
      launch_options: shortcut_text(fields, "LaunchOptions", false),
      tags,
    })
  }
  result
}

///|
/// Stable Steam shortcut identifier: CRC-32/ISO-HDLC(exe + name), high bit set.
pub fn shortcut_app_id(name : String, exe : String) -> UInt {
  let mut crc = 0xffffffffU
  for byte in @utf8.encode((exe + name)[:]) {
    crc = crc ^ byte.to_uint()
    for _ in 0..<8 {
      crc = if (crc & 1U) != 0U { (crc >> 1) ^ 0xedb88320U } else { crc >> 1 }
    }
  }
  (crc ^ 0xffffffffU) | 0x80000000U
}

///|
pub fn SteamShortcut::url(self : SteamShortcut) -> String {
  "steam://rungameid/" +
  ((self.app_id.to_uint64() << 32) | 0x02000000UL).to_string()
}

///|
pub fn ShortcutFile::to_binary(self : ShortcutFile) -> Array[BinaryEntry] {
  self.entries
}

///|
fn ShortcutFile::with_children(
  self : ShortcutFile,
  children : Array[BinaryEntry],
) -> ShortcutFile raise SteamError {
  let index = self.root_index()
  let entries = self.entries.map(e => e)
  entries[index] = { key: entries[index].key, value: ObjectValue(children), }
  read_shortcut_file(entries)
}

///|
/// Replace modeled fields while retaining unknown fields and all other entries.
/// The supplied app_id is kept exactly; callers can deliberately preserve identity.
pub fn ShortcutFile::set(
  self : ShortcutFile,
  shortcut : SteamShortcut,
) -> ShortcutFile raise SteamError {
  let children = self.children().map(e => e)
  let mut position = -1
  for i in 0.. BinaryEntry::{
    key: "",
    value: StringValue(s),
  })
  for i in 0.. e)
  for replacement in replacements {
    let mut replaced = false
    for i in 0.. ShortcutFile raise SteamError {
  if name.is_empty() || exe.is_empty() {
    raise InvalidManifest("Shortcut name and executable must not be empty")
  }
  let children = self.children().map(e => e)
  let indices : Map[String, Bool] = Map([])
  for entry in children {
    indices[entry.key] = true
  }
  let mut index = 0
  while indices.contains(index.to_string()) {
    index += 1
  }
  let key = index.to_string()
  children.push({ key, value: ObjectValue([]), })
  let raw = ShortcutFile::{ entries: self.entries.map(e => e), }
  let entries = raw.entries.map(e => e)
  let root = raw.root_index()
  entries[root] = { key: entries[root].key, value: ObjectValue(children), }
  ShortcutFile::{ entries, }.set({
    index: key,
    app_id: shortcut_app_id(name, exe),
    name,
    exe,
    start_dir,
    launch_options,
    tags,
  })
}

///|
pub fn ShortcutFile::remove(
  self : ShortcutFile,
  index : String,
) -> ShortcutFile raise SteamError {
  let children = self.children()
  if !children.any(e => e.key == index) {
    raise InvalidManifest("Shortcut index does not exist")
  }
  self.with_children(children.filter(e => e.key != index))
}