///|
/// ESI XML -> ESI JSON conversion. XML parsing stays in the standalone bridge,
/// while downstream consumers operate on the normalized JSON document.

///|
fn convert_esi_xml_to_json_string(
  xml_content : String,
) -> String raise @hal.EcError {
  @runtime.esi_json_to_string(esi_json_document_from_xml(xml_content))
}

///|
fn esi_json_document_from_xml(
  xml_content : String,
) -> @runtime.EsiJsonDocument raise @hal.EcError {
  let (devices, modules) = parse_esi_document_projection(xml_content)
  { devices, modules }
}

///|
fn parse_esi_document_projection(
  xml_content : String,
) -> (Array[@runtime.EsiJsonDevice], Array[@runtime.EsiJsonModule]) raise @hal.EcError {
  let reader = @xml.Reader::from_string(xml_content)
  let devices : Array[@runtime.EsiJsonDevice] = []
  let modules : Array[@runtime.EsiJsonModule] = []

  let mut vendor_id = 0U
  let mut in_vendor = false
  let mut in_device = false
  let mut in_module = false
  let mut in_dc_op_mode = false
  let mut in_init_cmd = false
  let mut current_slot_index = 0
  let mut slot_module_default = false
  let mut current_text_target : String? = None

  let mut current_device = empty_esi_device_projection()
  let mut current_module = empty_esi_module_projection()
  let mut current_sm : @runtime.EsiJsonSmEntry? = None
  let mut current_pdo : @runtime.EsiJsonPdo? = None
  let mut current_entry : @runtime.EsiJsonPdoEntry? = None
  let mut current_dc = empty_esi_dc_projection()
  let mut current_init_command = empty_esi_init_command_projection()
  let mut current_init_transitions : Array[String] = []

  while true {
    let event = reader.read_event() catch {
      err =>
        raise @hal.EcError::EsiParseFailed(
          "ESI XML parse failed: " + err.to_string(),
        )
    }
    match event {
      @xml.Start(elem) => {
        if elem.name == "Vendor" {
          in_vendor = true
          current_text_target = None
          continue
        }
        if in_vendor && elem.name == "Id" {
          current_text_target = Some("vendor-id")
          continue
        }
        if elem.name == "Device" {
          in_device = true
          current_slot_index = 0
          current_device = empty_esi_device_projection()
          current_device = {
            ..current_device,
            vendor_id,
            physics: optional_xml_attr(elem, "Physics"),
          }
          current_text_target = None
          continue
        }
        if elem.name == "Module" {
          in_module = true
          current_module = empty_esi_module_projection()
          current_text_target = None
          continue
        }

        if in_device && !in_module {
          match elem.name {
            "Type" => {
              current_device = {
                ..current_device,
                product_code: option_uint_or(
                  optional_numeric_xml_attr(elem, "ProductCode"),
                  current_device.product_code,
                ),
                revision: option_uint_or(
                  optional_numeric_xml_attr(elem, "RevisionNo"),
                  current_device.revision,
                ),
                serial: option_uint_or(
                  optional_numeric_xml_attr(elem, "SerialNo"),
                  current_device.serial,
                ),
              }
              current_text_target = Some("device-type-name")
            }
            "Name" =>
              current_text_target = if current_entry is Some(_) {
                Some("entry-name")
              } else if current_pdo is Some(_) {
                Some("pdo-name")
              } else if in_dc_op_mode {
                Some("dc-name")
              } else {
                Some("device-name")
              }
            "Fmmu" => current_text_target = Some("device-fmmu")
            "Sm" => {
              current_sm = Some({
                physical_start: option_uint_or(
                  optional_numeric_xml_attr(elem, "StartAddress"),
                  0U,
                ),
                length: option_uint_or(
                  optional_numeric_xml_attr(elem, "DefaultSize"),
                  option_uint_or(optional_numeric_xml_attr(elem, "MinSize"), 0U),
                ),
                control: option_uint_or(
                  optional_numeric_xml_attr(elem, "ControlByte"),
                  0U,
                ).to_byte(),
                status: b'\x00',
                enable: optional_numeric_xml_attr(elem, "Enable") == Some(1U),
                sm_type: "unused",
              })
              current_text_target = Some("sm-kind")
            }
            "Mailbox" =>
              current_device = {
                ..current_device,
                mailbox: {
                  ..current_device.mailbox,
                  data_link_layer: optional_numeric_xml_attr(
                    elem, "DataLinkLayer",
                  ) ==
                  Some(1U),
                },
              }
            "CoE" =>
              current_device = {
                ..current_device,
                mailbox: {
                  ..current_device.mailbox,
                  coe_present: true,
                  coe_sdo_info: optional_numeric_xml_attr(elem, "SdoInfo") ==
                  Some(1U),
                  coe_pdo_assign: optional_numeric_xml_attr(elem, "PdoAssign") ==
                  Some(1U),
                  coe_pdo_config: optional_numeric_xml_attr(elem, "PdoConfig") ==
                  Some(1U),
                  coe_complete_access: optional_numeric_xml_attr(
                    elem, "CompleteAccess",
                  ) ==
                  Some(1U),
                },
              }
            "EoE" =>
              current_device = {
                ..current_device,
                mailbox: { ..current_device.mailbox, eoe: true },
              }
            "FoE" =>
              current_device = {
                ..current_device,
                mailbox: { ..current_device.mailbox, foe: true },
              }
            "SoE" =>
              current_device = {
                ..current_device,
                mailbox: { ..current_device.mailbox, soe: true },
              }
            "VoE" =>
              current_device = {
                ..current_device,
                mailbox: { ..current_device.mailbox, voe: true },
              }
            "AoE" =>
              current_device = {
                ..current_device,
                mailbox: { ..current_device.mailbox, aoe: true },
              }
            "ProfileNo" => current_text_target = Some("profile-no")
            "InitCmd" => {
              in_init_cmd = true
              current_init_command = empty_esi_init_command_projection()
              current_init_transitions = []
            }
            "RxPdo" =>
              current_pdo = Some({
                ..empty_esi_pdo_projection("rx"),
                sync_manager: option_uint_or(
                  optional_numeric_xml_attr(elem, "Sm"),
                  0U,
                ).to_byte(),
              })
            "TxPdo" =>
              current_pdo = Some({
                ..empty_esi_pdo_projection("tx"),
                sync_manager: option_uint_or(
                  optional_numeric_xml_attr(elem, "Sm"),
                  0U,
                ).to_byte(),
              })
            "Index" =>
              if in_init_cmd {
                current_text_target = Some("initcmd-index")
              } else if current_entry is Some(_) {
                current_text_target = Some("entry-index")
                match current_entry {
                  Some(entry) =>
                    current_entry = Some({
                      ..entry,
                      depend_on_slot_index: optional_bool_xml_attr(
                        elem, "DependOnSlot",
                      ),
                    })
                  None => ()
                }
              } else if current_pdo is Some(_) {
                current_text_target = Some("pdo-index")
                match current_pdo {
                  Some(pdo) =>
                    current_pdo = Some({
                      ..pdo,
                      depend_on_slot_pdo: optional_bool_xml_attr(
                        elem, "DependOnSlot",
                      ),
                    })
                  None => ()
                }
              }
            "Entry" =>
              if !in_init_cmd {
                current_entry = Some(empty_esi_pdo_entry_projection())
              }
            "SubIndex" =>
              current_text_target = if in_init_cmd {
                Some("initcmd-subindex")
              } else {
                Some("entry-subindex")
              }
            "BitLen" => current_text_target = Some("entry-bitlen")
            "DataType" => current_text_target = Some("entry-datatype")
            "Transition" =>
              if in_init_cmd {
                current_text_target = Some("initcmd-transition")
              }
            "Data" =>
              if in_init_cmd {
                current_text_target = Some("initcmd-data")
              }
            "OpMode" => {
              in_dc_op_mode = true
              current_dc = empty_esi_dc_projection()
            }
            "Desc" => current_text_target = Some("dc-desc")
            "AssignActivate" => current_text_target = Some("dc-assign-activate")
            "CycleTimeSync0" => {
              current_dc = {
                ..current_dc,
                sync0_factor: option_int_or(
                  optional_numeric_xml_attr(elem, "Factor").map(fn(value) {
                    value.reinterpret_as_int()
                  }),
                  current_dc.sync0_factor,
                ),
              }
              current_text_target = Some("dc-cycle-time-sync0")
            }
            "CycleTimeSync1" => {
              current_dc = {
                ..current_dc,
                sync1_factor: option_int_or(
                  optional_numeric_xml_attr(elem, "Factor").map(fn(value) {
                    value.reinterpret_as_int()
                  }),
                  current_dc.sync1_factor,
                ),
              }
              current_text_target = Some("dc-cycle-time-sync1")
            }
            "Slots" =>
              current_device = {
                ..current_device,
                slot_index_increment: option_uint_or(
                  optional_numeric_xml_attr(elem, "SlotIndexIncrement"),
                  current_device.slot_index_increment,
                ),
                slot_pdo_increment: option_uint_or(
                  optional_numeric_xml_attr(elem, "SlotPdoIncrement"),
                  current_device.slot_pdo_increment,
                ),
              }
            "Slot" => slot_module_default = false
            "ModuleIdent" => {
              slot_module_default = optional_numeric_xml_attr(elem, "Default") ==
                Some(1U)
              current_text_target = Some("slot-module-ident")
            }
            "ByteSize" => current_text_target = Some("eeprom-byte-size")
            "ConfigData" => current_text_target = Some("eeprom-config-data")
            "BootStrap" => current_text_target = Some("eeprom-bootstrap")
            _ => ()
          }
        }

        if in_module {
          match elem.name {
            "Type" => {
              current_module = {
                ..current_module,
                module_ident: option_uint_or(
                  optional_numeric_xml_attr(elem, "ModuleIdent"),
                  current_module.module_ident,
                ),
              }
              current_text_target = Some("module-type-name")
            }
            "Name" =>
              if current_entry is Some(_) {
                current_text_target = Some("entry-name")
              } else if current_pdo is Some(_) {
                current_text_target = Some("pdo-name")
              } else {
                current_text_target = Some("module-name")
              }
            "RxPdo" =>
              current_pdo = Some({
                ..empty_esi_pdo_projection("rx"),
                sync_manager: option_uint_or(
                  optional_numeric_xml_attr(elem, "Sm"),
                  0U,
                ).to_byte(),
              })
            "TxPdo" =>
              current_pdo = Some({
                ..empty_esi_pdo_projection("tx"),
                sync_manager: option_uint_or(
                  optional_numeric_xml_attr(elem, "Sm"),
                  0U,
                ).to_byte(),
              })
            "Index" =>
              if current_entry is Some(_) {
                current_text_target = Some("entry-index")
                match current_entry {
                  Some(entry) =>
                    current_entry = Some({
                      ..entry,
                      depend_on_slot_index: optional_bool_xml_attr(
                        elem, "DependOnSlot",
                      ),
                    })
                  None => ()
                }
              } else {
                current_text_target = Some("pdo-index")
                match current_pdo {
                  Some(pdo) =>
                    current_pdo = Some({
                      ..pdo,
                      depend_on_slot_pdo: optional_bool_xml_attr(
                        elem, "DependOnSlot",
                      ),
                    })
                  None => ()
                }
              }
            "Entry" => current_entry = Some(empty_esi_pdo_entry_projection())
            "SubIndex" => current_text_target = Some("entry-subindex")
            "BitLen" => current_text_target = Some("entry-bitlen")
            "DataType" => current_text_target = Some("entry-datatype")
            _ => ()
          }
        }
      }
      @xml.Empty(elem) =>
        if in_device && !in_module {
          match elem.name {
            "CoE" =>
              current_device = {
                ..current_device,
                mailbox: {
                  ..current_device.mailbox,
                  coe_present: true,
                  coe_sdo_info: optional_numeric_xml_attr(elem, "SdoInfo") ==
                  Some(1U),
                  coe_pdo_assign: optional_numeric_xml_attr(elem, "PdoAssign") ==
                  Some(1U),
                  coe_pdo_config: optional_numeric_xml_attr(elem, "PdoConfig") ==
                  Some(1U),
                  coe_complete_access: optional_numeric_xml_attr(
                    elem, "CompleteAccess",
                  ) ==
                  Some(1U),
                },
              }
            "EoE" =>
              current_device = {
                ..current_device,
                mailbox: { ..current_device.mailbox, eoe: true },
              }
            "FoE" =>
              current_device = {
                ..current_device,
                mailbox: { ..current_device.mailbox, foe: true },
              }
            "SoE" =>
              current_device = {
                ..current_device,
                mailbox: { ..current_device.mailbox, soe: true },
              }
            "VoE" =>
              current_device = {
                ..current_device,
                mailbox: { ..current_device.mailbox, voe: true },
              }
            "AoE" =>
              current_device = {
                ..current_device,
                mailbox: { ..current_device.mailbox, aoe: true },
              }
            _ => ()
          }
        }
      @xml.Text(content) => {
        let text = compact_esi_xml_text(content)
        if text == "" {
          continue
        }
        match current_text_target {
          Some("vendor-id") => vendor_id = parse_esi_numeric(text, "Vendor/Id")
          Some("device-name") =>
            if current_device.device_name == "" {
              current_device = { ..current_device, device_name: text }
            }
          Some("device-type-name") =>
            if current_device.type_name == "" {
              current_device = { ..current_device, type_name: text }
            }
          Some("device-fmmu") =>
            match parse_esi_fmmu_direction(text) {
              Some(direction) => current_device.fmmu.push(direction)
              None => ()
            }
          Some("sm-kind") =>
            match current_sm {
              Some(sm) =>
                current_sm = Some({ ..sm, sm_type: parse_esi_sm_type(text) })
              None => ()
            }
          Some("dc-name") => current_dc = { ..current_dc, name: text }
          Some("dc-desc") => current_dc = { ..current_dc, description: text }
          Some("dc-assign-activate") =>
            current_dc = {
              ..current_dc,
              assign_activate: parse_esi_numeric(
                text, "Device/Dc/OpMode/AssignActivate",
              ),
            }
          Some("profile-no") =>
            current_device = {
              ..current_device,
              profile_no: Some(
                parse_esi_numeric(text, "Device/Profile/ProfileNo"),
              ),
            }
          Some("initcmd-transition") => {
            current_init_command = { ..current_init_command, transition: text }
            push_unique_init_transition(current_init_transitions, text)
          }
          Some("initcmd-index") =>
            current_init_command = {
              ..current_init_command,
              index: parse_esi_numeric(text, "Device/Mailbox/CoE/InitCmd/Index"),
            }
          Some("initcmd-subindex") =>
            current_init_command = {
              ..current_init_command,
              subindex: parse_esi_numeric(
                text, "Device/Mailbox/CoE/InitCmd/SubIndex",
              ).to_byte(),
            }
          Some("initcmd-data") =>
            current_init_command = {
              ..current_init_command,
              data: parse_hex_bytes(text, "Device/Mailbox/CoE/InitCmd/Data"),
            }
          Some("dc-cycle-time-sync0") =>
            current_dc = {
              ..current_dc,
              cycle_time_sync0: parse_esi_numeric(
                text, "Device/Dc/OpMode/CycleTimeSync0",
              ),
            }
          Some("slot-module-ident") =>
            if slot_module_default {
              current_device.default_modules.push({
                module_ident: parse_esi_numeric(
                  text, "Device/Slots/Slot/ModuleIdent",
                ),
                slot_index: current_slot_index,
              })
            }
          Some("eeprom-byte-size") =>
            current_device = {
              ..current_device,
              eeprom_byte_size: parse_esi_numeric(
                text, "Device/Eeprom/ByteSize",
              ),
            }
          Some("eeprom-config-data") =>
            current_device = {
              ..current_device,
              config_data: parse_hex_bytes(text, "Device/Eeprom/ConfigData"),
            }
          Some("eeprom-bootstrap") =>
            current_device = {
              ..current_device,
              bootstrap: parse_hex_bytes(text, "Device/Eeprom/BootStrap"),
            }
          Some("module-name") =>
            if current_module.module_name == "" {
              current_module = { ..current_module, module_name: text }
            }
          Some("module-type-name") =>
            if current_module.module_name == "" {
              current_module = { ..current_module, module_name: text }
            }
          Some("pdo-name") =>
            match current_pdo {
              Some(pdo) => current_pdo = Some({ ..pdo, name: text })
              None => ()
            }
          Some("pdo-index") =>
            match current_pdo {
              Some(pdo) =>
                current_pdo = Some({
                  ..pdo,
                  base_index: parse_esi_numeric(text, "PDO/Index"),
                })
              None => ()
            }
          Some("entry-name") =>
            match current_entry {
              Some(entry) => current_entry = Some({ ..entry, name: text })
              None => ()
            }
          Some("entry-index") =>
            match current_entry {
              Some(entry) =>
                current_entry = Some({
                  ..entry,
                  base_index: parse_esi_numeric(text, "PDO/Entry/Index"),
                })
              None => ()
            }
          Some("entry-subindex") =>
            match current_entry {
              Some(entry) =>
                current_entry = Some({
                  ..entry,
                  subindex: parse_esi_numeric(text, "PDO/Entry/SubIndex").to_byte(),
                })
              None => ()
            }
          Some("entry-bitlen") =>
            match current_entry {
              Some(entry) =>
                current_entry = Some({
                  ..entry,
                  bit_length: parse_esi_numeric(text, "PDO/Entry/BitLen").to_byte(),
                })
              None => ()
            }
          Some("entry-datatype") =>
            match current_entry {
              Some(entry) =>
                current_entry = Some({ ..entry, data_type_name: text })
              None => ()
            }
          _ => ()
        }
      }
      @xml.CData(content) => {
        let text = compact_esi_xml_text(content)
        if text != "" && current_text_target == Some("eeprom-config-data") {
          current_device = {
            ..current_device,
            config_data: parse_hex_bytes(text, "Device/Eeprom/ConfigData"),
          }
        }
      }
      @xml.End(name) =>
        match name {
          "Vendor" => in_vendor = false
          "Sm" =>
            match current_sm {
              Some(sm) => {
                current_device.sync_managers.push(sm)
                current_sm = None
              }
              None => ()
            }
          "Entry" =>
            match current_entry {
              Some(entry) => {
                match current_pdo {
                  Some(pdo) => {
                    pdo.entries.push(entry)
                    current_pdo = Some(pdo)
                  }
                  None => ()
                }
                current_entry = None
              }
              None => ()
            }
          "RxPdo" | "TxPdo" =>
            match current_pdo {
              Some(pdo) => {
                if in_module {
                  if pdo.direction == "rx" {
                    current_module.rx_pdos.push(pdo)
                  } else {
                    current_module.tx_pdos.push(pdo)
                  }
                } else if pdo.direction == "rx" {
                  current_device.rx_pdos.push(pdo)
                } else {
                  current_device.tx_pdos.push(pdo)
                }
                current_pdo = None
              }
              None => ()
            }
          "OpMode" =>
            if in_dc_op_mode {
              current_device.dc_modes.push(current_dc)
              current_dc = empty_esi_dc_projection()
              in_dc_op_mode = false
            }
          "InitCmd" => {
            if current_init_transitions.length() > 0 {
              for transition in current_init_transitions {
                current_device.init_commands.push({
                  ..current_init_command,
                  transition,
                })
              }
            } else if current_init_command.transition != "" {
              current_device.init_commands.push(current_init_command)
            }
            current_init_command = empty_esi_init_command_projection()
            current_init_transitions = []
            in_init_cmd = false
          }
          "Slot" => {
            current_slot_index = current_slot_index + 1
            slot_module_default = false
          }
          "Device" => {
            devices.push(current_device)
            current_device = empty_esi_device_projection()
            current_sm = None
            current_pdo = None
            current_entry = None
            current_dc = empty_esi_dc_projection()
            current_init_command = empty_esi_init_command_projection()
            current_init_transitions = []
            current_text_target = None
            in_device = false
            in_dc_op_mode = false
            in_init_cmd = false
          }
          "Module" => {
            modules.push(current_module)
            current_module = empty_esi_module_projection()
            current_pdo = None
            current_entry = None
            current_text_target = None
            in_module = false
          }
          "Id"
          | "Type"
          | "Name"
          | "ProfileNo"
          | "Fmmu"
          | "ModuleIdent"
          | "ByteSize"
          | "ConfigData"
          | "BootStrap"
          | "AssignActivate"
          | "CycleTimeSync0"
          | "CycleTimeSync1"
          | "Transition"
          | "Index"
          | "SubIndex"
          | "BitLen"
          | "Data"
          | "DataType" => current_text_target = None
          _ => ()
        }
      @xml.Eof => break
      _ => ()
    }
  }

  (devices, modules)
}

