///|
/// The variables of a mermaid theme: colors, fonts and sizes that diagram
/// stylesheets and renderers read by name (`primaryColor`, `pie1`, ...).
pub struct Theme {
  priv vars : Map[String, Json]
}

///|
/// The value of `name`, or `Json::null()` when the theme does not define it.
pub fn Theme::get(self : Theme, name : String) -> Json {
  self.vars.get(name).unwrap_or(Json::null())
}

///|
fn Theme::set(self : Theme, name : String, value : Json) -> Unit {
  self.vars[name] = value
}

///|
/// The value of `name` as text, converted the way a JavaScript template
/// literal would: a missing variable reads `undefined`.
pub fn Theme::text(self : Theme, name : String) -> String {
  to_string(self.get(name))
}

///|
/// The value of `name` when it is a number.
pub fn Theme::number(self : Theme, name : String) -> Double? {
  match self.get(name) {
    Number(n, ..) => Some(n)
    _ => None
  }
}

///|
/// Whether `name` is set to a truthy value.
pub fn Theme::flag(self : Theme, name : String) -> Bool {
  truthy(self.get(name))
}

///|
/// A field of an object-valued variable such as `xyChart` or `radar`.
pub fn Theme::nested(self : Theme, name : String, key : String) -> Json {
  field(self.get(name), key)
}

///|
/// The names of all variables, in definition order.
pub fn Theme::names(self : Theme) -> Array[String] {
  self.vars.keys().collect()
}

///|
pub impl ToJson for Theme with fn to_json(self) {
  Json::object(self.vars.copy())
}

///|
/// The names accepted by `Theme::new`.
pub let theme_names : Array[String] = [
  "base", "dark", "default", "forest", "neutral", "neo", "neo-dark", "redux", "redux-dark",
  "redux-color", "redux-dark-color",
]

///|
/// The value a variable takes once the user's override is applied. An
/// object-valued variable keeps every generated field and takes the user's
/// fields on top.
fn apply_override(generated : Json, user : Json) -> Json {
  match (generated, user) {
    (Object(base), Object(extra)) => {
      let merged = base.copy()
      for key, value in extra {
        merged[key] = value
      }
      Json::object(merged)
    }
    _ => user
  }
}

///|
/// Build the variables of the theme called `name`, applying `overrides`
/// (the user's `themeVariables`) both before and after derived colors are
/// computed. Returns `None` for an unknown theme name.
pub fn Theme::new(
  name : String,
  overrides? : Map[String, Json],
) -> Theme? raise @color.ColorError {
  guard theme_names.contains(name) else { return None }
  let theme = { vars: {}, }
  construct(name, theme)
  if name == "default" {
    // Placeholders left by the constructor are recomputed from scratch.
    for key in theme.names() {
      if theme.get(key) == str("calculated") {
        theme.set(key, Json::null())
      }
    }
  }
  guard overrides is Some(overrides) else {
    update_colors(name, theme)
    return Some(theme)
  }
  // Base colors first, so derived colors are computed from them...
  for key, value in overrides {
    theme.set(key, value)
  }
  update_colors(name, theme)
  // ...then again, so an override of a derived color wins.
  for key, value in overrides {
    theme.set(key, apply_override(theme.get(key), value))
  }
  if name == "base" &&
    overrides.contains("nodeBorder") &&
    !overrides.contains("useGradient") {
    // An explicit node border must not be painted over by the gradient.
    theme.set("useGradient", boolean(false))
  }
  Some(theme)
}

///|
/// Run the constructor of the theme class called `name`.
fn construct(name : String, theme : Theme) -> Unit raise @color.ColorError {
  match name {
    "base" => base_init(theme)
    "dark" => dark_init(theme)
    "default" => default_init(theme)
    "forest" => forest_init(theme)
    "neutral" => neutral_init(theme)
    "neo" => neo_init(theme)
    "neo-dark" => neo_dark_init(theme)
    "redux" => redux_init(theme)
    "redux-dark" => redux_dark_init(theme)
    "redux-color" => redux_color_init(theme)
    "redux-dark-color" => redux_dark_color_init(theme)
    _ => ()
  }
}

///|
/// Run `updateColors` of the theme class called `name`.
fn update_colors(name : String, theme : Theme) -> Unit raise @color.ColorError {
  match name {
    "base" => base_update_colors(theme)
    "dark" => dark_update_colors(theme)
    "default" => default_update_colors(theme)
    "forest" => forest_update_colors(theme)
    "neutral" => neutral_update_colors(theme)
    "neo" => neo_update_colors(theme)
    "neo-dark" => neo_dark_update_colors(theme)
    "redux" => redux_update_colors(theme)
    "redux-dark" => redux_dark_update_colors(theme)
    "redux-color" => redux_color_update_colors(theme)
    "redux-dark-color" => redux_dark_color_update_colors(theme)
    _ => ()
  }
}

///|
/// Rebuild a theme from variables previously exported with `to_json`,
/// without recomputing derived colors.
pub fn Theme::from_variables(variables : Map[String, Json]) -> Theme {
  { vars: variables.copy(), }
}

///|
/// Whether `name` is one of `theme_names`.
pub fn is_theme_name(name : String) -> Bool {
  theme_names.contains(name)
}