require 'gherkin/i18n'
require 'gherkin/lexer/i18n_lexer'
require 'gherkin/native'
require 'gherkin/listener/formatter_listener'

module Gherkin
  module Parser
    class ParseError < StandardError
      def initialize(state, new_state, expected_states, uri, line)
        super("Parse error at #{uri}:#{line}. Found #{new_state} when expecting one of: #{expected_states.join(', ')}. (Current state: #{state}).")
      end
    end

    class Parser
      native_impl('gherkin')

      # Initialize the parser. +machine_name+ refers to a state machine table.
      def initialize(formatter, raise_on_error=true, machine_name='root', force_ruby=false, iso_code = 'en')
        @formatter = formatter
        @listener = Listener::FormatterListener.new(@formatter)
        @raise_on_error = raise_on_error
        @machine_name = machine_name
        @machines = []
        @lexer = Gherkin::Lexer::I18nLexer.new(self, force_ruby, iso_code)
      end

      def parse(gherkin, feature_uri, line_offset)
        @formatter.uri(feature_uri)
        @line_offset = line_offset
        @feature_uri = feature_uri
        push_machine(@machine_name)
        begin
          @lexer.scan(gherkin)
        ensure
          pop_machine
        end
      end

      def i18n_language
        @lexer.i18n_language
      end

      def errors
        @lexer.errors
      end

      [:comment, :tag, :feature, :background, :scenario, :scenario_outline, :examples, :step, :doc_string, :row, :eof].each do |m|
        define_method(m) do |*args|
          if(event(m.to_s, args[-1]))
            @listener.__send__(m, *args)
          end
        end
      end
      
      def event(ev, line)
        l = line ? @line_offset+line : nil
        machine.event(ev, l) do |state, legal_events|
          if @raise_on_error
            raise ParseError.new(state, ev, legal_events, @feature_uri, l)
          else
            # Only used for testing
            @listener.syntax_error(state, ev, legal_events, @feature_uri, l)
          end
        end
      end

      def push_machine(name)
        @machines.push(Machine.new(self, name))
      end

      def pop_machine
        @machines.pop
      end

      def machine
        @machines[-1]
      end

      def expected
        machine.expected
      end

      def force_state(state)
        machine.instance_variable_set('@state', state)
      end

      class Machine
        def initialize(parser, name)
          @parser = parser
          @name = name
          @transition_map = transition_map(name)
          @state = name
        end

        def event(ev, line)
          states = @transition_map[@state]
          raise "Unknown state: #{@state.inspect} for machine #{@name}" if states.nil?
          new_state = states[ev]
          case new_state
          when "E"
            yield @state, expected
          when /push\((.+)\)/
            @parser.push_machine($1)
            @parser.event(ev, line)
          when "pop()"
            @parser.pop_machine()
            @parser.event(ev, line)
          else
            raise "Unknown transition: #{ev.inspect} among #{states.inspect} for machine #{@name}" if new_state.nil?
            @state = new_state
          end
        end

        def expected
          allowed = @transition_map[@state].find_all { |_, action| action != "E" }
          allowed.collect { |state| state[0] }.sort - ['eof']
        end

        private

        @@transition_maps = {}

        def transition_map(name)
          @@transition_maps[name] ||= build_transition_map(name)
        end

        def build_transition_map(name)
          table = transition_table(name)
          events = table.shift[1..-1]
          table.inject({}) do |machine, actions|
            state = actions.shift
            machine[state] = Hash[*events.zip(actions).flatten]
            machine
          end
        end

        def transition_table(name)
          state_machine_reader = StateMachineReader.new
          lexer = Gherkin::I18n.new('en').lexer(state_machine_reader)
          machine = File.dirname(__FILE__) + "/#{name}.txt"
          lexer.scan(File.read(machine))
          state_machine_reader.rows
        end

        class StateMachineReader
          attr_reader :rows

          def initialize
            @rows = []
          end

          def uri(uri)
          end

          def row(row, line_number)
            @rows << row
          end

          def eof
          end
        end

      end
    end
  end
end
