///|
/// Warning identifies a likely mapping/structure problem; Info is advisory.
pub(all) enum LintLevel {
  Warning
  Info
} derive(Debug, Eq)

///|
pub extend LintLevel with @debug.Debug::{to_repr}

///|
pub extend LintLevel with Eq::{equal, not_equal}

///|
/// offset is a descriptor item byte offset; field_index indexes Layout.fields.
/// Codes and levels are documented in docs/lint.md. Messages are explanatory.
pub(all) struct Lint {
  offset : Int
  code : String
  level : LintLevel
  message : String
  field_index : Int?
} derive(Debug, Eq)

///|
pub extend Lint with @debug.Debug::{to_repr}

///|
pub extend Lint with Eq::{equal, not_equal}

///|
// Only raw Maximum encodings are absent from Field. Minimum values and all
// semantic/local/collection state come from the existing compiler.
priv struct LintGlobals {
  logical_max : Item?
  physical_max : Item?
}

///|
fn lint_report_key(field : Field) -> Int {
  let kind = match field.kind {
    Input => 0
    Output => 1
    Feature => 2
  }
  kind * 256 + field.report_id
}

///|
// A nested Application is an ancestor, but is not a top-level Application.
fn lint_application(layout : Layout, field : Field) -> (Bool, Int?) {
  let mut index = field.collection_index
  let mut has_application = false
  let mut top_application = None
  while index is Some(i) {
    let collection = layout.collections[i]
    if collection.collection_type == 1 {
      has_application = true
      if collection.parent_index is None {
        top_application = Some(i)
      }
    }
    index = collection.parent_index
  }
  (has_application, top_application)
}

///|
fn lint_maximum(
  item : Item?,
  minimum_nonnegative : Bool,
  name : String,
  code : String,
  field_index : Int,
  seen : Map[Int, Bool],
  lints : Array[Lint],
) -> Unit {
  if minimum_nonnegative && item is Some(maximum) {
    let signed = item_signed(maximum)
    if signed < 0 && !seen.contains(maximum.offset) {
      seen[maximum.offset] = true
      let suggestion = if maximum.data.length() < 4 {
        "A wider short item can make the positive extent explicit."
      } else {
        "No wider short item exists; check the intended unsigned extent."
      }
      lints.push({
        offset: maximum.offset,
        code,
        level: Info,
        message: "\{name} Maximum encodes unsigned \{item_unsigned(maximum)} or signed \{signed} with a nonnegative minimum. \{suggestion}",
        field_index: Some(field_index),
      })
    }
  }
}

///|
fn lint_encodings(
  items : Array[Item],
  fields : Array[Field],
  lints : Array[Lint],
) -> Unit {
  let mut globals : LintGlobals = { logical_max: None, physical_max: None, }
  let stack : Array[LintGlobals] = []
  let seen : Map[Int, Bool] = Map([])
  let mut field_index = 0
  for item in items {
    if item.type_code == 1 {
      match item.tag {
        2 => globals = { ..globals, logical_max: Some(item), }
        4 => globals = { ..globals, physical_max: Some(item), }
        10 => stack.push(globals)
        11 =>
          match stack.pop() {
            Some(saved) => globals = saved
            None => () // compile_descriptor already checked balanced Push/Pop.
          }
        _ => ()
      }
    } else if item.type_code == 0 &&
      (item.tag == 8 || item.tag == 9 || item.tag == 11) {
      let field = fields[field_index]
      if (field.flags & 257) == 0 {
        lint_maximum(
          globals.logical_max,
          field.logical_min >= 0,
          "Logical",
          "logical_max_sign",
          field_index,
          seen,
          lints,
        )
        lint_maximum(
          globals.physical_max,
          field.physical_min is Some(minimum) && minimum >= 0,
          "Physical",
          "physical_max_sign",
          field_index,
          seen,
          lints,
        )
      }
      field_index = field_index + 1
    }
  }
}

///|
fn lint_field(field : Field, index : Int, lints : Array[Lint]) -> Unit {
  // Constant/padding fields have no control Usage or meaningful logical range.
  if (field.flags & 1) != 0 {
    return
  }
  if (field.flags & 256) == 0 && (field.flags & 64) != 0 && field.bit_size <= 32 {
    let minimum = if field.logical_min < 0 {
      -(1L << (field.bit_size - 1))
    } else {
      0L
    }
    let maximum = if field.logical_min < 0 {
      (1L << (field.bit_size - 1)) - 1
    } else {
      (1L << field.bit_size) - 1
    }
    if field.logical_min == minimum && field.logical_max == maximum {
      lints.push({
        offset: field.descriptor_offset,
        code: "null_without_room",
        level: Warning,
        message: "Null State is set, but the logical range fills all \{field.bit_size}-bit values",
        field_index: Some(index),
      })
    }
  }
  let usages = usage_count(field.usage_spans)
  if usages == 0 {
    lints.push({
      offset: field.descriptor_offset,
      code: "missing_usage",
      level: Warning,
      message: "Data field has no primary Usage; a host may ignore it as padding",
      field_index: Some(index),
    })
  } else if (field.flags & 256) != 0 {
    // A buffer has no numeric selectors or per-element Usage count.
    return
  } else if (field.flags & 2) != 0 {
    if usages > field.count {
      lints.push({
        offset: field.descriptor_offset,
        code: "usage_count",
        level: Warning,
        message: "\{usages} primary Usages exceed Report Count \{field.count}; extra Usages have no Variable element",
        field_index: Some(index),
      })
    } else if usages > 1 && usages < field.count {
      lints.push({
        offset: field.descriptor_offset,
        code: "usage_count",
        level: Info,
        message: "\{usages} primary Usages for \{field.count} Variable elements; the last Usage repeats",
        field_index: Some(index),
      })
    }
  } else {
    let selectors = field.logical_max - field.logical_min + 1
    if usages.to_int64() != selectors {
      lints.push({
        offset: field.descriptor_offset,
        code: "usage_count",
        level: Warning,
        message: "\{usages} primary Usages for \{selectors} logical Array selectors; inspect the intended mapping",
        field_index: Some(index),
      })
    }
  }
}

