InitHelpers.psm1

$PSDefaultParameterValues['*:Encoding'] = 'utf8'


function Get-CurrentScriptFullPath {
<#
.SYNOPSIS
Returns the full path of the current script or executable.
.DESCRIPTION
Determines the path with the following priority:
  1. $PSCommandPath
  2. $MyInvocation.MyCommand.Path
  3. $MyInvocation.MyCommand.Definition
  4. Current process MainModule file name (exe)
If none is found, returns $null.
.PARAMETER ThrowIfMissing
Throws an exception if the path cannot be determined.
.EXAMPLE
Get-CurrentScriptFullPath -ThrowIfMissing
#>

    param(
        [switch]$ThrowIfMissing  # 如果找不到则抛错
    )

    # 1. PSCommandPath(PowerShell 3+,模块/脚本中最可靠)
    if ($PSCommandPath) {
        try { return (Resolve-Path -LiteralPath $PSCommandPath).ProviderPath } catch { return $PSCommandPath }
    }

    # 2. MyInvocation.MyCommand.Path(脚本、模块、dot-sourced 时通常有效)
    if ($MyInvocation -and $MyInvocation.MyCommand -and $MyInvocation.MyCommand.Path) {
        try { return (Resolve-Path -LiteralPath $MyInvocation.MyCommand.Path).ProviderPath } catch { return $MyInvocation.MyCommand.Path }
    }

    # 3. MyInvocation.MyCommand.Definition(某些情形下包含定义)
    if ($MyInvocation -and $MyInvocation.MyCommand -and $MyInvocation.MyCommand.Definition) {
        $def = $MyInvocation.MyCommand.Definition
        if ($def -and ($def -is [string]) -and (Test-Path $def)) {
            try { return (Resolve-Path -LiteralPath $def).ProviderPath } catch { return $def }
        }
    }

    # 4. 回退到当前进程的可执行文件(ps2exe 编译后通常想要这个)
    try {
        $exe = [System.Diagnostics.Process]::GetCurrentProcess().MainModule.FileName
        if ($exe) {
            try { return (Resolve-Path -LiteralPath $exe).ProviderPath } catch { return $exe }
        }
    } catch {
        # 忽略权限 / 平台异常
    }

    if ($ThrowIfMissing) {
        throw "CAN'T DETERMINE CURRENT SCRIPT FULL PATH!"
    }

    return $null
}


function Get-CurrentScriptDirectory {
<#
.SYNOPSIS
Returns the directory of the current script.
.DESCRIPTION
Uses Get-CurrentScriptFullPath to determine the script directory.
.PARAMETER ThrowIfMissing
Throws an exception if the path cannot be determined.
.EXAMPLE
Get-CurrentScriptDirectory -ThrowIfMissing
#>

    param([switch]$ThrowIfMissing)
    $full = Get-CurrentScriptFullPath -ThrowIfMissing:$ThrowIfMissing
    if ($full) { return Split-Path -Path $full -Parent }
    return $null
}



function Get-ScriptPathInitialized {
<#
.SYNOPSIS
Initializes script path and related subfolder/file variables.
.DESCRIPTION
Sets script-wide variables:
  - $script:ps_script_path
  - $script:ps_script_file
Creates subfolder directories and file path variables based on input.
.PARAMETER SubFolders
Array of subfolder names to create under the script path.
.PARAMETER FilesWithExtensions
Array of file extensions to generate (default: log, toml).
.EXAMPLE
Get-ScriptPathInitialized -SubFolders @("logs","data") -FilesWithExtensions @("log","txt")
#>

    param(
        [string[]]$SubFolders,
        [string[]]$FilesWithExtensions = @("log", "toml")
    )
    $script:ps_script_path = Get-CurrentScriptDirectory
       $script:ps_script_file = Get-CurrentScriptFullPath
    if ($SubFolders -and $SubFolders.Count -gt 0) {
        foreach ($private:subfolder in $SubFolders) {
            $private:potential_path = Join-Path -Path $script:ps_script_path -ChildPath $private:subfolder
            $private:folder_var=(($private:subfolder.ToLower() -Replace "[\W|_]+","_") -replace "^(\d)","_`$1")+"_path"
            Set-Variable -Name $private:folder_var -Value $private:potential_path -Scope Script -Force
            New-Item -ItemType Directory -Path $private:potential_path -Force | Out-Null
        }
    }
    if ($FilesWithExtensions -and $FilesWithExtensions.Count -gt 0) {
        foreach ($private:ext in $FilesWithExtensions) {
            $private:ext=$private:ext.ToLower() -replace "[^a-z]+",""
            $private:file_var="{0}_file" -f $private:ext
            $private:potential_path = "{0}.{1}" -f (Join-Path -Path $script:ps_script_path -ChildPath ([System.IO.Path]::GetFileNameWithoutExtension($script:ps_script_file))), $private:ext
            Set-Variable -Name $private:file_var -Value $private:potential_path -Scope Script -Force
        }
    }

    return 
}


