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

module Rouge
  module Lexers
    class J < RegexLexer
      title 'J'
      desc "The J programming language (jsoftware.com)"
      tag 'j'
      filenames '*.ijs', '*.ijt'

      # For J-specific terms we use, see:
      #   https://code.jsoftware.com/wiki/Vocabulary/AET
      #   https://code.jsoftware.com/wiki/Vocabulary/Glossary

      # https://code.jsoftware.com/wiki/Vocabulary/PartsOfSpeech
      def self.token_map
        @token_map ||= {
          noun: Keyword::Constant,
          verb: Name::Function,
          modifier: Operator,
          name: Name,
          param: Name::Builtin::Pseudo,
          other: Punctuation,
          nil => Error,
        }
      end

      # https://code.jsoftware.com/wiki/NuVoc
      def self.inflection_list
        @inflection_list ||= ['', '.', ':', '..', '.:', ':.', '::']
      end

      def self.primitive_table
        @primitive_table ||= Hash.new([:name].freeze).tap do |h|
          {
            '()' => [:other],
            '=' => [:verb, :other, :other],
            '<>+-*%$|,#' => [:verb, :verb, :verb],
            '^' => [:verb, :verb, :modifier],
            '~"' => [:modifier, :verb, :verb],
            '.:@' => [:modifier, :modifier, :modifier],
            ';' => [:verb, :modifier, :verb],
            '!' => [:verb, :modifier, :modifier],
            '/\\' => [:modifier, :modifier, :verb],
            '[' => [:verb, nil, :verb],
            ']' => [:verb],
            '{' => [:verb, :verb, :verb, nil, nil, nil, :verb],
            '}' => [:modifier, :verb, :verb, nil, nil, nil, :modifier],
            '`' => [:modifier, nil, :modifier],
            '&' => [:modifier, :modifier, :modifier, nil, :modifier],
            '?' => [:verb, :verb],
            'a' => [:name, :noun, :noun],
            'ACeEIjorv' => [:name, :verb],
            'bdfHMT' => [:name, :modifier],
            'Dt' => [:name, :modifier, :modifier],
            'F' => [:name, :modifier, :modifier, :modifier, :modifier,
                    :modifier, :modifier],
            'iu' => [:name, :verb, :verb],
            'L' => [:name, :verb, :modifier],
            'mny' => [:param],
            'p' => [:name, :verb, :verb, :verb],
            'qsZ' => [:name, nil, :verb],
            'S' => [:name, nil, :modifier],
            'u' => [:param, :verb, :verb],
            'v' => [:param, :verb],
            'x' => [:param, nil, :verb],
          }.each {|k, v| k.each_char {|c| h[c] = v } }
        end
      end

      def self.primitive(char, inflection)
        i = inflection_list.index(inflection) or return Error
        token_map[primitive_table[char][i]]
      end

      def self.control_words
        @control_words ||= Set.new %w(
          assert break case catch catchd catcht continue do else elseif end
          fcase for if return select throw try while whilst
        )
      end

      def self.control_words_id
        @control_words_id ||= Set.new %w(for goto label)
      end

      state :expr do
        rule %r/\s+/, Text

        rule %r'([!-&(-/:-@\[-^`{-~]|[A-Za-z]\b)([.:]*)' do |m|
          token J.primitive(m[1], m[2])
        end

        rule %r/(?:\d|_\d?):([.:]*)/ do |m|
          token m[1].empty? ? J.token_map[:verb] : Error
        end

        rule %r/[\d_][\w.]*([.:]*)/ do |m|
          token m[1].empty? ? Num : Error
        end

        rule %r/'/, Str::Single, :str

        rule %r/NB\.(?![.:]).*/, Comment::Single

        rule %r/([A-Za-z]\w*)([.:]*)/ do |m|
          if m[2] == '.'
            word, sep, id = m[1].partition '_'
            list = if sep.empty?
              J.control_words
            elsif not id.empty?
              J.control_words_id
            end
            if list and list.include? word
              token Keyword, word + sep
              token((word == 'for' ? Name : Name::Label), id)
              token Keyword, m[2]
            else
              token Error
            end
          else
            token m[2].empty? ? Name : Error
          end
        end
      end

      state :str do
        rule %r/''/, Str::Escape
        rule %r/[^'\n]+/, Str::Single
        rule %r/'|$/, Str::Single, :pop!
      end

      start do
        @note_next = false
      end

      state :root do
        rule %r/\n/ do
          token Text
          if @note_next
            push :note
            @note_next = false
          end
        end

        # https://code.jsoftware.com/wiki/Vocabulary/com
        # https://code.jsoftware.com/wiki/Vocabulary/NounExplicitDefinition
        rule %r/
          ([0-4]|13|adverb|conjunction|dyad|monad|noun|verb)([\ \t]+)
          (def(?:ine)?\b|:)(?![.:])([\ \t]*)
        /x do |m|
          groups Keyword::Pseudo, Text, Keyword::Pseudo, Text
          @def_body = (m[1] == '0' || m[1] == 'noun') ? :noun : :code
          if m[3] == 'define'
            # stack: [:root]
            #    or  [:root, ..., :def_next]
            pop! if stack.size > 1
            push @def_body
            push :def_next  # [:root, ..., @def_body, :def_next]
          else
            push :expl_def
          end
        end

        rule %r/^([ \t]*)(Note\b(?![.:]))([ \t\r]*)(?!=[.:]|$)/ do
          groups Text, Name, Text
          @note_next = true
        end

        rule %r/[mnuvxy]\b(?![.:])/, Name
        mixin :expr
      end

      state :def_next do
        rule %r/\n/, Text, :pop!
        mixin :root
      end

      state :expl_def do
        rule %r/0\b(?![.:])/ do
          token Keyword::Pseudo
          # stack: [:root, :expl_def]
          #    or  [:root, ..., :def_next, :expl_def]
          pop! if stack.size > 2
          goto @def_body
          push :def_next  # [:root, ..., @def_body, :def_next]
        end
        rule %r/'/ do
          if @def_body == :noun
            token Str::Single
            goto :str
          else
            token Punctuation
            goto :q_expr
          end
        end
        rule(//) { pop! }
      end

      # `q_expr` lexes the content of a string literal which is a part of an
      # explicit definition.
      # e.g. dyad def 'x + y'
      state :q_expr do
        rule %r/''/, Str::Single, :q_str
        rule %r/'|$/, Punctuation, :pop!
        rule %r/NB\.(?![.:])([^'\n]|'')*/, Comment::Single
        mixin :expr
      end

      state :q_str do
        rule %r/''''/, Str::Escape
        rule %r/[^'\n]+/, Str::Single
        rule %r/''/, Str::Single, :pop!
        rule(/'|$/) { token Punctuation; pop! 2 }
      end

      state :note do
        mixin :delimiter
        rule %r/.+\n?/, Comment::Multiline
      end

      state :noun do
        mixin :delimiter
        rule %r/.+\n?/, Str::Heredoc
      end

      state :code do
        mixin :delimiter
        rule %r/^([ \t]*)(:)([ \t\r]*)$/ do
          groups Text, Punctuation, Text
        end
        mixin :expr
      end

      state :delimiter do
        rule %r/^([ \t]*)(\))([ \t\r]*$\n?)/ do
          groups Text, Punctuation, Text
          pop!
        end
      end
    end
  end
end
