require "./variabling/*"

module Ameba::AST
  # Represents a context of the local variable visibility.
  # This is where the local variables belong to.
  class Scope
    # Whether the scope yields.
    setter yields = false

    # Scope visibility level
    setter visibility : Crystal::Visibility?

    # Link to local variables
    getter variables = [] of Variable

    # Link to all variable references in current scope
    getter references = [] of Reference

    # Link to the arguments in current scope
    getter arguments = [] of Argument

    # Link to the instance variables used in current scope
    getter ivariables = [] of InstanceVariable

    # Link to the type declaration variables used in current scope
    getter type_dec_variables = [] of TypeDecVariable

    # Link to the outer scope
    getter outer_scope : Scope?

    # List of inner scopes
    getter inner_scopes = [] of Scope

    # The actual AST node that represents a current scope.
    getter node : Crystal::ASTNode

    delegate location, end_location, to_s,
      to: @node

    def_equals_and_hash node, location

    # Creates a new scope. Accepts the AST node and the outer scope.
    #
    # ```
    # scope = Scope.new(class_node, nil)
    # ```
    def initialize(@node, @outer_scope = nil)
      @outer_scope.try(&.inner_scopes.<< self)
    end

    # Creates a new variable in the current scope.
    #
    # ```
    # scope = Scope.new(class_node, nil)
    # scope.add_variable(var_node)
    # ```
    def add_variable(node)
      variables << Variable.new(node, self)
    end

    # Creates a new argument in the current scope.
    #
    # ```
    # scope = Scope.new(class_node, nil)
    # scope.add_argument(arg_node)
    # ```
    def add_argument(node)
      add_variable(Crystal::Var.new(node.name).at(node))
      arguments << Argument.new(node, variables.last)
    end

    # Adds a new instance variable to the current scope.
    #
    # ```
    # scope = Scope.new(class_node, nil)
    # scope.add_ivariable(ivar_node)
    # ```
    def add_ivariable(node)
      ivariables << InstanceVariable.new(node)
    end

    # Adds a new type declaration variable to the current scope.
    #
    # ```
    # scope = Scope.new(class_node, nil)
    # scope.add_type_dec_variable(node)
    # ```
    def add_type_dec_variable(node)
      type_dec_variables << TypeDecVariable.new(node)
    end

    # Returns variable by its name or `nil` if it does not exist.
    #
    # ```
    # scope = Scope.new(class_node, nil)
    # scope.find_variable("foo")
    # ```
    def find_variable(name : String)
      variables.find(&.name.==(name)) ||
        inherited? { outer_scope.try(&.find_variable(name)) }
    end

    # Creates a new assignment for the variable.
    #
    # ```
    # scope = Scope.new(class_node, nil)
    # scope.assign_variable(var_name, assign_node)
    # ```
    def assign_variable(name, node)
      find_variable(name).try(&.assign(node, self))
    end

    # Returns `true` if a local variable named *name* has a definition that
    # reaches the program point where *node* (an inner scope's node) is
    # introduced.
    #
    # ```
    # scope.declared_at?("foo", block_node)
    # ```
    def declared_at?(name : String, node) : Bool
      !!reaching_definitions[node.object_id]?.try(&.includes?(name))
    end

    # Reaching definitions snapshotted at each inner scope's node. Computed
    # lazily and memoized, since several inner scopes share the same outer one.
    protected getter reaching_definitions : Hash(UInt64, Set(String)) do
      ReachingDefinitionAnalyzer.new(self, entry_definitions).inner_scope_definitions
    end

    # The set of variable names already defined when this scope is entered:
    # its own arguments plus any captured definitions from the outer scope.
    protected def entry_definitions : Set(String)
      defined = arguments.to_set(&.name)

      if inherited? && (outer = outer_scope)
        outer.reaching_definitions[node.object_id]?.try do |defs|
          defined.concat(defs)
        end
      end

      defined
    end

    # Returns `true` if current scope represents a block (or proc),
    # `false` otherwise.
    def block?
      node.is_a?(Crystal::Block) ||
        node.is_a?(Crystal::ProcLiteral)
    end

    # Returns `true` if current scope represents a spawn block, e. g.
    #
    # ```
    # spawn do
    #   # ...
    # end
    # ```
    def spawn_block?
      node.as?(Crystal::Block).try(&.call).try(&.name) == "spawn"
    end

    # Returns `true` if current scope sits inside a macro.
    def in_macro?
      (node.is_a?(Crystal::Macro) ||
        node.is_a?(Crystal::MacroIf) ||
        node.is_a?(Crystal::MacroFor)) ||
        !!outer_scope.try(&.in_macro?)
    end

    # Returns `true` if instance variable is assigned in this scope.
    def assigns_ivar?(name)
      arguments.any?(&.name.== name) &&
        ivariables.any?(&.name.== "@#{name}")
    end

    # Returns `true` if type declaration variable is assigned in this scope.
    def assigns_type_dec_variable?(name)
      type_dec_variables.any?(&.name.== name) ||
        !!inherited? { outer_scope.try(&.assigns_type_dec_variable?(name)) }
    end

    # Returns `true` if and only if current scope represents some
    # type definition, for example a class.
    def type_definition?
      node.is_a?(Crystal::ClassDef) ||
        node.is_a?(Crystal::ModuleDef) ||
        node.is_a?(Crystal::EnumDef) ||
        node.is_a?(Crystal::LibDef) ||
        node.is_a?(Crystal::FunDef) ||
        node.is_a?(Crystal::TypeDef) ||
        node.is_a?(Crystal::CStructOrUnionDef)
    end

    # Returns `true` if current scope (or any of inner scopes) references variable,
    # `false` otherwise.
    def references?(variable : Variable, check_inner_scopes = true)
      variable.references.any? do |reference|
        (reference.scope == self) ||
          (check_inner_scopes && inner_scopes.any?(&.references?(variable)))
      end || variable.used_in_macro?
    end

    # Returns `true` if current scope (or any of inner scopes) yields,
    # `false` otherwise.
    def yields?(check_inner_scopes = true)
      @yields || (check_inner_scopes && inner_scopes.any?(&.yields?))
    end

    # Returns visibility of the current scope (could be inherited from the outer scope).
    def visibility
      @visibility || inherited? { outer_scope.try(&.visibility) }
    end

    {% for type in %w[Def ClassDef ModuleDef EnumDef LibDef FunDef].map(&.id) %}
      {% method_name = type.underscore %}
      # Returns `true` if current scope is a {{ method_name[0..-5] }} def, `false` otherwise.
      def {{ method_name }}?(*, check_outer_scopes = false)
        node.is_a?(Crystal::{{ type }}) ||
          !!(check_outer_scopes &&
            outer_scope.try(&.{{ method_name }}?(check_outer_scopes: true)))
      end
    {% end %}

    # Returns `true` if this scope is a top level scope, `false` otherwise.
    def top_level?
      outer_scope.nil?
    end

    def inherited?
      !(node.is_a?(Crystal::Def) ||
        node.is_a?(Crystal::FunDef) ||
        node.is_a?(Crystal::Assign) ||
        node.is_a?(Crystal::OpAssign))
    end

    def inherited?(&)
      yield if inherited?
    end

    # Returns `true` if var is an argument in current scope, `false` otherwise.
    def arg?(var)
      case current_node = node
      when Crystal::Def
        var.is_a?(Crystal::Arg) && any_arg?(current_node.args, var)
      when Crystal::Block
        var.is_a?(Crystal::Var) && any_arg?(current_node.args, var)
      when Crystal::ProcLiteral
        var.is_a?(Crystal::Var) && any_arg?(current_node.def.args, var)
      else
        false
      end
    end

    private def any_arg?(args, var)
      args.any? { |arg| arg.name == var.name && arg.location == var.location }
    end

    # Returns `true` if the *node* represents exactly
    # the same Crystal node as `@node`.
    def eql?(node)
      node == @node &&
        node.location == @node.location
    end
  end
end
