///|
/// Output dialect for generated DBC integration artifacts.
pub enum DbcCodegenTarget {
  DbcCodegenMoonBit
  DbcCodegenCHeader
  DbcCodegenJsonSchema
  DbcCodegenMarkdown
}

///|
pub fn dbc_codegen_target_variants() -> Array[DbcCodegenTarget] {
  [
    DbcCodegenMoonBit,
    DbcCodegenCHeader,
    DbcCodegenJsonSchema,
    DbcCodegenMarkdown,
  ]
}

///|
/// Options used by a deterministic DBC artifact generator.
pub struct DbcCodegenOptions {
  target : DbcCodegenTarget
  module_name : String
  include_comments : Bool
  include_validation : Bool
  line_ending : String
}

///|
pub fn dbc_codegen_options(
  target? : DbcCodegenTarget = DbcCodegenMoonBit,
  module_name? : String = "can_schema",
  include_comments? : Bool = true,
  include_validation? : Bool = true,
  line_ending? : String = "\n",
) -> DbcCodegenOptions {
  { target, module_name, include_comments, include_validation, line_ending }
}

///|
pub fn DbcCodegenOptions::target(self : DbcCodegenOptions) -> DbcCodegenTarget {
  self.target
}

///|
pub fn DbcCodegenOptions::module_name(self : DbcCodegenOptions) -> String {
  self.module_name
}

///|
pub fn DbcCodegenOptions::include_comments(self : DbcCodegenOptions) -> Bool {
  self.include_comments
}

///|
pub fn DbcCodegenOptions::include_validation(self : DbcCodegenOptions) -> Bool {
  self.include_validation
}

///|
pub fn DbcCodegenOptions::line_ending(self : DbcCodegenOptions) -> String {
  self.line_ending
}

///|
/// A generated artifact with reproducible metadata.
pub struct DbcGeneratedArtifact {
  path : String
  target : DbcCodegenTarget
  content : String
  line_count : Int
  message_count : Int
  signal_count : Int
  warnings : Array[String]
}

///|
pub fn DbcGeneratedArtifact::path(self : DbcGeneratedArtifact) -> String {
  self.path
}

///|
pub fn DbcGeneratedArtifact::target(
  self : DbcGeneratedArtifact,
) -> DbcCodegenTarget {
  self.target
}

///|
pub fn DbcGeneratedArtifact::content(self : DbcGeneratedArtifact) -> String {
  self.content
}

///|
pub fn DbcGeneratedArtifact::line_count(self : DbcGeneratedArtifact) -> Int {
  self.line_count
}

///|
pub fn DbcGeneratedArtifact::message_count(self : DbcGeneratedArtifact) -> Int {
  self.message_count
}

///|
pub fn DbcGeneratedArtifact::signal_count(self : DbcGeneratedArtifact) -> Int {
  self.signal_count
}

///|
pub fn DbcGeneratedArtifact::warnings(
  self : DbcGeneratedArtifact,
) -> Array[String] {
  self.warnings.copy()
}

///|
/// Render a safe identifier for generated code.
pub fn dbc_codegen_identifier(name : String) -> String {
  let builder = StringBuilder()
  for character in name {
    if (character >= 'a' && character <= 'z') ||
      (character >= 'A' && character <= 'Z') ||
      character == '_' ||
      (character >= '0' && character <= '9') {
      builder.write_char(character)
    } else {
      builder.write_char('_')
    }
  }
  let result = builder.to_string()
  if result.is_empty() {
    "unnamed"
  } else {
    result
  }
}

///|
pub fn dbc_codegen_target_name(target : DbcCodegenTarget) -> String {
  match target {
    DbcCodegenMoonBit => "moonbit"
    DbcCodegenCHeader => "c-header"
    DbcCodegenJsonSchema => "json-schema"
    DbcCodegenMarkdown => "markdown"
  }
}

///|
/// Generate an artifact for a collection of DBC messages.
pub fn dbc_codegen(
  messages : Array[Message],
  options : DbcCodegenOptions,
) -> DbcGeneratedArtifact {
  let lines : Array[String] = []
  let warnings : Array[String] = []
  if options.include_comments() {
    lines.push("generated by moonbit-canbus")
    lines.push("module=" + dbc_codegen_identifier(options.module_name()))
  }
  let mut signal_count = 0
  for item in messages {
    if !item.validate() {
      warnings.push("invalid message " + item.name())
    }
    lines.append(dbc_codegen_message(item, options))
    signal_count += item.signal_count()
  }
  if options.include_validation() {
    if messages.is_empty() {
      warnings.push("schema contains no messages")
    }
    for item in messages {
      if item.dlc() > 8 &&
        item.dlc() != 12 &&
        item.dlc() != 16 &&
        item.dlc() != 20 &&
        item.dlc() != 24 &&
        item.dlc() != 32 &&
        item.dlc() != 48 &&
        item.dlc() != 64 {
        warnings.push("non-canonical CAN-FD length in " + item.name())
      }
    }
  }
  let content = lines.join(options.line_ending())
  {
    path: options.module_name() +
    "." +
    dbc_codegen_target_name(options.target()),
    target: options.target(),
    content,
    line_count: lines.length(),
    message_count: messages.length(),
    signal_count,
    warnings,
  }
}

