///|
/// Master Object Dictionary (ETG.1500 #1301) — local OD for the EtherCAT master.
///
/// Exposes standard CiA-301 objects 0x1000–0x10FF that describe the master
/// device identity, runtime state, and network topology. This is a read-only
/// view synthesised from Runtime, ScanReport, Telemetry, and DiagnosticSurface;
/// the master does not store a persistent OD in EEPROM.

///|
/// Standard data types for OD entries (CiA-301 subset).
pub(all) enum MasterOdDataType {
  Unsigned8
  Unsigned16
  Unsigned32
  Integer32
  VisibleString
  OctetString
} derive(Eq, Debug)

///|
/// Access mode for an OD entry.
pub(all) enum MasterOdAccess {
  ReadOnly
  ReadWrite
} derive(Eq, Debug)

///|
/// One entry in the master object dictionary.
pub(all) struct MasterOdEntry {
  index : UInt
  subindex : Byte
  name : String
  data_type : MasterOdDataType
  access : MasterOdAccess
  value : MasterOdValue
} derive(Eq, Debug)

///|
/// Value stored (or computed) for an OD entry.
pub(all) enum MasterOdValue {
  U8(Byte)
  U16(UInt)
  U32(UInt)
  I32(Int)
  Str(String)
  Raw(Bytes)
} derive(Eq, Debug)

///|
/// The master object dictionary: a flat list of entries.
pub(all) struct MasterObjectDictionary {
  entries : Array[MasterOdEntry]
} derive(Eq, Debug)

///|
/// Identity used when building the master's own 0x1018.
pub(all) struct MasterIdentity {
  vendor_id : UInt
  product_code : UInt
  revision : UInt
  serial : UInt
} derive(Eq, Debug)

///|
pub fn default_master_identity() -> MasterIdentity {
  { vendor_id: 0U, product_code: 0U, revision: 0U, serial: 0U }
}

///|
let moonecat_device_name : String = "MoonECAT"

///|
let moonecat_hw_version : String = "0.1"

///|
let moonecat_sw_version : String = "0.2.0"

