///|
/// Severity used by parser and audit diagnostics.
pub(all) enum Severity {
  Note
  Warning
  Error
} derive(Eq, Debug)

///|
/// Stable machine-readable diagnostic codes.
pub(all) enum DiagnosticCode {
  InvalidLength
  InvalidHexLength
  InvalidHexCharacter
  InvalidHeader
  InvalidChecksum
  UnsupportedVersion
  SuspiciousManufactureDate
  MissingDetailedTiming
  EmptyMonitorName
  MissingPreferredTiming
  ExtensionBlockIgnored
  NonSquarePhysicalSize
  UnusualGamma
  UnknownDescriptor
  MissingMonitorName
  InvalidDisplaySize
  ProfileRequirementNotMet
  InterlacedModeUnsupported
  RangeLimitInconsistent
} derive(Eq, Debug)

///|
/// Overall EDID audit status.
pub(all) enum AuditStatus {
  Accepted
  NeedsAttention
  Rejected
} derive(Eq, Debug)

///|
/// Display color/input family reported by the video input byte.
pub(all) enum VideoSignal {
  Digital
  Analog
} derive(Eq, Debug)

///|
/// Standard timing aspect ratio code.
pub(all) enum AspectRatio {
  Ratio16_10
  Ratio4_3
  Ratio5_4
  Ratio16_9
} derive(Eq, Debug)

///|
/// EDID base block descriptor kind.
pub(all) enum DescriptorKind {
  DetailedTiming
  SerialNumberText
  MonitorName
  RangeLimits
  DisplayProductName
  DisplayProductSerial
  Text
  Empty
  Unknown(Int)
} derive(Eq, Debug)

///|
/// Source used to derive a normalized display mode.
pub(all) enum ModeSource {
  PreferredDetailed
  DetailedDescriptor
  StandardTimingSource
  EstablishedTimingSource
  DmtDatabase
  Inferred
} derive(Eq, Debug)

///|
/// Human-oriented monitor class derived from size and preferred mode.
pub(all) enum DisplayClass {
  UnknownDisplay
  EmbeddedPanel
  DesktopMonitor
  Television
  Projector
  LargeFormatDisplay
} derive(Eq, Debug)

///|
/// Catalog family for a known display timing.
pub(all) enum TimingFamily {
  EstablishedFamily
  DmtFamily
  CtaFamily
  CvtFamily
  RbFamily
  ProjectorFamily
} derive(Eq, Debug)

///|
/// Parse error returned when an EDID input cannot produce a base block.
pub(all) struct EdidError {
  code : DiagnosticCode
  message : String
  offset : Int?
} derive(Eq, Debug)

///|
/// Recoverable diagnostic produced while parsing or auditing EDID data.
pub(all) struct EdidDiagnostic {
  code : DiagnosticCode
  severity : Severity
  message : String
  offset : Int?
} derive(Eq, Debug)

///|
/// Three-letter EISA manufacturer identifier.
pub(all) struct ManufacturerId {
  raw : Int
  code : String
} derive(Eq, Debug)

///|
/// EDID manufacture date fields.
pub(all) struct ManufactureDate {
  week : Int
  year : Int
  model_year : Bool
} derive(Eq, Debug)

///|
/// Video input information decoded from byte 20.
pub(all) struct VideoInput {
  signal : VideoSignal
  interface_name : String
  bit_depth : String
  voltage : String
  sync : String
} derive(Eq, Debug)

///|
/// Basic display dimensions and gamma.
pub(all) struct BasicDisplay {
  width_cm : Int
  height_cm : Int
  gamma_x100 : Int?
  standby : Bool
  suspend : Bool
  active_off : Bool
  preferred_timing : Bool
  default_srgb : Bool
} derive(Eq, Debug)

///|
/// Chromaticity coordinates scaled by 10000.
pub(all) struct Chromaticity {
  red_x : Int
  red_y : Int
  green_x : Int
  green_y : Int
  blue_x : Int
  blue_y : Int
  white_x : Int
  white_y : Int
} derive(Eq, Debug)

///|
/// One established timing capability.
pub(all) struct EstablishedTiming {
  name : String
  width : Int
  height : Int
  refresh_hz : Int
  enabled : Bool
  byte_offset : Int
  bit_mask : Int
} derive(Eq, Debug)

