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

module Rouge
  module Lexers

    class Powershell < RegexLexer
      title 'powershell'
      desc 'powershell'
      tag 'powershell'
      aliases 'posh', 'microsoftshell', 'msshell', 'pwsh'
      filenames '*.ps1', '*.psm1', '*.psd1', '*.psrc', '*.pssc'
      mimetypes 'text/x-powershell'

      # https://docs.microsoft.com/en-us/powershell/module/microsoft.powershell.core/about/about_functions_cmdletbindingattribute?view=powershell-6
      ATTRIBUTES = %w(
        ConfirmImpact DefaultParameterSetName HelpURI PositionalBinding
        SupportsPaging SupportsShouldProcess
      )

      # https://docs.microsoft.com/en-us/powershell/module/microsoft.powershell.core/about/about_automatic_variables?view=powershell-6
      AUTO_VARS = %w(
        \$\$ \$\? \$\^ \$_
        \$args \$ConsoleFileName \$Error \$Event \$EventArgs \$EventSubscriber
        \$ExecutionContext \$false \$foreach \$HOME \$Host \$input \$IsCoreCLR
        \$IsLinux \$IsMacOS \$IsWindows \$LastExitCode \$Matches \$MyInvocation
        \$NestedPromptLevel \$null \$PID \$PROFILE \$PSBoundParameters \$PSCmdlet
        \$PSCommandPath \$PSCulture \$PSDebugContext \$PSHOME \$PSItem
        \$PSScriptRoot \$PSSenderInfo \$PSUICulture \$PSVersionTable \$PWD
        \$REPORTERRORSHOWEXCEPTIONCLASS \$REPORTERRORSHOWINNEREXCEPTION
        \$REPORTERRORSHOWSOURCE \$REPORTERRORSHOWSTACKTRACE
        \$SENDER \$ShellId \$StackTrace \$switch \$this \$true
      ).join('|')

      # https://docs.microsoft.com/en-us/powershell/module/microsoft.powershell.core/about/about_reserved_words?view=powershell-6
      KEYWORDS = %w(
        assembly exit process base filter public begin finally return break for
        sequence catch foreach static class from switch command function throw
        configuration hidden trap continue if try data in type define
        inlinescript until do interface using dynamicparam module var else
        namespace while elseif parallel workflow end param enum private
      ).join('|')

      # https://devblogs.microsoft.com/scripting/powertip-find-a-list-of-powershell-type-accelerators/
      # ([PSObject].Assembly.GetType("System.Management.Automation.TypeAccelerators")::Get).Keys -join ' '
      KEYWORDS_TYPE = %w(
        Alias AllowEmptyCollection AllowEmptyString AllowNull ArgumentCompleter
        array bool byte char CmdletBinding datetime decimal double DscResource
        float single guid hashtable int int32 int16 long int64 ciminstance
        cimclass cimtype cimconverter IPEndpoint NullString OutputType
        ObjectSecurity Parameter PhysicalAddress pscredential PSDefaultValue
        pslistmodifier psobject pscustomobject psprimitivedictionary ref
        PSTypeNameAttribute regex DscProperty sbyte string SupportsWildcards
        switch cultureinfo bigint securestring timespan uint16 uint32 uint64
        uri ValidateCount ValidateDrive ValidateLength ValidateNotNull
        ValidateNotNullOrEmpty ValidatePattern ValidateRange ValidateScript
        ValidateSet ValidateTrustedData ValidateUserDrive version void
        ipaddress DscLocalConfigurationManager WildcardPattern X509Certificate
        X500DistinguishedName xml CimSession adsi adsisearcher wmiclass wmi
        wmisearcher mailaddress scriptblock psvariable type psmoduleinfo
        powershell runspacefactory runspace initialsessionstate psscriptmethod
        psscriptproperty psnoteproperty psaliasproperty psvariableproperty
      ).join('|')

      OPERATORS = %w(
        -split -isplit -csplit -join -is -isnot -as -eq -ieq -ceq -ne -ine -cne
        -gt -igt -cgt -ge -ige -cge -lt -ilt -clt -le -ile -cle -like -ilike
        -clike -notlike -inotlike -cnotlike -match -imatch -cmatch -notmatch
        -inotmatch -cnotmatch -contains -icontains -ccontains -notcontains
        -inotcontains -cnotcontains -replace -ireplace -creplace -shl -shr -band
        -bor -bxor -and -or -xor -not \+= -= \*= \/= %=
      ).join('|')

      MULTILINE_KEYWORDS = %w(
        synopsis description parameter example inputs outputs notes link
        component role functionality forwardhelptargetname forwardhelpcategory
        remotehelprunspace externalhelp
      ).join('|')

      state :variable do
        rule %r/#{AUTO_VARS}/, Name::Builtin::Pseudo
        rule %r/(\$)(?:(\w+)(:))?(\w+|\{(?:[^`]|`.)+?\})/ do
          groups Name::Variable, Name::Namespace, Punctuation, Name::Variable
        end
        rule %r/\$\w+/, Name::Variable
        rule %r/\$\{(?:[^`]|`.)+?\}/, Name::Variable
      end

      state :multiline do
        rule %r/\.(?:#{MULTILINE_KEYWORDS})/i, Comment::Special
        rule %r/#>/, Comment::Multiline, :pop!
        rule %r/[^#.]+?/m, Comment::Multiline
        rule %r/[#.]+/, Comment::Multiline
      end

      state :interpol do
        rule %r/\)/, Str::Interpol, :pop!
        mixin :root
      end

      state :dq do
        # NB: "abc$" is literally the string abc$.
        # Here we prevent :interp from interpreting $" as a variable.
        rule %r/(?:\$#?)?"/, Str::Double, :pop!
        rule %r/\$\(/, Str::Interpol, :interpol
        rule %r/`$/, Str::Escape # line continuation
        rule %r/`./, Str::Escape
        rule %r/[^"`$]+/, Str::Double
        mixin :variable
      end

      state :sq do
        rule %r/'/, Str::Single, :pop!
        rule %r/[^']+/, Str::Single
      end

      state :heredoc do
        rule %r/(?:\$#?)?"@/, Str::Heredoc, :pop!
        rule %r/\$\(/, Str::Interpol, :interpol
        rule %r/`$/, Str::Escape # line continuation
        rule %r/`./, Str::Escape
        rule %r/[^"`$]+?/m, Str::Heredoc
        rule %r/"+/, Str::Heredoc
        mixin :variable
      end

      state :class do
        rule %r/\{/, Punctuation, :pop!
        rule %r/\s+/, Text::Whitespace
        rule %r/\w+/, Name::Class
        rule %r/[:,]/, Punctuation
      end

      state :expr do
        mixin :comments
        rule %r/"/, Str::Double, :dq
        rule %r/'/, Str::Single, :sq
        rule %r/@"/, Str::Heredoc, :heredoc
        rule %r/@'.*?'@/m, Str::Heredoc
        rule %r/\d*\.\d+/, Num::Float
        rule %r/\d+/, Num::Integer
        rule %r/@\{/, Punctuation, :hasht
        rule %r/@\(/, Punctuation, :array
        rule %r/{/, Punctuation, :brace
        rule %r/\[/, Punctuation, :bracket
      end

      state :hasht do
        rule %r/\}/, Punctuation, :pop!
        rule %r/=/, Operator
        rule %r/[,;]/, Punctuation
        mixin :expr
        rule %r/\w+/, Name::Other
        mixin :variable
      end

      state :array do
        rule %r/\s+/, Text::Whitespace
        rule %r/\)/, Punctuation, :pop!
        rule %r/[,;]/, Punctuation
        mixin :expr
        mixin :variable
      end

      state :brace do
        rule %r/[}]/, Punctuation, :pop!
        mixin :root
      end

      state :bracket do
        rule %r/\]/, Punctuation, :pop!
        rule %r/[A-Za-z]\w+\./, Name
        rule %r/([A-Za-z]\w+)/ do |m|
          if ATTRIBUTES.include? m[0]
            token Name::Builtin::Pseudo
          else
            token Name
          end
        end
        mixin :root
      end

      state :parameters do
        rule %r/`./m, Str::Escape
        rule %r/\)/ do
          token Punctuation
          pop!(2) if in_state?(:interpol) # pop :parameters and :interpol
        end
        rule %r/\s*?\n/, Text::Whitespace, :pop!
        rule %r/[;(){}\]]/, Punctuation, :pop!
        rule %r/[|=]/, Operator, :pop!
        rule %r/[\/\\~\w][-.:\/\\~\w]*/, Name::Other
        rule %r/\w[-\w]+/, Name::Other
        mixin :root
      end

      state :comments do
        rule %r/\s+/, Text::Whitespace
        rule %r/#.*/, Comment
        rule %r/<#/, Comment::Multiline, :multiline
      end

      state :root do
        mixin :comments
        rule %r/#requires\s-version \d(?:\.\d+)?/, Comment::Preproc

        rule %r/\.\.(?=\.?\d)/, Operator
        rule %r/(?:#{OPERATORS})\b/i, Operator

        rule %r/(class)(\s+)(\w+)/i do
          groups Keyword::Reserved, Text::Whitespace, Name::Class
          push :class
        end
        rule %r/(function)(\s+)(?:(\w+)(:))?(\w[-\w]+)/i do
          groups Keyword::Reserved, Text::Whitespace, Name::Namespace, Punctuation, Name::Function
        end
        rule %r/(?:#{KEYWORDS})\b(?![-.])/i, Keyword::Reserved

        rule %r/-{1,2}\w+/, Name::Tag

        rule %r/(\.)?([-\w]+)(\[)/ do |m|
          groups Operator, Name, Punctuation
          push :bracket
        end

        rule %r/([\/\\~[a-z]][-.:\/\\~\w]*)(\n)?/i do |m|
          groups Name, Text::Whitespace
          push :parameters
        end

        rule %r/(\.)([-\w]+)(?:(\()|(\n))?/ do |m|
          groups Operator, Name::Function, Punctuation, Text::Whitespace
          push :parameters unless m[3].nil?
        end

        rule %r/\?/, Name::Function, :parameters

        mixin :expr
        mixin :variable

        rule %r/[-+*\/%=!.&|]/, Operator
        rule %r/[{}(),:;]/, Punctuation

        rule %r/`$/, Str::Escape # line continuation
      end
    end
  end
end