///|
fn dbc_codegen_message(
  item : Message,
  options : DbcCodegenOptions,
) -> Array[String] {
  let lines : Array[String] = []
  let identifier = dbc_codegen_identifier(item.name())
  match options.target() {
    DbcCodegenMoonBit => {
      lines.push(
        "const " + identifier + "_id : UInt = " + item.id().to_string(),
      )
      lines.push(
        "const " + identifier + "_dlc : Int = " + item.dlc().to_string(),
      )
      for signal in item.signals() {
        lines.push(
          "const " +
          identifier +
          "_" +
          dbc_codegen_identifier(signal.name()) +
          "_start : Int = " +
          signal.start_bit().to_string(),
        )
        lines.push(
          "const " +
          identifier +
          "_" +
          dbc_codegen_identifier(signal.name()) +
          "_size : Int = " +
          signal.size().to_string(),
        )
      }
    }
    DbcCodegenCHeader => {
      lines.push("#define " + identifier + "_ID " + item.id().to_string() + "u")
      lines.push("#define " + identifier + "_DLC " + item.dlc().to_string())
      for signal in item.signals() {
        let name = identifier + "_" + dbc_codegen_identifier(signal.name())
        lines.push(
          "#define " + name + "_START " + signal.start_bit().to_string(),
        )
        lines.push("#define " + name + "_LENGTH " + signal.size().to_string())
      }
    }
    DbcCodegenJsonSchema => {
      lines.push("message " + item.id().to_string() + " " + item.name() + " {")
      lines.push("  dlc: " + item.dlc().to_string() + ",")
      for signal in item.signals() {
        lines.push(
          "  signal " +
          signal.name() +
          " { start: " +
          signal.start_bit().to_string() +
          ", size: " +
          signal.size().to_string() +
          " },",
        )
      }
      lines.push("}")
    }
    DbcCodegenMarkdown => {
      lines.push("### " + item.name())
      lines.push(
        "Identifier: `0x" +
        item.id().to_string() +
        "`; DLC: `" +
        item.dlc().to_string() +
        "`",
      )
      lines.push("| Signal | Start | Size | Factor | Offset | Unit |")
      lines.push("| --- | ---: | ---: | ---: | ---: | --- |")
      for signal in item.signals() {
        lines.push(
          "| " +
          signal.name() +
          " | " +
          signal.start_bit().to_string() +
          " | " +
          signal.size().to_string() +
          " | " +
          signal.factor().to_string() +
          " | " +
          signal.offset().to_string() +
          " | " +
          signal.unit() +
          " |",
        )
      }
    }
  }
  lines
}

///|
/// Lint a DBC schema for issues that affect generated integration code.
pub fn dbc_codegen_lint(messages : Array[Message]) -> Array[String] {
  let issues : Array[String] = []
  for index, left in messages {
    if !left.validate() {
      issues.push("invalid message: " + left.name())
    }
    if left.dlc() == 0 {
      issues.push("zero DLC message: " + left.name())
    }
    for right in messages[index + 1:] {
      if left.id() == right.id() {
        issues.push("duplicate identifier: " + left.id().to_string())
      }
      if left.name() == right.name() {
        issues.push("duplicate message name: " + left.name())
      }
    }
    for signal in left.signals() {
      if signal.size() <= 0 {
        issues.push("empty signal: " + signal.name())
      }
      if signal.factor() == 0.0 {
        issues.push("zero factor: " + signal.name())
      }
    }
  }
  issues
}

///|
/// Produce one artifact per supported target.
pub fn dbc_codegen_all_targets(
  messages : Array[Message],
  module_name : String,
) -> Array[DbcGeneratedArtifact] {
  let result : Array[DbcGeneratedArtifact] = []
  for target in dbc_codegen_target_variants() {
    result.push(
      dbc_codegen(messages, dbc_codegen_options(target~, module_name~)),
    )
  }
  result
}

///|
/// Return the total generated source size across artifacts.
pub fn dbc_codegen_total_lines(artifacts : Array[DbcGeneratedArtifact]) -> Int {
  let mut total = 0
  for artifact in artifacts {
    total += artifact.line_count()
  }
  total
}

///|
/// Return a deterministic schema report suitable for CI.
pub fn dbc_codegen_report(messages : Array[Message]) -> String {
  let artifacts = dbc_codegen_all_targets(messages, "can_schema")
  let issues = dbc_codegen_lint(messages)
  "messages=" +
  messages.length().to_string() +
  " issues=" +
  issues.length().to_string() +
  " generated_lines=" +
  dbc_codegen_total_lines(artifacts).to_string()
}

///|
/// Generate a DBC workspace artifact without mutating its schema.
pub fn dbc_codegen_workspace(
  workspace : DbcWorkspace,
  options : DbcCodegenOptions,
) -> DbcGeneratedArtifact {
  dbc_codegen(workspace.messages(), options)
}