///|
fn empty_esi_mailbox_projection() -> @runtime.EsiJsonMailbox {
  {
    data_link_layer: false,
    aoe: false,
    coe_present: false,
    coe_sdo_info: false,
    coe_pdo_assign: false,
    coe_pdo_config: false,
    coe_complete_access: false,
    eoe: false,
    foe: false,
    soe: false,
    voe: false,
  }
}

///|
fn empty_esi_pdo_entry_projection() -> @runtime.EsiJsonPdoEntry {
  {
    base_index: 0U,
    depend_on_slot_index: false,
    subindex: b'\x00',
    bit_length: b'\x00',
    name: "",
    data_type_name: "",
  }
}

///|
fn empty_esi_pdo_projection(direction : String) -> @runtime.EsiJsonPdo {
  {
    direction,
    base_index: 0U,
    depend_on_slot_pdo: false,
    sync_manager: b'\x00',
    name: "",
    entries: [],
  }
}

///|
fn empty_esi_dc_projection() -> @runtime.EsiJsonDcMode {
  {
    name: "",
    description: "",
    assign_activate: 0U,
    cycle_time_sync0: 0U,
    sync0_factor: 0,
    sync1_factor: 0,
  }
}

///|
fn empty_esi_device_projection() -> @runtime.EsiJsonDevice {
  {
    vendor_id: 0U,
    product_code: 0U,
    revision: 0U,
    serial: 0U,
    physics: "",
    device_name: "",
    type_name: "",
    profile_no: None,
    slot_index_increment: 0U,
    slot_pdo_increment: 0U,
    mailbox: empty_esi_mailbox_projection(),
    fmmu: [],
    sync_managers: [],
    dc_modes: [],
    eeprom_byte_size: 0U,
    config_data: b"",
    bootstrap: b"",
    default_modules: [],
    init_commands: [],
    rx_pdos: [],
    tx_pdos: [],
  }
}

