///|
let declaration_tags : Array[(String, Int, Int)] = [
  ("usage_page", 1, 0),
  ("logical_minimum", 1, 1),
  ("logical_maximum", 1, 2),
  ("physical_minimum", 1, 3),
  ("physical_maximum", 1, 4),
  ("unit_exponent", 1, 5),
  ("unit", 1, 6),
  ("report_size", 1, 7),
  ("report_id", 1, 8),
  ("report_count", 1, 9),
  ("push", 1, 10),
  ("pop", 1, 11),
  ("usage", 2, 0),
  ("usage_minimum", 2, 1),
  ("usage_maximum", 2, 2),
  ("designator_index", 2, 3),
  ("designator_minimum", 2, 4),
  ("designator_maximum", 2, 5),
  ("string_index", 2, 7),
  ("string_minimum", 2, 8),
  ("string_maximum", 2, 9),
  ("delimiter", 2, 10),
  ("input", 0, 8),
  ("output", 0, 9),
  ("collection", 0, 10),
  ("feature", 0, 11),
]

///|
priv struct DeclarationContext {
  builder : DescriptorBuilder
  sources : Array[(Int, Int, String)]
  saved : Array[(Int64, Int64)]
  mut logical_min : Int64
  mut physical_min : Int64
  mut count : Int
  mut active : Int
  mut error : Diagnostic?
}

///|
fn DeclarationContext::reject(
  self : DeclarationContext,
  path : String,
  message : String,
  code? : String = "declaration_value",
) -> Unit {
  if self.error is None {
    self.error = Some(diagnostic(self.active, code, "\{path}: \{message}"))
  }
}

///|
fn DeclarationContext::object(
  self : DeclarationContext,
  value : Json,
  path : String,
) -> Map[String, Json] {
  match value {
    Object(object) => object
    _ => {
      self.reject(path, "Expected an object")
      Map([])
    }
  }
}

///|
fn DeclarationContext::array(
  self : DeclarationContext,
  value : Json,
  path : String,
) -> Array[Json] {
  match value {
    Array(items) => items
    _ => {
      self.reject(path, "Expected an array")
      []
    }
  }
}

///|
fn DeclarationContext::number(
  self : DeclarationContext,
  value : Json,
  path : String,
) -> Int64 {
  let integer = match value {
    Number(number, ..) if !number.is_nan() &&
      !number.is_inf() &&
      number >= -2147483648.0 &&
      number <= 4294967295.0 &&
      number.to_int64().to_double() == number => number.to_int64()
    String(text) if text.has_prefix("0x") || text.has_prefix("-0x") =>
      @string.parse_int64(text, base=16) catch {
        _ => {
          self.reject(path, "Invalid hexadecimal integer")
          return 0L
        }
      }
    _ => {
      self.reject(path, "Expected a 32-bit integer or 0x string")
      return 0L
    }
  }
  if integer < -2147483648L || integer > 4294967295L {
    self.reject(path, "Integer exceeds the signed/unsigned 32-bit domain")
  }
  integer
}

///|
fn DeclarationContext::flags(
  self : DeclarationContext,
  value : Json,
  path : String,
) -> Int64 {
  guard value is Array(names) else { return self.number(value, path) }
  let tokens : Array[(String, Int, Bool)] = [
    ("data", 1, false),
    ("constant", 1, true),
    ("array", 2, false),
    ("variable", 2, true),
    ("absolute", 4, false),
    ("relative", 4, true),
    ("no_wrap", 8, false),
    ("wrap", 8, true),
    ("linear", 16, false),
    ("non_linear", 16, true),
    ("preferred_state", 32, false),
    ("no_preferred_state", 32, true),
    ("no_null_state", 64, false),
    ("null_state", 64, true),
    ("non_volatile", 128, false),
    ("volatile", 128, true),
    ("bit_field", 256, false),
    ("buffered_bytes", 256, true),
  ]
  let seen : Map[Int, Bool] = Map([])
  let mut flags = 0
  for index, name in names {
    let entry_path = "\{path}[\{index}]"
    let token = match name {
      String(text) => tokens.iter().find_first(entry => entry.0 == text)
      _ => None
    }
    match token {
      Some((_, bit, enabled)) => {
        if seen.get(bit) is Some(previous) && previous != enabled {
          self.reject(entry_path, "Conflicting flag names")
        }
        seen.set(bit, enabled)
        if enabled {
          flags = flags | bit
        }
      }
      None => self.reject(entry_path, "Unknown Main flag name")
    }
  }
  flags.to_int64()
}

