///|
/// ENI JSON interchange kept separate from the runtime-facing
/// ConfigurationModel. Runtime accepts only this normalized JSON surface,
/// while XML parsing lives in a standalone conversion tool.

///|
priv struct EniJsonIdentity {
  vendor_id : UInt
  product_code : UInt
  revision : UInt
  serial : UInt
} derive(ToJson)

///|
priv struct EniJsonMailboxProtocols {
  aoe : Bool
  coe : Bool
  eoe : Bool
  foe : Bool
  soe : Bool
  voe : Bool
} derive(ToJson)

///|
priv struct EniJsonProcessDataSummary {
  sync_manager_count : Int
  fmmu_count : Int
  rx_pdo_count : Int
  tx_pdo_count : Int
  mailbox_protocols : EniJsonMailboxProtocols?
  dc_op_mode_count : Int
  dc_assign_activate_count : Int
} derive(ToJson)

///|
priv struct EniJsonSlave {
  position : Int
  configured_address : UInt?
  alias_address : UInt?
  identity : EniJsonIdentity?
  identification : UInt?
  process_data_summary : EniJsonProcessDataSummary?
  presence : String?
  hot_connect_group : UInt?
} derive(ToJson)

///|
priv struct EniJsonDocument {
  slaves : Array[EniJsonSlave]
} derive(ToJson)

///|
fn parse_eni_json_document(
  json_content : String,
) -> EniJsonDocument raise @hal.EcError {
  let parsed = @json.parse(json_content[:]) catch {
    err =>
      raise @hal.EcError::EsiParseFailed(
        "ENI JSON parse failed: " + err.to_string(),
      )
  }
  parse_eni_json_document_value(parsed)
}

///|
fn parse_eni_json_document_value(
  json : Json,
) -> EniJsonDocument raise @hal.EcError {
  guard json is { "slaves": Array(slaves_json), .. } else {
    raise @hal.EcError::EsiParseFailed(
      "ENI JSON decode failed: missing slaves array",
    )
  }
  { slaves: slaves_json.map(slave_json => parse_eni_json_slave(slave_json)) }
}

///|
fn parse_eni_json_slave(json : Json) -> EniJsonSlave raise @hal.EcError {
  guard json
    is {
      "position": position_json,
      "configured_address"? : configured_address_json,
      "alias_address"? : alias_address_json,
      "identity"? : identity_json,
      "identification"? : identification_json,
      "process_data_summary"? : process_data_summary_json,
      "presence"? : presence_json,
      "hot_connect_group"? : hot_connect_group_json,
      ..
    } else {
    raise @hal.EcError::EsiParseFailed(
      "ENI JSON decode failed: invalid slave entry",
    )
  }
  {
    position: decode_eni_json_int(position_json),
    configured_address: decode_optional_eni_json_value_uint(
      configured_address_json,
    ),
    alias_address: decode_optional_eni_json_value_uint(alias_address_json),
    identity: decode_optional_eni_json_identity(identity_json),
    identification: decode_optional_eni_json_value_uint(identification_json),
    process_data_summary: decode_optional_eni_json_process_data_summary(
      process_data_summary_json,
    ),
    presence: decode_optional_eni_json_string(presence_json),
    hot_connect_group: decode_optional_eni_json_value_uint(
      hot_connect_group_json,
    ),
  }
}

///|
fn decode_optional_eni_json_string(json : Json?) -> String? raise @hal.EcError {
  match json {
    None | Some(Null) => None
    Some(value) => {
      let decoded : String = @json.from_json(value) catch {
        err =>
          raise @hal.EcError::EsiParseFailed(
            "ENI JSON decode failed: " + err.to_string(),
          )
      }
      Some(decoded)
    }
  }
}

///|
fn decode_optional_eni_json_identity(
  json : Json?,
) -> EniJsonIdentity? raise @hal.EcError {
  match json {
    None | Some(Null) => None
    Some(value) => {
      guard value
        is {
          "vendor_id": vendor_id_json,
          "product_code": product_code_json,
          "revision": revision_json,
          "serial": serial_json,
          ..
        } else {
        raise @hal.EcError::EsiParseFailed(
          "ENI JSON decode failed: invalid identity entry",
        )
      }
      Some({
        vendor_id: decode_eni_json_uint(vendor_id_json),
        product_code: decode_eni_json_uint(product_code_json),
        revision: decode_eni_json_uint(revision_json),
        serial: decode_eni_json_uint(serial_json),
      })
    }
  }
}

///|
fn decode_optional_eni_json_process_data_summary(
  json : Json?,
) -> EniJsonProcessDataSummary? raise @hal.EcError {
  match json {
    None | Some(Null) => None
    Some(value) => {
      guard value
        is {
          "sync_manager_count": sync_manager_count_json,
          "fmmu_count": fmmu_count_json,
          "rx_pdo_count": rx_pdo_count_json,
          "tx_pdo_count": tx_pdo_count_json,
          "mailbox_protocols"? : mailbox_protocols_json,
          "dc_op_mode_count": dc_op_mode_count_json,
          "dc_assign_activate_count": dc_assign_activate_count_json,
          ..
        } else {
        raise @hal.EcError::EsiParseFailed(
          "ENI JSON decode failed: invalid process_data_summary entry",
        )
      }
      Some({
        sync_manager_count: decode_eni_json_int(sync_manager_count_json),
        fmmu_count: decode_eni_json_int(fmmu_count_json),
        rx_pdo_count: decode_eni_json_int(rx_pdo_count_json),
        tx_pdo_count: decode_eni_json_int(tx_pdo_count_json),
        mailbox_protocols: decode_optional_eni_json_mailbox_protocols(
          mailbox_protocols_json,
        ),
        dc_op_mode_count: decode_eni_json_int(dc_op_mode_count_json),
        dc_assign_activate_count: decode_eni_json_int(
          dc_assign_activate_count_json,
        ),
      })
    }
  }
}

