///|
/// ENI XML to ENI JSON interchange conversion tool.
fn main {
  let _ = cli_main() catch {
    err => {
      println("moonecat eni-json error: " + @debug.to_string(err))
      ()
    }
  }
}

///|
fn cli_main() -> Unit raise @hal.EcError {
  let args = @env.args()
  let input_path = require_flag(args, "--input")
  let output_path = flag_value(args, "--output")
  let input_kind = optional_kind_flag(args)
  let xml_content = read_text_file(input_path, "ENI XML")
  let json_text = convert_xml_to_json_string(xml_content, input_kind)
  match output_path {
    Some(path) => write_text_file(path, json_text, "ENI JSON")
    None => println(json_text)
  }
}

///|
fn convert_xml_to_json_string(
  xml_content : String,
  input_kind : String,
) -> String raise @hal.EcError {
  match input_kind {
    "eni" => convert_eni_xml_to_json_string(xml_content)
    "esi" => convert_esi_xml_to_json_string(xml_content)
    _ =>
      raise @hal.EcError::ConfigMismatch(
        "unsupported --kind value: " + input_kind + " (expected eni or esi)",
      )
  }
}

///|
fn convert_eni_xml_to_json_string(
  xml_content : String,
) -> String raise @hal.EcError {
  @runtime.configuration_to_eni_json_string(
    configuration_from_eni_xml(xml_content),
  )
}

///|
fn optional_kind_flag(args : Array[String]) -> String {
  match flag_value(args, "--kind") {
    Some(value) => value
    None => "eni"
  }
}

///|
fn read_text_file(path : String, label : String) -> String raise @hal.EcError {
  let content_result : Result[String, Error] = Result::Ok(
    @fs.read_file_to_string(path),
  ) catch {
    err => Result::Err(err)
  }
  match content_result {
    Ok(content) => content
    Err(err) =>
      raise @hal.EcError::ConfigMismatch(
        "failed to read " + label + " file " + path + ": " + err.to_string(),
      )
  }
}

///|
fn write_text_file(
  path : String,
  content : String,
  label : String,
) -> Unit raise @hal.EcError {
  let write_result : Result[Unit, Error] = Result::Ok(
    @fs.write_string_to_file(path, content),
  ) catch {
    err => Result::Err(err)
  }
  match write_result {
    Ok(_) => ()
    Err(err) =>
      raise @hal.EcError::ConfigMismatch(
        "failed to write " + label + " file " + path + ": " + err.to_string(),
      )
  }
}

///|
fn require_flag(
  args : Array[String],
  flag : String,
) -> String raise @hal.EcError {
  match flag_value(args, flag) {
    Some(value) => value
    None =>
      raise @hal.EcError::ConfigMismatch(
        "missing required flag: " +
        flag +
        "\nusage: moon run cmd/eni_json -- --input  [--kind ] [--output ]",
      )
  }
}

///|
fn flag_value(args : Array[String], flag : String) -> String? {
  for i = 0; i + 1 < args.length(); i = i + 1 {
    if args[i] == flag {
      return Some(args[i + 1])
    }
  }
  None
}

///|
priv struct EniSlaveProjection {
  position : Int
  configured_address : UInt?
  vendor_id : UInt?
  product_code : UInt?
  revision : UInt?
  serial : UInt?
  identification : UInt?
  sync_manager_count : Int
  fmmu_count : Int
  rx_pdo_count : Int
  tx_pdo_count : Int
  mailbox_protocols : @runtime.ConfigurationMailboxProtocols?
  dc_op_mode_count : Int
  dc_assign_activate_count : Int
}

///|
priv struct EniCurrentValues {
  configured_address : UInt?
  vendor_id : UInt?
  product_code : UInt?
  revision : UInt?
  serial : UInt?
}

///|
priv struct EniMailboxProtocols {
  aoe : Bool
  coe : Bool
  eoe : Bool
  foe : Bool
  soe : Bool
  voe : Bool
}

///|
fn configuration_from_eni_xml(
  xml_content : String,
) -> @runtime.ConfigurationModel raise @hal.EcError {
  project_eni_slaves(parse_eni_slave_projection(xml_content))
}

