module PryStackExplorer
  class WhenStartedHook
    include Pry::Helpers::BaseHelpers

    def caller_bindings(target)
      bindings = binding.callers
      pre_callers = Thread.current[:pre_callers]
      bindings = bindings + pre_callers if pre_callers
      bindings = remove_internal_frames(bindings)
      mark_vapid_frames(bindings)
      bindings
    end

    def call(target, options, _pry_)
      start_from_console = target.eval('__callee__').nil? &&
        target.eval('__FILE__') == '<main>' &&
        target.eval('__LINE__') == 0
      return if start_from_console

      target ||= _pry_.binding_stack.first if _pry_
      options = {
        :call_stack    => true,
        :initial_frame => 0
      }.merge!(options)

      return if !options[:call_stack]

      if options[:call_stack].is_a?(Array)
        bindings = options[:call_stack]

        unless valid_call_stack?(bindings)
          raise ArgumentError, ":call_stack must be an array of bindings"
        end
      else
        bindings = caller_bindings(target)
        initial_frame = bindings.find do |b|
          not b.local_variable_defined?(:vapid_frame)
        end
        options[:initial_frame] = bindings.index initial_frame
      end

      PryStackExplorer.create_and_push_frame_manager bindings, _pry_, initial_frame: options[:initial_frame]
    end

    private

    def mark_vapid_frames(bindings)
      stepped_out = false
      actual_file, actual_method = nil, nil

      bindings.each do |binding|
        if stepped_out
          if actual_file == binding.eval("__FILE__") and actual_method == binding.eval("__method__")
            stepped_out = false
          else
            binding.local_variable_set :vapid_frame, true
          end
        elsif binding.frame_type == :block
          stepped_out = true
          actual_file = binding.eval("__FILE__")
          actual_method = binding.eval("__method__")
        end

        if binding.local_variable_defined? :hide_from_stack
          binding.local_variable_set :vapid_frame, true
        end
      end
    end

    # remove internal frames related to setting up the session
    def remove_internal_frames(bindings)
      i = top_internal_frame_index(bindings)
      # DEBUG:
      #bindings.each_with_index do |b, index|
      #  puts "#{index}: #{b.eval("self.class")} #{b.eval("__method__")}"
      #end
      #puts "FOUND top internal frame: #{bindings.size} => #{i}"

      bindings.drop i+1
    end

    def top_internal_frame_index(bindings)
      bindings.rindex do |b|
        if b.frame_type == :method
          self_, method = b.eval("self"), b.eval("__method__")
          self_.equal?(Pry) && method == :start ||
            self_.class == Binding && method == :pry ||
            self_.class == PryMoves::Tracer && method == :tracing_func
        end
      end
    end

    def valid_call_stack?(bindings)
      bindings.any? && bindings.all? { |v| v.is_a?(Binding) }
    end
  end
end