///|
fn attribute_value_to_json(value : AttributeValue) -> Json {
  match value {
    StringValue(text) => Json::string(text)
    StringListValue(values) =>
      Json::array([ for value in values => Json::string(value) ])
    ObjectValue(fields) => {
      let object : Map[String, Json] = {}
      for name, item in fields {
        object[name] = attribute_value_to_json(item)
      }
      Json::object(object)
    }
    BoolValue(flag) => Json::boolean(flag)
    IntValue(number) => Json::number(Double::from_int(number))
  }
}

///|
fn matcher_to_json(matcher : Matcher) -> Json {
  match matcher {
    Exact(value) => Json::string(value)
    Glob(pattern) => Json::object({ "glob": Json::string(pattern) })
    Any => Json::string("")
  }
}

///|
fn comparison_to_string(op : Comparison) -> String {
  match op {
    LessThan => "lt"
    LessOrEqual => "lte"
    GreaterThan => "gt"
    GreaterOrEqual => "gte"
  }
}

///|
fn condition_to_json(condition : Condition) -> Json {
  match condition {
    Always => Json::object({})
    Exists(path) => Json::object({ "exists": Json::string(path) })
    Equals(path~, value~) =>
      Json::object({
        "equals": Json::object({
          "path": Json::string(path),
          "value": attribute_value_to_json(value),
        }),
      })
    Same(left~, right~) =>
      Json::object({
        "same": Json::object({
          "left": Json::string(left),
          "right": Json::string(right),
        }),
      })
    OneOf(path~, values~) =>
      Json::object({
        "one_of": Json::object({
          "path": Json::string(path),
          "values": Json::array(
            [
              for value in values => attribute_value_to_json(value)
            ],
          ),
        }),
      })
    Contains(path~, value~) =>
      Json::object({
        "contains": Json::object({
          "path": Json::string(path),
          "value": Json::string(value),
        }),
      })
    StringMatches(path~, matcher~) =>
      Json::object({
        "string_matches": Json::object({
          "path": Json::string(path),
          "matcher": matcher_to_json(matcher),
        }),
      })
    ContainsAny(path~, values~) =>
      Json::object({
        "contains_any": Json::object({
          "path": Json::string(path),
          "values": Json::array([ for value in values => Json::string(value) ]),
        }),
      })
    ContainsAll(path~, values~) =>
      Json::object({
        "contains_all": Json::object({
          "path": Json::string(path),
          "values": Json::array([ for value in values => Json::string(value) ]),
        }),
      })
    Compare(path~, op~, value~) =>
      Json::object({
        "compare": Json::object({
          "path": Json::string(path),
          "op": Json::string(comparison_to_string(op)),
          "value": Json::number(Double::from_int(value)),
        }),
      })
    AllOf(conditions) =>
      Json::object({
        "all": Json::array(
          [
            for child in conditions => condition_to_json(child)
          ],
        ),
      })
    AnyOf(conditions) =>
      Json::object({
        "any": Json::array(
          [
            for child in conditions => condition_to_json(child)
          ],
        ),
      })
    Not(child) => Json::object({ "not": condition_to_json(child) })
  }
}

///|
fn Rule::to_json(self : Rule) -> Json {
  let object : Map[String, Json] = {
    "id": Json::string(self.id),
    "effect": Json::string(
      match self.effect {
        Allow => "allow"
        Deny => "deny"
      },
    ),
  }
  if !(self.subject is Any) {
    object["subject"] = matcher_to_json(self.subject)
  }
  if !(self.action is Any) {
    object["action"] = matcher_to_json(self.action)
  }
  if !(self.resource is Any) {
    object["resource"] = matcher_to_json(self.resource)
  }
  if !(self.condition is Always) {
    object["condition"] = condition_to_json(self.condition)
  }
  Json::object(object)
}

///|
fn RoleBinding::to_json(self : RoleBinding) -> Json {
  let object : Map[String, Json] = {
    "subject": Json::string(self.subject),
    "role": Json::string(self.role),
  }
  if !(self.resource is Any) {
    object["resource"] = matcher_to_json(self.resource)
  }
  Json::object(object)
}

///|
/// Serialize a policy to the documented JSON representation. A valid policy
/// can be loaded again with `policy_from_json` without changing its behavior.
pub fn Policy::to_json(self : Policy) -> Json {
  let roles : Map[String, Json] = {}
  for role, permissions in self.role_permissions {
    roles[role] = Json::array(
      [
        for permission in permissions => Json::string(permission)
      ],
    )
  }
  let parents : Map[String, Json] = {}
  for role, inherited in self.role_parents {
    parents[role] = Json::array(
      [
        for parent in inherited => Json::string(parent)
      ],
    )
  }
  Json::object({
    "roles": Json::object(roles),
    "role_parents": Json::object(parents),
    "bindings": Json::array(
      [
        for binding in self.bindings => binding.to_json()
      ],
    ),
    "rules": Json::array([ for rule in self.rules => rule.to_json() ]),
  })
}