///|
/// The limits Discord documents for the components of one message, for both
/// action-row layouts and Components V2.
pub fn message_component_limit_violations(
  components : Array[Component],
) -> Array[LimitViolation] {
  let checker = LimitChecker::new()
  if !requires_components_v2(components) {
    let rows = components.filter(component => component is ActionRow(_))
    checker.count("components", Some(rows.length()), min=0, max=5)
  }
  let mut total = 0
  for index, component in components {
    total += checker.message_component("components[\{index}]", component)
  }
  if requires_components_v2(components) && total > 40 {
    checker.report(
      "components",
      "\{total} total components including nested components (at most 40)",
    )
  }
  checker.violations
}

///|
fn LimitChecker::message_component(
  self : LimitChecker,
  path : String,
  component : Component,
) -> Int {
  let mut total = 1
  match component {
    Unknown(_) => return 0
    ActionRow(row) => {
      let mut buttons = 0
      let mut selects = 0
      for child in row.components {
        match child {
          Button(_) => buttons += 1
          StringSelect(_)
          | UserSelect(_)
          | RoleSelect(_)
          | MentionableSelect(_)
          | ChannelSelect(_) => selects += 1
          _ => ()
        }
      }
      self.count(
        "\{path}.components",
        Some(buttons + selects),
        min=0,
        max=if selects > 0 { 1 } else { 5 },
      )
      for index, child in row.components {
        total += self.message_component("\{path}.components[\{index}]", child)
      }
    }
    Button(button) => {
      let custom_id = button.custom_id
      self.text("\{path}.label", button.label, max=80, custom_id?)
      self.text("\{path}.custom_id", custom_id, min=1, max=100, custom_id?)
      self.text("\{path}.url", button.url, max=512, custom_id?)
    }
    StringSelect(select) =>
      self.select_component(path, select, string_select=true)
    UserSelect(select)
    | RoleSelect(select)
    | MentionableSelect(select)
    | ChannelSelect(select) =>
      self.select_component(path, select, string_select=false)
    Section(section) => {
      self.count(
        "\{path}.components",
        Some(section.components.length()),
        min=1,
        max=3,
      )
      for index, child in section.components {
        total += self.message_component("\{path}.components[\{index}]", child)
      }
      total += self.message_component("\{path}.accessory", section.accessory)
    }
    Thumbnail(thumbnail) =>
      self.text(
        "\{path}.description",
        thumbnail.description.bind(value => value.to_option()),
        max=1024,
      )
    MediaGallery(gallery) => {
      self.count("\{path}.items", Some(gallery.items.length()), min=1, max=10)
      for index, item in gallery.items {
        self.text(
          "\{path}.items[\{index}].description",
          item.description.bind(value => value.to_option()),
          max=1024,
        )
      }
    }
    Separator(separator) =>
      self.count("\{path}.spacing", separator.spacing, min=1, max=2)
    Container(container) => {
      self.count(
        "\{path}.accent_color",
        container.accent_color.bind(value => value.to_option()),
        min=0,
        max=0xFFFFFF,
      )
      for index, child in container.components {
        total += self.message_component("\{path}.components[\{index}]", child)
      }
    }
    Label(label) =>
      total += self.message_component("\{path}.component", label.component)
    _ => ()
  }
  total
}

///|
/// The select limits are the same in messages and modals.
fn LimitChecker::select_component(
  self : LimitChecker,
  path : String,
  select : SelectMenu,
  string_select~ : Bool,
) -> Unit {
  let custom_id = select.custom_id
  self.text("\{path}.custom_id", Some(custom_id), min=1, max=100, custom_id~)
  if string_select {
    self.count(
      "\{path}.options",
      select.options.map(options => options.length()),
      min=0,
      max=25,
      custom_id~,
    )
  }
  self.text("\{path}.placeholder", select.placeholder, max=150, custom_id~)
  self.count("\{path}.min_values", select.min_values, min=0, max=25, custom_id~)
  self.count("\{path}.max_values", select.max_values, min=0, max=25, custom_id~)
  self.ordered(
    "\{path}.min_values",
    select.min_values,
    select.max_values,
    custom_id~,
  )
  if string_select {
    if select.options is Some(options) {
      for index, option in options {
        let path = "\{path}.options[\{index}]"
        self.text("\{path}.label", Some(option.label), max=100, custom_id~)
        self.text("\{path}.value", Some(option.value), max=100, custom_id~)
        self.text(
          "\{path}.description",
          option.description,
          max=100,
          custom_id~,
        )
      }
    }
  } else if select.default_values is Some(values) && !values.is_empty() {
    // An empty list preselects nothing, like an omitted one.
    self.count(
      "\{path}.default_values",
      Some(values.length()),
      min=select.min_values.unwrap_or(1),
      max=select.max_values.unwrap_or(1),
      custom_id~,
    )
  }
}