///|
/// One standard timing capability.
pub(all) struct StandardTiming {
  width : Int
  height : Int
  refresh_hz : Int
  aspect : AspectRatio
  byte_offset : Int
  valid : Bool
} derive(Eq, Debug)

///|
/// Detailed timing descriptor decoded from an 18-byte EDID descriptor.
pub(all) struct DetailedTimingInfo {
  pixel_clock_khz : Int
  h_active : Int
  h_blanking : Int
  v_active : Int
  v_blanking : Int
  h_sync_offset : Int
  h_sync_pulse : Int
  v_sync_offset : Int
  v_sync_pulse : Int
  h_size_mm : Int
  v_size_mm : Int
  h_border : Int
  v_border : Int
  interlaced : Bool
  positive_hsync : Bool
  positive_vsync : Bool
} derive(Eq, Debug)

///|
/// Normalized display mode from EDID timing sources or a known timing table.
pub(all) struct DisplayMode {
  width : Int
  height : Int
  refresh_millihz : Int
  pixel_clock_khz : Int?
  source : ModeSource
  interlaced : Bool
  preferred : Bool
  name : String
} derive(Eq, Debug)

///|
/// Known timing entry used for matching and reporting.
pub(all) struct TimingCatalogEntry {
  id : String
  family : TimingFamily
  width : Int
  height : Int
  refresh_millihz : Int
  pixel_clock_khz : Int
  progressive : Bool
  aspect : String
  name : String
} derive(Eq, Debug)

///|
/// Range limits descriptor decoded from tag `0xFD`.
pub(all) struct RangeLimits {
  min_vertical_hz : Int
  max_vertical_hz : Int
  min_horizontal_khz : Int
  max_horizontal_khz : Int
  max_pixel_clock_mhz : Int
} derive(Eq, Debug)

///|
/// One EDID descriptor slot.
pub(all) struct Descriptor {
  kind : DescriptorKind
  index : Int
  text : String?
  detailed_timing : DetailedTimingInfo?
  range_limits : RangeLimits?
} derive(Eq, Debug)

///|
/// Parsed EDID base block.
pub(all) struct Edid {
  bytes : Array[Int]
  manufacturer : ManufacturerId
  product_code : Int
  serial_number : Int
  manufacture : ManufactureDate
  version_major : Int
  version_minor : Int
  video_input : VideoInput
  display : BasicDisplay
  chromaticity : Chromaticity
  established_timings : Array[EstablishedTiming]
  standard_timings : Array[StandardTiming]
  descriptors : Array[Descriptor]
  extension_count : Int
  checksum : Int
  diagnostics : Array[EdidDiagnostic]
} derive(Eq, Debug)

///|
/// Aggregate counters for EDID diagnostics.
pub(all) struct AuditSummary {
  status : AuditStatus
  diagnostics : Int
  notes : Int
  warnings : Int
  errors : Int
  detailed_timings : Int
  standard_timings : Int
  established_timings : Int
} derive(Eq, Debug)

///|
/// Compatibility profile used by EDID audits.
pub(all) struct DisplayProfile {
  name : String
  min_width : Int
  min_height : Int
  min_refresh_hz : Int
  require_preferred_timing : Bool
  allow_interlaced : Bool
  max_extension_blocks : Int
} derive(Eq, Debug)

///|
/// Complete audit result for one EDID block.
pub(all) struct EdidAudit {
  status : AuditStatus
  profile_name : String
  diagnostics : Array[EdidDiagnostic]
  summary : AuditSummary
  preferred_mode : DisplayMode?
  display_class : DisplayClass
} derive(Eq, Debug)

///|
/// One profile matrix row.
pub(all) struct ProfileCheck {
  profile_name : String
  status : AuditStatus
  score : Int
  minimum_mode_met : Bool
  preferred_timing_met : Bool
  interlaced_policy_met : Bool
  extension_policy_met : Bool
} derive(Eq, Debug)

///|
pub fn Severity::name(self : Severity) -> String {
  match self {
    Note => "note"
    Warning => "warning"
    Error => "error"
  }
}