function Get-StringSha256Hash {
<#
.SYNOPSIS
Computes the SHA256 hash of a string.
.PARAMETER InputString
The string to compute the hash for (mandatory).
.EXAMPLE
Get-StringSha256Hash -InputString "Hello World"
#>

    [CmdletBinding()]
    param (
        [Parameter(Mandatory = $true)]
        [string]$InputString
    )

    $private:sha256 = [System.Security.Cryptography.SHA256Managed]::new()
    $private:bytes = [System.Text.Encoding]::UTF8.GetBytes($InputString)
    $private:hashBytes = $private:sha256.ComputeHash($private:bytes)

    $private:hash = -join ($private:hashBytes | ForEach-Object { $_.ToString("X2") })
    return $private:hash
}


function Write-Log {
<#
.SYNOPSIS
Writes a log message to console and log file.
.PARAMETER Message
The message to log (mandatory).
.PARAMETER Level
Log level: Info, Warning, Error, Success, Debug (default: Info).
.PARAMETER MuteForDebug
Do not display debug message.
.PARAMETER NoNewLine
Do not add a newline after output.
.PARAMETER StartNewLine
Start the log message on a new line.
.EXAMPLE
Write-Log -message "Operation completed" -level Info
#>

    [CmdletBinding()]
    param(
        [Parameter(Mandatory = $true)]
        [string]$Message,
        [ValidateSet("Info", "Warning", "Error", "Success", "Debug")]
        [string]$Level = "Info",
        [switch]$MuteForDebug,
        [switch]$NoNewLine,
        [switch]$StartNewLine
    )
    
    $private:timestamp = Get-Date -Format "yyyy-MM-dd HH:mm:ss"
    $private:log_entry = "[$private:timestamp] [$Level] $Message"
    if ($StartNewLine) {
        $private:log_entry = "`n$private:log_entry"
        Write-Host ""
    }
    # Write to log file
    try {
        Add-Content -Path $script:log_file -Value $private:log_entry -ErrorAction Stop
    }
    catch {
        Write-Host "Failed to write to log file: $($_.Exception.Message)" -ForegroundColor Red
    }
    
    # Write to console with appropriate color
    $private:foreground_color = switch ($Level.ToLower()) {
        "info" { "White" }
        "warning" { "Yellow" }
        "error" { "Red" }
        "success" { "Green" }
        "debug" { "Cyan" }
        default { "White" }
    }
    if($Level -eq "Debug" -and $MuteForDebug) {
        return
    }
    Write-Host -NoNewline "[$private:timestamp]" -ForegroundColor "DarkGray"
    Write-Host " [$Level] $Message" -ForegroundColor $private:foreground_color -NoNewline
    if (-not $NoNewLine) {
        Write-Host ""   
    }
}


function Get-TempFileName() {
<#
.SYNOPSIS
Generates a temporary file name.
.PARAMETER ext
File extension (default: tmp).
.PARAMETER prefix
Optional file name prefix.
.PARAMETER suffix
Optional file name suffix.
.EXAMPLE
Get-TempFileName -ext "txt" -prefix "log" -suffix "2025"
#>

    [CmdletBinding()]
    param(
        [string]$ext = "tmp", 
        [string]$prefix = "", 
        [string]$suffix = "" 
    )
    return Join-Path -Path ([System.IO.Path]::GetTempPath()) -ChildPath ((@([System.IO.Path]::GetRandomFileName().Replace(".", ""), $prefix, $suffix, $ext) | Where-Object { -not [string]::IsNullOrEmpty($_) }) -join ".").TrimEnd(".")
}


function Get-TypeByName() {
<#
.SYNOPSIS
Gets the Type object by type name.
.PARAMETER TypeName
Full name of the type.
.PARAMETER Mute
Suppresses exception if type is not found.
.EXAMPLE
Get-TypeByName -type_name "System.Text.StringBuilder"
#>
    
    [CmdletBinding()]
    param(
        [Parameter(Mandatory = $true)][string]$TypeName,
        [switch]$Mute
    )
    $private:type_by_name = [Type]::GetType($TypeName)

    if (-not $private:type_by_name) {
        # fallback: search loaded assemblies for the type:
        $private:full_name = $TypeName.split(',')[0].Trim()
        $private:type_by_name = [AppDomain]::CurrentDomain.GetAssemblies() |
        ForEach-Object { $_.GetTypes() } |
        Where-Object { $_.FullName -eq $private:full_name } |
        Select-Object -First 1
    }
    if (-not $private:type_by_name -and -not $Mute) {
        Write-Log -Message ("Failed to Find Type '{0}' in Loaded Assemblies." -f $TypeName) -Level "Error"
        throw ("Failed to Find Type '{0}' in Loaded Assemblies." -f $TypeName)
    }
    return $private:type_by_name
}