///|
/// Build the full master OD from current runtime state.
pub fn build_master_od(
  identity : MasterIdentity,
  scan : ScanReport?,
  telemetry : Telemetry?,
  surface : DiagnosticSurface?,
) -> MasterObjectDictionary {
  let entries : Array[MasterOdEntry] = []
  // 0x1000 Device Type  — 0 for generic master
  entries.push({
    index: 0x1000U,
    subindex: b'\x00',
    name: "Device Type",
    data_type: Unsigned32,
    access: ReadOnly,
    value: U32(0U),
  })
  // 0x1001 Error Register
  let err_reg : Byte = match surface {
    Some(s) =>
      match s.error_register {
        Some(e) => e
        None => b'\x00'
      }
    None => b'\x00'
  }
  entries.push({
    index: 0x1001U,
    subindex: b'\x00',
    name: "Error Register",
    data_type: Unsigned8,
    access: ReadOnly,
    value: U8(err_reg),
  })
  // 0x1008 Manufacturer Device Name
  entries.push({
    index: 0x1008U,
    subindex: b'\x00',
    name: "Manufacturer Device Name",
    data_type: VisibleString,
    access: ReadOnly,
    value: Str(moonecat_device_name),
  })
  // 0x1009 Hardware Version
  entries.push({
    index: 0x1009U,
    subindex: b'\x00',
    name: "Manufacturer Hardware Version",
    data_type: VisibleString,
    access: ReadOnly,
    value: Str(moonecat_hw_version),
  })
  // 0x100A Software Version
  entries.push({
    index: 0x100AU,
    subindex: b'\x00',
    name: "Manufacturer Software Version",
    data_type: VisibleString,
    access: ReadOnly,
    value: Str(moonecat_sw_version),
  })
  // 0x1018 Identity Object (sub0=number of entries, sub1–sub4)
  entries.push({
    index: 0x1018U,
    subindex: b'\x00',
    name: "Identity Object / Number of Entries",
    data_type: Unsigned8,
    access: ReadOnly,
    value: U8(b'\x04'),
  })
  entries.push({
    index: 0x1018U,
    subindex: b'\x01',
    name: "Identity Object / Vendor ID",
    data_type: Unsigned32,
    access: ReadOnly,
    value: U32(identity.vendor_id),
  })
  entries.push({
    index: 0x1018U,
    subindex: b'\x02',
    name: "Identity Object / Product Code",
    data_type: Unsigned32,
    access: ReadOnly,
    value: U32(identity.product_code),
  })
  entries.push({
    index: 0x1018U,
    subindex: b'\x03',
    name: "Identity Object / Revision Number",
    data_type: Unsigned32,
    access: ReadOnly,
    value: U32(identity.revision),
  })
  entries.push({
    index: 0x1018U,
    subindex: b'\x04',
    name: "Identity Object / Serial Number",
    data_type: Unsigned32,
    access: ReadOnly,
    value: U32(identity.serial),
  })
  // 0x10F3 Diagnosis History — aggregated from surface
  let diag_count : UInt = match surface {
    Some(s) =>
      match s.diagnosis_counter {
        Some(c) => c
        None => 0U
      }
    None => 0U
  }
  entries.push({
    index: 0x10F3U,
    subindex: b'\x00',
    name: "Diagnosis History / Maximum Messages",
    data_type: Unsigned8,
    access: ReadOnly,
    value: U8(b'\x01'),
  })
  entries.push({
    index: 0x10F3U,
    subindex: b'\x01',
    name: "Diagnosis History / Newest Message",
    data_type: Unsigned32,
    access: ReadOnly,
    value: U32(diag_count),
  })
  // 0x2000 Slave Count (vendor-specific range)
  let slave_count : Int = match scan {
    Some(s) => s.slave_count
    None => 0
  }
  entries.push({
    index: 0x2000U,
    subindex: b'\x00',
    name: "Slave Count",
    data_type: Unsigned32,
    access: ReadOnly,
    value: U32(slave_count.reinterpret_as_uint()),
  })
  // 0x2001 Runtime Telemetry sub-objects
  match telemetry {
    Some(t) => {
      entries.push({
        index: 0x2001U,
        subindex: b'\x00',
        name: "Runtime Telemetry / Number of Entries",
        data_type: Unsigned8,
        access: ReadOnly,
        value: U8(b'\x09'),
      })
      entries.push({
        index: 0x2001U,
        subindex: b'\x01',
        name: "Runtime Telemetry / Cycles",
        data_type: Unsigned32,
        access: ReadOnly,
        value: U32(t.cycles.to_int().reinterpret_as_uint()),
      })
      entries.push({
        index: 0x2001U,
        subindex: b'\x02',
        name: "Runtime Telemetry / TX Frames",
        data_type: Unsigned32,
        access: ReadOnly,
        value: U32(t.tx_frames.to_int().reinterpret_as_uint()),
      })
      entries.push({
        index: 0x2001U,
        subindex: b'\x03',
        name: "Runtime Telemetry / RX Frames",
        data_type: Unsigned32,
        access: ReadOnly,
        value: U32(t.rx_frames.to_int().reinterpret_as_uint()),
      })
      entries.push({
        index: 0x2001U,
        subindex: b'\x04',
        name: "Runtime Telemetry / Timeouts",
        data_type: Unsigned32,
        access: ReadOnly,
        value: U32(t.timeouts.to_int().reinterpret_as_uint()),
      })
      entries.push({
        index: 0x2001U,
        subindex: b'\x05',
        name: "Runtime Telemetry / Retries",
        data_type: Unsigned32,
        access: ReadOnly,
        value: U32(t.retries.to_int().reinterpret_as_uint()),
      })
      entries.push({
        index: 0x2001U,
        subindex: b'\x06',
        name: "Runtime Telemetry / WKC Errors",
        data_type: Unsigned32,
        access: ReadOnly,
        value: U32(t.wkc_errors.to_int().reinterpret_as_uint()),
      })
      entries.push({
        index: 0x2001U,
        subindex: b'\x07',
        name: "Runtime Telemetry / IRQ Events",
        data_type: Unsigned32,
        access: ReadOnly,
        value: U32(t.irq_events.to_int().reinterpret_as_uint()),
      })
      entries.push({
        index: 0x2001U,
        subindex: b'\x08',
        name: "Runtime Telemetry / DC Delay Updates",
        data_type: Unsigned32,
        access: ReadOnly,
        value: U32(t.dc_delay_updates.to_int().reinterpret_as_uint()),
      })
      entries.push({
        index: 0x2001U,
        subindex: b'\x09',
        name: "Runtime Telemetry / Emergency Events",
        data_type: Unsigned32,
        access: ReadOnly,
        value: U32(t.emergency_events.to_int().reinterpret_as_uint()),
      })
    }
    None => ()
  }
  // 0x2002 Diagnostic Issues count (vendor-specific)
  let issue_count : Int = match surface {
    Some(s) => s.issues.length()
    None => 0
  }
  entries.push({
    index: 0x2002U,
    subindex: b'\x00',
    name: "Diagnostic Issue Count",
    data_type: Unsigned32,
    access: ReadOnly,
    value: U32(issue_count.reinterpret_as_uint()),
  })
  // 0x2003 Overall Diagnostic Level
  let level_code : UInt = match surface {
    Some(s) => diagnostic_level_code(s.overall_level)
    None => 0U
  }
  entries.push({
    index: 0x2003U,
    subindex: b'\x00',
    name: "Overall Diagnostic Level",
    data_type: Unsigned8,
    access: ReadOnly,
    value: U8(level_code.to_byte()),
  })
  { entries, }
}

///|
fn diagnostic_level_code(level : DiagnosticLevel) -> UInt {
  match level {
    Block => 2U
    Degrade => 1U
    Info => 0U
  }
}

///|
/// Lookup a single entry by index and subindex.
pub fn MasterObjectDictionary::lookup(
  self : MasterObjectDictionary,
  index : UInt,
  subindex : Byte,
) -> MasterOdEntry? {
  for entry in self.entries {
    if entry.index == index && entry.subindex == subindex {
      return Some(entry)
    }
  }
  None
}

