require 'sass/script/css_parser'

module Sass
  module SCSS
    # A parser for a static SCSS tree.
    # Parses with SCSS extensions, like nested rules and parent selectors,
    # but without dynamic SassScript.
    # This is useful for e.g. \{#parse\_selector parsing selectors}
    # after resolving the interpolation.
    class StaticParser < Parser
      # Parses the text as a selector.
      #
      # @param filename [String, nil] The file in which the selector appears,
      #   or nil if there is no such file.
      #   Used for error reporting.
      # @return [Selector::CommaSequence] The parsed selector
      # @raise [Sass::SyntaxError] if there's a syntax error in the selector
      def parse_selector
        init_scanner!
        seq = expr!(:selector_comma_sequence)
        expected("selector") unless @scanner.eos?
        seq.line = @line
        seq.filename = @filename
        seq
      end

      # Parses a static at-root query.
      #
      # @return [(Symbol, Array<String>)] The type of the query
      #   (`:with` or `:without`) and the values that are being filtered.
      # @raise [Sass::SyntaxError] if there's a syntax error in the query,
      #   or if it doesn't take up the entire input string.
      def parse_static_at_root_query
        init_scanner!
        tok!(/\(/); ss
        type = tok!(/\b(without|with)\b/).to_sym; ss
        tok!(/:/); ss
        directives = expr!(:at_root_directive_list); ss
        tok!(/\)/)
        expected("@at-root query list") unless @scanner.eos?
        return type, directives
      end

      def parse_keyframes_selector
        init_scanner!
        sel = expr!(:keyframes_selector)
        expected("keyframes selector") unless @scanner.eos?
        sel
      end

      # @see Parser#initialize
      # @param allow_parent_ref [Boolean] Whether to allow the
      #   parent-reference selector, `&`, when parsing the document.
      # @comment
      #   rubocop:disable ParameterLists
      def initialize(str, filename, importer, line = 1, offset = 1, allow_parent_ref = true)
        # rubocop:enable ParameterLists
        super(str, filename, importer, line, offset)
        @allow_parent_ref = allow_parent_ref
      end

      private

      def moz_document_function
        val = tok(URI) || tok(URL_PREFIX) || tok(DOMAIN) || function(!:allow_var)
        return unless val
        ss
        [val]
      end

      def variable; nil; end
      def script_value; nil; end
      def interpolation(warn_for_color = false); nil; end
      def var_expr; nil; end
      def interp_string; (s = tok(STRING)) && [s]; end
      def interp_uri; (s = tok(URI)) && [s]; end
      def interp_ident(ident = IDENT); (s = tok(ident)) && [s]; end
      def use_css_import?; true; end

      def special_directive(name, start_pos)
        return unless %w[media import charset -moz-document].include?(name)
        super
      end

      def selector_comma_sequence
        sel = selector
        return unless sel
        selectors = [sel]
        ws = ''
        while tok(/,/)
          ws << str {ss}
          if (sel = selector)
            selectors << sel
            if ws.include?("\n")
              selectors[-1] = Selector::Sequence.new(["\n"] + selectors.last.members)
            end
            ws = ''
          end
        end
        Selector::CommaSequence.new(selectors)
      end

      def selector_string
        sel = selector
        return unless sel
        sel.to_s
      end

      def selector
        start_pos = source_position
        # The combinator here allows the "> E" hack
        val = combinator || simple_selector_sequence
        return unless val
        nl = str {ss}.include?("\n")
        res = []
        res << val
        res << "\n" if nl

        while (val = combinator || simple_selector_sequence)
          res << val
          res << "\n" if str {ss}.include?("\n")
        end
        seq = Selector::Sequence.new(res.compact)

        if seq.members.any? {|sseq| sseq.is_a?(Selector::SimpleSequence) && sseq.subject?}
          location = " of #{@filename}" if @filename
          Sass::Util.sass_warn <<MESSAGE