///|
fn parse_eni_slave_projection(
  xml_content : String,
) -> Array[EniSlaveProjection] raise @hal.EcError {
  let reader = @xml.Reader::from_string(xml_content)
  let slaves : Array[EniSlaveProjection] = []

  let mut in_slave = false
  let mut in_info = false
  let mut in_mailbox = false
  let mut in_dc = false
  let mut current_field : String? = None
  let mut current_mailbox_field : String? = None
  let mut current_dc_field : String? = None
  let mut current_position = 0
  let mut current_values : EniCurrentValues = {
    configured_address: None,
    vendor_id: None,
    product_code: None,
    revision: None,
    serial: None,
  }
  let mut current_identification : UInt? = None
  let mut current_sync_manager_count = 0
  let mut current_fmmu_count = 0
  let mut current_rx_pdo_count = 0
  let mut current_tx_pdo_count = 0
  let mut current_mailbox_protocols : EniMailboxProtocols? = None
  let mut current_dc_op_mode_count = 0
  let mut current_dc_assign_activate_count = 0

  while true {
    let event = reader.read_event() catch {
      err =>
        raise @hal.EcError::EsiParseFailed(
          "ENI XML parse failed: " + err.to_string(),
        )
    }
    match event {
      @xml.Start(elem) => {
        if elem.name == "Slave" {
          in_slave = true
          in_info = false
          in_mailbox = false
          in_dc = false
          current_field = None
          current_mailbox_field = None
          current_dc_field = None
          current_values = {
            configured_address: None,
            vendor_id: None,
            product_code: None,
            revision: None,
            serial: None,
          }
          current_identification = None
          current_sync_manager_count = 0
          current_fmmu_count = 0
          current_rx_pdo_count = 0
          current_tx_pdo_count = 0
          current_mailbox_protocols = None
          current_dc_op_mode_count = 0
          current_dc_assign_activate_count = 0
          continue
        }

        if !in_slave {
          continue
        }

        if is_eni_sync_manager_tag(elem.name) {
          current_sync_manager_count = current_sync_manager_count + 1
        }
        if elem.name == "Fmmu" {
          current_fmmu_count = current_fmmu_count + 1
        }
        if elem.name == "RxPdo" {
          current_rx_pdo_count = current_rx_pdo_count + 1
        }
        if elem.name == "TxPdo" {
          current_tx_pdo_count = current_tx_pdo_count + 1
        }

        if elem.name == "Mailbox" {
          in_mailbox = true
          current_mailbox_field = None
          continue
        }

        if elem.name == "Dc" {
          in_dc = true
          current_dc_field = None
          continue
        }

        if in_mailbox {
          if is_eni_mailbox_protocol_tag(elem.name) {
            current_mailbox_protocols = Some(
              enable_mailbox_protocol(current_mailbox_protocols, elem.name),
            )
            current_mailbox_field = None
          } else if elem.name == "Protocol" {
            current_mailbox_field = Some(elem.name)
          } else {
            current_mailbox_field = None
          }
        }

        if in_dc {
          if elem.name == "OpMode" {
            current_dc_op_mode_count = current_dc_op_mode_count + 1
            current_dc_field = None
          } else if elem.name == "AssignActivate" {
            current_dc_field = Some(elem.name)
          } else {
            current_dc_field = None
          }
        }

        if elem.name == "Info" {
          in_info = true
          current_field = None
          continue
        }

        if in_info {
          if elem.name == "Identification" {
            current_identification = parse_optional_numeric_attribute(
              elem, "Value", "Slave/Info/Identification@Value",
            )
            current_field = None
          } else if is_eni_info_field(elem.name) {
            current_field = Some(elem.name)
          } else {
            current_field = None
          }
        }
      }
      @xml.Empty(elem) =>
        if in_slave {
          if is_eni_sync_manager_tag(elem.name) {
            current_sync_manager_count = current_sync_manager_count + 1
          }
          if elem.name == "Fmmu" {
            current_fmmu_count = current_fmmu_count + 1
          }
          if elem.name == "RxPdo" {
            current_rx_pdo_count = current_rx_pdo_count + 1
          }
          if elem.name == "TxPdo" {
            current_tx_pdo_count = current_tx_pdo_count + 1
          }
          if in_mailbox && is_eni_mailbox_protocol_tag(elem.name) {
            current_mailbox_protocols = Some(
              enable_mailbox_protocol(current_mailbox_protocols, elem.name),
            )
          }
          if in_dc && elem.name == "OpMode" {
            current_dc_op_mode_count = current_dc_op_mode_count + 1
          }
          if in_info && elem.name == "Identification" {
            current_identification = parse_optional_numeric_attribute(
              elem, "Value", "Slave/Info/Identification@Value",
            )
          }
        }
      @xml.Text(content) => {
        current_values = update_eni_field(
          content, current_field, current_values,
        )
        current_mailbox_protocols = update_mailbox_protocol_text(
          content, current_mailbox_field, current_mailbox_protocols,
        )
        current_dc_assign_activate_count = update_dc_assign_activate_count(
          content, current_dc_field, current_dc_assign_activate_count,
        )
      }
      @xml.CData(content) => {
        current_values = update_eni_field(
          content, current_field, current_values,
        )
        current_mailbox_protocols = update_mailbox_protocol_text(
          content, current_mailbox_field, current_mailbox_protocols,
        )
        current_dc_assign_activate_count = update_dc_assign_activate_count(
          content, current_dc_field, current_dc_assign_activate_count,
        )
      }
      @xml.End(name) => {
        if in_info && current_field == Some(name) {
          current_field = None
        }
        if in_mailbox && current_mailbox_field == Some(name) {
          current_mailbox_field = None
        }
        if in_dc && current_dc_field == Some(name) {
          current_dc_field = None
        }
        if name == "Info" {
          in_info = false
          current_field = None
          continue
        }
        if name == "Mailbox" {
          in_mailbox = false
          current_mailbox_field = None
          continue
        }
        if name == "Dc" {
          in_dc = false
          current_dc_field = None
          continue
        }
        if name == "Slave" && in_slave {
          slaves.push({
            position: current_position,
            configured_address: current_values.configured_address,
            vendor_id: current_values.vendor_id,
            product_code: current_values.product_code,
            revision: current_values.revision,
            serial: current_values.serial,
            identification: current_identification,
            sync_manager_count: current_sync_manager_count,
            fmmu_count: current_fmmu_count,
            rx_pdo_count: current_rx_pdo_count,
            tx_pdo_count: current_tx_pdo_count,
            mailbox_protocols: current_mailbox_protocols.map(fn(protocols) {
              {
                aoe: protocols.aoe,
                coe: protocols.coe,
                eoe: protocols.eoe,
                foe: protocols.foe,
                soe: protocols.soe,
                voe: protocols.voe,
              }
            }),
            dc_op_mode_count: current_dc_op_mode_count,
            dc_assign_activate_count: current_dc_assign_activate_count,
          })
          current_position = current_position + 1
          in_slave = false
          in_info = false
          in_mailbox = false
          in_dc = false
          current_field = None
          current_mailbox_field = None
          current_dc_field = None
        }
      }
      @xml.Eof => break
      _ => ()
    }
  }

  slaves
}

