///|
/// Compile-time semantic analysis for bindings and context-sensitive syntax.
priv struct Resolver {
  diagnostics : Array[Diagnostic]
  scopes : Array[Array[String]]
  globals : Array[String]
  forward_references : Array[String]
  mut function_depth : Int
  mut loop_depth : Int
  mut class_depth : Int
  mut in_static_method : Bool
  mut in_constructor : Bool
  mut in_foreign_class : Bool
  mut local_count : Int
}

///|
fn string_array_contains(values : Array[String], name : String) -> Bool {
  for value in values {
    if value == name {
      return true
    }
  }
  false
}

///|
fn Resolver::new(statements : Array[Stmt]) -> Resolver {
  let globals = [
    "System", "Object", "Bool", "Class", "Fiber", "Fn", "List", "Map", "MapEntry",
    "MapKeySequence", "MapValueSequence", "Null", "Num", "Range", "Sequence", "String",
    "StringByteSequence", "StringCodePointSequence",
  ]
  for statement in statements {
    match statement {
      VariableStatement(name, _, _) | ClassStatement(name, _, _, _, _, _) =>
        if !string_array_contains(globals, name) {
          globals.push(name)
        }
      ImportStatement(_, bindings, _) =>
        for binding in bindings {
          if !string_array_contains(globals, binding.local_name) {
            globals.push(binding.local_name)
          }
        }
      _ => ()
    }
  }
  {
    diagnostics: [],
    scopes: [[]],
    globals,
    forward_references: [],
    function_depth: 0,
    loop_depth: 0,
    class_depth: 0,
    in_static_method: false,
    in_constructor: false,
    in_foreign_class: false,
    local_count: 0,
  }
}

///|
fn Resolver::report(
  self : Resolver,
  code : DiagnosticCode,
  message : String,
  span : SourceSpan,
) -> Unit {
  self.diagnostics.push(Diagnostic::new(Resolve, code, message, span))
}

///|
fn Resolver::push_scope(self : Resolver) -> Unit {
  self.scopes.push([])
}

///|
fn Resolver::pop_scope(self : Resolver) -> Unit {
  ignore(self.scopes.pop())
}

///|
fn Resolver::is_declared(self : Resolver, name : String) -> Bool {
  let mut index = self.scopes.length() - 1
  while index >= 0 {
    if string_array_contains(self.scopes[index], name) {
      return true
    }
    index = index - 1
  }
  false
}

///|
fn Resolver::define_name(
  self : Resolver,
  name : String,
  span : SourceSpan,
) -> Unit {
  if string_array_contains(self.forward_references, name) {
    self.report(
      UndefinedVariable,
      "variable '\{name}' is referenced before its definition",
      span,
    )
  }
  let scope = self.scopes[self.scopes.length() - 1]
  if string_array_contains(scope, name) {
    self.report(
      DuplicateVariable,
      "variable '\{name}' is already declared in this scope",
      span,
    )
    return
  }
  scope.push(name)
  if self.scopes.length() > 1 {
    self.local_count = self.local_count + 1
    if self.local_count > 255 {
      self.report(
        InvalidOperand,
        "a function may not contain more than 256 local variables",
        span,
      )
    }
  }
}

///|
fn starts_with_uppercase(name : String) -> Bool {
  if name.length() == 0 {
    return false
  }
  let code = name.unsafe_get(0).to_int()
  code >= 65 && code <= 90
}

///|
fn Resolver::resolve_variable(
  self : Resolver,
  name : String,
  span : SourceSpan,
) -> Unit {
  if name == "this" {
    if self.class_depth == 0 {
      self.report(
        UndefinedVariable,
        "'this' cannot be used outside a class",
        span,
      )
    }
    return
  }
  if name == "$super" {
    if self.class_depth == 0 {
      self.report(
        UndefinedVariable,
        "'super' cannot be used outside a class",
        span,
      )
    }
    return
  }
  if self.is_declared(name) {
    return
  }
  if string_array_contains(self.globals, name) {
    if self.function_depth == 0 && !starts_with_uppercase(name) {
      if !string_array_contains(self.forward_references, name) {
        self.forward_references.push(name)
      }
    }
    return
  }
  if self.class_depth > 0 {
    return
  }
  self.report(UndefinedVariable, "undefined variable '\{name}'", span)
}

