# -*- coding: utf-8 -*- #
# frozen_string_literal: true

module Rouge
  module Lexers
    class GraphQL < RegexLexer
      desc 'GraphQL'
      tag 'graphql'
      filenames '*.graphql', '*.gql'
      mimetypes 'application/graphql'

      name = /[_A-Za-z][_0-9A-Za-z]*/

      state :root do
        rule %r/\b(?:query|mutation|subscription)\b/, Keyword, :query_definition
        rule %r/\{/ do
          token Punctuation
          push :query_definition
          push :selection_set
        end

        rule %r/\bfragment\b/, Keyword, :fragment_definition
        
        rule %r/\bscalar\b/, Keyword, :value

        rule %r/\b(?:type|interface|enum)\b/, Keyword, :type_definition

        rule %r/\b(?:input|schema)\b/, Keyword, :type_definition

        rule %r/\bunion\b/, Keyword, :union_definition

        rule %r/\bextend\b/, Keyword

        mixin :basic

        # Markdown descriptions
        rule %r/(""")(\n)(.*?)(\n)(""")/m do |m|
          token Str::Double, m[1]
          token Text::Whitespace, m[2]
          delegate Markdown, m[3]
          token Text::Whitespace, m[4]
          token Str::Double, m[5]
        end
      end

      state :basic do
        rule %r/\s+/m, Text::Whitespace
        rule %r/#.*$/, Comment

        rule %r/[!,]/, Punctuation
      end

      state :has_directives do
        rule %r/(@#{name})(\s*)(\()/ do
          groups Keyword, Text::Whitespace, Punctuation
          push :arguments
        end
        rule %r/@#{name}\b/, Keyword
      end

      state :fragment_definition do
        rule %r/\bon\b/, Keyword

        mixin :query_definition
      end

      state :query_definition do
        mixin :has_directives

        rule %r/\b#{name}\b/, Name
        rule %r/\(/, Punctuation, :variable_definitions
        rule %r/\{/, Punctuation, :selection_set

        mixin :basic
      end

      state :type_definition do
        rule %r/\bimplements\b/, Keyword
        rule %r/\b#{name}\b/, Name
        rule %r/\(/, Punctuation, :variable_definitions
        rule %r/\{/, Punctuation, :type_definition_set

        mixin :basic
      end

      state :union_definition do
        rule %r/\b#{name}\b/, Name
        rule %r/\=/, Punctuation, :union_definition_variant

        mixin :basic
      end

      state :union_definition_variant do
        rule %r/\b#{name}\b/ do
          token Name
          pop!
          push :union_definition_pipe
        end

        mixin :basic
      end

      state :union_definition_pipe do
        rule %r/\|/ do
          token Punctuation
          pop!
          push :union_definition_variant
        end

        rule %r/(?!\||\s+|#[^\n]*)/ do
          pop! 2
        end

        mixin :basic
      end

      state :type_definition_set do
        rule %r/\}/ do
          token Punctuation
          pop! 2
        end

        rule %r/\b(#{name})(\s*)(\()/ do
          groups Name, Text::Whitespace, Punctuation
          push :variable_definitions
        end
        rule %r/\b#{name}\b/, Name

        rule %r/:/, Punctuation, :type_names

        mixin :basic
      end

      state :arguments do
        rule %r/\)/ do
          token Punctuation
          pop!
        end

        rule %r/\b#{name}\b/, Name
        rule %r/:/, Punctuation, :value

        mixin :basic
      end

      state :variable_definitions do
        rule %r/\)/ do
          token Punctuation
          pop!
        end

        rule %r/\$#{name}\b/, Name::Variable
        rule %r/\b#{name}\b/, Name
        rule %r/:/, Punctuation, :type_names
        rule %r/\=/, Punctuation, :value

        mixin :basic
      end

      state :type_names do
        rule %r/\b(?:Int|Float|String|Boolean|ID)\b/, Name::Builtin, :pop!
        rule %r/\b#{name}\b/, Name, :pop!

        rule %r/\[/, Punctuation, :type_name_list

        mixin :basic
      end

      state :type_name_list do
        rule %r/\b(?:Int|Float|String|Boolean|ID)\b/, Name::Builtin
        rule %r/\b#{name}\b/, Name

        rule %r/\]/ do
          token Punctuation
          pop! 2
        end

        mixin :basic
      end

      state :selection_set do
        mixin :has_directives

        rule %r/\}/ do
          token Punctuation
          pop!
          pop! if state?(:query_definition) || state?(:fragment_definition)
        end

        rule %r/\b(#{name})(\s*)(\()/ do
          groups Name, Text::Whitespace, Punctuation
          push :arguments
        end

        rule %r/\b(#{name})(\s*)(:)/ do
          groups Name, Text::Whitespace, Punctuation
        end

        rule %r/\b#{name}\b/, Name

        rule %r/(\.\.\.)(\s+)(on)\b/ do
          groups Punctuation, Text::Whitespace, Keyword
        end
        rule %r/\.\.\./, Punctuation

        rule %r/\{/, Punctuation, :selection_set

        mixin :basic
      end

      state :list do
        rule %r/\]/ do
          token Punctuation
          pop!
          pop! if state?(:value)
        end

        mixin :value
      end

      state :object do
        rule %r/\}/ do
          token Punctuation
          pop!
          pop! if state?(:value)
        end

        rule %r/\b(#{name})(\s*)(:)/ do
          groups Name, Text::Whitespace, Punctuation
          push :value
        end

        mixin :basic
      end

      state :value do
        pop_unless_list = ->(t) {
          ->(m) {
            token t
            pop! unless state?(:list)
          }
        }

        # Multiline strings
        rule %r/""".*?"""/m, Str::Double

        rule %r/\$#{name}\b/, &pop_unless_list[Name::Variable]
        rule %r/\b(?:true|false|null)\b/, &pop_unless_list[Keyword::Constant]
        rule %r/[+-]?[0-9]+\.[0-9]+(?:[eE][+-]?[0-9]+)?/, &pop_unless_list[Num::Float]
        rule %r/[+-]?[1-9][0-9]*(?:[eE][+-]?[0-9]+)?/, &pop_unless_list[Num::Integer]
        rule %r/"(\\[\\"]|[^"])*"/, &pop_unless_list[Str::Double]
        rule %r/\b#{name}\b/, &pop_unless_list[Name]

        rule %r/\{/, Punctuation, :object
        rule %r/\[/, Punctuation, :list

        mixin :basic
      end
    end
  end
end
