Integrom.Module.Bootstrap.psm1

$dynamicEnumBlock = @"
   public enum %%ENUM_NAME%% {
      %%ENUM_DATA%%
   }
"@


$IntegromLibraryExtension = @'
psil
'@


function logMessage([string]$message, [PSCustomObject[]]$values) {
   [System.ConsoleColor]$enteringColor = $session.Host.UI.RawUI.ForegroundColor
   if ($suppressOutput) {return}
   $chunks = [regex]::Split($message, '(?<!%)%%\w+%%(?!%)')
   for ($x = 0; $x -lt $chunks.count; $x ++) {
      if ($chunks.Count -eq 1) {
         $session.Host.UI.WriteLine($chunks)
         return
      } 
      $session.Host.UI.Write($chunks[$x])
      if ($null -ne $values[$x]) {
         $session.Host.UI.RawUI.ForegroundColor = $values[$x].color
         $session.Host.UI.Write($values[$x].value)
         $session.Host.UI.RawUI.ForegroundColor = $enteringColor
      }
   }
   $session.Host.UI.WriteLine()
}

function getPossibleLibPaths{[CmdletBinding()]param([string]$libName, [string]$baseLibPath, [System.Management.Automation.PSCmdlet]$session = $PSCmdlet)
   # [CmdletBinding()] is necessary to get the correct session to look for the _igmLibPaths environment variable
   [string[]]$pathSuffixes = '', '_lib\'
   [string[]]$result = @()
   
   $baseLibPath = $baseLibPath -replace '([\\/]+$)|(?<=\w$)', '\'
   $regexResult = [regex]::Match("$($baseLibPath)$($libName)", '(?<root>^[a-z]:|/|\.\.|\.|\\\\|(?:http|ftp|https|sftp)(://))?(?<path>.*(\\|/))?((?<libName>.+)(?<ext>\.psil$)|(?<libName>.+))', [Text.RegularExpressions.RegexOptions]::IgnoreCase)
   $root = $regexResult[0].Groups['root'].value
   $path = $regexResult[0].Groups['path'].value
   $libSname = $regexResult[0].Groups['libName'].value
   [string]$envPath = $ExecutionContext.SessionState.InvokeCommand.InvokeScript($session.SessionState,
      {param()
         # This section of code runs in the caller's session, not the module's session
         if ($null -ne $_igmLibPaths) {return $_igmLibPaths} else {return $null}
      }.Ast.GetScriptBlock()
   )
   if ([string]::IsNullOrEmpty($root)) {
      $rootsToTest = '.\', '..\'
      if ($null -ne $envPath) {$rootsToTest += $envPath -replace '([\\/]+$)|(?<=\w$)', '\'}
      if (Test-Path -Path "$($env:ProgramFiles)\Integrom\Libraries\") {$rootsToTest += "$($env:ProgramFiles)\Integrom\Libraries\"}
   } else {
      $rootsToTest = $root
   }
   $pathSuffixes += "$($libSname)\"
   foreach ($rootEntry in $rootsToTest) {
      foreach ($pathSuffix in $pathSuffixes) {
         $result += "$($rootEntry)$($path)$($pathSuffix)$($libSname).$($IntegromLibraryExtension)"
      }
   }
   return $result
}

function getLoadedLibraries{[CmdletBinding()][OutputType([hashtable])]param([System.Management.Automation.PSCmdlet]$session)
   # [CmdletBinding()] is necessary to get the correct session state to search for loaded libraries
   if ($null -eq $session) {$session = $PSCmdlet}
   return $ExecutionContext.SessionState.InvokeCommand.InvokeScript(
      $session.SessionState,
      {param()
         # This section of code runs in the caller's session, not the module's session
         if ($null -ne $_igmLoadedLibraries) {return $_igmLoadedLibraries} else {return @{}}
      }.Ast.GetScriptBlock()
   )
}

function loadLibraries{[CmdletBinding()][OutputType([hashtable])]param([string[]]$libraryNames, [string]$libPath, [System.Management.Automation.PSCmdlet]$session, [switch]$allowDowngrade = $false, [switch]$forceReload = $false, [switch]$suppressOutput = $false)
   # [CmdletBinding()] is necessary to get the correct session state to load the library into
   [hashtable]$resultData = @{statusCode = 0; failedLibs = @()}
   $libColor = [System.ConsoleColor]::DarkMagenta
   $libVerColor = [System.ConsoleColor]::Cyan
   if ($null -eq $session) {$session = $PSCmdlet}

   function updateFailedList() {
      $resultData.statusCode -= 1
      $resultData.failedLibs += $libraryName
   }

   function selectMsgTemplate([int]$template, [string[]]$paramBlock) {
      # need to add bounds check on paramBlock
      switch ($template) {
         1 {if ($paramBlock.Count -ge 1) {return @(@{value = $paramBlock[0]; color = $libColor})}}
         2 {if ($paramBlock.Count -ge 3) {return @(@{value = $paramBlock[0]; color = $libColor}, @{value = $paramBlock[1]; color = $libVerColor}, @{value = $paramBlock[2]; color = $libVerColor})}}
         3 {if ($paramBlock.Count -ge 2) {return @(@{value = $paramBlock[0]; color = $libColor}, @{value = $paramBlock[1]; color = $libVerColor})}}
         4 {if ($paramBlock.Count -ge 3) {return @(@{value = $paramBlock[0]; color = $libColor}, @{value = $paramBlock[1]; color = [System.ConsoleColor]::White}, @{value = $paramBlock[2]; color = [System.ConsoleColor]::DarkCyan})}}
      }
   }

   if ($libraryNames.Count -lt 1) {
      $resultData.statusCode = -5
      return $resultData
   }      
   :libraryCollectionLoop foreach ($libraryName in $libraryNames) {
      $libSourceVerInfo = getLibrarySourceVersion($libraryName)
      $libLoadedVerInfo = (getLoadedLibraries $session)[$libraryName]
      if ($libLoadedVerInfo) {
         if ($libLoadedVerInfo.train -ne $libSourceVerInfo.train) {
            logMessage "❌ Replacing the loaded version of %%1%% with a release from a different train is not supported." (selectMsgTemplate 1 $libraryName)
            updateFailedList
            continue libraryCollectionLoop
         }
         switch ($libLoadedVerInfo.version) {
            {$_ -gt $libSourceVerInfo.version} {
               if ($allowDowngrade) {
                  logMessage "ℹ Downgrading the in-memory version of %%1%% from %%2%% to %%3%%....." (selectMsgTemplate 2 @($libraryName, $libLoadedVerInfo.versionStr, $libSourceVerInfo.versionStr))
                  logMessage " ⚠ Beware: Any current functions, constants, and variables not referenced by the old source will remain as-is."
               } else {
                  logMessage "❌ Downgrading the in-memory version of %%1%% was requested but is not permitted. Set the %%2%% flag in the call to %%3%%." (selectMsgTemplate 4 @($libraryName, '[allowDowngrade]', 'loadLibraries()'))
                  updateFailedList
                  continue libraryCollectionLoop
               }
            }
            {$_ -lt $libSourceVerInfo.version} {
               logMessage "ℹ Upgrading in-memory version of %%1%% from %%2%% to %%3%%......." (selectMsgTemplate 2 @($libraryName, $libLoadedVerInfo.versionStr, $libSourceVerInfo.versionStr))
               logMessage " ⚠ Beware: Any current functions, constants, and variables not referenced by the new source will remain in memory as-is."
            }
            {$_ -eq $libSourceVerInfo.version} {
               logMessage "ℹ The current version of %%1%% is already loaded." (selectMsgTemplate 1 $libraryName)
               if ($forceReload) {
                  logMessage "ℹ A forced reload of %%1%% has been requested." (selectMsgTemplate 1 $libraryName)
                  logMessage " ⚠ Beware: Any current functions, constants, and variables not referenced by the new source will remain in memory as-is."
               } else {
                  continue libraryCollectionLoop
               }
            }
         }
      } else {
         logMessage "ℹ Loading library %%1%%...." (selectMsgTemplate 1 $libraryName)
      }
      foreach ($fqLibPath in (getPossibleLibPaths -libName $libraryName -baseLibPath $libPath -session $session)) {
         if (Test-Path $fqLibPath) {
            $rawLibCode = Get-Content -Path $fqLibPath -Raw -ErrorAction SilentlyContinue -WarningAction SilentlyContinue
            if ($null -eq $rawLibCode) {break}
            $regexResult = [regex]::Match($rawLibCode, '^[\s|\t]*#_depends:[\s|\t]*(?<depends>.+)', [Text.RegularExpressions.RegexOptions]::IgnoreCase -bor [Text.RegularExpressions.RegexOptions]::Multiline)
            $dependsRaw = $regexResult[0].Groups['depends'].value
            if (-not [string]::IsNullOrEmpty($dependsRaw)) {
               logMessage "ℹ Processing library dependencies...."
               $dependsList = $dependsRaw -split ','
               foreach ($dependancy in $dependsList) {
                  $dependancyResult = loadLibraries ($dependancy.Trim()) $libPath $session
                  $resultData.statusCode += $dependancyResult.statusCode
                  foreach ($failedLib in $dependancyResult.failedLibs) {$resultData.failedLibs += $dependancyResult.failedLib}
               }
            }
            $parsedLibCode = $rawLibCode -replace '%%PSIL_LOCATION%%', ($fqLibPath -replace '(?:\\|/)[^\\|/]+$', '')
            try {
               $ExecutionContext.SessionState.InvokeCommand.InvokeScript(
                  $session.SessionState,
                  {param($libCode, $libName, $libVersion)
                     # This section of code runs in the caller's session, not the module's session
                     if ($null -eq $_igmLoadedLibraries) {$_igmLoadedLibraries = @{}}
                     if ($null -eq (Invoke-Expression -Command ($libCode))) {
                        if ($null -eq $_igmLoadedLibraries.$libName) {
                           [hashtable]$_igmLoadedLibraries.Add($libName, $libVersion)
                        } else {
                           $_igmLoadedLibraries.$libName = $libVersion
                        }
                     }
                  }.Ast.GetScriptBlock(),
                  $parsedLibCode,
                  $libraryName,
                  $libSourceVerInfo
               )
               $libLoadedVerInfo = (getLoadedLibraries $session)[$libraryName]
               logMessage "✅ %%1%% version %%2%% has been loaded." (selectMsgTemplate 3 @($libraryName, $libLoadedVerInfo.versionStr))
               break
            } catch {
               break
            }
         }
      }
      if ($null -eq $libLoadedVerInfo) {
         logMessage "❌ Failed to load library %%1%%." (selectMsgTemplate 1 $libraryName)
         updateFailedList
      }
   }
   return $resultData
}

function getLibrarySourceVersion{param([string]$libraryName, [string]$libPath)
   foreach ($fqLibPath in (getPossibleLibPaths -libName $libraryName -baseLibPath $libPath)) {
      if (Test-Path $fqLibPath) {
         $libHeader = Get-Content -Path $fqLibPath -Head 10 -ErrorAction SilentlyContinue -WarningAction SilentlyContinue | Out-String
         $regexResult = [regex]::Match($libHeader, '^[\s|\t]*#_version:[\s|\t]*(?<version>[\w|.]+)', [Text.RegularExpressions.RegexOptions]::IgnoreCase -bor [Text.RegularExpressions.RegexOptions]::Multiline)
         return [PSCustomObject]@{
            versionStr = $regexResult[0].Groups['version'].value
            version = [float][regex]::Replace($regexResult[0].Groups['version'].value, '^(?:[a-z]|[A-Z])*|(?<!^\w+)\.', '')
            train = [string][regex]::Match($regexResult[0].Groups['version'].value, '((?:[a-z]|[A-Z])+)', [Text.RegularExpressions.RegexOptions]::IgnoreCase)
         }
      }
   }
   return $null
}

function injectEnumFromJSON{[CmdletBinding()]param([string]$jsonFile, [string]$jsonKey, [string]$enumDef = $dynamicEnumBlock)
   # [CmdletBinding()] is necessary to get the correct session state to inject the Enum into
   [int]$v = 4001
   [scriptblock]$unboundCode = {param([string]$enumdef); Add-Type -TypeDefinition $enumdef}.Ast.GetScriptBlock() # This section of code runs in the caller's session, not the module's session
   [PSCustomObject]$jsonFileData = Get-Content $jsonfile | ConvertFrom-Json
   if (($jsonFileData.$jsonKey).count -lt 1) {return -5}
   foreach ($item in $jsonFileData.$jsonKey) {$enumItems += "@$($item) = $($v),`n"; $v ++}
   $enumDef = $enumDef`
      -replace '%%ENUM_NAME%%', $jsonKey`
      -replace '%%ENUM_DATA%%', $enumItems
   try {
      $ExecutionContext.SessionState.InvokeCommand.InvokeScript($PSCmdlet.SessionState, $unboundCode, $enumDef)
      return 0
   } catch {
      return -17
   }
}