///|
fn Resolver::resolve_function(
  self : Resolver,
  parameters : Array[String],
  body : Array[Stmt],
  span : SourceSpan,
) -> Unit {
  if parameters.length() > 16 {
    self.report(
      InvalidArity,
      "a function may not have more than 16 parameters",
      span,
    )
  }
  let saved_function_depth = self.function_depth
  let saved_loop_depth = self.loop_depth
  let saved_local_count = self.local_count
  let saved_constructor = self.in_constructor
  self.function_depth = self.function_depth + 1
  self.loop_depth = 0
  self.local_count = 0
  self.in_constructor = false
  self.push_scope()
  for parameter in parameters {
    self.define_name(parameter, span)
  }
  for statement in body {
    self.resolve_statement(statement)
  }
  self.pop_scope()
  self.function_depth = saved_function_depth
  self.loop_depth = saved_loop_depth
  self.local_count = saved_local_count
  self.in_constructor = saved_constructor
}

///|
fn Resolver::resolve_expression(self : Resolver, expression : Expr) -> Unit {
  match expression {
    Literal(_, _) => ()
    Variable(name, span) => self.resolve_variable(name, span)
    Assign(name, value, span) => {
      self.resolve_expression(value)
      if !self.is_declared(name) &&
        !string_array_contains(self.globals, name) &&
        self.class_depth == 0 {
        self.report(UndefinedVariable, "undefined variable '\{name}'", span)
      }
    }
    FieldGet(_, span) | FieldAssign(_, _, span) => {
      if self.class_depth == 0 {
        self.report(
          UndefinedVariable,
          "instance fields may only be used inside a class",
          span,
        )
      } else if self.in_static_method {
        self.report(
          UndefinedVariable,
          "instance fields cannot be used in a static method",
          span,
        )
      } else if self.in_foreign_class {
        self.report(
          UndefinedVariable,
          "foreign classes cannot declare instance fields",
          span,
        )
      }
      match expression {
        FieldAssign(_, value, _) => self.resolve_expression(value)
        _ => ()
      }
    }
    StaticFieldGet(_, span) | StaticFieldAssign(_, _, span) => {
      if self.class_depth == 0 {
        self.report(
          UndefinedVariable,
          "static fields may only be used inside a class",
          span,
        )
      }
      match expression {
        StaticFieldAssign(_, value, _) => self.resolve_expression(value)
        _ => ()
      }
    }
    ListLiteral(items, _) =>
      for item in items {
        self.resolve_expression(item)
      }
    MapLiteral(entries, _) =>
      for entry in entries {
        self.resolve_expression(entry.key)
        self.resolve_expression(entry.value)
      }
    SubscriptGet(receiver, index, _) => {
      self.resolve_expression(receiver)
      self.resolve_expression(index)
    }
    SubscriptSet(receiver, index, value, _) => {
      self.resolve_expression(receiver)
      self.resolve_expression(index)
      self.resolve_expression(value)
    }
    PropertyGet(receiver, name, span) => {
      self.resolve_expression(receiver)
      if name.length() > 64 {
        self.report(
          InvalidOperand,
          "method names may not be longer than 64 characters",
          span,
        )
      }
    }
    PropertySet(receiver, _, value, _) => {
      self.resolve_expression(receiver)
      self.resolve_expression(value)
    }
    MethodCall(receiver, name, arguments, span) => {
      self.resolve_expression(receiver)
      if name.length() > 64 {
        self.report(
          InvalidOperand,
          "method names may not be longer than 64 characters",
          span,
        )
      }
      if arguments.length() > 16 {
        self.report(
          InvalidArity,
          "a call may not have more than 16 arguments",
          span,
        )
      }
      for argument in arguments {
        self.resolve_expression(argument)
      }
    }
    SuperConstructorCall(arguments, span) => {
      if self.class_depth == 0 {
        self.report(
          UndefinedVariable,
          "'super' cannot be used outside a class",
          span,
        )
      }
      for argument in arguments {
        self.resolve_expression(argument)
      }
    }
    FunctionLiteral(parameters, body, span) =>
      self.resolve_function(parameters, body, span)
    FiberCallResume(_, _, _, _)
    | FiberSequenceResume(_, _, _, _, _, _, _, _, _) => ()
    Unary(_, value, _) | Grouping(value, _) => self.resolve_expression(value)
    Binary(left, _, right, _) => {
      self.resolve_expression(left)
      self.resolve_expression(right)
    }
    Conditional(condition, then_branch, else_branch, _) => {
      self.resolve_expression(condition)
      self.resolve_expression(then_branch)
      self.resolve_expression(else_branch)
    }
  }
}

