///|
/// Resolved IDL modules. No filesystem access is performed by this value.
pub struct Schema {
  priv root : String
  priv modules : Map[String, IdlModule]
  priv imports : Map[String, Map[String, String]]
  priv definitions : Map[String, IdlDefinition]
  priv diagnostics : Array[String]
}

///|
fn idl_path(path : String) -> String {
  let path = path.replace_all(old="\\", new="/")
  let absolute = path.has_prefix("/")
  let parts = []
  for part in path.split("/") {
    if part == "" || part == "." {
      continue
    }
    if part == ".." && !parts.is_empty() && parts[parts.length() - 1] != ".." {
      ignore(parts.pop())
    } else {
      parts.push(part.to_owned())
    }
  }
  (if absolute { "/" } else { "" }) + parts.join("/")
}

///|
fn idl_join(owner : String, path : String) -> String {
  let path = path.replace_all(old="\\", new="/")
  if path.has_prefix("/") || (path.length() >= 2 && path[1:2] == ":") {
    return idl_path(path)
  }
  let parts = owner.split("/").map(x => x.to_owned()).collect()
  ignore(parts.pop())
  parts.push(path)
  idl_path(parts.join("/"))
}

///|
fn idl_stem(path : String) -> String {
  let parts = path.replace_all(old="\\", new="/").split("/").collect()
  let name = parts[parts.length() - 1].to_owned()
  if name.has_suffix(".thrift") {
    name[:name.length() - 7].to_owned()
  } else {
    name
  }
}

///|
fn definition_name(definition : IdlDefinition) -> String {
  match definition {
    Alias(n, _, _)
    | Constant(n, _, _)
    | Enumeration(n, _, _)
    | Record(n, _, _, _)
    | Service(n, _, _, _) => n
  }
}

///|
fn definition_owner(name : String) -> String {
  name.split("#").next().unwrap_or("").to_owned()
}

///|
fn Schema::find(
  self : Schema,
  owner : String,
  name : String,
) -> (String, IdlDefinition) raise SchemaError {
  if name.contains("#") {
    return match self.definitions.get(name) {
      Some(d) => (name, d)
      None => raise InvalidSchema("unknown definition " + name)
    }
  }
  let local_name = owner + "#" + name
  if self.definitions.get(local_name) is Some(definition) {
    return (local_name, definition)
  }
  let parts = name.split(".").map(x => x.to_owned()).collect()
  if parts.length() > 1 &&
    self.imports.get(owner) is Some(imports) &&
    imports.get(parts[0]) is Some(imported) {
    let name = imported + "#" + parts[1:].to_owned().join(".")
    if self.definitions.get(name) is Some(definition) {
      return (name, definition)
    }
  }
  raise InvalidSchema("unknown definition " + owner + ":" + name)
}

///|
fn Schema::resolve(
  self : Schema,
  owner : String,
  t : IdlType,
  depth : Int,
) -> IdlType raise SchemaError {
  if depth > 64 {
    raise InvalidSchema("type alias or nesting cycle/limit")
  }
  match t {
    Base(_) => t
    Named(name) => {
      let (name, definition) = self.find(owner, name)
      match definition {
        Alias(_, target, _) =>
          self.resolve(definition_owner(name), target, depth + 1)
        Record(_, _, _, _) | Enumeration(_, _, _) => Named(name)
        _ => raise InvalidSchema("not a field type: " + name)
      }
    }
    ListOf(elem) => {
      let elem = self.resolve(owner, elem, depth + 1)
      if elem == Base("void") {
        raise InvalidSchema("void list element")
      }
      ListOf(elem)
    }
    SetOf(elem) => {
      let elem = self.resolve(owner, elem, depth + 1)
      if elem == Base("void") {
        raise InvalidSchema("void set element")
      }
      SetOf(elem)
    }
    MapOf(key, value) => {
      let key = self.resolve(owner, key, depth + 1)
      let value = self.resolve(owner, value, depth + 1)
      if key == Base("void") || value == Base("void") {
        raise InvalidSchema("void map type")
      }
      MapOf(key, value)
    }
  }
}

