///|
priv struct SchemaCompiler {
  definitions : Map[String, XmlNode]
  compiled : Map[String, TypeDef]
  visiting : Map[String, Bool]
}

///|
fn primitive_type(name : String) -> Primitive? {
  try Primitive::parse(name) catch {
    _ => None
  } noraise {
    primitive => Some(primitive)
  }
}

///|
fn default_encoding(primitive : Primitive) -> TypeEncoding {
  {
    primitive,
    length: 1,
    presence: Required,
    character_encoding: "",
    min_value: None,
    max_value: None,
    null_value: None,
    constant: None,
  }
}

///|
fn SchemaCompiler::resolve(
  self : SchemaCompiler,
  name : String,
  depth : Int32,
) -> TypeDef raise SbeError {
  if depth > 64 {
    raise LimitExceeded("type reference depth exceeds 64")
  }
  match self.compiled.get(name) {
    Some(value) => return value
    None => ()
  }
  if self.visiting.get(name).unwrap_or(false) {
    raise InvalidSchema("recursive type reference: \{name}", 0)
  }
  let node = match self.definitions.get(name) {
    Some(value) => value
    None =>
      match primitive_type(name) {
        Some(primitive) => {
          let value = Encoded(default_encoding(primitive))
          self.compiled[name] = value
          return value
        }
        None => raise InvalidSchema("unknown encoding type: \{name}", 0)
      }
  }
  self.visiting[name] = true
  let result = match node.local_name() {
    "type" => Encoded(parse_encoding(node))
    "enum" => {
      let primitive = self.encoding_primitive(
        node.required("encodingType"),
        depth + 1,
      )
      if primitive.is_float() {
        raise InvalidSchema(
          "enum cannot use a floating encoding",
          node.position,
        )
      }
      let values = parse_enum_cases(node, "validValue")
      if values.is_empty() {
        raise InvalidSchema(
          "enum must contain validValue entries",
          node.position,
        )
      }
      Enumeration(
        primitive,
        values,
        parse_presence(node.attributes.get("presence").unwrap_or("required")),
      )
    }
    "set" => {
      let primitive = self.encoding_primitive(
        node.required("encodingType"),
        depth + 1,
      )
      if primitive.is_float() || primitive.is_signed() || primitive == Char {
        raise InvalidSchema(
          "set encoding must be an unsigned integer",
          node.position,
        )
      }
      let values = parse_enum_cases(node, "choice")
      for entry in values {
        let bit = @string.parse_int(entry.value) catch {
          _ =>
            raise InvalidSchema("set choice must be a bit index", node.position)
        }
        if bit < 0 || bit >= primitive.width() * 8 {
          raise InvalidSchema(
            "set bit index is outside its encoding",
            node.position,
          )
        }
      }
      BitSet(primitive, values)
    }
    "composite" => {
      let members : Array[CompositeMember] = []
      let names : Map[String, Bool] = Map([])
      let mut offset = 0
      for child in node.children {
        if child.integer("sinceVersion", default=0) != 0 {
          raise Unsupported("versioned composite members")
        }
        let member_name = child.required("name")
        if names.contains(member_name) {
          raise InvalidSchema(
            "duplicate composite member: \{member_name}",
            child.position,
          )
        }
        names[member_name] = true
        let member_type_name = if child.local_name() == "ref" {
          child.required("type")
        } else {
          let qualified = "\{name}.\{member_name}"
          self.definitions[qualified] = child
          qualified
        }
        let resolved = self.resolve(member_type_name, depth + 1)
        let start = child.integer("offset", default=offset)
        if start < offset {
          raise InvalidSchema(
            "composite offset overlaps previous member",
            child.position,
          )
        }
        members.push({
          name: member_name,
          type_name: member_type_name,
          offset: start,
        })
        offset = start + type_size(resolved)
        if offset > 1048576 {
          raise LimitExceeded("composite size exceeds 1 MiB")
        }
      }
      Composite(members, offset)
    }
    _ => raise Unsupported("schema type element: \{node.name}")
  }
  self.compiled[name] = result
  self.visiting[name] = false
  result
}

///|
fn SchemaCompiler::encoding_primitive(
  self : SchemaCompiler,
  name : String,
  depth : Int32,
) -> Primitive raise SbeError {
  match self.resolve(name, depth) {
    Encoded(encoding) if encoding.length == 1 && encoding.presence != Constant =>
      encoding.primitive
    _ =>
      raise InvalidSchema("encodingType must be a scalar primitive: \{name}", 0)
  }
}

///|
fn parse_encoding(node : XmlNode) -> TypeEncoding raise SbeError {
  let primitive = Primitive::parse(node.required("primitiveType"))
  let presence = parse_presence(
    node.attributes.get("presence").unwrap_or("required"),
  )
  let value = node.text.trim().to_owned()
  let length = node.integer(
    "length",
    default=if presence == Constant && primitive == Char {
      @utf8.encode(value).length()
    } else {
      1
    },
  )
  if primitive.width() * length > 1048576 {
    raise LimitExceeded("primitive array size exceeds 1 MiB")
  }
  if presence == Constant && value.is_empty() {
    raise InvalidSchema("constant type requires a value", node.position)
  }
  let character_encoding = node.attributes
    .get("characterEncoding")
    .unwrap_or(if primitive == Char { "ASCII" } else { "" })
  if character_encoding != "" &&
    character_encoding != "ASCII" &&
    character_encoding != "US-ASCII" &&
    character_encoding != "UTF-8" {
    raise Unsupported("characterEncoding: \{character_encoding}")
  }
  {
    primitive,
    length,
    presence,
    character_encoding,
    min_value: node.attributes.get("minValue"),
    max_value: node.attributes.get("maxValue"),
    null_value: node.attributes.get("nullValue"),
    constant: if presence == Constant {
      Some(value)
    } else {
      None
    },
  }
}

///|
fn parse_enum_cases(
  node : XmlNode,
  child_name : String,
) -> Array[EnumCase] raise SbeError {
  let result : Array[EnumCase] = []
  let names : Map[String, Bool] = Map([])
  let values : Map[String, Bool] = Map([])
  for child in node.children {
    if child.local_name() != child_name {
      raise InvalidSchema("unexpected element in \{node.name}", child.position)
    }
    let name = child.required("name")
    let value = child.text.trim().to_owned()
    if names.contains(name) || values.contains(value) || value.is_empty() {
      raise InvalidSchema("duplicate or empty enum/set entry", child.position)
    }
    names[name] = true
    values[value] = true
    result.push({
      name,
      value,
      since_version: child.integer("sinceVersion", default=0),
    })
  }
  result
}