///|
fn decode_optional_eni_json_mailbox_protocols(
  json : Json?,
) -> EniJsonMailboxProtocols? raise @hal.EcError {
  match json {
    None | Some(Null) => None
    Some(value) => {
      guard value
        is {
          "aoe": aoe_json,
          "coe": coe_json,
          "eoe": eoe_json,
          "foe": foe_json,
          "soe": soe_json,
          "voe": voe_json,
          ..
        } else {
        raise @hal.EcError::EsiParseFailed(
          "ENI JSON decode failed: invalid mailbox_protocols entry",
        )
      }
      Some({
        aoe: decode_eni_json_bool(aoe_json),
        coe: decode_eni_json_bool(coe_json),
        eoe: decode_eni_json_bool(eoe_json),
        foe: decode_eni_json_bool(foe_json),
        soe: decode_eni_json_bool(soe_json),
        voe: decode_eni_json_bool(voe_json),
      })
    }
  }
}

///|
fn decode_optional_eni_json_value_uint(
  json : Json?,
) -> UInt? raise @hal.EcError {
  match json {
    None | Some(Null) => None
    Some(value) => Some(decode_eni_json_uint(value))
  }
}

///|
fn decode_eni_json_int(json : Json) -> Int raise @hal.EcError {
  @json.from_json(json) catch {
    err =>
      raise @hal.EcError::EsiParseFailed(
        "ENI JSON decode failed: " + err.to_string(),
      )
  }
}

///|
fn decode_eni_json_uint(json : Json) -> UInt raise @hal.EcError {
  @json.from_json(json) catch {
    err =>
      raise @hal.EcError::EsiParseFailed(
        "ENI JSON decode failed: " + err.to_string(),
      )
  }
}

///|
fn decode_eni_json_bool(json : Json) -> Bool raise @hal.EcError {
  @json.from_json(json) catch {
    err =>
      raise @hal.EcError::EsiParseFailed(
        "ENI JSON decode failed: " + err.to_string(),
      )
  }
}

///|
pub fn configuration_to_eni_json(model : ConfigurationModel) -> Json {
  eni_json_document_from_configuration(model).to_json()
}

///|
pub fn configuration_to_eni_json_string(
  model : ConfigurationModel,
  indent? : Int = 2,
) -> String {
  configuration_to_eni_json(model).stringify(indent~)
}

///|
fn project_eni_json_document(document : EniJsonDocument) -> ConfigurationModel {
  {
    source: EniImport,
    total_slaves: document.slaves.length(),
    slaves: document.slaves.map(fn(slave) {
      {
        position: slave.position,
        configured_address: slave.configured_address,
        alias_address: slave.alias_address,
        identity: slave.identity.map(fn(identity) {
          {
            vendor_id: identity.vendor_id,
            product_code: identity.product_code,
            revision: identity.revision,
            serial: identity.serial,
          }
        }),
        identification: slave.identification,
        process_data_summary: slave.process_data_summary.map(fn(summary) {
          {
            sync_manager_count: summary.sync_manager_count,
            fmmu_count: summary.fmmu_count,
            rx_pdo_count: summary.rx_pdo_count,
            tx_pdo_count: summary.tx_pdo_count,
            mailbox_protocols: summary.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: summary.dc_op_mode_count,
            dc_assign_activate_count: summary.dc_assign_activate_count,
          }
        }),
        presence: decode_eni_json_presence(
          slave.presence,
          slave.hot_connect_group,
        ),
      }
    }),
  }
}

///|
fn eni_json_document_from_configuration(
  model : ConfigurationModel,
) -> EniJsonDocument {
  {
    slaves: model.slaves.map(fn(slave) {
      {
        position: slave.position,
        configured_address: slave.configured_address,
        alias_address: slave.alias_address,
        identity: slave.identity.map(fn(identity) {
          {
            vendor_id: identity.vendor_id,
            product_code: identity.product_code,
            revision: identity.revision,
            serial: identity.serial,
          }
        }),
        identification: slave.identification,
        process_data_summary: slave.process_data_summary.map(fn(summary) {
          {
            sync_manager_count: summary.sync_manager_count,
            fmmu_count: summary.fmmu_count,
            rx_pdo_count: summary.rx_pdo_count,
            tx_pdo_count: summary.tx_pdo_count,
            mailbox_protocols: summary.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: summary.dc_op_mode_count,
            dc_assign_activate_count: summary.dc_assign_activate_count,
          }
        }),
        presence: match slave.presence {
          @hal.Mandatory => Some("mandatory")
          @hal.Optional(..) => Some("optional")
        },
        hot_connect_group: match slave.presence {
          @hal.Mandatory => None
          @hal.Optional(group_id~) => group_id
        },
      }
    }),
  }
}

///|
fn decode_eni_json_presence(
  presence : String?,
  hot_connect_group : UInt?,
) -> @hal.SlavePresence {
  match presence {
    Some("optional") => @hal.Optional(group_id=hot_connect_group)
    _ => @hal.Mandatory
  }
}