///|
/// Defaults that upstream sets in `defaultConfig.ts` on top of the schema:
/// values the schema cannot express, and diagram sections built by hand.
fn handwritten_defaults() -> Map[String, Json] {
  {
    "elk": {
      "mergeEdges": false,
      "straightenEdges": true,
      "lineHops": true,
      "preset": "default",
      "layeringLayerBound": 4,
      "forceNodeModelOrder": false,
      "considerModelOrder": "NODES_AND_EDGES",
      "keepEntryNodeOnTop": false,
      "orientFeedbackEdges": true,
    },
    "pie": { "useWidth": 984 },
    "railroad": {},
    "treemap": {
      "useMaxWidth": true,
      "padding": 10,
      "diagramPadding": 8,
      "showValues": true,
      "nodeWidth": 100,
      "nodeHeight": 40,
      "borderWidth": 1,
      "valueFontSize": 12,
      "labelFontSize": 14,
      "valueFormat": ",",
    },
  }
}

///|
/// The `class` section is built from scratch upstream: only the appearance
/// defaults are carried over from the schema.
fn class_defaults(schema : Json) -> Json {
  let section : Map[String, Json] = Map([])
  for key in ["theme", "look", "layout"] {
    if schema is Object({ "class": Object(fields), .. }) &&
      fields.get(key) is Some(value) {
      section[key] = value
    }
  }
  section["hideEmptyMembersBox"] = false
  section["hierarchicalNamespaces"] = true
  Json::object(section)
}

///|
/// Keys that `defaultConfig.ts` lists with an `undefined` value. They carry
/// no default but are still accepted in directives.
let undefined_keys : Array[String] = [
  "deterministicIDSeed", "themeCSS", "nodePlacementStrategy", "layeringStrategy",
  "cycleBreakingStrategy", "nodePlacementAlignment", "tickInterval", "useWidth",
  "fontSize", "fontFamily", "terminalFill", "terminalStroke", "terminalTextColor",
  "nonTerminalFill", "nonTerminalStroke", "nonTerminalTextColor", "lineColor", "markerFill",
  "commentFill", "commentStroke", "commentTextColor", "specialFill", "specialStroke",
  "ruleNameColor", "nodeColors",
]

///|
/// Font accessors that upstream defines as functions on the `sequence` and
/// `c4` sections; they count as known configuration keys.
let font_function_keys : Array[String] = [
  "messageFont", "noteFont", "actorFont", "personFont", "external_personFont", "systemFont",
  "external_systemFont", "system_dbFont", "external_system_dbFont", "system_queueFont",
  "external_system_queueFont", "containerFont", "external_containerFont", "container_dbFont",
  "external_container_dbFont", "container_queueFont", "external_container_queueFont",
  "componentFont", "external_componentFont", "component_dbFont", "external_component_dbFont",
  "component_queueFont", "external_component_queueFont", "boundaryFont",
]

///|
/// The complete default configuration, `themeVariables` included.
fn build_defaults() -> Map[String, Json] {
  let schema = schema_defaults()
  guard assign_with_depth(schema, Json::object(handwritten_defaults()))
    is Object(config) else {
    {}
  }
  config["class"] = class_defaults(schema)
  // Upstream nests a copy of the flowchart defaults under `c4`.
  if config.get("flowchart") is Some(flowchart) &&
    config.get("c4") is Some(Object(c4)) {
    c4["flowchart"] = assign_with_depth(clone(flowchart), {
      "inheritDir": false,
    })
  }
  if (try! @theme.Theme::new("default")) is Some(variables) {
    config["themeVariables"] = Json(variables)
  }
  config
}

///|
let default_data : Map[String, Json] = build_defaults()

///|
/// Collect the keys of `value` at every depth, skipping arrays: a key
/// whose default is an array is not collected at all, as in upstream's
/// `keyify`, so a directive cannot set it.
fn collect_keys(value : Json, keys : Map[String, Unit]) -> Unit {
  guard value is Object(fields) else { return }
  for key, item in fields {
    if item is Array(_) {
      continue
    }
    keys[key] = ()
    collect_keys(item, keys)
  }
}

///|
/// Every key that appears anywhere in the default configuration. A
/// directive may only set these.
let config_keys : Map[String, Unit] = {
  let keys : Map[String, Unit] = Map([])
  collect_keys(Json::object(default_data), keys)
  for key in undefined_keys {
    keys[key] = ()
  }
  for key in font_function_keys {
    keys[key] = ()
  }
  keys
}