///|
pub struct InterfaceFunction {
  name : String
  params : Array[TypeExpr]
  result : TypeExpr
  raises : Bool
  unsupported : String?
}

///|
/// Any declared type name, including abstract `type X` and external types.
pub struct InterfaceType {
  name : String
  generic : Bool
}

///|
pub struct InterfaceRecord {
  name : String
  fields : Array[FieldDef]
  unsupported : String?
}

///|
pub struct PackageInterface {
  functions : Array[InterfaceFunction]
  records : Array[InterfaceRecord]
  types : Array[InterfaceType]
}

///|
priv struct InterfaceCursor {
  tokens : Array[String]
  mut offset : Int
}

///|
fn interface_tokens(text : String) -> Array[String] {
  let tokens : Array[String] = []
  let mut word = ""
  let mut quoted = false
  let mut escaped = false
  let mut comment = false
  let mut slash = false
  for ch in text {
    if comment {
      if ch == '\n' {
        comment = false
      }
      continue
    }
    if quoted {
      if escaped {
        escaped = false
      } else if ch == '\\' {
        escaped = true
      } else if ch == '"' {
        quoted = false
        tokens.push("")
      }
      continue
    }
    if slash {
      slash = false
      if ch == '/' {
        comment = true
        continue
      }
      tokens.push("/")
    }
    if (ch >= 'a' && ch <= 'z') ||
      (ch >= 'A' && ch <= 'Z') ||
      (ch >= '0' && ch <= '9') ||
      ch == '_' ||
      ch == '@' {
      word += ch.to_string()
      continue
    }
    if !word.is_empty() {
      tokens.push(word)
      word = ""
    }
    if ch == '"' {
      quoted = true
    } else if ch == '/' {
      slash = true
    } else if ch != ' ' && ch != '\t' && ch != '\n' && ch != '\r' {
      tokens.push(ch.to_string())
    }
  }
  if !word.is_empty() {
    tokens.push(word)
  }
  if slash {
    tokens.push("/")
  }
  tokens
}

///|
fn InterfaceCursor::peek(self : InterfaceCursor) -> String {
  self.tokens.get(self.offset).unwrap_or("")
}

///|
fn InterfaceCursor::take(self : InterfaceCursor) -> String raise ConfigError {
  if self.offset >= self.tokens.length() {
    raise Invalid("interface: unexpected end of declaration")
  }
  let value = self.tokens[self.offset]
  self.offset += 1
  value
}

///|
fn InterfaceCursor::expect(
  self : InterfaceCursor,
  token : String,
) -> Unit raise ConfigError {
  let actual = self.take()
  if actual != token {
    raise Invalid("interface: expected " + token + ", found " + actual)
  }
}

///|
fn InterfaceCursor::kind(
  self : InterfaceCursor,
  depth : Int,
) -> TypeExpr raise ConfigError {
  if depth > 32 {
    raise Invalid("interface: type nesting exceeds 32")
  }
  let name = self.take()
  if name == "Array" {
    self.expect("[")
    let element = self.kind(depth + 1)
    self.expect("]")
    List(element)
  } else if name.has_prefix("@") && self.peek() == "." {
    self.take() |> ignore
    let type_name = self.take()
    if !identifier(name[1:].to_owned()) ||
      !identifier(type_name) ||
      self.peek() == "[" ||
      self.peek() == "?" {
      raise Invalid("interface: unsupported qualified type " + name)
    }
    External(name[1:].to_owned(), type_name)
  } else {
    let kind = parse_type_expr(name, result=true)
    if self.peek() == "[" ||
      self.peek() == "?" ||
      self.peek() == "." ||
      self.peek() == "::" {
      raise Invalid(
        "interface: unsupported generic, optional or qualified type " + name,
      )
    }
    kind
  }
}

///|
fn parse_interface_function(tokens : Array[String]) -> InterfaceFunction {
  let cursor = InterfaceCursor::{ tokens, offset: 0, }
  let mut name = ""
  let params : Array[TypeExpr] = []
  let mut result = Scalar(Void)
  let mut raises = false
  let unsupported = try {
    name = cursor.take()
    if !identifier(name) {
      raise Invalid("generic functions are unsupported")
    }
    cursor.expect("(")
    if cursor.peek() != ")" {
      while true {
        params.push(cursor.kind(0))
        if cursor.peek() != "," {
          break
        }
        cursor.take() |> ignore
        if cursor.peek() == ")" {
          break
        }
        continue
      }
    }
    cursor.expect(")")
    cursor.expect("-")
    cursor.expect(">")
    result = cursor.kind(0)
    if cursor.peek() == "raise" {
      cursor.take() |> ignore
      raises = true
      // Optional error type: `Error`, `E` or `@pkg.E`.
      if cursor.offset < tokens.length() {
        let error = cursor.take()
        if error.has_prefix("@") {
          cursor.expect(".")
          cursor.take() |> ignore
        } else if !identifier(error) {
          raise Invalid("unsupported raise clause")
        }
      }
    }
    if cursor.offset != tokens.length() {
      raise Invalid(
        "labels, polymorphic effects and other signature syntax are unsupported",
      )
    }
    None
  } catch {
    Invalid(message) => Some(message)
  }
  { name, params, result, raises, unsupported, }
}

