///|
/// Canonical lower-case, space-separated byte pairs.
pub fn format_hex(bytes : Bytes) -> String {
  bytes
  .to_array()
  .map(byte => byte.to_int().to_string(radix=16).pad_start(2, '0'))
  .join(" ")
}

///|
pub fn report_kind_name(kind : ReportKind) -> String {
  match kind {
    Input => "input"
    Output => "output"
    Feature => "feature"
  }
}

///|
fn optional_int(value : Int?) -> Json {
  match value {
    Some(value) => value.to_json()
    None => Json::null()
  }
}

///|
fn optional_int64(value : Int64?) -> Json {
  match value {
    Some(value) => number64(value)
    None => Json::null()
  }
}

///|
// Int64's generic ToJson uses strings. HID integers fit exactly in a JSON number.
fn number64(value : Int64) -> Json {
  Json::number(value.to_double())
}

///|
fn usage_json(usage : Usage?) -> Json {
  match usage {
    Some(usage) => { "page": usage.page.to_json(), "id": usage.id.to_json() }
    None => Json::null()
  }
}

///|
fn usage_span_json(span : UsageSpan) -> Json {
  {
    "page": span.page.to_json(),
    "min": span.min.to_json(),
    "max": span.max.to_json(),
  }
}

///|
fn index_span_json(span : IndexSpan) -> Json {
  { "min": number64(span.min), "max": number64(span.max) }
}

///|
fn unit_json(raw : Int64?) -> Json {
  match raw {
    Some(raw) =>
      match decode_unit(raw) {
        Ok(unit) =>
          {
            "raw": number64(unit.raw),
            "system": unit.system.to_json(),
            "length": unit.length.to_json(),
            "mass": unit.mass.to_json(),
            "time": unit.time.to_json(),
            "temperature": unit.temperature.to_json(),
            "current": unit.current.to_json(),
            "luminous_intensity": unit.luminous_intensity.to_json(),
            "reserved": unit.reserved.to_json(),
          }
        Err(_) => Json::null()
      }
    None => Json::null()
  }
}

///|
/// A JSON component. Wrap in the schema_version=2 descriptor export for storage.
pub fn layout_to_json(layout : Layout) -> Json {
  let reports : Array[Json] = []
  let seen : Map[Int, Bool] = Map([])
  for field in layout.fields {
    let kind_index = match field.kind {
      Input => 0
      Output => 1
      Feature => 2
    }
    let key = kind_index * 256 + field.report_id
    if seen.contains(key) {
      continue
    }
    seen[key] = true
    let mut bits = 0
    for candidate in layout.fields {
      if candidate.kind == field.kind && candidate.report_id == field.report_id {
        bits = bits + candidate.bit_size * candidate.count
      }
    }
    reports.push({
      "kind": Json::string(report_kind_name(field.kind)),
      "report_id": field.report_id.to_json(),
      "payload_bits": bits.to_json(),
      "wire_bytes": ((bits + 7) / 8 +
      (if layout.has_report_ids { 1 } else { 0 })).to_json(),
    })
  }
  {
    "has_report_ids": Json::boolean(layout.has_report_ids),
    "reports": Json::array(reports),
    "collections": Json::array(
      layout.collections.map(c => {
        "descriptor_offset": c.descriptor_offset.to_json(),
        "end_offset": c.end_offset.to_json(),
        "collection_type": c.collection_type.to_json(),
        "usage": usage_json(c.usage),
        "parent_index": optional_int(c.parent_index),
        "string_spans": Json::array(c.string_spans.map(index_span_json)),
        "designator_spans": Json::array(c.designator_spans.map(index_span_json)),
        "alternate_usages": Json::array(
          c.alternate_usages.map(group => {
            Json::array(group.map(usage_span_json))
          }),
        ),
      }),
    ),
    "fields": Json::array(
      layout.fields.map(f => {
        let (physical_min, physical_max) = effective_physical_range(f)
        {
          "descriptor_offset": f.descriptor_offset.to_json(),
          "kind": Json::string(report_kind_name(f.kind)),
          "report_id": f.report_id.to_json(),
          "bit_offset": f.bit_offset.to_json(),
          "bit_size": f.bit_size.to_json(),
          "count": f.count.to_json(),
          "flags": f.flags.to_json(),
          "logical_min": number64(f.logical_min),
          "logical_max": number64(f.logical_max),
          "physical_min": optional_int64(f.physical_min),
          "physical_max": optional_int64(f.physical_max),
          "effective_physical_min": number64(physical_min),
          "effective_physical_max": number64(physical_max),
          "unit": unit_json(f.unit),
          "unit_exponent": optional_int(f.unit_exponent),
          "collection_index": optional_int(f.collection_index),
          "usage_spans": Json::array(f.usage_spans.map(usage_span_json)),
          "usage_count": usage_count(f.usage_spans).to_json(),
          "string_spans": Json::array(f.string_spans.map(index_span_json)),
          "designator_spans": Json::array(
            f.designator_spans.map(index_span_json),
          ),
          "alternate_usages": Json::array(
            f.alternate_usages.map(group => {
              Json::array(group.map(usage_span_json))
            }),
          ),
        }
      }),
    ),
  }
}

///|
pub fn report_to_json(report : DecodedReport) -> Json {
  {
    "kind": Json::string(report_kind_name(report.kind)),
    "report_id": report.report_id.to_json(),
    "opaque_values": Json::array(
      report.opaque_values.map(v => {
        "field_index": v.field_index.to_json(),
        "element_index": v.element_index.to_json(),
        "bit_offset": v.bit_offset.to_json(),
        "bit_size": v.bit_size.to_json(),
        "hex": Json::string(format_hex(v.bytes)),
      }),
    ),
    "values": Json::array(
      report.values.map(v => {
        "field_index": v.field_index.to_json(),
        "element_index": v.element_index.to_json(),
        "bit_offset": v.bit_offset.to_json(),
        "value": number64(v.value),
        "usage": usage_json(v.usage),
        "is_array": Json::boolean(v.is_array),
        "in_logical_range": Json::boolean(v.in_logical_range),
        "is_null": Json::boolean(v.is_null),
      }),
    ),
  }
}

///|
pub fn diagnostic_to_json(error : Diagnostic) -> Json {
  {
    "offset": error.offset.to_json(),
    "code": Json::string(error.code),
    "message": Json::string(error.message),
  }
}

///|
/// Schema version 2. Array order follows the descriptor; absent metadata is null.
/// All numbers are exact within JavaScript's integer range for compiled layouts.
pub fn descriptor_to_json(bytes : Bytes) -> Result[Json, Diagnostic] {
  let layout = match compile_descriptor(bytes) {
    Ok(layout) => layout
    Err(error) => return Err(error)
  }
  let items = match parse_items(bytes) {
    Ok(items) => items
    Err(error) => return Err(error)
  }
  Ok({
    "schema_version": 2,
    "descriptor_hex": Json::string(format_hex(bytes)),
    "byte_length": bytes.length().to_json(),
    "items": Json::array(
      items.map(item => {
        "offset": item.offset.to_json(),
        "type_code": item.type_code.to_json(),
        "tag": item.tag.to_json(),
        "data_hex": Json::string(format_hex(item.data)),
        "is_long": Json::boolean(item.is_long),
      }),
    ),
    "layout": layout_to_json(layout),
  })
}