///|
priv enum ValueType {
  NullType
  BooleanType
  NumberType
  IntegerType
  StringType
  ArrayType
  ObjectType
}

///|
/// `Located` wraps every compiled schema node with the index of the schema
/// resource it belongs to. The arena (`Program.nodes`) stores `Located`
/// nodes, so evaluation can always recover the resource identity — and from
/// it the source document URI — of the plan it is standing in. Nested plans
/// (branches, property schemas, ref targets) are `Located` too.
priv enum Plan {
  Pending
  Accept
  Reject(String)
  Rules(Array[Rule])
  Located(Int32, Plan)
}

///|
priv enum Rule {
  Types(Array[ValueType], String)
  Constant(Json, String)
  Enumeration(Array[Json], String)
  Lower(ExactNumber, Bool, String)
  Upper(ExactNumber, Bool, String)
  Multiple(ExactNumber, String)
  StringLength(CountBound?, CountBound?, String)
  Pattern(PatternSupport, String)
  Objects(ObjectPlan)
  Arrays(ArrayPlan)
  All(Array[Plan], String)
  Any(Array[Plan], String)
  One(Array[Plan], String)
  Negate(Plan, String)
  Conditional(Plan, Plan?, Plan?)
  Ref(Int32)
  /// `$dynamicRef`: the arena slot is the initially resolved target; the name
  /// is present only when that target actually carries a `$dynamicAnchor`
  /// (otherwise the reference behaves exactly like `Ref`).
  DynamicRef(Int32, String?)
  /// Compiled last within a node so the evaluator can run them as a final
  /// pass over the annotations collected by every earlier keyword.
  UnevaluatedProperties(Plan, String)
  UnevaluatedItems(Plan, String)
}

///|
priv struct ObjectPlan {
  path : String
  properties : Map[String, Plan]
  patterns : Array[(PatternSupport, Plan)]
  additional : Plan?
  required : Array[String]
  names : Plan?
  minimum : CountBound?
  maximum : CountBound?
  dependent_required : Array[(String, Array[String])]
  dependent_schemas : Array[(String, Plan)]
}

///|
priv struct ArrayPlan {
  path : String
  prefix : Array[Plan]
  items : Plan?
  contains : Plan?
  min_contains : CountBound
  max_contains : CountBound?
  minimum : CountBound?
  maximum : CountBound?
  unique : Bool
}

///|
/// Metadata for one schema resource (a `$id`-identified subtree or a whole
/// source document). `uri` is the resource identity: the canonical URI the
/// resource calls itself (`None` only for an anonymous root document).
/// `document_uri` identifies the source document the resource was supplied
/// in, so a schema pointer (which is relative to that document) can be made
/// absolute by appending it to `document_uri`. `dynamic_anchors` maps
/// `$dynamicAnchor` names to arena slots for `$dynamicRef` bookending.
priv struct ResourceMeta {
  uri : String?
  document_uri : String?
  dynamic_anchors : Map[String, Int32]
  dialect : Dialect
}

///|
/// An owned indexed node graph: `$ref` rules jump to arena slots instead of
/// inlining targets, so recursive schemas compile once and validation never
/// builds cyclic heap references or expands plans exponentially. `resources`
/// describes every schema resource the plan spans; `collect_evaluated` is
/// true when a compiled node uses `unevaluatedProperties`/`unevaluatedItems`
/// and the evaluator must track evaluated-instance annotations.
priv struct Program {
  nodes : Array[Plan]
  resources : Array[ResourceMeta]
  collect_evaluated : Bool
  track_dynamic_scope : Bool
}

///|
/// An owned, precompiled 2020-12 validation plan. Compilation does not mutate
/// the source document; constants and enumerations are copied into the plan.
/// After compilation the schema depends only on the plan: later mutations to
/// the documents or the registry map are not observed.
pub struct Schema {
  priv plan : Program
}

///|
fn pointer_step(path : String, key : String) -> String {
  path + "/" + key.replace_all(old="~", new="~0").replace_all(old="/", new="~1")
}

///|
fn owned_json(value : Json) -> Json {
  match value {
    Object(members) => {
      let copy : Map[String, Json] = Map([])
      for key, child in members {
        copy.set(key, owned_json(child))
      }
      Json::object(copy)
    }
    Array(values) => Json::array(values.map(owned_json))
    _ => value
  }
}