///|
fn is_eni_info_field(name : String) -> Bool {
  name == "PhysAddr" ||
  name == "VendorId" ||
  name == "ProductCode" ||
  name == "RevisionNo" ||
  name == "SerialNo"
}

///|
fn is_eni_sync_manager_tag(name : String) -> Bool {
  name == "Sm" ||
  name == "Sm0" ||
  name == "Sm1" ||
  name == "Sm2" ||
  name == "Sm3"
}

///|
fn is_eni_mailbox_protocol_tag(name : String) -> Bool {
  name == "AoE" ||
  name == "CoE" ||
  name == "EoE" ||
  name == "FoE" ||
  name == "SoE" ||
  name == "VoE"
}

///|
fn empty_mailbox_protocols() -> EniMailboxProtocols {
  { aoe: false, coe: false, eoe: false, foe: false, soe: false, voe: false }
}

///|
fn enable_mailbox_protocol(
  protocols : EniMailboxProtocols?,
  protocol_name : String,
) -> EniMailboxProtocols {
  let current = match protocols {
    Some(value) => value
    None => empty_mailbox_protocols()
  }
  match protocol_name {
    "AoE" => { ..current, aoe: true }
    "CoE" => { ..current, coe: true }
    "EoE" => { ..current, eoe: true }
    "FoE" => { ..current, foe: true }
    "SoE" => { ..current, soe: true }
    "VoE" => { ..current, voe: true }
    _ => current
  }
}