///|
fn empty_esi_init_command_projection() -> @runtime.EsiJsonInitCommand {
  { transition: "", index: 0U, subindex: b'\x00', data: b"" }
}

///|
fn push_unique_init_transition(
  transitions : Array[String],
  transition : String,
) -> Unit {
  if transitions.iter().any(fn(value) { value == transition }) {
    return
  }
  transitions.push(transition)
}

///|
fn empty_esi_module_projection() -> @runtime.EsiJsonModule {
  { module_ident: 0U, module_name: "", rx_pdos: [], tx_pdos: [] }
}

///|
fn option_uint_or(value : UInt?, default : UInt) -> UInt {
  match value {
    Some(actual) => actual
    None => default
  }
}

///|
fn option_int_or(value : Int?, default : Int) -> Int {
  match value {
    Some(actual) => actual
    None => default
  }
}

///|
fn optional_xml_attr(elem : @xml.XmlElement, attr_name : String) -> String {
  match elem.get(attr_name) {
    Some(value) => value
    None => ""
  }
}

///|
fn optional_numeric_xml_attr(
  elem : @xml.XmlElement,
  attr_name : String,
) -> UInt? raise @hal.EcError {
  match elem.get(attr_name) {
    Some(value) => Some(parse_esi_numeric(value, elem.name + "/@" + attr_name))
    None => None
  }
}