///|
fn parse_interface_record(
  tokens : Array[String],
  value_type : Bool,
) -> InterfaceRecord {
  let cursor = InterfaceCursor::{ tokens, offset: 0, }
  let mut name = ""
  let fields : Array[FieldDef] = []
  let unsupported = try {
    name = cursor.take()
    if value_type {
      raise Invalid("#value structures are unsupported")
    }
    cursor.expect("{")
    while cursor.peek() != "}" {
      if cursor.peek() == "mut" {
        cursor.take() |> ignore
      }
      let field_name = cursor.take()
      if !identifier(field_name) {
        raise Invalid("private/opaque record fields are unsupported")
      }
      cursor.expect(":")
      let kind = cursor.kind(0)
      fields.push({ name: field_name, kind, })
      if cursor.peek() == "," || cursor.peek() == ";" {
        cursor.take() |> ignore
      }
    }
    cursor.expect("}")
    if cursor.offset != tokens.length() {
      raise Invalid("generic or attributed record syntax is unsupported")
    }
    None
  } catch {
    Invalid(message) => Some(message)
  }
  { name, fields, unsupported, }
}

///|
/// Parses selected declarations structurally; unrelated traits remain opaque.
pub fn parse_package_interface(text : String) -> PackageInterface {
  let tokens = interface_tokens(text)
  let functions : Array[InterfaceFunction] = []
  let records : Array[InterfaceRecord] = []
  let types : Array[InterfaceType] = []
  // Abstract types (`type X`) and other non-pub items also end a declaration.
  let boundaries = [
    "pub", "#", "type", "fn", "struct", "enum", "suberror", "trait", "impl", "let",
    "const", "typealias", "fnalias", "async",
  ]
  let mut index = 0
  let mut value_type = false
  while index < tokens.length() {
    if tokens[index] == "#" {
      if tokens.get(index + 1) == Some("value") {
        value_type = true
      }
      index += 2
      continue
    }
    if tokens[index] == "pub" {
      index += 1
      if tokens.get(index) == Some("(") {
        while index < tokens.length() && tokens[index] != ")" {
          index += 1
        }
        index += 1
      }
    } else if !boundaries.contains(tokens[index]) {
      index += 1
      continue
    }
    let category = tokens.get(index).unwrap_or("")
    index += 1
    let start = index
    while index < tokens.length() && !boundaries.contains(tokens[index]) {
      index += 1
    }
    let declaration = tokens[start:index].to_owned()
    match category {
      "fn" => functions.push(parse_interface_function(declaration))
      "struct" => records.push(parse_interface_record(declaration, value_type))
      _ => ()
    }
    if ["struct", "type", "enum", "suberror"].contains(category) &&
      !declaration.is_empty() {
      types.push({
        name: declaration[0],
        generic: declaration.get(1) == Some("["),
      })
    }
    value_type = false
  }
  { functions, records, types, }
}

///|
/// Maps default package aliases (last path segment) to imported packages.
pub fn interface_imports(text : String) -> Map[String, String] {
  let result : Map[String, String] = Map([])
  let mut inside = false
  for line in text.split("\n") {
    let line = line.trim().to_owned()
    if line.has_prefix("import {") {
      inside = true
      continue
    }
    if inside {
      if line.has_prefix("}") {
        inside = false
        continue
      }
      let pkg = line.trim_end(chars=",").trim(chars="\"").to_owned()
      let pkg_alias = pkg.split("/").last().unwrap_or("").to_owned()
      if pkg_alias != "" {
        result[pkg_alias] = pkg
      }
    }
  }
  result
}

///|
pub fn Project::verify_interface(
  self : Project,
  text : String,
) -> Unit raise ConfigError {
  self.validate()
  let interface = parse_package_interface(text)
  for expected in self.functions {
    let actual = interface.functions
      .search_by(f => f.name == expected.name)
      .map(i => interface.functions[i])
    let actual = actual.unwrap_or_else(() => {
      raise Invalid("interface: missing public function " + expected.name)
    })
    if actual.unsupported is Some(reason) {
      raise Invalid("interface: " + expected.name + ": " + reason)
    }
    // Handles are declared opaque in the manifest but appear as names in mbti.
    if actual.params.map(k => k.moon_name()) !=
      expected.params.map(p => p.kind.moon_name()) ||
      actual.result.moon_name() != expected.result.moon_name() {
      raise Invalid(
        "interface: signature mismatch; expected " + expected.signature(),
      )
    }
    if actual.raises != expected.raises {
      raise Invalid(
        "interface: " +
        expected.name +
        (if actual.raises {
          " raises; declare \"raises\": true (manifest version 2)"
        } else {
          " does not raise; remove \"raises\""
        }),
      )
    }
  }
  for handle in self.handles {
    match interface.types.search_by(t => t.name == handle) {
      Some(i) if !interface.types[i].generic => ()
      Some(_) => raise Invalid("interface: generic handle type " + handle)
      None => raise Invalid("interface: missing public type " + handle)
    }
  }
  for expected in self.records {
    let actual = interface.records
      .search_by(r => r.name == expected.name)
      .map(i => interface.records[i])
    let actual = actual.unwrap_or_else(() => {
      raise Invalid("interface: missing public record " + expected.name)
    })
    if actual.unsupported is Some(reason) {
      raise Invalid("interface: " + expected.name + ": " + reason)
    }
    if actual.fields.length() != expected.fields.length() {
      raise Invalid("interface: incomplete fields for " + expected.name)
    }
    for field in expected.fields {
      let actual_field = actual.fields
        .search_by(f => f.name == field.name)
        .map(i => actual.fields[i])
      match actual_field {
        Some(actual_field) if actual_field.kind == field.kind => ()
        _ =>
          raise Invalid(
            "interface: field mismatch " + expected.name + "." + field.name,
          )
      }
    }
  }
}