require 'set'

module JMESPath
  # @api private
  class Parser

    # @api private
    AFTER_DOT = Set.new([
        :identifier,        # foo.bar
        :quoted_identifier, # foo."bar"
        :star,              # foo.*
        :lbrace,            # foo[1]
        :lbracket,          # foo{a: 0}
        :function,          # foo.*.to_string(@)
        :filter,            # foo.[?bar==10]
    ])

    CURRENT_NODE = Nodes::Current.new

    # @option options [Lexer] :lexer
    def initialize(options = {})
      @lexer = options[:lexer] || Lexer.new()
      @disable_visit_errors = options[:disable_visit_errors]
    end

    # @param [String<JMESPath>] expression
    def parse(expression)
      stream = TokenStream.new(expression, @lexer.tokenize(expression))
      result = expr(stream)
      if stream.token.type != :eof
        raise Errors::SyntaxError, "expected :eof got #{stream.token.type}"
      else
        result
      end
    end

    # @api private
    def method_missing(method_name, *args)
      if matches = method_name.match(/^(nud_|led_)(.*)/)
        raise Errors::SyntaxError, "unexpected token #{matches[2]}"
      else
        super
      end
    end

    private

    # @param [TokenStream] stream
    # @param [Integer] rbp Right binding power
    def expr(stream, rbp = 0)
      left = send("nud_#{stream.token.type}", stream)
      while rbp < stream.token.binding_power
        left = send("led_#{stream.token.type}", stream, left)
      end
      left
    end

    def nud_current(stream)
      stream.next
      CURRENT_NODE
    end

    def nud_expref(stream)
      stream.next
      Nodes::Expression.new(expr(stream, 2))
    end

    def nud_filter(stream)
      led_filter(stream, CURRENT_NODE)
    end

    def nud_flatten(stream)
      led_flatten(stream, CURRENT_NODE)
    end

    def nud_identifier(stream)
      token = stream.token
      n = stream.next
      if n.type == :lparen
        Nodes::Function::FunctionName.new(token.value)
      else
        Nodes::Field.new(token.value)
      end
    end

    def nud_lbrace(stream)
      valid_keys = Set.new([:quoted_identifier, :identifier])
      stream.next(match:valid_keys)
      pairs = []
      begin
        pairs << parse_key_value_pair(stream)
        if stream.token.type == :comma
          stream.next(match:valid_keys)
        end
      end while stream.token.type != :rbrace
      stream.next
      Nodes::MultiSelectHash.new(pairs)
    end

    def nud_lbracket(stream)
      stream.next
      type = stream.token.type
      if type == :number || type == :colon
        parse_array_index_expression(stream)
      elsif type == :star && stream.lookahead(1).type == :rbracket
        parse_wildcard_array(stream)
      else
        parse_multi_select_list(stream)
      end
    end

    def nud_literal(stream)
      value = stream.token.value
      stream.next
      Nodes::Literal.new(value)
    end

    def nud_quoted_identifier(stream)
      token = stream.token
      next_token = stream.next
      if next_token.type == :lparen
        msg = 'quoted identifiers are not allowed for function names'
        raise Errors::SyntaxError, msg
      else
        Nodes::Field.new(token[:value])
      end
    end

    def nud_star(stream)
      parse_wildcard_object(stream, CURRENT_NODE)
    end

    def led_comparator(stream, left)
      token = stream.token
      stream.next
      right = expr(stream)
      Nodes::Comparator.create(token.value, left, right)
    end

    def led_dot(stream, left)
      stream.next(match:AFTER_DOT)
      if stream.token.type == :star
        parse_wildcard_object(stream, left)
      else
        right = parse_dot(stream, Token::BINDING_POWER[:dot])
        Nodes::Subexpression.new(left, right)
      end
    end

    def led_filter(stream, left)
      stream.next
      expression = expr(stream)
      if stream.token.type != :rbracket
        raise Errors::SyntaxError, 'expected a closing rbracket for the filter'
      end
      stream.next
      rhs = parse_projection(stream, Token::BINDING_POWER[:filter])
      left ||= CURRENT_NODE
      right = Nodes::Condition.new(expression, rhs)
      Nodes::ArrayProjection.new(left, right)
    end

    def led_flatten(stream, left)
      stream.next
      left = Nodes::Flatten.new(left)
      right = parse_projection(stream, Token::BINDING_POWER[:flatten])
      Nodes::ArrayProjection.new(left, right)
    end

    def led_lbracket(stream, left)
      stream.next(match: Set.new([:number, :colon, :star]))
      type = stream.token.type
      if type == :number || type == :colon
        right = parse_array_index_expression(stream)
        Nodes::Subexpression.new(left, right)
      else
        parse_wildcard_array(stream, left)
      end
    end

    def led_lparen(stream, left)
      args = []
      name = left.name
      stream.next
      while stream.token.type != :rparen
        args << expr(stream, 0)
        if stream.token.type == :comma
          stream.next
        end
      end
      stream.next
      Nodes::Function.create(name, args, :disable_visit_errors => @disable_visit_errors)
    end

    def led_or(stream, left)
      stream.next
      right = expr(stream, Token::BINDING_POWER[:or])
      Nodes::Or.new(left, right)
    end

    def led_pipe(stream, left)
      stream.next
      right = expr(stream, Token::BINDING_POWER[:pipe])
      Nodes::Pipe.new(left, right)
    end

    def parse_array_index_expression(stream)
      pos = 0
      parts = [nil, nil, nil]
      begin
        if stream.token.type == :colon
          pos += 1
        else
          parts[pos] = stream.token.value
        end
        stream.next(match:Set.new([:number, :colon, :rbracket]))
      end while stream.token.type != :rbracket
      stream.next
      if pos == 0
        Nodes::Index.new(parts[0])
      elsif pos > 2
        raise Errors::SyntaxError, 'invalid array slice syntax: too many colons'
      else
        Nodes::Slice.new(*parts)
      end
    end

    def parse_dot(stream, binding_power)
      if stream.token.type == :lbracket
        stream.next
        parse_multi_select_list(stream)
      else
        expr(stream, binding_power)
      end
    end

    def parse_key_value_pair(stream)
      key = stream.token.value
      stream.next(match:Set.new([:colon]))
      stream.next
      Nodes::MultiSelectHash::KeyValuePair.new(key, expr(stream))
    end

    def parse_multi_select_list(stream)
      nodes = []
      begin
        nodes << expr(stream)
        if stream.token.type == :comma
          stream.next
          if stream.token.type == :rbracket
            raise Errors::SyntaxError, 'expression epxected, found rbracket'
          end
        end
      end while stream.token.type != :rbracket
      stream.next
      Nodes::MultiSelectList.new(nodes)
    end

    def parse_projection(stream, binding_power)
      type = stream.token.type
      if stream.token.binding_power < 10
        CURRENT_NODE
      elsif type == :dot
        stream.next(match:AFTER_DOT)
        parse_dot(stream, binding_power)
      elsif type == :lbracket || type == :filter
        expr(stream, binding_power)
      else
        raise Errors::SyntaxError, 'syntax error after projection'
      end
    end

    def parse_wildcard_array(stream, left = nil)
      stream.next(match:Set.new([:rbracket]))
      stream.next
      left ||= CURRENT_NODE
      right = parse_projection(stream, Token::BINDING_POWER[:star])
      Nodes::ArrayProjection.new(left, right)
    end

    def parse_wildcard_object(stream, left = nil)
      stream.next
      left ||= CURRENT_NODE
      right = parse_projection(stream, Token::BINDING_POWER[:star])
      Nodes::ObjectProjection.new(left, right)
    end

  end
end