///|
fn Resolver::resolve_method(self : Resolver, declaration : MethodDecl) -> Unit {
  if declaration.name.length() > 64 {
    self.report(
      InvalidOperand,
      "method names may not be longer than 64 characters",
      declaration.span,
    )
  }
  if declaration.parameters.length() > 16 {
    self.report(
      InvalidArity,
      "a method may not have more than 16 parameters",
      declaration.span,
    )
  }
  let saved_function_depth = self.function_depth
  let saved_loop_depth = self.loop_depth
  let saved_static = self.in_static_method
  let saved_constructor = self.in_constructor
  let saved_local_count = self.local_count
  self.function_depth = self.function_depth + 1
  self.loop_depth = 0
  self.in_static_method = declaration.is_static
  self.in_constructor = declaration.is_constructor
  self.local_count = 0
  self.push_scope()
  for parameter in declaration.parameters {
    self.define_name(parameter, declaration.span)
  }
  for statement in declaration.body {
    self.resolve_statement(statement)
  }
  self.pop_scope()
  self.function_depth = saved_function_depth
  self.loop_depth = saved_loop_depth
  self.in_static_method = saved_static
  self.in_constructor = saved_constructor
  self.local_count = saved_local_count
}

///|
fn Resolver::resolve_statement(self : Resolver, statement : Stmt) -> Unit {
  match statement {
    ExpressionStatement(expression, _) => self.resolve_expression(expression)
    VariableStatement(name, initializer, span) => {
      match initializer {
        Some(expression) => self.resolve_expression(expression)
        None => ()
      }
      self.define_name(name, span)
    }
    BlockStatement(statements, _) => {
      let saved_local_count = self.local_count
      self.push_scope()
      for child in statements {
        self.resolve_statement(child)
      }
      self.pop_scope()
      self.local_count = saved_local_count
    }
    IfStatement(condition, then_branch, else_branch, _) => {
      self.resolve_expression(condition)
      self.resolve_statement(then_branch)
      match else_branch {
        Some(branch) => self.resolve_statement(branch)
        None => ()
      }
    }
    WhileStatement(condition, body, _) => {
      self.resolve_expression(condition)
      self.loop_depth = self.loop_depth + 1
      self.resolve_statement(body)
      self.loop_depth = self.loop_depth - 1
    }
    ForStatement(name, sequence, body, span) => {
      self.resolve_expression(sequence)
      let saved_local_count = self.local_count
      self.push_scope()
      self.define_name(name, span)
      self.loop_depth = self.loop_depth + 1
      self.resolve_statement(body)
      self.loop_depth = self.loop_depth - 1
      self.pop_scope()
      self.local_count = saved_local_count
    }
    ReturnStatement(value, span) => {
      if self.function_depth == 0 {
        self.report(
          ReturnOutsideFunction,
          "return can only be used inside a function or method",
          span,
        )
      } else if self.in_constructor {
        match value {
          Some(_) =>
            self.report(
              ReturnOutsideFunction,
              "a constructor cannot return a value",
              span,
            )
          None => ()
        }
      }
      match value {
        Some(expression) => self.resolve_expression(expression)
        None => ()
      }
    }
    BreakStatement(span) =>
      if self.loop_depth == 0 {
        self.report(
          BreakOutsideLoop,
          "break can only be used inside a loop",
          span,
        )
      }
    ContinueStatement(span) =>
      if self.loop_depth == 0 {
        self.report(
          ContinueOutsideLoop,
          "continue can only be used inside a loop",
          span,
        )
      }
    ImportStatement(_, bindings, span) =>
      for binding in bindings {
        self.define_name(binding.local_name, span)
      }
    ClassStatement(name, superclass, methods, _, is_foreign, span) => {
      match superclass {
        Some(expression) => self.resolve_expression(expression)
        None => ()
      }
      self.define_name(name, span)
      for left_index = 0
          left_index < methods.length()
          left_index = left_index + 1 {
        for right_index = left_index + 1
            right_index < methods.length()
            right_index = right_index + 1 {
          let left = methods[left_index]
          let right = methods[right_index]
          if left.name == right.name &&
            left.parameters.length() == right.parameters.length() &&
            left.is_constructor == right.is_constructor &&
            left.is_getter == right.is_getter &&
            left.is_static == right.is_static {
            self.report(
              DuplicateMethod,
              "duplicate method '\{right.name}'",
              right.span,
            )
          }
        }
      }
      self.class_depth = self.class_depth + 1
      let saved_foreign_class = self.in_foreign_class
      self.in_foreign_class = is_foreign
      for declaration in methods {
        self.resolve_method(declaration)
      }
      self.in_foreign_class = saved_foreign_class
      self.class_depth = self.class_depth - 1
    }
  }
}

///|
fn resolve_program(statements : Array[Stmt]) -> Array[Diagnostic] {
  let resolver = Resolver::new(statements)
  for statement in statements {
    resolver.resolve_statement(statement)
  }
  resolver.diagnostics
}