///|
/// Parse the supported DBC subset using strict syntax and default budgets.
pub fn parse_dbc(source : String) -> Database raise DbcError {
  parse_dbc_with_options(source, ParseOptions::default())
}

///|
/// Bounded parsing. Semantic diagnostics remain inspectable via validation.
pub fn parse_dbc_with_options(
  source : String,
  options : ParseOptions,
) -> Database raise DbcError {
  let p = Parser::{
    tokens: lex(source, options.limits),
    index: 0,
    limits: options.limits,
  }
  let db = Database::new()
  let pending : Array[(UInt, String, Array[ValueDescription], SourceLocation)] = []
  let mut current : Message? = None
  let mut total_signals = 0
  let mut metadata_records = 0
  while true {
    p.skip_lines()
    if p.peek().text == "" && !p.peek().quoted {
      break
    }
    let head = p.take()
    if head.quoted {
      p.error("parse.keyword", "quoted statement keyword")
    }
    match head.text {
      "VERSION" => {
        db.version = p.quoted()
        p.end_line()
      }
      "NS_" => {
        p.expect(":")
        while p.peek().text != "BS_" && p.peek().text != "" {
          let declaration = p.take()
          if declaration.text != "\n" &&
            (declaration.quoted || !valid_name(declaration.text)) {
            p.error(
              "parse.namespace", "invalid namespace declaration or missing BS_ terminator",
            )
          }
        }
        if p.peek().text == "" {
          p.error("parse.namespace", "NS_ must terminate at BS_")
        }
      }
      "BS_" => {
        p.expect(":")
        p.end_line()
      }
      "BU_" => {
        p.expect(":")
        while p.peek().text != "\n" && p.peek().text != "" {
          let location = p.peek().location
          if db.nodes.length() >= p.limits.max_nodes {
            p.error("limit.nodes", "node budget exceeded")
          }
          db.nodes.push({ name: p.name(), location })
        }
        p.end_line()
      }
      "BO_" => {
        let raw_dbc_id = p.id()
        let frame_id = frame_id_from_dbc(raw_dbc_id) catch {
          Failure(d) =>
            raise Failure(diagnostic(d.code, d.message, location=head.location))
        }
        let name = p.name()
        p.expect(":")
        let length = p.small_integer()
        let transmitter = p.name()
        p.end_line()
        if length > 64 {
          p.error("message.length", "payload must be 0..64 bytes")
        }
        if db.messages.length() >= p.limits.max_messages {
          p.error("limit.messages", "message budget exceeded")
        }
        let message = Message::{
          raw_dbc_id,
          frame_id,
          name,
          length,
          transmitter,
          signals: [],
          location: head.location,
        }
        db.messages.push(message)
        current = Some(message)
      }
      "SG_" => {
        let message = match current {
          Some(m) => m
          None => {
            p.error("parse.orphan_signal", "SG_ must follow BO_")
            continue
          }
        }
        let signal = parse_signal(p, head.location)
        try signal_bits(signal, message.length) catch {
          Failure(d) =>
            raise Failure(signal_diagnostic(message, signal, d.code, d.message))
        } noraise {
          _ => ()
        }
        if signal.factor == 0.0 {
          raise Failure(
            signal_diagnostic(
              message, signal, "signal.factor", "zero scaling factor",
            ),
          )
        }
        if signal.minimum > signal.maximum {
          raise Failure(
            signal_diagnostic(
              message, signal, "signal.range", "minimum exceeds maximum",
            ),
          )
        }
        if message.signals.length() >= p.limits.max_signals_per_message ||
          total_signals >= p.limits.max_total_signals {
          p.error("limit.signals", "signal budget exceeded")
        }
        message.signals.push(signal)
        total_signals += 1
      }
      "CM_" => {
        let target = p.object_ref()
        let text = p.quoted()
        p.expect(";")
        db.comments.push({ target, text, location: head.location })
      }
      "VAL_" => {
        let id = p.id()
        let name = p.name()
        let values = []
        while p.peek().text != ";" {
          p.skip_lines()
          if p.peek().text == ";" {
            break
          }
          let raw = p.integer()
          let label = p.quoted()
          metadata_records += 1
          if metadata_records > p.limits.max_metadata_records {
            p.error("limit.metadata", "metadata budget exceeded")
          }
          values.push({ raw, label })
        }
        p.expect(";")
        pending.push((id, name, values, head.location))
      }
      "BA_DEF_" =>
        db.attribute_definitions.push(
          parse_attribute_definition(p, head.location),
        )
      "BA_DEF_DEF_" => {
        let name = p.quoted()
        let value = parse_attribute_value(p)
        p.expect(";")
        db.attributes.push({
          name,
          target: None,
          value,
          location: head.location,
        })
      }
      "BA_" => {
        let name = p.quoted()
        let target = p.object_ref()
        let value = parse_attribute_value(p)
        p.expect(";")
        db.attributes.push({
          name,
          target: Some(target),
          value,
          location: head.location,
        })
      }
      _ => preserve_statement(p, source, head, options.mode, db)
    }
    if head.text != "BO_" &&
      head.text != "SG_" &&
      head.text != "BU_" &&
      head.text != "VERSION" &&
      head.text != "BS_" &&
      head.text != "NS_" {
      metadata_records += 1
      if metadata_records > p.limits.max_metadata_records {
        p.error("limit.metadata", "metadata budget exceeded")
      }
    }
    if head.text.has_prefix("BA_") &&
      p.tokens[p.index - 1].end_offset - head.location.offset >
      p.limits.max_attribute_size {
      p.error("limit.attribute", "attribute exceeds budget")
    }
  }
  for entry in pending {
    let (id, name, values, location) = entry
    let message = db.message_by_id(frame_id_from_dbc(id))
    match message {
      Some(m) =>
        match m.signal_by_name(name) {
          Some(s) =>
            for value in values {
              s.values.push(value)
            }
          None =>
            raise Failure(
              diagnostic(
                "values.signal",
                "VAL_ references unknown signal \{name}",
                location~,
              ),
            )
        }
      None =>
        raise Failure(
          diagnostic(
            "values.message",
            "VAL_ references unknown message \{id}",
            location~,
          ),
        )
    }
  }
  db
}

