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

module Rouge
  module Lexers
    class Coffeescript < RegexLexer
      tag 'coffeescript'
      aliases 'coffee', 'coffee-script'
      filenames '*.coffee', 'Cakefile'
      mimetypes 'text/coffeescript'

      title "CoffeeScript"
      desc 'The Coffeescript programming language (coffeescript.org)'

      def self.detect?(text)
        return true if text.shebang? 'coffee'
      end

      def self.keywords
        @keywords ||= Set.new %w(
          for in of while break return continue switch when then if else
          throw try catch finally new delete typeof instanceof super
          extends this class by
        )
      end

      def self.constants
        @constants ||= Set.new %w(
          true false yes no on off null NaN Infinity undefined
        )
      end

      def self.builtins
        @builtins ||= Set.new %w(
          Array Boolean Date Error Function Math netscape Number Object
          Packages RegExp String sun decodeURI decodeURIComponent
          encodeURI encodeURIComponent eval isFinite isNaN parseFloat
          parseInt document window
        )
      end

      id = /[$a-zA-Z_][a-zA-Z0-9_]*/

      state :comments_and_whitespace do
        rule /\s+/m, Text
        rule /###\s*\n.*?###/m, Comment::Multiline
        rule /#.*$/, Comment::Single
      end

      state :multiline_regex do
        # this order is important, so that #{ isn't interpreted
        # as a comment
        mixin :has_interpolation
        mixin :comments_and_whitespace

        rule %r(///([gim]+\b|\B)), Str::Regex, :pop!
        rule %r(/), Str::Regex
        rule %r([^/#]+), Str::Regex
      end

      state :slash_starts_regex do
        mixin :comments_and_whitespace
        rule %r(///) do
          token Str::Regex
          goto :multiline_regex
        end

        rule %r(
          /(\\.|[^\[/\\\n]|\[(\\.|[^\]\\\n])*\])+/ # a regex
          ([gim]+\b|\B)
        )x, Str::Regex, :pop!

        rule(//) { pop! }
      end

      state :root do
        rule(%r(^(?=\s|/|<!--))) { push :slash_starts_regex }
        mixin :comments_and_whitespace
        rule %r(
          [+][+]|--|~|&&|\band\b|\bor\b|\bis\b|\bisnt\b|\bnot\b|[?]|:|=|
          [|][|]|\\(?=\n)|(<<|>>>?|==?|!=?|[-<>+*`%&|^/])=?
        )x, Operator, :slash_starts_regex

        rule /[-=]>/, Name::Function

        rule /(@)([ \t]*)(#{id})/ do
          groups Name::Variable::Instance, Text, Name::Attribute
          push :slash_starts_regex
        end

        rule /([.])([ \t]*)(#{id})/ do
          groups Punctuation, Text, Name::Attribute
          push :slash_starts_regex
        end

        rule /#{id}(?=\s*:)/, Name::Attribute, :slash_starts_regex

        rule /#{id}/ do |m|
          if self.class.keywords.include? m[0]
            token Keyword
          elsif self.class.constants.include? m[0]
            token Name::Constant
          elsif self.class.builtins.include? m[0]
            token Name::Builtin
          else
            token Name::Other
          end

          push :slash_starts_regex
        end

        rule /[{(\[;,]/, Punctuation, :slash_starts_regex
        rule /[})\].]/, Punctuation

        rule /\d+[.]\d+([eE]\d+)?[fd]?/, Num::Float
        rule /0x[0-9a-fA-F]+/, Num::Hex
        rule /\d+/, Num::Integer
        rule /"""/, Str, :tdqs
        rule /'''/, Str, :tsqs
        rule /"/, Str, :dqs
        rule /'/, Str, :sqs
      end

      state :strings do
        # all coffeescript strings are multi-line
        rule /[^#\\'"]+/m, Str

        rule /\\./, Str::Escape
        rule /#/, Str
      end

      state :double_strings do
        rule /'/, Str
        mixin :has_interpolation
        mixin :strings
      end

      state :single_strings do
        rule /"/, Str
        mixin :strings
      end

      state :interpolation do
        rule /}/, Str::Interpol, :pop!
        mixin :root
      end

      state :has_interpolation do
        rule /[#][{]/, Str::Interpol, :interpolation
      end

      state :dqs do
        rule /"/, Str, :pop!
        mixin :double_strings
      end

      state :tdqs do
        rule /"""/, Str, :pop!
        rule /"/, Str
        mixin :double_strings
      end

      state :sqs do
        rule /'/, Str, :pop!
        mixin :single_strings
      end

      state :tsqs do
        rule /'''/, Str, :pop!
        rule /'/, Str
        mixin :single_strings
      end
    end
  end
end