///|
fn DeclarationContext::emit(
  self : DeclarationContext,
  type_code : Int,
  tag : Int,
  value : Int64,
  size : Int?,
  path : String,
) -> Unit {
  if self.error is Some(_) {
    return
  }
  self.sources.push((self.builder.length, self.active, path))
  let empty = (type_code == 1 && (tag == 10 || tag == 11)) ||
    (type_code == 0 && tag == 12)
  if size is Some(width) {
    let valid = if width == 0 {
      value == 0L
    } else {
      integer_fits(value, width * 8, value < 0L)
    }
    if !valid {
      self.reject(path, "Value does not fit the requested raw item size")
      return
    }
    self.builder.append(
      type_code,
      tag,
      Bytes::makei(width, i => ((value >> (8 * i)) & 255L).to_byte()),
    )
    |> ignore
  } else if empty {
    self.builder.append(type_code, tag, b"") |> ignore
  } else if type_code == 1 && (tag == 2 || tag == 4) {
    self.builder.maximum_item(
      tag,
      if tag == 2 {
        self.logical_min
      } else {
        self.physical_min
      },
      value,
    )
    |> ignore
  } else if type_code == 1 && tag == 5 {
    self.builder.unit_exponent(value.to_int()) |> ignore
  } else {
    self.builder.short_item(
      type_code,
      tag,
      value,
      signed=type_code == 1 && (tag == 1 || tag == 3),
    )
    |> ignore
  }
  if self.builder.error is Some(error) {
    self.reject(
      path,
      "\{error.code}: \{error.message}",
      code=if error.code == "descriptor_limit" {
        "declaration_limit"
      } else {
        "declaration_value"
      },
    )
    return
  }
  if type_code == 1 {
    if tag == 1 || tag == 3 {
      let item = self.builder.items[self.builder.items.length() - 1]
      if tag == 1 {
        self.logical_min = item_signed(item)
      } else {
        self.physical_min = item_signed(item)
      }
    } else if tag == 10 {
      if self.saved.length() >= 64 {
        self.reject(path, "Global stack exceeds 64", code="declaration_limit")
      } else {
        self.saved.push((self.logical_min, self.physical_min))
      }
    } else if tag == 11 {
      match self.saved.pop() {
        Some((logical, physical)) => {
          self.logical_min = logical
          self.physical_min = physical
        }
        None => self.reject(path, "global_stack: Pop without Push")
      }
    }
  }
}

///|
fn DeclarationContext::visit(
  self : DeclarationContext,
  items : Array[Json],
  path : String,
  depth : Int,
) -> Unit {
  for position, entry in items {
    if self.error is Some(_) {
      return
    }
    let index = self.count
    self.active = index
    self.count += 1
    let node_path = "\{path}[\{position}]"
    if self.count > 65536 {
      self.reject(
        node_path,
        "More than 65536 declaration items",
        code="declaration_limit",
      )
      return
    }
    let object = self.object(entry, node_path)
    let mut name = ""
    let mut value = Json::null()
    for key, item in object {
      if key == "size" || key == "items" {
        continue
      }
      if name != "" ||
        (
          !declaration_tags.any(tag => tag.0 == key) &&
          key != "logical_range" &&
          key != "usage_range"
        ) {
        self.reject(
          "\{node_path}.\{key}",
          "Expected exactly one supported item key",
          code="declaration_key",
        )
        return
      }
      name = key
      value = item
    }
    if name == "" {
      self.reject(node_path, "Missing item key", code="declaration_key")
      return
    }
    let size = match object.get("size") {
      None => None
      Some(value) => {
        let width = self.number(value, "\{node_path}.size")
        if width != 0L && width != 1L && width != 2L && width != 4L {
          self.reject("\{node_path}.size", "Size must be 0, 1, 2 or 4")
          return
        }
        Some(width.to_int())
      }
    }
    let item_path = "\{node_path}.\{name}"
    if name != "collection" && object.contains("items") {
      self.reject(
        "\{node_path}.items",
        "Only Collection has child items",
        code="declaration_key",
      )
      return
    }
    if name == "logical_range" || name == "usage_range" {
      let range = self.array(value, item_path)
      if range.length() != 2 {
        self.reject(item_path, "Range needs two endpoints")
        return
      }
      let low = self.number(range[0], "\{item_path}[0]")
      let high = self.number(range[1], "\{item_path}[1]")
      if low > high {
        self.reject(item_path, "Range endpoints are reversed")
        return
      }
      let type_code = if name == "logical_range" { 1 } else { 2 }
      if type_code == 2 && (low < 0L || high > 65535L) {
        self.reject(item_path, "Usage range endpoints must fit 16 bits")
        return
      }
      self.emit(type_code, 1, low, size, "\{item_path}[0]")
      self.emit(type_code, 2, high, size, "\{item_path}[1]")
      continue
    }
    let (type_code, tag) = match
      declaration_tags.iter().find_first(tag => tag.0 == name) {
      Some((_, type_code, tag)) => (type_code, tag)
      None => {
        self.reject(item_path, "Unknown item", code="declaration_key")
        return
      }
    }
    let mut actual_size = size
    let number = if name == "push" || name == "pop" {
      if value != Json(true) {
        self.reject(item_path, "Push/Pop value must be true")
      }
      0L
    } else if type_code == 0 && (tag == 8 || tag == 9 || tag == 11) {
      self.flags(value, item_path)
    } else if name == "usage" && value is Object(_) {
      let usage = self.object(value, item_path)
      for key, _ in usage {
        if key != "page" && key != "id" {
          self.reject(
            "\{item_path}.\{key}",
            "Unknown extended Usage key",
            code="declaration_key",
          )
        }
      }
      let page = self.number(
        usage.get("page").unwrap_or(Json::null()),
        "\{item_path}.page",
      )
      let id = self.number(
        usage.get("id").unwrap_or(Json::null()),
        "\{item_path}.id",
      )
      if page < 0L ||
        page > 65535L ||
        id < 0L ||
        id > 65535L ||
        (size is Some(width) && width != 4) {
        self.reject(item_path, "Extended Usage needs 16-bit page/id and size 4")
      }
      actual_size = Some(4)
      (page << 16) | id
    } else if name == "collection" {
      let names = [
        "physical", "application", "logical", "report", "named_array", "usage_switch",
        "usage_modifier",
      ]
      match value {
        String(text) if names.contains(text) =>
          names.search(text).unwrap().to_int64()
        _ => self.number(value, item_path)
      }
    } else {
      let number = self.number(value, item_path)
      if type_code == 2 &&
        tag <= 2 &&
        size is None &&
        (number < 0L || number > 65535L) {
        self.reject(
          item_path, "Plain Usage IDs must fit 16 bits; use extended Usage or explicit raw size",
        )
      }
      number
    }
    self.emit(type_code, tag, number, actual_size, item_path)
    if name == "collection" {
      if depth >= 64 {
        self.reject(
          item_path,
          "Collection depth exceeds 64",
          code="declaration_limit",
        )
        return
      }
      let children = self.array(
        object.get("items").unwrap_or(Json::null()),
        "\{node_path}.items",
      )
      self.visit(children, "\{node_path}.items", depth + 1)
      self.active = index
      self.emit(0, 12, 0L, None, item_path)
    }
  }
}

