///|
/// Unified configuration model for online scan, offline config, and ENI JSON import.

///|
pub(all) enum ConfigurationSource {
  OnlineScan
  OfflineConfig
  EniImport
} derive(Eq, Debug)

///|
pub(all) struct ConfigurationSlave {
  position : Int
  configured_address : UInt?
  alias_address : UInt?
  identity : @hal.SlaveIdentity?
  identification : UInt?
  process_data_summary : ConfigurationProcessDataSummary?
  presence : @hal.SlavePresence
} derive(Eq, Debug)

///|
pub(all) struct ConfigurationMailboxProtocols {
  aoe : Bool
  coe : Bool
  eoe : Bool
  foe : Bool
  soe : Bool
  voe : Bool
} derive(Eq, Debug)

///|
pub(all) struct ConfigurationProcessDataSummary {
  sync_manager_count : Int
  fmmu_count : Int
  rx_pdo_count : Int
  tx_pdo_count : Int
  mailbox_protocols : ConfigurationMailboxProtocols?
  dc_op_mode_count : Int
  dc_assign_activate_count : Int
} derive(Eq, Debug)

///|
pub(all) struct ConfigurationModel {
  source : ConfigurationSource
  total_slaves : Int
  slaves : Array[ConfigurationSlave]
} derive(Eq, Debug)

///|
pub(all) enum ConfigurationDifference {
  Missing(position~ : Int)
  OptionalMissing(position~ : Int, group_id~ : UInt?)
  Extra(position~ : Int)
  StationAddressMismatch(position~ : Int, expected~ : UInt?, actual~ : UInt?)
  AliasAddressMismatch(position~ : Int, expected~ : UInt?, actual~ : UInt?)
  IdentityMismatch(
    position~ : Int,
    expected~ : @hal.SlaveIdentity?,
    actual~ : @hal.SlaveIdentity?
  )
  IdentificationMismatch(position~ : Int, expected~ : UInt?, actual~ : UInt?)
  ProcessDataSummaryMismatch(
    position~ : Int,
    expected~ : ConfigurationProcessDataSummary,
    actual~ : ConfigurationProcessDataSummary
  )
} derive(Eq, Debug)

///|
pub(all) struct ConfigurationComparisonReport {
  expected_source : ConfigurationSource
  actual_source : ConfigurationSource
  total_expected : Int
  total_found : Int
  differences : Array[ConfigurationDifference]
  all_ok : Bool
} derive(Eq, Debug)

///|
pub fn configuration_from_scan(scan_report : ScanReport) -> ConfigurationModel {
  {
    source: OnlineScan,
    total_slaves: scan_report.slave_count,
    slaves: scan_report.slaves.map(fn(slave) {
      {
        position: slave.position,
        configured_address: Some(slave.station_address),
        alias_address: slave.alias_address,
        identity: Some({
          vendor_id: slave.vendor_id,
          product_code: slave.product_code,
          revision: slave.revision,
          serial: slave.serial,
        }),
        identification: Some(slave.identification),
        process_data_summary: None,
        presence: @hal.Mandatory,
      }
    }),
  }
}

///|
pub fn configuration_from_expected(
  expected : Array[@hal.SlaveConfig],
) -> ConfigurationModel {
  {
    source: OfflineConfig,
    total_slaves: expected.length(),
    slaves: expected.map(fn(slave) {
      {
        position: slave.position.reinterpret_as_int(),
        configured_address: Some(slave.configured_address),
        alias_address: slave.expected_alias_address,
        identity: slave.expected_identity,
        identification: slave.expected_identification,
        process_data_summary: None,
        presence: slave.presence,
      }
    }),
  }
}

///|
/// Parse ENI JSON interchange and project slave info into the unified configuration model.
pub fn configuration_from_eni_json(
  json_content : String,
) -> ConfigurationModel raise @hal.EcError {
  project_eni_json_document(parse_eni_json_document(json_content))
}

///|
pub fn compare_configurations(
  expected : ConfigurationModel,
  actual : ConfigurationModel,
) -> ConfigurationComparisonReport {
  let differences : Array[ConfigurationDifference] = []
  let mut all_ok = true

  for exp in expected.slaves {
    let pos = exp.position
    match find_configuration_slave_at_position(actual.slaves, pos) {
      Some(found) => {
        if exp.configured_address is Some(expected_address) {
          match found.configured_address {
            Some(actual_address) =>
              if actual_address != expected_address {
                differences.push(
                  StationAddressMismatch(
                    position=pos,
                    expected=Some(expected_address),
                    actual=Some(actual_address),
                  ),
                )
                all_ok = false
                continue
              }
            None => {
              differences.push(
                StationAddressMismatch(
                  position=pos,
                  expected=Some(expected_address),
                  actual=None,
                ),
              )
              all_ok = false
              continue
            }
          }
        }

        if exp.alias_address is Some(expected_alias) {
          match found.alias_address {
            Some(actual_alias) =>
              if actual_alias != expected_alias {
                differences.push(
                  AliasAddressMismatch(
                    position=pos,
                    expected=Some(expected_alias),
                    actual=Some(actual_alias),
                  ),
                )
                all_ok = false
              }
            None => {
              differences.push(
                AliasAddressMismatch(
                  position=pos,
                  expected=Some(expected_alias),
                  actual=None,
                ),
              )
              all_ok = false
            }
          }
        }

        if exp.identity is Some(expected_identity) {
          if found.identity != Some(expected_identity) {
            differences.push(
              IdentityMismatch(
                position=pos,
                expected=Some(expected_identity),
                actual=found.identity,
              ),
            )
            all_ok = false
          }
        }

        if exp.identification is Some(expected_identification) {
          if found.identification != Some(expected_identification) {
            differences.push(
              IdentificationMismatch(
                position=pos,
                expected=Some(expected_identification),
                actual=found.identification,
              ),
            )
            all_ok = false
          }
        }

        match (exp.process_data_summary, found.process_data_summary) {
          (Some(expected_summary), Some(actual_summary)) =>
            if actual_summary != expected_summary {
              differences.push(
                ProcessDataSummaryMismatch(
                  position=pos,
                  expected=expected_summary,
                  actual=actual_summary,
                ),
              )
              all_ok = false
            }
          _ => ()
        }
      }
      None => {
        match exp.presence {
          @hal.Mandatory => differences.push(Missing(position=pos))
          @hal.Optional(group_id~) =>
            differences.push(OptionalMissing(position=pos, group_id~))
        }
        all_ok = false
      }
    }
  }

  for found in actual.slaves {
    if !has_configuration_position(expected.slaves, found.position) {
      differences.push(Extra(position=found.position))
      all_ok = false
    }
  }

  {
    expected_source: expected.source,
    actual_source: actual.source,
    total_expected: expected.total_slaves,
    total_found: actual.total_slaves,
    differences,
    all_ok,
  }
}

///|
fn find_configuration_slave_at_position(
  slaves : Array[ConfigurationSlave],
  position : Int,
) -> ConfigurationSlave? {
  for slave in slaves {
    if slave.position == position {
      return Some(slave)
    }
  }
  None
}

///|
fn has_configuration_position(
  slaves : Array[ConfigurationSlave],
  position : Int,
) -> Bool {
  for slave in slaves {
    if slave.position == position {
      return true
    }
  }
  false
}