///|
fn severity(
  config : LintConfig,
  default : @diagnostic.Severity,
) -> @diagnostic.Severity {
  if config.warnings_as_errors && default is @diagnostic.Warning {
    @diagnostic.Error
  } else {
    default
  }
}

///|
fn make_diagnostic(
  config : LintConfig,
  code : String,
  default : @diagnostic.Severity,
  message : String,
  span : @source.Span,
) -> @diagnostic.Diagnostic? {
  if config.enabled(code) {
    Some(
      @diagnostic.Diagnostic::new(
        code,
        severity(config, default),
        message,
        span,
      ),
    )
  } else {
    None
  }
}

///|
fn diagnostics_for_signal(
  config : LintConfig,
  mod : @semantic.ModuleInfo,
  signal : @semantic.SignalInfo,
) -> Array[@diagnostic.Diagnostic] {
  let diagnostics : Array[@diagnostic.Diagnostic] = []
  if !signal.from_port && !signal.has_driver() {
    match
      make_diagnostic(
        config,
        "RTL201",
        @diagnostic.Warning,
        "signal is never driven",
        signal.declaration_span,
      ) {
      Some(value) => diagnostics.push(value)
      None => ()
    }
  }
  if signal.drivers.length() > 1 {
    match
      make_diagnostic(
        config,
        "RTL202",
        @diagnostic.Error,
        "signal has multiple drivers",
        signal.declaration_span,
      ) {
      Some(value) => diagnostics.push(value)
      None => ()
    }
  }
  if signal.from_port &&
    mod.ports.any(fn(port) {
      port.name == signal.name && port.direction is @syntax.Output
    }) &&
    !signal.has_driver() {
    match
      make_diagnostic(
        config,
        "RTL203",
        @diagnostic.Warning,
        "output port is never assigned",
        signal.declaration_span,
      ) {
      Some(value) => diagnostics.push(value)
      None => ()
    }
  }
  diagnostics
}

///|
fn diagnostics_for_issue(
  config : LintConfig,
  issue : @semantic.SemanticIssue,
) -> @diagnostic.Diagnostic? {
  let default = if issue.code == "RTL102" {
    @diagnostic.Error
  } else {
    @diagnostic.Warning
  }
  make_diagnostic(config, issue.code, default, issue.message, issue.span)
}

///|
fn diagnostics_for_contract(
  config : LintConfig,
  contract : @semantic.PortContract,
) -> @diagnostic.Diagnostic? {
  match contract.status {
    @semantic.WidthMismatch =>
      make_diagnostic(
        config,
        "RTL205",
        @diagnostic.Error,
        "instance port width does not match the connected expression",
        contract.span,
      )
    @semantic.UnknownModule | @semantic.MissingPort =>
      make_diagnostic(
        config,
        "RTL206",
        @diagnostic.Warning,
        "instance port contract cannot be resolved",
        contract.span,
      )
    @semantic.DirectionMismatch =>
      make_diagnostic(
        config,
        "RTL205",
        @diagnostic.Error,
        "instance port direction does not match the connection",
        contract.span,
      )
    @semantic.Match | @semantic.UnknownWidth => None
  }
}

///|
fn diagnostics_for_control_flow(
  config : LintConfig,
  index : @semantic.SemanticIndex,
  module_name : String,
) -> Array[@diagnostic.Diagnostic] {
  let diagnostics : Array[@diagnostic.Diagnostic] = []
  match index.control_flow(module_name) {
    None => ()
    Some(summary) =>
      for name in summary.conditionally_assigned {
        match index.find_module(module_name) {
          Some(mod) =>
            match mod.signal(name) {
              Some(signal) =>
                match
                  make_diagnostic(
                    config,
                    "RTL207",
                    @diagnostic.Info,
                    "procedural assignment is conditional on control flow",
                    signal.declaration_span,
                  ) {
                  Some(value) => diagnostics.push(value)
                  None => ()
                }
              None => ()
            }
          None => ()
        }
      }
  }
  diagnostics
}

///|
pub fn default_rules() -> Array[Rule] {
  [
    UndrivenSignal,
    MultipleDrivers,
    UnresolvedInstance,
    DuplicateDeclaration,
    OutputNotAssigned,
    WidthMismatch,
    ContractWidthMismatch,
    UnknownContract,
    ConditionalAssignment,
  ]
}

///|
pub fn run(
  index : @semantic.SemanticIndex,
  config : LintConfig,
) -> Array[@diagnostic.Diagnostic] {
  let diagnostics : Array[@diagnostic.Diagnostic] = []
  for issue in index.issues {
    match diagnostics_for_issue(config, issue) {
      Some(value) => diagnostics.push(value)
      None => ()
    }
  }
  for mod in index.modules {
    for signal in mod.signals {
      for diagnostic in diagnostics_for_signal(config, mod, signal) {
        diagnostics.push(diagnostic)
      }
    }
    for diagnostic in diagnostics_for_control_flow(config, index, mod.name) {
      diagnostics.push(diagnostic)
    }
  }
  for contract in index.port_contracts() {
    match diagnostics_for_contract(config, contract) {
      Some(value) => diagnostics.push(value)
      None => ()
    }
  }
  diagnostics
}