///|
fn compact_esi_xml_text(content : String) -> String {
  let bytes = @utf8.encode(content)
  let filtered : Array[Byte] = []
  for ch in bytes {
    if ch == b'\x00' || ch == b' ' || ch == b'\t' || ch == b'\r' || ch == b'\n' {
      continue
    }
    filtered.push(ch)
  }
  @utf8.decode(Bytes::from_array(filtered)) catch {
    _ => ""
  }
}

///|
fn parse_esi_numeric(
  text : String,
  field_name : String,
) -> UInt raise @hal.EcError {
  let bytes = @utf8.encode(text)
  let filtered : Array[Byte] = []
  for ch in bytes {
    if ch == b'\x00' || ch == b' ' || ch == b'\t' || ch == b'\r' || ch == b'\n' {
      continue
    }
    filtered.push(ch)
  }
  if filtered.length() == 0 {
    raise @hal.EcError::EsiParseFailed(
      "missing numeric ESI field at " + field_name,
    )
  }
  let mut index = 0
  let mut base = 10U
  if filtered.length() >= 2 &&
    filtered[0] == b'#' &&
    (filtered[1] == b'x' || filtered[1] == b'X') {
    base = 16U
    index = 2
  } else if filtered.length() >= 2 &&
    filtered[0] == b'0' &&
    (filtered[1] == b'x' || filtered[1] == b'X') {
    base = 16U
    index = 2
  }
  if index >= filtered.length() {
    raise @hal.EcError::EsiParseFailed(
      "invalid numeric ESI field at " + field_name + ": " + text,
    )
  }
  let mut value = 0U
  for i = index; i < filtered.length(); i = i + 1 {
    let digit = esi_digit_value(filtered[i]) catch {
      _ =>
        raise @hal.EcError::EsiParseFailed(
          "invalid numeric ESI field at " + field_name + ": " + text,
        )
    }
    if digit >= base {
      raise @hal.EcError::EsiParseFailed(
        "invalid numeric ESI field at " + field_name + ": " + text,
      )
    }
    value = value * base + digit
  }
  value
}