///|
fn parse_signal(p : Parser, location : SourceLocation) -> Signal raise DbcError {
  let name = p.name()
  let multiplex = if p.peek().text == ":" {
    Always
  } else {
    let mux = p.name()
    if mux == "M" {
      Multiplexer
    } else if mux.has_prefix("m") {
      let value = @string.parse_int64(mux[1:]) catch {
        _ => {
          p.error("mux.syntax", "expected m followed by a nonnegative integer")
          0L
        }
      }
      if value < 0L {
        p.error("mux.syntax", "negative branch")
      }
      Branch(value)
    } else {
      p.error("mux.syntax", "unsupported multiplex marker")
      Always
    }
  }
  p.expect(":")
  let start_bit = p.small_integer()
  p.expect("|")
  let bit_length = p.small_integer()
  if bit_length < 1 || bit_length > 64 {
    p.error("signal.length", "integer signals must be 1..64 bits")
  }
  p.expect("@")
  if p.peek().quoted {
    p.error("signal.order", "quoted byte order is invalid")
  }
  let byte_order = match p.take().text {
    "1" => Intel
    "0" => Motorola
    _ => {
      p.error("signal.order", "byte order must be 0 or 1")
      Intel
    }
  }
  if p.peek().quoted {
    p.error("signal.sign", "quoted signedness is invalid")
  }
  let signed = match p.take().text {
    "+" => false
    "-" => true
    _ => {
      p.error("signal.sign", "signedness must be + or -")
      false
    }
  }
  p.expect("(")
  let factor = p.real()
  p.expect(",")
  let offset = p.real()
  p.expect(")")
  p.expect("[")
  let minimum = p.real()
  p.expect("|")
  let maximum = p.real()
  p.expect("]")
  let unit = p.quoted()
  let receivers = []
  while p.peek().text != "\n" && p.peek().text != "" {
    receivers.push(p.name())
    if p.peek().text == "," {
      p.index += 1
      if p.peek().text == "\n" || p.peek().text == "" {
        p.error("parse.receiver", "missing receiver after comma")
      }
    } else {
      break
    }
  }
  p.end_line()
  {
    name,
    start_bit,
    bit_length,
    byte_order,
    signed,
    factor,
    offset,
    minimum,
    maximum,
    unit,
    receivers,
    multiplex,
    values: [],
    location,
  }
}

