require 'grammar/common' require 'grammar/entity'

module Less

grammar StyleSheet
  include Common
  include Entity

  rule primary
    (import / declaration / ruleset / mixin / comment)* {
      def build env = Less::Element.new
        elements.map do |e|
          e.build env if e.respond_to? :build
        end; env
      end
    }
  end

  rule comment
    ws '  ' (!'  ' . )* '*/' ws / ws '//' (!"\n" .)* "\n" ws
  end

  #
  # div, .class, body > p {...}
  #
  rule ruleset
    selectors "{" ws primary ws "}" s hide:(';'?) ws {
      def build env
        # Build the ruleset for each selector
        selectors.build(env, :ruleset).each do |sel|
          sel.hide unless hide.empty?
          primary.build sel
        end
      end
      # Mixin Declaration
    } / ws '.' name:[-a-zA-Z0-9_]+ ws parameters ws "{" ws primary ws "}" ws {
      def build env
        env << Node::Mixin::Def.new(name.text_value, parameters.build(env))
        primary.build env.last
      end
    }
  end

  rule mixin
    name:('.' [-a-zA-Z0-9_]+) args:(arguments) s ';' ws {
      def build env
        definition = env.nearest(name.text_value, :mixin) or raise MixinNameError, "#{name.text_value}() in #{env}"
        params = args.build.map {|i| Node::Expression.new i } unless args.empty?
        env << Node::Mixin::Call.new(definition, params || [], env)
      end
  } / ws selectors ';' ws {
      def build env
        selectors.build(env, :mixin).each do |path|
          rules = path.inject(env.root) do |current, node|
            current.descend(node.selector, node) or raise MixinNameError, "#{selectors.text_value} in #{env}"
          end.rules
          env.rules += rules
        end
      end
    }
  end

  rule selectors
    ws selector tail:(s ',' ws selector)* ws {
      def build env, method
        all.map do |e|
          e.send(method, env) if e.respond_to? method
        end.compact
      end

      def all
        [selector] + tail.elements.map {|e| e.selector }
      end
    }
  end

  #
  # div > p a {...}
  #
  rule selector
    sel:(s select element s)+ '' {
      def ruleset env
        sel.elements.inject(env) do |node, e|
          node << Node::Element.new(e.element.text_value, e.select.text_value)
          node.last
        end
      end

      def mixin env
        sel.elements.map do |e|
          Node::Element.new(e.element.text_value, e.select.text_value)
        end
      end
    }
  end

  rule parameters
    '(' s ')' {
      def build env
        []
      end
    } / '(' parameter tail:(s ',' s parameter)* ')' {
      def build env
        all.map do |e|
          e.build(env)
        end
      end

      def all
        [parameter] + tail.elements.map {|e| e.parameter }
      end
    }
  end

  rule parameter
    variable s ':' s expressions {
      def build env
        Node::Variable.new(variable.text_value, expressions.build(env), env)
      end
    }
  end

  rule import
    ws "@import" S url:(string / url) medias? s ';' ws {
      def build env
        standard_path = File.join(env.root.file || Dir.pwd, url.value)

        # Compile a list of possible paths for this file
        paths = $LESS_LOAD_PATH.map { |p| File.join(p, url.value) } + [standard_path]
        # Standardize and make uniq
        paths = paths.map do |p|
          p = File.expand_path(p)
          p += '.less' unless p =~ /\.(le|c)ss$/
          p
        end.uniq

        # Use the first that exists if any
        if path = paths.detect {|p| File.exists?(p)}
          unless env.root.imported.include?(path)
            env.root.imported << path
            env.rules += Less::Engine.new(File.new(path)).to_tree.rules
          end
        else
          raise ImportError, standard_path
        end

      end
    }
  end

  rule url
    'url(' path:(string / [-a-zA-Z0-9_%$/.&=:;#+?]+) ')' {
      def build env = nil
        Node::Function.new('url', value)
      end

      def value
        Node::Quoted.new CGI.unescape(path.text_value)
      end
    }
  end

  rule medias
    [-a-z]+ (s ',' s [a-z]+)*
  end

  #
  # @my-var: 12px;
  # height: 100%;
  #
  rule declaration
    ws name:(ident / variable) s ':' ws expressions tail:(ws ',' ws expressions)* s (';'/ ws &'}') ws {
      def build env
        result = all.map {|e| e.build(env) if e.respond_to? :build }.compact
        env << (name.text_value =~ /^@/ ?
          Node::Variable : Node::Property).new(name.text_value, result, env)
      end

      def all
        [expressions] + tail.elements.map {|f| f.expressions }
      end
    # Empty rule
    } / ws ident s ':' s ';' ws
  end

  #
  # An operation or compound value
  #
  rule expressions
    # Operation
    expression tail:(operator expression)+ {
      def build env = nil
        all.map {|e| e.build(env) }.dissolve
      end

      def all
        [expression] + tail.elements.map {|i| [i.operator, i.expression] }.flatten.compact
      end
    # Space-delimited expressions
    } / expression tail:(WS expression)* i:important? {
      def build env = nil
        all.map {|e| e.build(env) if e.respond_to? :build }.compact
      end

      def all
        [expression] + tail.elements.map {|f| f.expression } + [i]
      end
    # Catch-all rule
    } / [-a-zA-Z0-9_.&*/=:,+? \[\]()#%]+ {
      def build env
        [Node::Anonymous.new(text_value)]
      end
    }
  end

  rule expression
    '(' s expressions s ')' {
      def build env = nil
        Node::Expression.new(['('] + expressions.build(env).flatten + [')'])
      end
    } / entity '' {
      def build env = nil
        e = entity.method(:build).arity.zero?? entity.build : entity.build(env)
        e.respond_to?(:dissolve) ? e.dissolve : e
      end
    }
  end

  # !important
  rule important
    s '!' s 'important' {
      def build env = nil
        Node::Keyword.new(text_value.strip)
      end
    }
  end

  #
  # An identifier
  #
  rule ident
    '*'? '-'? [-a-z_] [-a-z0-9_]*
  end

  rule variable
    '@' [-a-zA-Z0-9_]+  {
      def build
        Node::Variable.new(text_value)
      end
    }
  end

  #
  # div / .class / #id / input[type="text"] / lang(fr)
  #
  rule element
    ((class / id / tag / ident) attribute* ('(' [a-zA-Z]+ ')' / '(' (pseudo_exp / selector / [0-9]+) ')' )?)+
    / attribute+ / '@media' / '@font-face'
  end

  #
  # 4n+1
  #
  rule pseudo_exp
    '-'? ([0-9]+)? 'n' ([-+] [0-9]+)?
  end

  #
  # [type="text"]
  #
  rule attribute
    '[' tag ([|~*$^]? '=') (string / [-a-zA-Z_0-9]+) ']' / '[' (tag / string) ']'
  end

  rule class
    '.' [_a-zA-Z] [-a-zA-Z0-9_]*
  end

  rule id
    '#' [_a-zA-Z] [-a-zA-Z0-9_]*
  end

  rule tag
    [a-zA-Z] [-a-zA-Z]* [0-9]? / '*'
  end

  rule select
    (s [+>~] s / '::' / s ':' / S)?
  end

  # TODO: Merge this with attribute rule
  rule accessor
    ident:(class / id / tag) '[' attr:(string / variable) ']' {
      def build env
        env.nearest(ident.text_value)[attr.text_value.delete(%q["'])].evaluate
      end
    }
  end

  rule operator
    S [-+*/] S {
      def build env
        Node::Operator.new(text_value.strip)
      end
    } / [-+*/] {
      def build env
        Node::Operator.new(text_value)
      end
    }
  end

  #
  # Functions and arguments
  #
  rule function
    name:([-a-zA-Z_]+) arguments {
      def build
        Node::Function.new(name.text_value, arguments.build)
      end
    }
  end

  rule arguments
    '(' s expressions s tail:(',' s expressions s)* ')' {
      def build
        all.map do |e|
          e.build if e.respond_to? :build
        end.compact
      end

      def all
        [expressions] + tail.elements.map {|e| e.expressions }
      end
    } / '(' s ')' {
      def build
        []
      end
    }
  end
end

end