///|
fn esi_digit_value(ch : Byte) -> UInt raise @hal.EcError {
  if ch >= b'0' && ch <= b'9' {
    return ch.to_uint() - b'0'.to_uint()
  }
  if ch >= b'a' && ch <= b'f' {
    return 10U + (ch.to_uint() - b'a'.to_uint())
  }
  if ch >= b'A' && ch <= b'F' {
    return 10U + (ch.to_uint() - b'A'.to_uint())
  }
  raise @hal.EcError::EsiParseFailed(
    "invalid hexadecimal digit in ESI numeric field",
  )
}

///|
fn parse_hex_bytes(
  text : String,
  field_name : String,
) -> Bytes raise @hal.EcError {
  let compact = compact_esi_xml_text(text)
  if compact == "" {
    return b""
  }
  let bytes = @utf8.encode(compact)
  if bytes.length() % 2 != 0 {
    raise @hal.EcError::EsiParseFailed(
      "invalid hex byte string at " + field_name + ": odd length",
    )
  }
  let values : Array[Byte] = []
  for i = 0; i < bytes.length(); i = i + 2 {
    let high = esi_digit_value(bytes[i]) catch {
      _ =>
        raise @hal.EcError::EsiParseFailed(
          "invalid hex ESI field at " + field_name + ": " + text,
        )
    }
    let low = esi_digit_value(bytes[i + 1]) catch {
      _ =>
        raise @hal.EcError::EsiParseFailed(
          "invalid hex ESI field at " + field_name + ": " + text,
        )
    }
    values.push(((high << 4) | low).reinterpret_as_int().to_byte())
  }
  Bytes::from_array(values)
}

///|
fn parse_esi_fmmu_direction(text : String) -> String? {
  match text {
    "Outputs" => Some("outputs")
    "Inputs" => Some("inputs")
    "MBoxState" => Some("mailbox-state")
    _ => None
  }
}

///|
fn parse_esi_sm_type(text : String) -> String {
  match text {
    "MBoxOut" => "mailbox-out"
    "MBoxIn" => "mailbox-in"
    "Outputs" => "process-data-out"
    "Inputs" => "process-data-in"
    _ => "unused"
  }
}

///|
fn optional_bool_xml_attr(elem : @xml.XmlElement, attr_name : String) -> Bool {
  match elem.get(attr_name) {
    Some("1") | Some("true") | Some("TRUE") => true
    _ => false
  }
}