module Ass
  class Parser
    
    OPEN_BRACE = " {\n"
    CLOSING_BRACE = "}\n"
    
    AT = /^ *@/
    COMMENT = /^ *\/\*(.*)(?:\*\/)?$/
    ASS_COMMENT = /&\/\//
    BLANK = /^ *$/ # TODO: Single \n char as well, update test.ass
    WORD = /([\w]+)/
    HYPHENATED_WORD = /([\w\-]+)/
    MIXIN = /^ *\+#{WORD}/
    SELECTOR = /^([#\w\-\.,:\[\]\~\* ]+) *$/ # TODO: review CSS specification
    EQUALS = / *= */
    CONSTANT = /:#{WORD}/
    CONSTANT_VALUE = /([#\w]+)/
    CONSTANT_WITH_VALUE = /^ *#{CONSTANT}#{EQUALS}#{CONSTANT_VALUE}/
    PROPERTY = /^ *#{HYPHENATED_WORD}/
    
    attr_accessor :ass, :constants, :css
    alias :to_s :ass
    
    def initialize ass = ''
      @ass, @css, @constants = ass, '', {}
    end
    
    def parse!
      mixins = Hash.new ''
      in_block, in_mixin = false, false
      last_selector = ''
      lines = @ass.split "\n"
      lines.each do |line|
        if line.match COMMENT
          @css << "/* #{$1.strip} */\n"
        elsif line.match CONSTANT_WITH_VALUE
          @constants[$1.to_sym] = $2
        elsif not in_block and line.match MIXIN
          in_mixin, in_block = $1, true
        elsif in_block and line.match MIXIN
          next if in_mixin
          @css << mixins[$1]
        elsif in_block and line.match BLANK
          in_block = false
          next if in_mixin
          @css << CLOSING_BRACE
        elsif line.match AT
          @css << line << "\n"
        elsif line.match BLANK or line.match ASS_COMMENT
          # Do nothing
        elsif not in_block and line.match SELECTOR 
          in_block = true
          if sub_selector?($1, last_selector)
            next if in_mixin
            selector = join_selectors last_selector, $1.strip
            @css << selector << OPEN_BRACE
          else
            in_mixin = false
            selector = $1.strip
            @css << selector << OPEN_BRACE
          end
          last_selector = selector
        elsif line.match PROPERTY
          if mixin = in_mixin
            mixins[mixin] << format_property(line)
          else
            @css << format_property(line)
          end
        end
      end
      self
    end
    
    def join_selectors selector_a, selector_b
      if selector_b[0,1] == ':'
        if selector_a.include? ','
          selector_a.split(',').collect { |selector| selector << selector_b }.join(',')  
        else
          selector_a << selector_b
        end
      else
        if selector_a.include? ','
          selector_a.split(',').collect { |selector| selector << ' ' << selector_b }.join(',')  
        else
          selector_a << ' ' << selector_b
        end
      end
    end
    
    def sub_selector? selector_a, selector_b
      true if indents_in(selector_a) > indents_in(selector_b)
    end
    
    def indents_in string
      string.match(/^( *)/).captures.first.length / 2 rescue 0
    end
    
    def format_property line
      line.gsub! PROPERTY, '  \1:'
      line.gsub! CONSTANT do |constant|
        constant_sym = constant.gsub(':', '').to_sym
        @constants[constant_sym] || 'undefined_constant'
      end
      line << ";\n"
    end
    
    def self.parse ass
      new(ass).parse!.css
    end
    
    def self.parse_file file
      new(File.read(file)).parse!.css
    end
    
  end
end