///|
priv struct Definition {
  name : String
  mut kind : Kind?
  mut prompt : String?
  mut prompt_if : Expr
  mut depends : Expr
  defaults : Array[(Expr, Expr)]
  selects : Array[(String, Expr)]
  implies : Array[(String, Expr)]
  ranges : Array[(Expr, Expr, Expr)]
  mut modules : Bool
  mut help : String
  location : String
  mut choice_owner : String?
}

///|
fn definition(name : String, kind : Kind?, location : String) -> Definition {
  {
    name,
    kind,
    prompt: None,
    prompt_if: truth(),
    depends: truth(),
    defaults: [],
    selects: [],
    implies: [],
    ranges: [],
    modules: false,
    help: "",
    location,
    choice_owner: None,
  }
}

///|
/// Opaque schema; callers cannot mutate internal dependency structures.
pub struct Schema {
  priv defs : Array[Definition]
  priv choices : Array[Choice]
  priv nodes : Array[Definition]
  priv linux612 : Bool
}

///|
pub fn Schema::new(linux612~ : Bool = false) -> Schema {
  { defs: [], choices: [], nodes: [], linux612, }
}

///|
fn Schema::index(self : Schema, name : String) -> Int32 raise ConfigError {
  for i, d in self.defs {
    if d.name == name {
      return i
    }
  }
  raise Invalid("undefined configuration symbol " + name)
}

///|
pub fn Schema::names(self : Schema) -> Array[String] {
  self.defs.map(fn(d) { d.name })
}

///|
pub fn Schema::kind(self : Schema, name : String) -> Kind raise ConfigError {
  let d = self.defs[self.index(name)]
  match d.kind {
    Some(k) => k
    None =>
      if self.linux612 {
        StringType
      } else {
        raise Invalid("missing type: " + name)
      }
  }
}

///|
pub fn Schema::add(
  self : Schema,
  name : String,
  kind : Kind,
  prompt? : String,
) -> Unit raise ConfigError {
  if !identifier(name) || name == "n" || name == "m" || name == "y" {
    raise Invalid("invalid symbol name " + name)
  }
  if self.defs.any(fn(d) { d.name == name }) {
    raise Invalid("duplicate builder symbol " + name)
  }
  if self.defs.length() >= 4096 {
    raise Invalid("too many symbols")
  }
  let d = definition(name, Some(kind), "builder")
  d.prompt = prompt
  self.defs.push(d)
}

///|
pub fn Schema::dependency(
  self : Schema,
  name : String,
  expr : Expr,
) -> Unit raise ConfigError {
  let d = self.defs[self.index(name)]
  d.depends = Conjunction(d.depends, expr)
}

///|
pub fn Schema::default_value(
  self : Schema,
  name : String,
  value : Expr,
  condition~ : Expr = truth(),
) -> Unit raise ConfigError {
  self.defs[self.index(name)].defaults.push((value, condition))
}

///|
pub fn Schema::reverse_dependency(
  self : Schema,
  source : String,
  target : String,
  weak~ : Bool = false,
  condition~ : Expr = truth(),
) -> Unit raise ConfigError {
  let d = self.defs[self.index(source)]
  if !identifier(target) {
    raise Invalid("invalid reverse dependency target")
  }
  if weak {
    d.implies.push((target, condition))
  } else {
    d.selects.push((target, condition))
  }
}

///|
pub fn Schema::validate(self : Schema) -> Unit raise ConfigError {
  for c in self.choices {
    if self.linux612 && c.prompt == None {
      raise Invalid("choice must have a prompt")
    }
    if c.members.is_empty() && !self.linux612 {
      raise Invalid("empty choice " + c.name)
    }
    for name in c.members {
      if self.linux612 &&
        self.nodes.any(fn(node) {
          node.name == name &&
          node.prompt != None &&
          node.choice_owner != Some(c.name)
        }) {
        raise Invalid("choice member prompt outside its choice group")
      }
      let d = self.defs[self.index(name)]
      if self.linux612 && d.prompt == None {
        raise Invalid("choice member must have a prompt")
      }
      if self.kind(name) != Boolean {
        raise Invalid("only boolean choice members supported")
      }
      if !d.defaults.is_empty() {
        raise Invalid(
          "defaults on choice members unsupported; use choice default",
        )
      }
    }
    for (name, _) in c.defaults {
      if !c.members.contains(name) && !self.linux612 {
        raise Invalid("choice default must name a member")
      }
    }
  }
  let module_symbols = self.defs.filter(fn(d) { d.modules })
  if module_symbols.length() > 1 {
    raise Invalid("multiple module switches")
  }
  for d in self.defs {
    let kind = self.kind(d.name)
    d.depends.validate_bounds()
    d.prompt_if.validate_bounds()
    for (a, b) in d.defaults {
      a.validate_bounds()
      b.validate_bounds()
    }
    for (_, cond) in d.selects + d.implies {
      cond.validate_bounds()
    }
    for (a, b, c) in d.ranges {
      a.validate_bounds()
      b.validate_bounds()
      c.validate_bounds()
    }
    if d.modules && kind != Boolean && !self.linux612 {
      raise Invalid("module switch must be bool")
    }
    if !self.linux612 &&
      (kind == StringType || kind == Integer || kind == Hex) &&
      (!d.selects.is_empty() || !d.implies.is_empty()) {
      raise Invalid("select/imply source must be logical: " + d.name)
    }
    for edge in d.selects + d.implies {
      if !self.linux612 &&
        self.choices.any(fn(c) { c.members.contains(edge.0) }) {
        raise Invalid("select/imply to choice member unsupported")
      }
      if self.linux612 && !self.defs.any(fn(target) { target.name == edge.0 }) {
        continue
      }
      let target_kind = self.kind(edge.0)
      if !self.linux612 && target_kind != Boolean && target_kind != Tristate {
        raise Invalid("select/imply target must be logical: " + edge.0)
      }
    }
    if !self.linux612 && !d.ranges.is_empty() && kind != Integer && kind != Hex {
      raise Invalid("range needs numeric type")
    }
  }
}

///|
pub fn Schema::prompt(
  self : Schema,
  name : String,
) -> (String?, Expr) raise ConfigError {
  let d = self.defs[self.index(name)]
  (d.prompt, d.prompt_if)
}

///|
pub fn Schema::source_location(
  self : Schema,
  name : String,
) -> String raise ConfigError {
  self.defs[self.index(name)].location
}

///|
pub fn Schema::help(self : Schema, name : String) -> String raise ConfigError {
  self.defs[self.index(name)].help
}