module Prawn::SVG::CSS
  class SelectorParser
    def self.parse(selector)
      tokens = tokenise_css_selector(selector) or return

      result = [{}]
      part = nil

      tokens.each do |token|
        case token
        when Modifier
          part = token.type
          result.last[part] ||= part == :name ? +'' : []
        when Identifier
          return unless part

          result.last[part] << token.name
        when Attribute
          return unless ['=', '*=', '~=', '^=', '|=', '$=', nil].include?(token.operator)

          (result.last[:attribute] ||= []) << [token.key, token.operator, token.value]
        when Combinator
          result << { combinator: token.type }
          part = nil
        end
      end

      result
    end

    VALID_CSS_IDENTIFIER_CHAR = /[a-zA-Z0-9_\u00a0-\uffff-]/.freeze
    Identifier = Struct.new(:name)
    Modifier = Struct.new(:type)
    Combinator = Struct.new(:type)
    Attribute = Struct.new(:key, :operator, :value)

    def self.tokenise_css_selector(selector)
      result = []
      brackets = false
      attribute = false
      quote = false

      selector.strip.chars do |char|
        if brackets
          result.last.name << char
          brackets = false if char == ')'
        elsif attribute
          case attribute
          when :pre_key
            if VALID_CSS_IDENTIFIER_CHAR.match(char)
              result.last.key = String.new(char)
              attribute = :key
            elsif char != ' ' && char != "\t"
              return
            end

          when :key
            if VALID_CSS_IDENTIFIER_CHAR.match(char)
              result.last.key << char
            elsif char == ']'
              attribute = nil
            elsif '=*~^|$'.include?(char)
              result.last.operator = String.new(char)
              attribute = :operator
            elsif [' ', "\t"].include?(char)
              attribute = :pre_operator
            else
              return
            end

          when :pre_operator
            if '=*~^|$'.include?(char)
              result.last.operator = String.new(char)
              attribute = :operator
            elsif char != ' ' && char != "\t"
              return
            end

          when :operator
            if '=*~^|$'.include?(char)
              result.last.operator << char
            elsif [' ', "\t"].include?(char)
              attribute = :pre_value
            elsif ['"', "'"].include?(char)
              result.last.value = +''
              attribute = String.new(char)
            else
              result.last.value = String.new(char)
              attribute = :value
            end

          when :pre_value
            if ['"', "'"].include?(char)
              result.last.value = +''
              attribute = String.new(char)
            elsif char != ' ' && char != "\t"
              result.last.value = String.new(char)
              attribute = :value
            end

          when :value
            if char == ']'
              result.last.value = String.new(result.last.value.rstrip)
              attribute = nil
            else
              result.last.value << char
            end

          when '"', "'"
            if char == '\\' && !quote
              quote = true
            elsif char == attribute && !quote
              attribute = :post_string
            else
              quote = false
              result.last.value << char
            end

          when :post_string
            if char == ']'
              attribute = nil
            elsif char != ' ' && char != "\t"
              return
            end
          end

        elsif VALID_CSS_IDENTIFIER_CHAR.match(char)
          case result.last
          when Identifier
            result.last.name << char
          else
            result << Modifier.new(:name) unless result.last.is_a?(Modifier)
            result << Identifier.new(char)
          end
        else
          case char
          when '.'
            result << Modifier.new(:class)
          when '#'
            result << Modifier.new(:id)
          when ':'
            result << Modifier.new(:pseudo_class)
          when ' ', "\t"
            result << Combinator.new(:descendant) unless result.last.is_a?(Combinator)
          when '>'
            result.pop if result.last == Combinator.new(:descendant)
            result << Combinator.new(:child)
          when '+'
            result.pop if result.last == Combinator.new(:descendant)
            result << Combinator.new(:adjacent)
          when '~'
            result.pop if result.last == Combinator.new(:descendant)
            result << Combinator.new(:siblings)
          when '*'
            return unless result.empty? || result.last.is_a?(Combinator)

            result << Modifier.new(:name)
            result << Identifier.new('*')
          when '(' # e.g. :nth-child(3n+4)
            unless result.last.is_a?(Identifier) && result[-2] && result[-2].is_a?(Modifier) && result[-2].type == :pseudo_class
              return
            end

            result.last.name << '('
            brackets = true
          when '['
            result << Attribute.new
            attribute = :pre_key
          else
            return # unsupported Combinator
          end
        end
      end

      result unless brackets || attribute
    end
  end
end