///|
fn parse_attribute_definition(
  p : Parser,
  location : SourceLocation,
) -> AttributeDefinition raise DbcError {
  let scope = if p.peek().quoted {
    DatabaseScope
  } else {
    match p.peek().text {
      "BU_" => {
        p.index += 1
        NodeScope
      }
      "BO_" => {
        p.index += 1
        MessageScope
      }
      "SG_" => {
        p.index += 1
        SignalScope
      }
      _ => DatabaseScope
    }
  }
  let name = p.quoted()
  let type_token = p.take()
  if type_token.quoted {
    raise Failure(
      diagnostic(
        "attribute.type",
        "quoted attribute type",
        location=type_token.location,
      ),
    )
  }
  let kind = match type_token.text {
    "INT" => IntType(p.integer(), p.integer())
    "HEX" => HexType(p.integer(), p.integer())
    "FLOAT" => FloatType(p.real(), p.real())
    "STRING" => StringType
    "ENUM" => {
      let choices = [p.quoted()]
      while p.peek().text == "," {
        p.expect(",")
        choices.push(p.quoted())
      }
      EnumType(choices)
    }
    _ => {
      p.error("attribute.type", "unsupported attribute definition type")
      StringType
    }
  }
  p.expect(";")
  if p.peek().location.offset - location.offset > p.limits.max_attribute_size {
    p.error("limit.attribute", "attribute exceeds budget")
  }
  { name, scope, kind, location }
}

///|
fn parse_attribute_value(p : Parser) -> AttributeValue raise DbcError {
  if p.peek().quoted {
    return Text(p.quoted())
  }
  let location = p.peek().location
  let text = p.number_text()
  if !text.has_prefix("0x") &&
    !text.has_prefix("0X") &&
    (text.contains(".") || text.contains("e") || text.contains("E")) {
    let value = @string.parse_double(text) catch {
      _ =>
        raise Failure(
          diagnostic("attribute.value", "invalid numeric attribute", location~),
        )
    }
    if !finite(value) {
      p.error("attribute.value", "nonfinite attribute")
    }
    Real(value)
  } else {
    Integer(
      @string.parse_int64(text) catch {
        _ =>
          raise Failure(
            diagnostic(
              "attribute.value",
              "invalid integer attribute",
              location~,
            ),
          )
      },
    )
  }
}

///|
fn preserve_statement(
  p : Parser,
  source : String,
  head : Token,
  mode : ParseMode,
  db : Database,
) -> Unit raise DbcError {
  if head.text == "SG_MUL_VAL_" ||
    head.text == "SIG_VALTYPE_" ||
    head.text == "SIG_TYPE_REF_" {
    raise Failure(
      diagnostic(
        "parse.critical_extension",
        "unsupported codec-critical statement \{head.text}",
        location=head.location,
      ),
    )
  }
  if mode == Strict {
    raise Failure(
      diagnostic(
        "parse.unsupported",
        "unsupported statement \{head.text}",
        location=head.location,
      ),
    )
  }
  let mut end_offset = head.end_offset
  while (p.peek().text != "\n" || p.peek().quoted) &&
        (p.peek().text != "" || p.peek().quoted) {
    let t = p.take()
    end_offset = t.end_offset
    if t.text == ";" && !t.quoted {
      break
    }
  }
  db.unsupported.push({
    keyword: head.text,
    text: source[head.location.offset:end_offset].to_owned(),
    location: head.location,
  })
  db.diagnostics.push(
    diagnostic(
      "parse.preserved",
      "preserved unsupported statement \{head.text}",
      location=head.location,
      severity=Warning,
    ),
  )
}