DEPRECATION WARNING on line #{start_pos.line}, column #{start_pos.offset}#{location}:
The subject selector operator "!" is deprecated and will be removed in a future release.
This operator has been replaced by ":has()" in the CSS spec.
For example: #{seq.subjectless}
MESSAGE
        end

        seq
      end

      def combinator
        tok(PLUS) || tok(GREATER) || tok(TILDE) || reference_combinator
      end

      def reference_combinator
        return unless tok(/\//)
        res = '/'
        ns, name = expr!(:qualified_name)
        res << ns << '|' if ns
        res << name << tok!(/\//)
        res
      end

      def simple_selector_sequence
        start_pos = source_position
        e = element_name || id_selector || class_selector || placeholder_selector || attrib ||
            pseudo || parent_selector
        return unless e
        res = [e]

        # The tok(/\*/) allows the "E*" hack
        while (v = id_selector || class_selector || placeholder_selector ||
                   attrib || pseudo || (tok(/\*/) && Selector::Universal.new(nil)))
          res << v
        end

        pos = @scanner.pos
        line = @line
        if (sel = str? {simple_selector_sequence})
          @scanner.pos = pos
          @line = line
          begin
            # If we see "*E", don't force a throw because this could be the
            # "*prop: val" hack.
            expected('"{"') if res.length == 1 && res[0].is_a?(Selector::Universal)
            throw_error {expected('"{"')}
          rescue Sass::SyntaxError => e
            e.message << "\n\n\"#{sel}\" may only be used at the beginning of a compound selector."
            raise e
          end
        end

        Selector::SimpleSequence.new(res, tok(/!/), range(start_pos))
      end

      def parent_selector
        return unless @allow_parent_ref && tok(/&/)
        Selector::Parent.new(tok(NAME))
      end

      def class_selector
        return unless tok(/\./)
        @expected = "class name"
        Selector::Class.new(tok!(IDENT))
      end

      def id_selector
        return unless tok(/#(?!\{)/)
        @expected = "id name"
        Selector::Id.new(tok!(NAME))
      end

      def placeholder_selector
        return unless tok(/%/)
        @expected = "placeholder name"
        Selector::Placeholder.new(tok!(IDENT))
      end

      def element_name
        ns, name = Sass::Util.destructure(qualified_name(:allow_star_name))
        return unless ns || name

        if name == '*'
          Selector::Universal.new(ns)
        else
          Selector::Element.new(name, ns)
        end
      end

      def qualified_name(allow_star_name = false)
        name = tok(IDENT) || tok(/\*/) || (tok?(/\|/) && "")
        return unless name
        return nil, name unless tok(/\|/)

        return name, tok!(IDENT) unless allow_star_name
        @expected = "identifier or *"
        return name, tok(IDENT) || tok!(/\*/)
      end

      def attrib
        return unless tok(/\[/)
        ss
        ns, name = attrib_name!
        ss

        op = tok(/=/) ||
             tok(INCLUDES) ||
             tok(DASHMATCH) ||
             tok(PREFIXMATCH) ||
             tok(SUFFIXMATCH) ||
             tok(SUBSTRINGMATCH)
        if op
          @expected = "identifier or string"
          ss
          val = tok(IDENT) || tok!(STRING)
          ss
        end
        flags = tok(IDENT) || tok(STRING)
        tok!(/\]/)

        Selector::Attribute.new(name, ns, op, val, flags)
      end

      def attrib_name!
        if (name_or_ns = tok(IDENT))
          # E, E|E
          if tok(/\|(?!=)/)
            ns = name_or_ns
            name = tok(IDENT)
          else
            name = name_or_ns
          end
        else
          # *|E or |E
          ns = tok(/\*/) || ""
          tok!(/\|/)
          name = tok!(IDENT)
        end
        return ns, name
      end

      SELECTOR_PSEUDO_CLASSES = %w[not matches current any has host host-context].to_set

      PREFIXED_SELECTOR_PSEUDO_CLASSES = %w[nth-child nth-last-child].to_set

      def pseudo
        s = tok(/::?/)
        return unless s
        @expected = "pseudoclass or pseudoelement"
        name = tok!(IDENT)
        if tok(/\(/)
          ss
          deprefixed = deprefix(name)
          if s == ':' && SELECTOR_PSEUDO_CLASSES.include?(deprefixed)
            sel = selector_comma_sequence
          elsif s == ':' && PREFIXED_SELECTOR_PSEUDO_CLASSES.include?(deprefixed)
            arg, sel = prefixed_selector_pseudo
          else
            arg = expr!(:pseudo_args)
          end

          tok!(/\)/)
        end
        Selector::Pseudo.new(s == ':' ? :class : :element, name, arg, sel)
      end

      def pseudo_args
        arg = expr!(:pseudo_expr)
        while tok(/,/)
          arg << ',' << str {ss}
          arg.concat expr!(:pseudo_expr)
        end
        arg
      end

      def pseudo_expr
        res = pseudo_expr_token
        return unless res
        res << str {ss}
        while (e = pseudo_expr_token)
          res << e << str {ss}
        end
        res
      end

      def pseudo_expr_token
        tok(PLUS) || tok(/[-*]/) || tok(NUMBER) || tok(STRING) || tok(IDENT)
      end

      def prefixed_selector_pseudo
        prefix = str do
          expr = str {expr!(:a_n_plus_b)}
          ss
          return expr, nil unless tok(/of/)
          ss
        end
        return prefix, expr!(:selector_comma_sequence)
      end

      def a_n_plus_b
        if (parity = tok(/even|odd/i))
          return parity
        end

        if tok(/[+-]?[0-9]+/)
          ss
          return true unless tok(/n/)
        else
          return unless tok(/[+-]?n/i)
        end
        ss

        return true unless tok(/[+-]/)
        ss
        @expected = "number"
        tok!(/[0-9]+/)
        true
      end

      def keyframes_selector
        ss
        str do
          return unless keyframes_selector_component
          ss
          while tok(/,/)
            ss
            expr!(:keyframes_selector_component)
            ss
          end
        end
      end

      def keyframes_selector_component
        tok(IDENT) || tok(PERCENTAGE)
      end

      @sass_script_parser = Class.new(Sass::Script::CssParser)
      @sass_script_parser.send(:include, ScriptParser)
    end
  end
end
