module Voodoo
  # Voodoo parser.
  # The parser reads Voodoo[http://inglorion.net/documents/designs/voodoo/]
  # source code and turns it into Ruby[http://www.ruby-lang.org/] objects.
  #
  # The public interface to Parser consists of the methods #new and
  # #parse_top_level
  #
  # Example usage:
  #
  #   require 'voodoo/parser'
  #
  #   File.open('example.voo') do |infile|
  #     parser = Voodoo::Parser.new infile
  #
  #     while (element = parser.parse_top_level)
  #       puts element.inspect
  #     end
  #   end
  class Parser
    # Creates a parser using the specified object as input.
    # The input object must support a method +getc+, which must
    # return the next character of the input, or +nil+ to indicate
    # the end of the input has been reached.
    def initialize input
      @input = input
      @line = 1
      @char = 0
      @lookahead = nil
    end

    # Consumes the current lookahead character
    def consume
      old = @lookahead
      if old == 10
        @line = @line.succ
        @char = 0
      end
      @lookahead = @input.getc
      @lookahead = :eof if @lookahead == nil
      @char = @char.succ unless @lookahead == :eof
      old
    end

    # Returns the current lookahead character,
    # or +nil+ when the end of the input has been reached.
    def lookahead
      if @lookahead == nil
        @lookahead = @input.getc
        @char = @char.succ
      end
      case @lookahead
      when :eof
        :eof
      else
        @lookahead.chr
      end
    end

    # Parses a top-level element.
    # Returns an array containing the parts of the element.
    # Eeach element of the array is a Symbol, a String, or an
    # Integer.
    #
    # For a label, returns:
    #   [:label, label_name]
    #
    # For a function definition, returns:
    #   [:function, [formala, formalb, ...], [statementa, statementb, ...]]
    #
    # For a conditional, returns:
    #   [condition, expression, [truea, trueb, ...], [falsea, falseb, ...]]
    #
    def parse_top_level
      words = []
      word = ""

      while true
        case lookahead
        when :eof
          # End of input
          break
        when "\n"
          # Newline
          consume
          # Exit the loop, but only if the line wasn't empty
          break unless words.empty?
        when "#"
          # Skip comment
          while lookahead != :eof && lookahead != "\n"
            word << lookahead
            consume
          end
        when /\d|-/
          # Digit; parse number
          words << parse_number
        when /\w|\\/
          # Letter, underscore, or backslash; parse symbol
          # Note: \w matches digites, too, so keep this case after \d
          words << parse_symbol
          if words.length == 1 && is_label?(words[-1])
            # We have a label; return it
            return [:label, words[-1].to_s[0..-2].to_sym]
          end
        when "\""
          # Double quote; parse string
          words << parse_string
        when /\s/
          # Skip whitespace
          consume
        else
          raise "Invalid character (#{@lookahead}) at #{@line}:#{@char}"
        end
      end

      # We have a line of input. Conditionals and function declarations
      # must be handled specially, because they consist of more than one
      # line.
      if words.empty?
        # Nothing to parse; return nil
        nil
      elsif words[0] == :function
        # Function declaration. Parse function body
        body = parse_body :function
        [:function, words[1..-1], body]
      elsif is_conditional?(words[0])
        # Conditional. Parse true part
        true_body = parse_body :conditional
        # Parse false part, if present
        if true_body[-1] == :else
          true_body = true_body[0..-2]
          false_body = parse_body :conditional
        else
          false_body = []
        end
        [words[0], words[1..-1], true_body, false_body]
      else
        # Statement or data declaration; simply return it
        words
      end
    end

    # Parses a body for a function or a conditional
    def parse_body kind
      body = []
      case kind
      when :function
        kind_text = 'function definition'
      else
        kind_text = kind.to_s
      end
      while true
        statement = parse_top_level
        if statement == nil
          raise "End of input while inside #{kind_text}"
        elsif statement[0] == :end
          # Done parsing body
          break
        elsif kind == :conditional && statement[0] == :else
          # Done parsing body, but there is another one coming up
          body << :else
          break
        else
          # Parsed a statement. Add it to body.
          body << statement
        end
      end
      body
    end

    # Parses an escape sequence.
    # This method should be called while the lookahead is the escape
    # character (backslash). It decodes the escape sequence and returns
    # the result as a string.
    def parse_escape
      result = nil
      consume
      case lookahead
      when :eof
        raise "Unexpected end of input in escape sequence"
      when "\\", "\"", " "
        result = lookahead
        consume
      when "n"
        # \n is newline
        consume
        result = "\n"
      when "r"
        # \r is carriage return
        consume
        result = "\r"
      when "x"
        # \xXX is byte with hex value XX
        code = @input.read 2
        @char = @char + 2
        consume
        result = [code].pack('H2')
      when "\n"
        # \<newline> is line continuation character
        consume
        # Skip indentation of next line
        while lookahead =~ /\s/
          consume
        end
        result = ''
      else
        # Default to just passing on next character
        result = lookahead
        consume
      end
      result
    end

    # Parses a number.
    # This method should be called while the lookahead is the first
    # character of the number.
    def parse_number
      text = lookahead
      consume
      while lookahead =~ /\d/
        text << lookahead
        consume
      end
      text.to_i
    end

    # Parses a string.
    # This method should be called while the lookahead is the opening
    # double quote.
    def parse_string
      consume
      result = ''
      while true
        case lookahead
        when "\""
          consume
          break
        when "\\"
          result << parse_escape
        else
          result << lookahead
          consume
        end
      end
      result
    end

    # Parses a symbol.
    # This method should be called while the lookahead is the first
    # character of the symbol name.
    def parse_symbol
      name = ''
      while true
        case lookahead
        when "\\"
          name << parse_escape
        when /\w|-/
          name << lookahead
          consume
        when ':'
          # Colon parsed as last character of the symbol name
          name << lookahead
          consume
          break
        else
          break
        end
      end
      name.to_sym
    end

    # Tests if a symbol is a label
    def is_label? symbol
      symbol.to_s[-1] == ?:
    end

    # Tests if a symbol is a conditional starter
    def is_conditional? symbol
      [:ifeq, :ifge, :ifgt, :ifle, :iflt, :ifne].member? symbol
    end
  end

end