///|
fn lint_layout(layout : Layout, lints : Array[Lint]) -> Unit {
  let last_fields : Map[Int, Int] = Map([])
  let owners : Map[Int, Array[Int]] = Map([])
  let unnumbered_conflicts : Map[Int, Bool] = Map([])
  for i = 0; i < layout.fields.length(); i = i + 1 {
    let field = layout.fields[i]
    let key = lint_report_key(field)
    last_fields[key] = i
    lint_field(field, i, lints)
    let (has_application, top_application) = lint_application(layout, field)
    if (field.flags & 1) == 0 && !has_application {
      lints.push({
        offset: field.descriptor_offset,
        code: "field_outside_application",
        level: Warning,
        message: "Data field has no Application Collection ancestor",
        field_index: Some(i),
      })
    }
    // Padding also contributes to a report: it must stay in that report's TLC.
    if top_application is Some(application) {
      let applications = owners.get_or_default(key, [])
      if !applications.contains(application) {
        if !applications.is_empty() {
          if field.report_id == 0 {
            unnumbered_conflicts[application] = true
          } else {
            lints.push({
              offset: field.descriptor_offset,
              code: "report_id_shared_across_applications",
              level: Warning,
              message: "\{report_kind_name(field.kind)} Report ID \{field.report_id} spans different top-level Application Collections",
              field_index: Some(i),
            })
          }
        }
        applications.push(application)
        owners[key] = applications
      }
    }
  }
  for i = 0; i < layout.fields.length(); i = i + 1 {
    let field = layout.fields[i]
    if last_fields[lint_report_key(field)] == i {
      let bits = field.bit_offset + field.bit_size * field.count
      if bits % 8 != 0 {
        lints.push({
          offset: field.descriptor_offset,
          code: "unaligned_report",
          level: Info,
          message: "\{report_kind_name(field.kind)} Report ID \{field.report_id} has \{bits} payload bits; pad \{8 - bits % 8} bits with zero to finish the wire byte",
          field_index: Some(i),
        })
      }
    }
  }
  let mut applications = 0
  for collection in layout.collections {
    if collection.usage is None {
      lints.push({
        offset: collection.descriptor_offset,
        code: "collection_without_usage",
        level: Warning,
        message: "Collection has no primary Usage",
        field_index: None,
      })
    }
  }
  for i = 0; i < layout.collections.length(); i = i + 1 {
    let collection = layout.collections[i]
    if collection.parent_index is None && collection.collection_type == 1 {
      applications = applications + 1
      if applications > 1 && !layout.has_report_ids {
        let conflict = unnumbered_conflicts.contains(i)
        lints.push({
          offset: collection.descriptor_offset,
          code: "multiple_applications_without_report_id",
          level: if conflict {
            Warning
          } else {
            Info
          },
          message: if conflict {
            "Unnumbered reports of the same kind span top-level Application Collections; assign separate Report IDs"
          } else {
            "Multiple top-level Application Collections have no Report IDs; check report routing if adding reports of the same kind"
          },
          field_index: None,
        })
      }
    }
  }
}

///|
// Only call with items/layout from the same successfully compiled descriptor.
// JSON export reuses these validated inputs instead of compiling again.
fn lint_compiled(layout : Layout, items : Array[Item]) -> Array[Lint] {
  let lints : Array[Lint] = []
  lint_encodings(items, layout.fields, lints)
  lint_layout(layout, lints)
  lints.sort_by((a, b) => {
    let offset = a.offset.compare(b.offset)
    if offset == 0 {
      a.code.lexical_compare(b.code)
    } else {
      offset
    }
  })
  lints
}

///|
/// Compiles first and returns the identical Diagnostic on failure. Lints never
/// change the layout. Results sort by offset, then code lexicographically.
/// Each Maximum item and each later application/report pair produce at most
/// one matching lint.
pub fn lint_descriptor(bytes : Bytes) -> Result[Array[Lint], Diagnostic] {
  let layout = match compile_descriptor(bytes) {
    Ok(layout) => layout
    Err(error) => return Err(error)
  }
  let items = match parse_items(bytes) {
    Ok(items) => items
    Err(error) => return Err(error)
  }
  Ok(lint_compiled(layout, items))
}