function Get-InstanceByTypeName() {
<#
.SYNOPSIS
Creates an instance of a type by name.
.PARAMETER TypeName
Full name of the type.
.PARAMETER Mute
Suppresses exception if instance creation fails.
.EXAMPLE
Get-InstanceByTypeName -type_name "System.Text.StringBuilder"
#>

    [CmdletBinding()]
    param(
        [Parameter(Mandatory = $true)][string]$TypeName,
        [switch]$Mute
    )
    $private:type_by_name = Get-TypeByName -type_name $TypeName -mute:$Mute
    $private:instance = [Activator]::CreateInstance($private:type_by_name)
    if (-not $private:instance -and -not $Mute) {
        Write-Log -Message ("Failed to Create Instance of Type '{0}'." -f $TypeName) -Level "Error"
        throw ("Failed to Create Instance of Type '{0}'." -f $TypeName)
    }
    return $private:instance
}


function Install-NuGet {
<#
.SYNOPSIS
Installs the NuGet command-line tool.
.DESCRIPTION
Downloads and adds nuget.exe to PATH if not already installed.
.EXAMPLE
Install-NuGet
#>
    
    if (-not (Get-Command nuget -ErrorAction SilentlyContinue)) {
        $private:nuget_path = Join-Path -Path (Join-Path -Path $env:LOCALAPPDATA -ChildPath "Programs") -ChildPath "NuGet"
        if (-not (Test-Path -Path $private:nuget_path)) {
            New-Item -ItemType Directory -Path $private:nuget_path -Force | Out-Null
        }
        $private:nuget_path = Join-Path -Path $private:nuget_path -ChildPath "nuget.exe"
        if (-not (Test-Path -Path $private:nuget_path)) {
            try {
                Invoke-WebRequest -Uri "https://dist.nuget.org/win-x86-commandline/latest/nuget.exe" -OutFile $private:nuget_path -UseBasicParsing -ErrorAction Stop
            }
            catch {
                Write-Log -Message ("Failed to download nuget.exe: {0}" -f $_.Exception.Message) -Level "Error"
                throw ("Failed to download nuget.exe: {0}" -f $_.Exception.Message)
            }
        }
        $env:PATH = "{0};{1}" -f ($env:PATH), ([System.IO.Path]::GetDirectoryName($private:nuget_path))
        [System.Environment]::SetEnvironmentVariable("PATH", $env:PATH, [System.EnvironmentVariableTarget]::Process)
        if (-not (Get-Command nuget -ErrorAction SilentlyContinue)) {
            Write-Log -Message "NuGet installation failed or nuget.exe not found in PATH." -Level "Error"
            throw "NuGet installation failed or nuget.exe not found in PATH."
        }
        Write-Log -Message ("NuGet installed at path: {0}" -f $private:nuget_path) -Level "Debug"
    }
    return $true
}


function Install-Library() {
<#
.SYNOPSIS
Installs a library using NuGet.
.PARAMETER LibName
Library name (mandatory).
.PARAMETER Version
Optional version to install.
.EXAMPLE
Install-Library -lib_name "Newtonsoft.Json" -version "13.0.1"
#>

    [CmdletBinding()]
    param(
        [Parameter(Mandatory = $true)][string]$LibName,
        [string]$Version = ""
    )
    if ([string]::IsNullOrEmpty($LibName)) {
        Write-Log -Message "Library name is required for Install-Library." -Level "Error"
        throw "Library name is required for Install-Library."
    }
    if (-not (test-Path -Path $script:libs_path)) {
        New-Item -ItemType Directory -Path $script:libs_path -Force | Out-Null
    }
    try {
        if ([string]::IsNullOrEmpty($Version)) {
            nuget install $LibName -OutputDirectory $script:libs_path -ExcludeVersion -NonInteractive -Source "https://api.nuget.org/v3/index.json" | Where-Object { -not [string]::IsNullOrEmpty($_) } | ForEach-Object { Write-Log -Message $_ -Level "Debug" }
        }
        else {
            nuget install $LibName -Version $Version -OutputDirectory $script:libs_path -NonInteractive -Source "https://api.nuget.org/v3/index.json" | Where-Object { -not [string]::IsNullOrEmpty($_) } | ForEach-Object { Write-Log -Message $_ -Level "Debug" }
        }
    }
    catch {
        Write-Log -Message ("Failed to Install Library '{0}': {1}" -f $LibName, $_.Exception.Message) -Level "Error"
        throw ("Failed to Install Library '{0}': {1}" -f $LibName, $_.Exception.Message)
    }
    return $true
}


