require 'strscan'

module LaTeX
  # Lexical scanner.
  # Define language in sub-classes using Scanner.rule meta-method
  class BaseScanner
    DEFAULT = Object.new

    class ScanError < StandardError; end
    class ContextError < ScanError; end

    def rule(regex, token = nil, &block)
      context << [token, regex, block]
    end

    def initialize
      @contexts = []
      @context_names = []
      @cache = {}
      @start = :start
    end
    
    attr_accessor :start

    def context
      @contexts.last
    end

    def dynamic_context_method_name(context)
      "#{context.to_s}_context"
    end
    
    def push(new_context,*args)
      m = dynamic_context_method_name(new_context)
      key = [m,args]
      if @cache[key] then
        @contexts << @cache[key]
      else
        if respond_to?(m) then
          @contexts << []
          self.send(m,*args)
        else
          @contexts << self.class.rules[new_context]
        end
        @cache[key] = @contexts.last
      end
      @context_names << new_context
      DEFAULT
    end

    def pop
      if @contexts.length == 1
        raise ContextError, "Attempt to pop too much1"
      end
      @contexts.pop
      @context_names.pop
      DEFAULT
    end 

    def check_stack
      if @contexts.length > 1 
        raise ContextError, "Context stack unprocessed: #{@context_names.inspect}"
      end
    end

    def matches
      @strscan
    end

    def match(re)
      @strscan.scan(re)
    end

    def text
      @strscan.matched
    end

    def scan(str)
      push @start
      result = []
      strscan = @strscan = StringScanner.new(str)

      until strscan.eos?
        found = context.detect do |token, pat, handler|
          if text = strscan.scan(pat) then
            if handler then
              v = nil
              case handler
              when Symbol
                v = self.send(handler)
              when Proc
                v = handler.call
              end
              if v == DEFAULT then
                result << [token, text]
              elsif Array === v then
                result << v
              end
            else
              result << [token, text]
            end
            true
          end
        end
        if not found then
          line = str[0,strscan.pos].scan("\n").length
          $stderr.puts result[-5,5].inspect
          raise ScanError, "Unexpected token at line #{line} #{strscan.rest[0,100].inspect}"
        end
      end
      result
    end
  end

end