///|
pub fn DiagnosticCode::name(self : DiagnosticCode) -> String {
  match self {
    InvalidLength => "invalid-length"
    InvalidHexLength => "invalid-hex-length"
    InvalidHexCharacter => "invalid-hex-character"
    InvalidHeader => "invalid-header"
    InvalidChecksum => "invalid-checksum"
    UnsupportedVersion => "unsupported-version"
    SuspiciousManufactureDate => "suspicious-manufacture-date"
    MissingDetailedTiming => "missing-detailed-timing"
    EmptyMonitorName => "empty-monitor-name"
    MissingPreferredTiming => "missing-preferred-timing"
    ExtensionBlockIgnored => "extension-block-ignored"
    NonSquarePhysicalSize => "non-square-physical-size"
    UnusualGamma => "unusual-gamma"
    UnknownDescriptor => "unknown-descriptor"
    MissingMonitorName => "missing-monitor-name"
    InvalidDisplaySize => "invalid-display-size"
    ProfileRequirementNotMet => "profile-requirement-not-met"
    InterlacedModeUnsupported => "interlaced-mode-unsupported"
    RangeLimitInconsistent => "range-limit-inconsistent"
  }
}

///|
pub fn AuditStatus::name(self : AuditStatus) -> String {
  match self {
    Accepted => "accepted"
    NeedsAttention => "needs-attention"
    Rejected => "rejected"
  }
}

///|
pub fn VideoSignal::name(self : VideoSignal) -> String {
  match self {
    Digital => "digital"
    Analog => "analog"
  }
}

///|
pub fn AspectRatio::name(self : AspectRatio) -> String {
  match self {
    Ratio16_10 => "16:10"
    Ratio4_3 => "4:3"
    Ratio5_4 => "5:4"
    Ratio16_9 => "16:9"
  }
}

///|
pub fn DescriptorKind::name(self : DescriptorKind) -> String {
  match self {
    DetailedTiming => "detailed-timing"
    SerialNumberText => "serial-number-text"
    MonitorName => "monitor-name"
    RangeLimits => "range-limits"
    DisplayProductName => "display-product-name"
    DisplayProductSerial => "display-product-serial"
    Text => "text"
    Empty => "empty"
    Unknown(tag) => "unknown-" + tag.to_string()
  }
}

///|
pub fn ModeSource::name(self : ModeSource) -> String {
  match self {
    PreferredDetailed => "preferred-detailed"
    DetailedDescriptor => "detailed-descriptor"
    StandardTimingSource => "standard-timing"
    EstablishedTimingSource => "established-timing"
    DmtDatabase => "dmt-database"
    Inferred => "inferred"
  }
}

///|
pub fn DisplayClass::name(self : DisplayClass) -> String {
  match self {
    UnknownDisplay => "unknown"
    EmbeddedPanel => "embedded-panel"
    DesktopMonitor => "desktop-monitor"
    Television => "television"
    Projector => "projector"
    LargeFormatDisplay => "large-format-display"
  }
}

///|
pub fn TimingFamily::name(self : TimingFamily) -> String {
  match self {
    EstablishedFamily => "established"
    DmtFamily => "dmt"
    CtaFamily => "cta"
    CvtFamily => "cvt"
    RbFamily => "reduced-blanking"
    ProjectorFamily => "projector"
  }
}

///|
pub fn TimingCatalogEntry::label(self : TimingCatalogEntry) -> String {
  self.width.to_string() +
  "x" +
  self.height.to_string() +
  "@" +
  ((self.refresh_millihz + 500) / 1000).to_string() +
  "Hz"
}

///|
pub fn TimingCatalogEntry::pixel_count(self : TimingCatalogEntry) -> Int {
  self.width * self.height
}

///|
pub fn TimingCatalogEntry::is_high_refresh(self : TimingCatalogEntry) -> Bool {
  self.refresh_millihz >= 120000
}

///|
pub fn EdidError::make(
  code : DiagnosticCode,
  message : String,
  offset : Int?,
) -> EdidError {
  { code, message, offset }
}

///|
pub fn EdidDiagnostic::make(
  code : DiagnosticCode,
  severity : Severity,
  message : String,
  offset : Int?,
) -> EdidDiagnostic {
  { code, severity, message, offset }
}

///|
pub fn EdidDiagnostic::to_error(self : EdidDiagnostic) -> EdidError {
  { code: self.code, message: self.message, offset: self.offset }
}

///|
pub fn ManufactureDate::description(self : ManufactureDate) -> String {
  if self.model_year {
    "model-year " + self.year.to_string()
  } else if self.week == 0 {
    "year " + self.year.to_string()
  } else {
    "week " + self.week.to_string() + " of " + self.year.to_string()
  }
}