///|
fn compact_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 update_mailbox_protocol_text(
  content : String,
  current_mailbox_field : String?,
  current_protocols : EniMailboxProtocols?,
) -> EniMailboxProtocols? {
  if current_mailbox_field != Some("Protocol") {
    return current_protocols
  }
  let protocol_name = compact_xml_text(content)
  if protocol_name == "" {
    return current_protocols
  }
  Some(enable_mailbox_protocol(current_protocols, protocol_name))
}

///|
fn update_dc_assign_activate_count(
  content : String,
  current_dc_field : String?,
  current_count : Int,
) -> Int raise @hal.EcError {
  if current_dc_field != Some("AssignActivate") || is_blank_xml_text(content) {
    return current_count
  }
  if parse_eni_numeric(content, "Slave/Dc/OpMode/AssignActivate") == 0U {
    return current_count
  }
  current_count + 1
}

///|
fn update_eni_field(
  content : String,
  current_field : String?,
  current_values : EniCurrentValues,
) -> EniCurrentValues raise @hal.EcError {
  let field = match current_field {
    Some(field) => field
    None => return current_values
  }
  if is_blank_xml_text(content) {
    return current_values
  }
  let value = parse_eni_numeric(content, "Slave/Info/" + field)
  match field {
    "PhysAddr" => { ..current_values, configured_address: Some(value) }
    "VendorId" => { ..current_values, vendor_id: Some(value) }
    "ProductCode" => { ..current_values, product_code: Some(value) }
    "RevisionNo" => { ..current_values, revision: Some(value) }
    "SerialNo" => { ..current_values, serial: Some(value) }
    _ => current_values
  }
}

///|
fn is_blank_xml_text(content : String) -> Bool {
  let bytes = @utf8.encode(content)
  for ch in bytes {
    if ch == b'\x00' {
      continue
    }
    if ch != b' ' && ch != b'\t' && ch != b'\r' && ch != b'\n' {
      return false
    }
  }
  true
}

///|
fn parse_optional_numeric_attribute(
  elem : @xml.XmlElement,
  attr_name : String,
  field_name : String,
) -> UInt? raise @hal.EcError {
  match elem.get(attr_name) {
    Some(value) => Some(parse_eni_numeric(value, field_name))
    None => None
  }
}

///|
fn parse_eni_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 ENI 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 ENI field at " + field_name + ": " + text,
    )
  }

  let mut value = 0U
  for i = index; i < filtered.length(); i = i + 1 {
    let digit = eni_digit_value(filtered[i])
    if digit >= base {
      raise @hal.EcError::EsiParseFailed(
        "invalid numeric ENI field at " + field_name + ": " + text,
      )
    }
    value = value * base + digit
  }
  value
}

///|
fn eni_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 ENI numeric field",
  )
}

///|
fn project_eni_slaves(
  slaves : Array[EniSlaveProjection],
) -> @runtime.ConfigurationModel {
  {
    source: @runtime.EniImport,
    total_slaves: slaves.length(),
    slaves: slaves.map(fn(slave) {
      {
        position: slave.position,
        configured_address: slave.configured_address,
        alias_address: None,
        identity: build_eni_identity(slave),
        identification: slave.identification,
        process_data_summary: Some({
          sync_manager_count: slave.sync_manager_count,
          fmmu_count: slave.fmmu_count,
          rx_pdo_count: slave.rx_pdo_count,
          tx_pdo_count: slave.tx_pdo_count,
          mailbox_protocols: slave.mailbox_protocols,
          dc_op_mode_count: slave.dc_op_mode_count,
          dc_assign_activate_count: slave.dc_assign_activate_count,
        }),
        presence: @hal.Mandatory,
      }
    }),
  }
}

///|
fn build_eni_identity(slave : EniSlaveProjection) -> @hal.SlaveIdentity? {
  match slave.vendor_id {
    Some(vendor_id) =>
      match slave.product_code {
        Some(product_code) =>
          match slave.revision {
            Some(revision) =>
              match slave.serial {
                Some(serial) =>
                  Some({ vendor_id, product_code, revision, serial })
                None => None
              }
            None => None
          }
        None => None
      }
    None => None
  }
}