require 'cucumber/errors'
require 'cucumber/step_match'
require 'cucumber/ast/table'
require 'gherkin/rubify'

module Cucumber
  module Ast
    class StepInvocation #:nodoc:
      include Gherkin::Rubify

      BACKTRACE_FILTER_PATTERNS = [
        /vendor\/rails|lib\/cucumber|bin\/cucumber:|lib\/rspec|gems\//
      ]

      attr_writer :step_collection, :background
      attr_reader :name, :matched_cells, :status, :reported_exception
      attr_accessor :exception

      class << self
        SEVERITY = [:passed, :undefined, :pending, :skipped, :failed]
        def worst_status(statuses)
          SEVERITY[statuses.map{|status| SEVERITY.index(status)}.max]
        end
      end

      def initialize(step, name, multiline_arg, matched_cells)
        @step, @name, @multiline_arg, @matched_cells = step, name, multiline_arg, matched_cells
        status!(:skipped)
        @skip_invoke = @exception = @step_match = @different_table = @reported_exception = @background = nil
      end

      def background?
        @background
      end

      def skip_invoke!
        @skip_invoke = true
      end

      def accept(visitor)
        return if Cucumber.wants_to_quit
        invoke(visitor.step_mother, visitor.configuration)
        visit_step_result(visitor)
      end

      def visit_step_result(visitor)
        visitor.visit_step_result(
          keyword,
          @step_match,
          (@different_table || @multiline_arg),
          @status,
          @reported_exception,
          source_indent,
          @background
        )
      end

      def invoke(step_mother, configuration)
        find_step_match!(step_mother, configuration)
        unless @skip_invoke || configuration.dry_run? || @exception || @step_collection.exception
          @skip_invoke = true
          begin
            @step_match.invoke(@multiline_arg)
            step_mother.after_step
            status!(:passed)
          rescue Pending => e
            failed(configuration, e, false)
            status!(:pending)
          rescue Undefined => e
            failed(configuration, e, false)
            status!(:undefined)
          rescue Cucumber::Ast::Table::Different => e
            @different_table = e.table
            failed(configuration, e, false)
            status!(:failed)
          rescue Exception => e
            failed(configuration, e, false)
            status!(:failed)
          end
        end
      end

      def find_step_match!(step_mother, configuration)
        return if @step_match
        begin
          @step_match = step_mother.step_match(@name)
        rescue Undefined => e
          failed(configuration, e, true)
          status!(:undefined)
          @step_match = NoStepMatch.new(@step, @name)
        rescue Ambiguous => e
          failed(configuration, e, false)
          status!(:failed)
          @step_match = NoStepMatch.new(@step, @name)
        end
        step_mother.step_visited(self)
      end

      def failed(configuration, e, clear_backtrace)
        e = filter_backtrace(e)
        e.set_backtrace([]) if clear_backtrace
        e.backtrace << @step.backtrace_line unless @step.backtrace_line.nil?
        @exception = e
        if(configuration.strict? || !(Undefined === e) || e.nested?)
          @reported_exception = e
        else
          @reported_exception = nil
        end
      end

      PWD_PATTERN = /#{Regexp.escape(Dir.pwd)}\//m

      def filter_backtrace(e)
        return e if Cucumber.use_full_backtrace
        (e.backtrace || []).each{|line| line.gsub!(PWD_PATTERN, "./")}
        
        filtered = (e.backtrace || []).reject do |line|
          BACKTRACE_FILTER_PATTERNS.detect { |p| line =~ p }
        end
        
        if Cucumber::JRUBY && e.class.name == 'NativeException'
          # JRuby's NativeException ignores #set_backtrace.
          # We're fixing it.
          e.instance_eval do
            def set_backtrace(backtrace)
              @backtrace = backtrace
            end

            def backtrace
              @backtrace
            end
          end
        end
        e.set_backtrace(filtered)
        e
      end

      def status!(status)
        @status = status
        @matched_cells.each do |cell|
          cell.status = status
        end
      end

      def previous
        @step_collection.previous_step(self)
      end

      def actual_keyword
        repeat_keywords = rubify([language.keywords('but'), language.keywords('and')]).flatten.uniq.reject{|kw| kw == '* '}
        if repeat_keywords.index(@step.keyword) && previous
          previous.actual_keyword
        else
          keyword == '* ' ? language.code_keywords.first : keyword
        end
      end

      def source_indent
        @step.feature_element.source_indent(text_length)
      end

      def text_length
        @step.text_length(@name)
      end

      def keyword
        @step.keyword
      end

      def multiline_arg
        @step.multiline_arg
      end

      def file_colon_line
        @step.file_colon_line
      end

      def dom_id
        @step.dom_id
      end

      def backtrace_line
        @step.backtrace_line
      end

      def language
        @step.language
      end

      def to_sexp
        [:step_invocation, @step.line, @step.keyword, @name, (@multiline_arg.nil? ? nil : @multiline_arg.to_sexp)].compact
      end
    end
  end
end