///|
pub fn BasicDisplay::has_physical_size(self : BasicDisplay) -> Bool {
  self.width_cm > 0 && self.height_cm > 0
}

///|
pub fn BasicDisplay::aspect_x1000(self : BasicDisplay) -> Int? {
  if self.height_cm == 0 {
    None
  } else {
    Some(self.width_cm * 1000 / self.height_cm)
  }
}

///|
pub fn DetailedTimingInfo::resolution(self : DetailedTimingInfo) -> String {
  self.h_active.to_string() + "x" + self.v_active.to_string()
}

///|
pub fn DetailedTimingInfo::refresh_millihz(self : DetailedTimingInfo) -> Int {
  let h_total = self.h_active + self.h_blanking
  let v_total = self.v_active + self.v_blanking
  if h_total <= 0 || v_total <= 0 {
    0
  } else {
    self.pixel_clock_khz * 1000 / h_total * 1000 / v_total
  }
}

///|
pub fn DetailedTimingInfo::refresh_hz_rounded(self : DetailedTimingInfo) -> Int {
  (self.refresh_millihz() + 500) / 1000
}

///|
pub fn DisplayMode::refresh_hz_rounded(self : DisplayMode) -> Int {
  (self.refresh_millihz + 500) / 1000
}

///|
pub fn DisplayMode::resolution(self : DisplayMode) -> String {
  self.width.to_string() + "x" + self.height.to_string()
}

///|
pub fn DisplayMode::label(self : DisplayMode) -> String {
  self.resolution() + "@" + self.refresh_hz_rounded().to_string() + "Hz"
}

///|
pub fn DisplayMode::pixel_count(self : DisplayMode) -> Int {
  self.width * self.height
}

///|
pub fn DisplayMode::is_portrait(self : DisplayMode) -> Bool {
  self.height > self.width
}

///|
pub fn DisplayMode::is_4k_or_better(self : DisplayMode) -> Bool {
  self.width >= 3840 && self.height >= 2160
}

///|
pub fn DisplayMode::is_high_refresh(self : DisplayMode) -> Bool {
  self.refresh_millihz >= 120000
}

///|
pub fn Descriptor::is_detailed_timing(self : Descriptor) -> Bool {
  self.kind == DetailedTiming
}

///|
pub fn Edid::monitor_name(self : Edid) -> String? {
  for descriptor in self.descriptors {
    if descriptor.kind == MonitorName {
      return descriptor.text
    }
  }
  None
}

///|
pub fn Edid::preferred_timing(self : Edid) -> DetailedTimingInfo? {
  for descriptor in self.descriptors {
    if descriptor.kind == DetailedTiming {
      return descriptor.detailed_timing
    }
  }
  None
}

///|
pub fn Edid::detailed_timing_count(self : Edid) -> Int {
  let mut count = 0
  for descriptor in self.descriptors {
    if descriptor.kind == DetailedTiming {
      count = count + 1
    }
  }
  count
}

///|
pub fn Edid::valid_standard_timing_count(self : Edid) -> Int {
  let mut count = 0
  for timing in self.standard_timings {
    if timing.valid {
      count = count + 1
    }
  }
  count
}

///|
pub fn Edid::identity(self : Edid) -> String {
  self.manufacturer.code + "-" + self.product_code.to_string()
}

///|
pub fn Edid::version_text(self : Edid) -> String {
  self.version_major.to_string() + "." + self.version_minor.to_string()
}

///|
pub fn Edid::checksum_valid(self : Edid) -> Bool {
  checksum_valid(self.bytes, 0)
}

///|
pub fn Edid::display_class(self : Edid) -> DisplayClass {
  if !self.display.has_physical_size() {
    UnknownDisplay
  } else if self.display.width_cm <= 36 && self.display.height_cm <= 24 {
    EmbeddedPanel
  } else if self.display.width_cm >= 100 || self.display.height_cm >= 60 {
    LargeFormatDisplay
  } else {
    match self.preferred_timing() {
      Some(timing) =>
        if timing.h_active >= 3840 && timing.v_active >= 2160 {
          Television
        } else {
          DesktopMonitor
        }
      None => DesktopMonitor
    }
  }
}

///|
pub fn Edid::enabled_established_timing_count(self : Edid) -> Int {
  let mut count = 0
  for timing in self.established_timings {
    if timing.enabled {
      count = count + 1
    }
  }
  count
}