///|
fn Schema::wire_kind(self : Schema, t : IdlType) -> Kind raise SchemaError {
  match t {
    Base(name) =>
      match name {
        "bool" => BoolKind
        "byte" => ByteKind
        "i16" => I16Kind
        "i32" => I32Kind
        "i64" => I64Kind
        "double" => DoubleKind
        "string" | "binary" => BinaryKind
        "uuid" => UuidKind
        _ => raise InvalidSchema("void is not a wire value")
      }
    ListOf(_) => ListKind
    SetOf(_) => SetKind
    MapOf(_, _) => MapKind
    Named(name) =>
      match self.definitions.get(name) {
        Some(Enumeration(_, _, _)) => I32Kind
        Some(Record(_, _, _, _)) => StructKind
        _ => raise InvalidSchema("unresolved schema type")
      }
  }
}

///|
fn load_idl_modules(
  name : String,
  sources : Map[String, String],
  modules : Map[String, IdlModule],
  imports : Map[String, Map[String, String]],
  active : Array[String],
  diagnostics : Array[String],
  include_paths : Array[String],
) -> Unit raise SchemaError {
  if active.contains(name) {
    raise InvalidSchema("circular IDL include " + name)
  }
  if modules.contains(name) {
    return
  }
  if active.length() >= 32 || modules.length() >= 128 {
    raise InvalidSchema("IDL include limit")
  }
  let text = match sources.get(name) {
    Some(s) => s
    None => raise InvalidSchema("missing IDL source " + name)
  }
  let parsed = parse_idl(text, source=name)
  active.push(name)
  defer ignore(active.pop())
  let aliases = Map([])
  for include_path in parsed.includes {
    let mut target = idl_join(name, include_path)
    let import_name = idl_stem(include_path)
    if !sources.contains(target) {
      for directory in include_paths {
        let candidate = idl_path(directory + "/" + include_path)
        if sources.contains(candidate) {
          target = candidate
          break
        }
      }
    }
    if !sources.contains(target) {
      diagnostics.push(
        "missing include " + target + " (ignored unless referenced)",
      )
      continue
    }
    if aliases.contains(import_name) {
      raise InvalidSchema("duplicate include alias " + import_name)
    }
    aliases[import_name] = target
    load_idl_modules(
      target, sources, modules, imports, active, diagnostics, include_paths,
    )
  }
  modules[name] = parsed
  imports[name] = aliases
}

///|
/// Compile in-memory .thrift files, following includes relative to each source.
pub fn compile_schema(
  root : String,
  sources : Map[String, String],
  include_paths? : Array[String] = [],
) -> Schema raise SchemaError {
  if sources.length() > 128 {
    raise InvalidSchema("IDL source count limit")
  }
  let normalized = Map([])
  let mut size = 0
  for path, text in sources {
    let path = idl_path(path)
    if normalized.contains(path) {
      raise InvalidSchema("duplicate normalized IDL path")
    }
    size += text.length()
    if size > 4000000 {
      raise InvalidSchema("IDL aggregate source limit")
    }
    normalized[path] = text
  }
  let root = idl_path(root)
  let modules = Map([])
  let imports = Map([])
  let definitions = Map([])
  let diagnostics = []
  load_idl_modules(
    root,
    normalized,
    modules,
    imports,
    [],
    diagnostics,
    include_paths,
  )
  for owner, module_ in modules {
    for definition in module_.definitions {
      definitions[owner + "#" + definition_name(definition)] = definition
    }
  }
  if definitions.length() > 20000 {
    raise InvalidSchema("IDL aggregate definition limit")
  }
  let schema : Schema = { root, modules, imports, definitions, diagnostics, }
  for name, definition in definitions {
    let owner = definition_owner(name)
    match definition {
      Alias(_, target, _) => {
        let t = schema.resolve(owner, target, 0)
        if t == Base("void") {
          raise InvalidSchema("void typedef")
        }
      }
      Enumeration(_, _, _) => ()
      Constant(_, t, value) => {
        let t = schema.resolve(owner, t, 0)
        ignore(schema.const_json(owner, t, value, [], 0))
      }
      Record(_, flavor, fields, _) => {
        if flavor == "union" &&
          fields.filter(f => f.default_value != None).length() > 1 {
          raise InvalidSchema("union may have only one default field")
        }
        schema.check_fields(owner, fields, false)
      }
      Service(_, parent, methods, _) => {
        if parent is Some(parent) {
          let (_, base) = schema.find(owner, parent)
          if !(base is Service(_, _, _, _)) {
            raise InvalidSchema("service parent is not a service")
          }
        }
        for function_ in methods {
          ignore(schema.resolve(owner, function_.return_type, 0))
          schema.check_fields(owner, function_.arguments, false)
          schema.check_fields(owner, function_.exceptions, true)
        }
        ignore(schema.service_methods(owner, name, []))
      }
    }
  }
  schema
}

