require_relative 'context/method_context'
require_relative 'context/module_context'
require_relative 'context/root_context'
require_relative 'context/singleton_method_context'
require_relative 'context/attribute_context'
require_relative 'context/send_context'
require_relative 'context/class_context'
require_relative 'ast/node'

module Reek
  #
  # Traverses an abstract syntax tree and fires events whenever it encounters
  # specific node types.
  #
  # TODO: This class is responsible for statements and reference
  # counting. Ideally `ContextBuilder` would only build up the context tree and leave the
  # statement and reference counting to the contexts.
  #
  # :reek:TooManyMethods: { max_methods: 27 }
  # :reek:UnusedPrivateMethod: { exclude: [ !ruby/regexp /process_/ ] }
  class ContextBuilder
    attr_reader :context_tree
    private_attr_accessor :element
    private_attr_reader :exp

    def initialize(syntax_tree)
      @exp = syntax_tree
      @element = Context::RootContext.new(exp)
      @context_tree = build(exp)
    end

    private

    # Processes the given AST, memoizes it and returns a tree of nested
    # contexts.
    #
    # For example this ruby code:
    #
    #   class Car; def drive; end; end
    #
    # would get compiled into this AST:
    #
    #   (class
    #     (const nil :Car) nil
    #     (def :drive
    #       (args) nil))
    #
    # Processing this AST would result in a context tree where each node
    # contains the outer context, the AST and the child contexts. The top
    # node is always Reek::Context::RootContext. Using the example above,
    # the tree would look like this:
    #
    # RootContext -> children: 1 ModuleContext -> children: 1 MethodContext
    #
    # @return [Reek::Context::RootContext] tree of nested contexts
    def build(exp)
      context_processor = "process_#{exp.type}"
      if context_processor_exists?(context_processor)
        send(context_processor, exp)
      else
        process exp
      end
      element
    end

    # Handles every node for which we have no context_processor.
    #
    def process(exp)
      exp.children.grep(AST::Node).each(&method(:build))
    end

    # Handles `module` and `class` nodes.
    #
    def process_module(exp)
      inside_new_context(Context::ModuleContext, exp) do
        process(exp)
      end
    end

    alias_method :process_class, :process_module

    # Handles `casgn` ("class assign") nodes.
    #
    # An input example that would trigger this method would be:
    #
    #   Foo = Class.new Bar
    #
    def process_casgn(exp)
      if exp.defines_module?
        process_module(exp)
      else
        process(exp)
      end
    end

    # Handles `def` nodes.
    #
    # An input example that would trigger this method would be:
    #
    #   def call_me; foo = 2; bar = 5; end
    #
    # Given the above example we would count 2 statements overall.
    #
    def process_def(exp)
      inside_new_context(Context::MethodContext, exp) do
        increase_statement_count_by(exp.body)
        process(exp)
      end
    end

    # Handles `defs` nodes ("define singleton").
    #
    # An input example that would trigger this method would be:
    #
    #   def self.call_me; foo = 2; bar = 5; end
    #
    # Given the above example we would count 2 statements overall.
    #
    def process_defs(exp)
      inside_new_context(Context::SingletonMethodContext, exp) do
        increase_statement_count_by(exp.body)
        process(exp)
      end
    end

    # Handles `send` nodes a.k.a. method calls.
    #
    # An input example that would trigger this method would be:
    #
    #   call_me()
    #
    # Besides checking if it's a visibility modifier or an attribute writer
    # we also record to what the method call is referring to
    # which we later use for smell detectors like FeatureEnvy.
    #
    # :reek:TooManyStatements: { max_statements: 7 }
    # :reek:FeatureEnvy
    def process_send(exp)
      method_name = exp.method_name
      if exp.visibility_modifier?
        element.track_visibility(method_name, exp.arg_names)
      elsif exp.attribute_writer?
        exp.args.each do |arg|
          append_new_context(Context::AttributeContext, arg, exp)
        end
      else
        append_new_context(Context::SendContext, exp, method_name)
      end
      element.record_call_to(exp)
      process(exp)
    end

    # Handles `op_asgn` nodes a.k.a. Ruby's assignment operators.
    #
    # An input example that would trigger this method would be:
    #
    #   x += 5
    #
    # or
    #
    #   x *= 3
    #
    # We record one reference to `x` given the example above.
    #
    def process_op_asgn(exp)
      element.record_call_to(exp)
      process(exp)
    end

    # Handles `ivasgn` and `ivar` nodes a.k.a. nodes related to instance variables.
    #
    # An input example that would trigger this method would be:
    #
    #   @item = 5
    #
    # for instance assignments (`ivasgn`) and
    #
    #   call_me(@item)
    #
    # for just using instance variables (`ivar`).
    #
    # We record one reference to `self`.
    #
    def process_ivar(exp)
      element.record_use_of_self
      process(exp)
    end

    alias_method :process_ivasgn, :process_ivar

    # Handles `self` nodes.
    #
    # An input example that would trigger this method would be:
    #
    #   def self.foo; end
    #
    def process_self(_)
      element.record_use_of_self
    end

    # Handles `zsuper` nodes a.k.a. calls to `super` without any arguments but a block possibly.
    #
    # An input example that would trigger this method would be:
    #
    #   def call_me; super; end
    #
    # or
    #
    #   def call_me; super do end; end
    #
    # but not
    #
    #   def call_me; super(); end
    #
    # We record one reference to `self`.
    #
    def process_zsuper(_)
      element.record_use_of_self
    end

    # Handles `block` nodes.
    #
    # An input example that would trigger this method would be:
    #
    #   list.map { |element| puts element }
    #
    # Counts non-empty blocks as one statement.
    #
    def process_block(exp)
      increase_statement_count_by(exp.block)
      process(exp)
    end

    # Handles `begin` and `kwbegin` nodes. `begin` nodes are created implicitly
    # e.g. when parsing method bodies (see example below), `kwbegin` nodes are created
    # by explicitly using the `begin` keyword.
    #
    # An input example that would trigger this method would be:
    #
    #   def foo; call_me(); @x = 5; end
    #
    # In this case the whole method body would be hanging below the `begin` node.
    #
    # Counts all statements in the method body.
    #
    # At the end we subtract one statement because the surrounding context was already counted
    # as one (e.g. via `process_def`).
    #
    def process_begin(exp)
      increase_statement_count_by(exp.children)
      decrease_statement_count
      process(exp)
    end

    alias_method :process_kwbegin, :process_begin

    # Handles `if` nodes.
    #
    # An input example that would trigger this method would be:
    #
    # if a > 5 && b < 3
    #   puts 'bingo'
    # else
    #   3
    # end
    #
    # Counts the `if` body as one statement and the `else` body as another statement.
    #
    # At the end we subtract one statement because the surrounding context was already counted
    # as one (e.g. via `process_def`).
    #
    # `children[1]` refers to the `if` body (so `puts 'bingo'` from above) and
    # `children[2]` to the `else` body (so `3` from above), which might be nil.
    #
    def process_if(exp)
      children = exp.children
      increase_statement_count_by(children[1])
      increase_statement_count_by(children[2])
      decrease_statement_count
      process(exp)
    end

    # Handles `while` and `until` nodes.
    #
    # An input example that would trigger this method would be:
    #
    # while x < 5
    #   puts 'bingo'
    # end
    #
    # Counts the `while` body as one statement.
    #
    # At the end we subtract one statement because the surrounding context was already counted
    # as one (e.g. via `process_def`).
    #
    # `children[1]` below refers to the `while` body (so `puts 'bingo'` from above)
    #
    def process_while(exp)
      increase_statement_count_by(exp.children[1])
      decrease_statement_count
      process(exp)
    end

    alias_method :process_until, :process_while

    # Handles `for` nodes.
    #
    # An input example that would trigger this method would be:
    #
    # for i in [1,2,3,4]
    #   puts i
    # end
    #
    # Counts the `for` body as one statement.
    #
    # At the end we subtract one statement because the surrounding context was already counted
    # as one (e.g. via `process_def`).
    #
    # `children[2]` below refers to the `while` body (so `puts i` from above)
    #
    def process_for(exp)
      increase_statement_count_by(exp.children[2])
      decrease_statement_count
      process(exp)
    end

    # Handles `rescue` nodes.
    #
    # An input example that would trigger this method would be:
    #
    # def simple
    #   raise ArgumentError, 'raising...'
    # rescue => e
    #   puts 'rescued!'
    # end
    #
    # Counts everything before the `rescue` body as one statement.
    #
    # At the end we subtract one statement because the surrounding context was already counted
    # as one (e.g. via `process_def`).
    #
    # `exp.children.first` below refers to everything before the actual `rescue`
    # which would be the
    #
    # raise ArgumentError, 'raising...'
    #
    # in the example above.
    # `exp` would be the whole method body wrapped under a `rescue` node.
    # See `process_resbody` for additional reference.
    #
    def process_rescue(exp)
      increase_statement_count_by(exp.children.first)
      decrease_statement_count
      process(exp)
    end

    # Handles `resbody` nodes.
    #
    # An input example that would trigger this method would be:
    #
    # def simple
    #   raise ArgumentError, 'raising...'
    # rescue => e
    #   puts 'rescued!'
    # end
    #
    # Counts the exception capturing and every statement related to it.
    #
    # So `exp.children[1..-1]` from the code below would be an array with the following 2 elements:
    # [
    #   (lvasgn :e),
    #   (send nil :puts (str "rescued!"))
    # ]
    #
    # which thus counts as 2 statements.
    # `exp` would be the whole `rescue` body.
    # See `process_rescue` for additional reference.
    #
    def process_resbody(exp)
      increase_statement_count_by(exp.children[1..-1].compact)
      process(exp)
    end

    # Handles `case` nodes.
    #
    # An input example that would trigger this method would be:
    #
    # foo = 5
    # case foo
    # when 1..100
    #   puts 'In between'
    # else
    #   puts 'Not sure what I got here'
    # end
    #
    # Counts the `else` body.
    #
    # At the end we subtract one statement because the surrounding context was already counted
    # as one (e.g. via `process_def`).
    #
    def process_case(exp)
      increase_statement_count_by(exp.else_body)
      decrease_statement_count
      process(exp)
    end

    # Handles `when` nodes.
    #
    # An input example that would trigger this method would be:
    #
    # foo = 5
    # case foo
    # when (1..100)
    #   puts 'In between'
    # else
    #   puts 'Not sure what I got here'
    # end
    #
    # Note that input like
    #
    # if foo then :holla else :nope end
    #
    # does not trigger this method.
    #
    # Counts the `when` body.
    #
    def process_when(exp)
      increase_statement_count_by(exp.body)
      process(exp)
    end

    def context_processor_exists?(name)
      self.class.private_method_defined?(name)
    end

    # :reek:ControlParameter
    def increase_statement_count_by(sexp)
      element.statement_counter.increase_by sexp
    end

    def decrease_statement_count
      element.statement_counter.decrease_by 1
    end

    # Stores a reference to the current context, creates a nested new one,
    # yields to the given block and then restores the previous context.
    #
    # @param klass [Context::*Context] - context class
    # @param exp - current expression
    # @yield block
    #
    def inside_new_context(klass, exp)
      new_context = append_new_context(klass, exp)

      orig, self.element = element, new_context
      yield
      self.element = orig
    end

    # Append a new child context to the current element.
    #
    # @param klass [Context::*Context] - context class
    # @param args - arguments for the class initializer
    #
    # @return [Context::*Context] - the context that was appended
    #
    def append_new_context(klass, *args)
      klass.new(element, *args).tap do |new_context|
        element.append_child_context new_context
      end
    end
  end
end