///|
/// Compiles declaration schema v1, validates with the existing builder/compiler.
/// Errors use zero-based preorder declaration item indices and JSON paths.
pub fn descriptor_from_declaration(text : String) -> Result[Bytes, Diagnostic] {
  if text.length() > 8388608 {
    return Err(
      diagnostic(
        0, "declaration_limit", "$: Declaration text exceeds 8388608 UTF-16 code units",
      ),
    )
  }
  let json = @json.parse(text) catch {
    _ =>
      return Err(
        diagnostic(
          0, "declaration_json", "$: Invalid JSON or JSON nesting limit exceeded",
        ),
      )
  }
  let context : DeclarationContext = {
    builder: DescriptorBuilder::new(),
    sources: [],
    saved: [],
    logical_min: 0L,
    physical_min: 0L,
    count: 0,
    active: 0,
    error: None,
  }
  let object = context.object(json, "$")
  for key, _ in object {
    if key != "version" && key != "items" {
      context.reject("$.\{key}", "Unknown document key", code="declaration_key")
    }
  }
  match object.get("version") {
    Some(Number(version, ..)) if version == 1.0 => ()
    _ => context.reject("$.version", "Declaration version must be the number 1")
  }
  let items = context.array(
    object.get("items").unwrap_or(Json::null()),
    "$.items",
  )
  context.visit(items, "$.items", 0)
  if context.error is Some(error) {
    return Err(error)
  }
  match context.builder.build() {
    Ok(bytes) => Ok(bytes)
    Err(error) => {
      let mut index = 0
      let mut path = "$.items"
      for (offset, source_index, source_path) in context.sources {
        if offset <= error.offset {
          index = source_index
          path = source_path
        }
      }
      Err(
        diagnostic(
          index,
          "declaration_value",
          "\{path}: \{error.code}: \{error.message}",
        ),
      )
    }
  }
}

///|
/// Exports supported descriptor bytes with explicit raw widths for exact replay.
/// The declaration schema is independent from display JSON schema v2.
pub fn declaration_from_descriptor(bytes : Bytes) -> Result[Json, Diagnostic] {
  match compile_descriptor(bytes) {
    Ok(_) => ()
    Err(error) => return Err(error)
  }
  let items = match parse_items(bytes) {
    Ok(items) => items
    Err(error) => return Err(error)
  }
  let root : Array[Json] = []
  let stack : Array[Array[Json]] = [root]
  for item in items {
    if item.type_code == 0 && item.tag == 12 {
      stack.pop() |> ignore
      continue
    }
    let name = match
      declaration_tags
      .iter()
      .find_first(entry => entry.1 == item.type_code && entry.2 == item.tag) {
      Some((name, _, _)) => name
      None =>
        return Err(
          diagnostic(
            item.offset,
            "unsupported_item",
            "Item has no declaration representation",
          ),
        )
    }
    let object : Map[String, Json] = Map([])
    object.set(
      name,
      if name == "push" || name == "pop" {
        Json(true)
      } else {
        number64(item_unsigned(item))
      },
    )
    object.set("size", Json(item.data.length()))
    let current = stack[stack.length() - 1]
    if name == "collection" {
      let children : Array[Json] = []
      object.set("items", Json::array(children))
      current.push(Json::object(object))
      stack.push(children)
    } else {
      current.push(Json::object(object))
    }
  }
  Ok({ "version": 1, "items": Json::array(root) })
}