///|
fn Schema::check_fields(
  self : Schema,
  owner : String,
  fields : Array[IdlField],
  exceptions : Bool,
) -> Unit raise SchemaError {
  for field in fields {
    let t = self.resolve(owner, field.field_type, 0)
    if t == Base("void") {
      raise InvalidSchema("void field")
    }
    if exceptions {
      let valid = match t {
        Named(name) =>
          self.definitions.get(name) is Some(Record(_, "exception", _, _))
        _ => false
      }
      if !valid {
        raise InvalidSchema("throws field must name an exception")
      }
    }
    if field.default_value is Some(value) {
      ignore(self.const_json(owner, t, value, [], 0))
    }
  }
}

///|
fn Schema::service_methods(
  self : Schema,
  owner : String,
  name : String,
  trail : Array[String],
) -> Array[(String, IdlMethod)] raise SchemaError {
  let (name, definition) = self.find(owner, name)
  if trail.contains(name) || trail.length() >= 64 {
    raise InvalidSchema("service inheritance cycle/limit")
  }
  match definition {
    Service(_, parent, methods, _) => {
      let owner = definition_owner(name)
      trail.push(name)
      defer ignore(trail.pop())
      let result = match parent {
        None => []
        Some(parent) => self.service_methods(owner, parent, trail)
      }
      for function_ in methods {
        if result.any(x => x.1.name == function_.name) {
          raise InvalidSchema("duplicate inherited method " + function_.name)
        }
        result.push((owner, function_))
      }
      result
    }
    _ => raise InvalidSchema("not a service: " + name)
  }
}

///|
pub fn Schema::get_method(
  self : Schema,
  service : String,
  name : String,
) -> IdlMethod raise SchemaError {
  for pair in self.service_methods(self.root, service, []) {
    if pair.1.name == name {
      return pair.1
    }
  }
  raise InvalidSchema("unknown service method " + name)
}

///|
pub fn Schema::service_functions(
  self : Schema,
  service : String,
) -> Array[IdlMethod] raise SchemaError {
  self.service_methods(self.root, service, []).map(pair => pair.1)
}

///|
fn Schema::method_owner(
  self : Schema,
  service : String,
  name : String,
) -> (String, IdlMethod) raise SchemaError {
  for pair in self.service_methods(self.root, service, []) {
    if pair.1.name == name {
      return pair
    }
  }
  raise InvalidSchema("unknown service method " + name)
}

///|
pub fn Schema::services(self : Schema) -> Array[String] {
  let out = []
  for definition in self.modules[self.root].definitions {
    if definition is Service(name, _, _, _) {
      out.push(name)
    }
  }
  out
}

///|
pub fn Schema::get_module(self : Schema) -> IdlModule {
  self.modules[self.root]
}

///|
pub fn Schema::warnings(self : Schema) -> Array[String] {
  self.diagnostics.copy()
}