///|
/// List all unique indices in the dictionary.
pub fn MasterObjectDictionary::indices(
  self : MasterObjectDictionary,
) -> Array[UInt] {
  let seen : Array[UInt] = []
  for entry in self.entries {
    let mut found = false
    for idx in seen {
      if idx == entry.index {
        found = true
        break
      }
    }
    if !found {
      seen.push(entry.index)
    }
  }
  seen
}

///|
/// Get all entries for a given index.
pub fn MasterObjectDictionary::entries_at(
  self : MasterObjectDictionary,
  index : UInt,
) -> Array[MasterOdEntry] {
  self.entries.filter(fn(e) { e.index == index })
}

///|
/// Format an OD value as a human-readable string.
pub fn MasterOdValue::format(self : MasterOdValue) -> String {
  match self {
    U8(v) => "0x" + hex_byte(v)
    U16(v) => "0x" + hex_uint16(v)
    U32(v) => "0x" + hex_uint32(v)
    I32(v) => v.to_string()
    Str(s) => s
    Raw(b) => {
      let mut out = ""
      for i = 0; i < b.length(); i = i + 1 {
        if i > 0 {
          out = out + " "
        }
        out = out + hex_byte(b[i])
      }
      out
    }
  }
}

///|
fn hex_byte(b : Byte) -> String {
  let hi = (b.to_uint() >> 4) & 0xFU
  let lo = b.to_uint() & 0xFU
  hex_nibble(hi).to_string() + hex_nibble(lo).to_string()
}

///|
fn hex_uint16(v : UInt) -> String {
  hex_byte(((v >> 8) & 0xFFU).to_byte()) + hex_byte((v & 0xFFU).to_byte())
}

///|
fn hex_uint32(v : UInt) -> String {
  hex_uint16((v >> 16) & 0xFFFFU) + hex_uint16(v & 0xFFFFU)
}

///|
fn hex_nibble(n : UInt) -> Char {
  if n < 10U {
    (n.reinterpret_as_int() + 0x30).unsafe_to_char()
  } else {
    (n.reinterpret_as_int() - 10 + 0x61).unsafe_to_char()
  }
}

///|
/// Serialize the full OD to JSON.
pub fn MasterObjectDictionary::to_json(self : MasterObjectDictionary) -> Json {
  let items = self.entries.map(fn(entry) { master_od_entry_json(entry) })
  Json::object({
    "object_count": Json::number(self.indices().length().to_double()),
    "entry_count": Json::number(self.entries.length().to_double()),
    "entries": Json::array(items),
  })
}

///|
fn master_od_entry_json(entry : MasterOdEntry) -> Json {
  Json::object({
    "index": Json::number(entry.index.to_double()),
    "index_hex": Json::string("0x" + hex_uint16(entry.index)),
    "subindex": Json::number(entry.subindex.to_uint().to_double()),
    "name": Json::string(entry.name),
    "data_type": Json::string(@debug.to_string(entry.data_type)),
    "access": Json::string(@debug.to_string(entry.access)),
    "value": master_od_value_json(entry.value),
    "value_formatted": Json::string(entry.value.format()),
  })
}

///|
fn master_od_value_json(value : MasterOdValue) -> Json {
  match value {
    U8(v) => Json::number(v.to_uint().to_double())
    U16(v) => Json::number(v.to_double())
    U32(v) => Json::number(v.to_double())
    I32(v) => Json::number(v.to_double())
    Str(s) => Json::string(s)
    Raw(b) => {
      let mut hex = ""
      for i = 0; i < b.length(); i = i + 1 {
        hex = hex + hex_byte(b[i])
      }
      Json::string(hex)
    }
  }
}

///|
/// Format the full OD as a human-readable text table.
pub fn MasterObjectDictionary::format_text(
  self : MasterObjectDictionary,
) -> String {
  let buf = StringBuilder(size_hint=1024)
  buf.write_string("Master Object Dictionary\n")
  buf.write_string("========================\n")
  let current_indices = self.indices()
  for idx in current_indices {
    let subs = self.entries_at(idx)
    buf.write_string("\n[0x" + hex_uint16(idx) + "] ")
    // Use first entry name (strip "/ ..." suffix if compound)
    match subs[0] {
      { name, .. } => {
        let base_name = od_base_name(name)
        buf.write_string(base_name)
      }
    }
    buf.write_string("\n")
    for entry in subs {
      buf.write_string(
        "  Sub " +
        entry.subindex.to_uint().to_string() +
        ": " +
        entry.value.format() +
        "  (" +
        entry.name +
        ")\n",
      )
    }
  }
  buf.to_string()
}

///|
fn od_base_name(name : String) -> String {
  // Extract text before " / " separator, if present
  let mut i = 0
  while i + 2 < name.length() {
    if name[i] == ' ' && name[i + 1] == '/' && name[i + 2] == ' ' {
      return name[:i].to_owned()
    }
    i = i + 1
  }
  name
}