///|
priv struct Choice {
  name : String
  mut prompt : String?
  mut prompt_if : Expr
  mut depends : Expr
  mut help : String
  mut optional : Bool
  defaults : Array[(String, Expr)]
  members : Array[String]
}

///|
fn choice_property(
  c : Choice,
  ts : Array[Token],
  linux612~ : Bool = false,
) -> Unit raise ConfigError {
  match ts[0].text {
    "bool" | "prompt" => {
      if ts.length() == 1 && ts[0].text == "bool" {
        return
      }
      let (tokens, cond) = conditional(ts, 1)
      if tokens.length() != 1 ||
        !tokens[0].quoted ||
        (!linux612 && c.prompt != None) {
        raise Invalid("choice needs one quoted prompt")
      }
      let had_prompt = c.prompt != None
      c.prompt = Some(tokens[0].text)
      c.prompt_if = if linux612 && had_prompt {
        Disjunction(c.prompt_if, cond)
      } else {
        cond
      }
    }
    "default" => {
      let (tokens, cond) = conditional(ts, 1)
      if tokens.length() != 1 || tokens[0].quoted || !identifier(tokens[0].text) {
        raise Invalid("choice default must name member")
      }
      c.defaults.push((tokens[0].text, cond))
    }
    "depends" => {
      if ts.length() < 3 || ts[1].text != "on" {
        raise Invalid("expected choice depends on")
      }
      c.depends = Conjunction(c.depends, parse_expression_tokens(suffix(ts, 2)))
    }
    "optional" => {
      if ts.length() != 1 {
        raise Invalid("optional takes no arguments")
      }
      c.optional = true
    }
    _ =>
      raise Invalid(
        "unsupported choice property (only boolean choices supported): " +
        ts[0].text,
      )
  }
}

///|
fn apply_choices(
  schema : Schema,
  requests : Fragment,
  previous : Map[String, Value],
  next : Map[String, Value],
  reasons : Map[String, String],
) -> Unit raise ConfigError {
  let modules = schema.modules_enabled(previous)
  for c in schema.choices {
    let visibility = if c.prompt == None {
      N
    } else {
      c.depends
      .condition(previous, modules, linux612=schema.linux612)
      .meet(c.prompt_if.condition(previous, modules, linux612=schema.linux612))
    }
    let visible_members = c.members.filter(fn(name) raise ConfigError {
      let d = schema.defs[schema.index(name)]
      direct_visibility(schema, d, Boolean, previous) != N
    })
    let mut selected : String? = None
    let enabled = visibility != N &&
      (
        !c.optional ||
        requests.entries.any(fn(e) {
          c.members.contains(e.0) && e.1 == Logic(Y)
        })
      )
    if enabled {
      for (name, value) in requests.entries {
        if c.members.contains(name) &&
          visible_members.contains(name) &&
          value == Logic(Y) {
          selected = Some(name)
        }
      }
      if selected == None {
        for (name, cond) in c.defaults {
          if cond.condition(previous, modules, linux612=schema.linux612) != N &&
            visible_members.contains(name) {
            selected = Some(name)
            break
          }
        }
        if selected == None && !visible_members.is_empty() {
          selected = Some(visible_members[0])
        }
      }
    }
    for name in c.members {
      next[name] = Logic(if selected == Some(name) { Y } else { N })
      reasons[name] = if selected == Some(name) {
        "choice-selected"
      } else {
        "choice-excluded"
      }
    }
  }
}

///|
pub fn Schema::choice_names(self : Schema) -> Array[String] {
  self.choices.map(fn(c) { c.name })
}

///|
pub fn Schema::choice_members(
  self : Schema,
  name : String,
) -> Array[String] raise ConfigError {
  for c in self.choices {
    if c.name == name {
      return c.members.copy()
    }
  }
  raise Invalid("unknown choice " + name)
}

///|
/// Help attached to a choice, addressed by its name or generated choice_N name.
pub fn Schema::choice_help(
  self : Schema,
  name : String,
) -> String raise ConfigError {
  for c in self.choices {
    if c.name == name {
      return c.help
    }
  }
  raise Invalid("unknown choice " + name)
}