function Install-Assembly() {
<#
.SYNOPSIS
Loads assembly DLLs from the library path.
.PARAMETER LibName
Optional library name filter.
.PARAMETER version
Library version folder (default: net4).
.EXAMPLE
Install-Assembly -lib_name "Newtonsoft.Json"
#>

    [CmdletBinding()]
    param(
        [string]$LibName = $null,
        [string]$Version = "net4"
    )
    if (-not (test-Path -Path $script:libs_path)) {
        New-Item -ItemType Directory -Path $script:libs_path -Force | Out-Null
    }
    $private:lib_dlls = (Get-ChildItem -Path $script:libs_path -Filter "*.dll" -Recurse | Where-Object { $_.FullName -match ('\\lib\\{0}' -f $Version) -and ($null -eq $LibName -or [System.IO.Path]::GetFileNameWithoutExtension($_.Name) -match $LibName) }).FullName
    if ($private:lib_dlls) {
        foreach ($private:lib_dll in $private:lib_dlls) {
            [Reflection.Assembly]::LoadFile($private:lib_dll) | out-null
            Write-Log -Message ("Loaded Assembly: {0}" -f ([System.IO.Path]::GetFileNameWithoutExtension($private:lib_dll))) -Level "Debug"
        }
    }
    else {
        if (Install-Library -lib_name $LibName) {
            Install-Assembly -lib_name $LibName -version $Version
            return
        }
        Write-Log -Message ("No Assemblies Found in Path: {0}" -f $script:libs_path) -Level "Error"
        throw ("No Assemblies Found in Path: {0}" -f $script:libs_path)
    }
}


function Get-ModulesInstalled {
<#
.SYNOPSIS
Gets or installs PowerShell modules.
.PARAMETER name
Array of module names.
.PARAMETER force
Force reinstall even if module exists.
.PARAMETER detailed
Output detailed logging.
.EXAMPLE
Get-ModulesInstalled -name @("PSReadLine") -force -detailed
#>

    [CmdletBinding()]
    Param(
        [Parameter(Position = 0, ValueFromPipeline = $true, ValueFromPipelineByPropertyName = $true)]
        [string[]]$name,
        [switch]$force,
        [switch]$detailed
    )
    Begin {
        $private:results = @()
    }
    Process {
        if ($null -eq $name) {
            return
        }
        foreach ($private:module_name in $name) {
            $private:error_msg = $null
            if ($detailed) {
                Write-Host "Processing module '$private:module_name'..."
            }
            $private:existing_module = Get-Module -Name $private:module_name -ListAvailable -ErrorAction SilentlyContinue
            if ($null -eq $private:existing_module) {
                # Module not installed
                if ($detailed) {
                    Write-Log -Message "Module '$private:module_name' is not installed. Installing..." -Level "Debug"
                }
                try {
                    $null = Install-Module -Name $private:module_name -Scope CurrentUser -Force:$force -ErrorAction Stop
                    $private:status = 'Installed'
                    if ($detailed) {
                        Write-Log -Message "Module '$private:module_name' installed successfully." -Level "Success"
                    }
                }
                catch {
                    $private:status = 'Failed'
                    $private:error_msg = $_.Exception.Message
                    Write-Log -Message "Failed to install module '$private:module_name': $private:error_msg" -Level "Error"
                    throw "Failed to install module '$private:module_name': $private:error_msg"
                }
            }
            elseif ($force) {
                # Module is installed but Force is specified
                if ($detailed) {
                    Write-Log -Message "Module '$private:module_name' is already installed. Reinstalling (Force)..." -Level "Debug"
                }
                try {
                    $null = Install-Module -Name $private:module_name -Scope CurrentUser -Force -ErrorAction Stop
                    $private:status = 'Reinstalled'
                    if ($detailed) {
                        Write-Log -Message "Module '$private:module_name' reinstalled successfully." -Level "Success"
                    }
                }
                catch {
                    $private:status = 'Failed'
                    $private:error_msg = $_.Exception.Message
                    Write-Log -Message "Failed to reinstall module '$private:module_name': $private:error_msg" -Level "Error"
                    throw "Failed to reinstall module '$private:module_name': $private:error_msg"
                }
            }
            else {
                # Module is already installed and no Force flag
                $private:status = 'AlreadyInstalled'
                if ($detailed) {
                    Write-Log -Message "Module '$private:module_name' is already installed." -Level "Debug"
                }
            }
            $private:results += [PsCustomObject]@{
                Name   = $private:module_name
                Status = $private:status
                Error  = $private:error_msg
            }
        }
    }
    End {
        return $private:results
    }
}