trymsae.memeshell.psm1

# MemeShell Module Load Message
$messageFile = Join-Path $PSScriptRoot "templates\texts\load-messages.b64"
if (Test-Path $messageFile) {
    try {
        $base64Content = Get-Content $messageFile -Raw
        $bytes = [Convert]::FromBase64String($base64Content.Trim())
        $decodedText = [System.Text.Encoding]::UTF8.GetString($bytes)
        $messages = $decodedText -split "`r?`n" | Where-Object { $_.Trim() -ne "" }
        $randomMessage = $messages | Get-Random
        Write-Host "█▀▄▀█ █▀▀ █▀▄▀█ █▀▀ █▀ █░█ █▀▀ █░░ █░░ █░░ █▀█ ▄▀█ █▀▄ █▀▀ █▀▄" -ForegroundColor Magenta
        Write-Host "█░▀░█ ██▄ █░▀░█ ██▄ ▄█ █▀█ ██▄ █▄▄ █▄▄ █▄▄ █▄█ █▀█ █▄▀ ██▄ █▄▀" -ForegroundColor Magenta
        Write-host "$($randomMessage)"
    }
    catch {
        Write-host "Catch these hands"
    }
}
function ConvertTo-MockText {
    <#
        .SYNOPSIS
            Converts text to mocking SpongeBob casing
        .DESCRIPTION
            tUrNs AnY tExT iNtO tHiS. Letters alternate case, non-letters are left alone.
            Supports pipeline input so you can mock literally anything fr fr.
        .PARAMETER text
            The text to mock. Accepts pipeline input (no cap).
        .PARAMETER noClipboard
            Don't copy the result to clipboard, just output it.
        .EXAMPLE
            PS > ConvertTo-MockText "code review approved"
            cOdE rEvIeW aPpRoVeD
        .EXAMPLE
            PS > "we ship on friday" | mock
            wE sHiP oN fRiDaY
        .EXAMPLE
            PS > git log --oneline | Select-Object -First 5 | mock
            mocks your entire commit history, as it deserves
    #>

    [alias('mocktext')]
    [CmdletBinding()]
    param (
        [Parameter(Position = 0, Mandatory = $true, ValueFromPipeline = $true)]
        [string]$text,
        [Parameter(Mandatory = $false)]
        [switch]$noClipboard
    )
    begin {
        # accumulate piped lines so clipboard gets the whole thing (big brain)
        $allOutput = @()
    }
    process {
        # the sauce - alternate case per letter, skip non-letters so spacing doesn't break the toggle
        $result = ""
        $toggle = $false
        foreach ($char in $text.ToCharArray()) {
            if ([char]::IsLetter($char)) {
                $result += if ($toggle) { [char]::ToUpper($char) } else { [char]::ToLower($char) }
                $toggle = -not $toggle
            } else {
                $result += $char
            }
        }

        $allOutput += $result
        Write-Output $result
    }
    end {
        # yoink to clipboard (the whole thing, not just the last line)
        if (-not $noClipboard -and $allOutput.Count -gt 0) {
            $clipboardContent = $allOutput -join "`n"
            Set-Clipboard -Value $clipboardContent
            Write-Host "mocked and " -NoNewline -ForegroundColor DarkGray
            Write-Host "blæsted to clipboard" -ForegroundColor Green -NoNewline
            Write-Host " 🤡" -ForegroundColor DarkGray
        }
    }
}
function Disable-MemePrompt {
    <#
    .SYNOPSIS
        Disables the MemeShell prompt and restores your original prompt
    .DESCRIPTION
        Returns your prompt to its pre-MemeShell state
    .EXAMPLE
        Disable-MemePrompt
    #>

    [CmdletBinding()]
    param(
        [Parameter(Position = 0, Mandatory = $false)]
        [switch]$noBitches
    )
    begin {
        # No bitches?
    }
    process {
        if ($script:OriginalPrompt) {
            # Restore the original prompt function, boring.
            Set-Content -Path function:global:prompt -Value $script:OriginalPrompt
        }
        else {
            Write-Warning "No original prompt found. noob."
        }
    }
    end {
        if ($script:OriginalPrompt) {
            Write-Host "MemePrompt disabled. normalcy restored, back to work I guess?" -ForegroundColor Green
        }
    }
}
function Enable-MemePrompt {
    <#
        .SYNOPSIS
            Enables the highly sophisticated meme prompt.
        .DESCRIPTION
            Replaces your boring prompt with some dank lines.
        .PARAMETER allTheBitches
            Specifies all the bitches you aint getting son.
        .EXAMPLE
            PS> Enable-MemePrompt
    #>

    [CmdletBinding()]
    param(
        [Parameter(Position = 0, Mandatory = $false)]
        [switch]$allTheBitches
    )
    begin {
        # Save the current prompt before replacing it (save the actual function definition)
        if (-not $script:OriginalPrompt) {
            $script:OriginalPrompt = ${function:prompt}
        }
        # Load prompt messages from file (the juice)
        $promptMessagesFile = Join-Path $PSScriptRoot "templates\texts\prompt-messages.b64"
        if (Test-Path $promptMessagesFile) {
            try {
                $base64Content = Get-Content $promptMessagesFile -Raw
                $bytes = [Convert]::FromBase64String($base64Content.Trim())
                $decodedText = [System.Text.Encoding]::UTF8.GetString($bytes)
                $script:MemePrompts = $decodedText -split "`r?`n" | Where-Object { $_.Trim() -ne "" }
            }
            catch {
                # catch these hands (fallback to basics)
                $script:MemePrompts = @("DANK", "BASED", "no cap", "fr fr", "💀", "skill issue")
            }
        }
        else {
            # if file missing, lmao
            $script:MemePrompts = @("DANK", "BASED", "no cap", "fr fr", "💀", "skill issue")
        }
        # Dank colors for the prompt
        $script:MemeColors = @(
            "Cyan"
            "Magenta"
            "Yellow"
            "Green"
            "Red"
            "Blue"
            "DarkCyan"
            "DarkMagenta"
            "DarkYellow"
            "DarkGreen"
            "DarkRed"
        )
    }
    process {
        # Create the meme prompt function with random colors
        function global:prompt {
            $prefix = $script:MemePrompts | Get-Random
            $color1 = $script:MemeColors | Get-Random
            $color2 = $script:MemeColors | Get-Random
            $Path = (Get-Location).Path
            $Path = $Path -replace [regex]::Escape($env:USERPROFILE + "\"), "~\"
            # lmao
            if ((Get-Random -Minimum 1 -Maximum 35) -eq 1) {
                try {
                    $soundsPath = Join-Path $PSScriptRoot "templates\sounds"
                    if (Test-Path $soundsPath) {
                        $sounds = Get-ChildItem $soundsPath -Filter "*.wav" -ErrorAction SilentlyContinue
                        if ($sounds) {
                            $randomSound = $sounds | Get-Random
                            $player = New-Object System.Media.SoundPlayer
                            $player.SoundLocation = $randomSound.FullName
                            $player.Play()
                        }
                    }
                }
                catch {
                    # catch these hands
                }
            }
            # Chaos level selection (doing ur mom)
            $chaosRoll = Get-Random -Minimum 1 -Maximum 11
            if ($chaosRoll -le 5) {
                # 50% - Classic format
                Write-Host "[" -ForegroundColor $color2 -NoNewline
                Write-Host $prefix -ForegroundColor $color1 -NoNewline
                Write-Host "] - " -ForegroundColor $color2 -NoNewline
                Write-Host "(" -ForegroundColor $color2 -NoNewline
                Write-Host "$($Path)" -NoNewline
                Write-Host ")" -ForegroundColor $color2 -NoNewline
                Write-host " >" -NoNewline
            }
            elseif ($chaosRoll -eq 6) {
                # ALL CAPS SCREAMING
                Write-Host "[" -ForegroundColor $color2 -NoNewline
                Write-Host $prefix.ToUpper() -ForegroundColor $color1 -NoNewline
                Write-Host "] - " -ForegroundColor $color2 -NoNewline
                Write-Host "(" -ForegroundColor $color2 -NoNewline
                Write-Host "$($Path)" -NoNewline
                Write-Host ")" -ForegroundColor $color2 -NoNewline
                Write-host " >>>" -NoNewline
            }
            elseif ($chaosRoll -eq 7) {
                # dubblare
                $prefix2 = $script:MemePrompts | Get-Random
                Write-Host "[[" -ForegroundColor $color2 -NoNewline
                Write-Host $prefix -ForegroundColor $color1 -NoNewline
                Write-Host "] [" -ForegroundColor $color2 -NoNewline
                Write-Host $prefix2 -ForegroundColor $color1 -NoNewline
                Write-Host "]] " -ForegroundColor $color2 -NoNewline
                Write-Host "$($Path)" -NoNewline
                Write-host " $" -NoNewline
            }
            elseif ($chaosRoll -eq 8) {
                # no brackets?
                Write-Host $prefix -ForegroundColor $color1 -NoNewline
                Write-Host " @ " -ForegroundColor $color2 -NoNewline
                Write-Host "$($Path)" -NoNewline
                Write-host " #" -NoNewline
            }
            elseif ($chaosRoll -eq 9) {
                # spin it around bby
                Write-Host "]" -ForegroundColor $color2 -NoNewline
                Write-Host $prefix -ForegroundColor $color1 -NoNewline
                Write-Host "[ - " -ForegroundColor $color2 -NoNewline
                Write-Host ")" -ForegroundColor $color2 -NoNewline
                Write-Host "$($Path)" -NoNewline
                Write-Host "(" -ForegroundColor $color2 -NoNewline
                Write-host " <" -NoNewline
            }
            else {
                # lmao symbols
                $symbols = @(">", ">>", ">>>", "$", "#", "λ", "~>", "=>", "!!")
                $randomSymbol = $symbols | Get-Random
                Write-Host "{" -ForegroundColor $color2 -NoNewline
                Write-Host $prefix -ForegroundColor $color1 -NoNewline
                Write-Host "} " -ForegroundColor $color2 -NoNewline
                Write-Host "$($Path)" -NoNewline
                Write-host " $randomSymbol" -NoNewline
            }
            return " "
        }
    }
    end {
        Write-Host "MemePrompt enabled. your terminal is now " -ForegroundColor Cyan -NoNewline
        Write-Host "cooked" -ForegroundColor Red -NoNewline
        Write-Host "." -ForegroundColor Cyan
    }
}
function Get-Emoji {
    <#
        .SYNOPSIS
            This function adds emojis to clipboard or directly in console
        .DESCRIPTION
            This function is a quick access for my personal used emojis. Now with more chaos.
        .PARAMETER emoji
            Specify your emoji or use 'random' for surprise
        .EXAMPLE
            PS > Get-Emoji -emoji dunno
            ¯\_(ツ)_/¯ | yoinked to clipboard
        .EXAMPLE
            PS > emoji random
            (╯°□°)╯︵ ┻━┻ | yoinked to clipboard
    #>

    [alias('emoji')]
    [CmdletBinding()]
    param (
        [parameter(Position = 0, Mandatory = $true)]
        [ValidateSet('dunno', 'beer', 'surfsup', 'tableflip', 'unflip', 'shrug', 'lenny', 'disapprove',
                     'dealwithit', 'rage', 'happy', 'sad', 'dead', 'fire', 'skull', 'eyes', 'point',
                     'think', 'clap', 'pray', 'facepalm', 'knife', 'gun', 'bomb',
                     'sus', 'based', 'cringe', 'yeet', 'oof', 'bruh', 'fr', 'nocap', 'ratio',
                     'random')]
        [string]$emoji,
        [parameter(Position = 1, Mandatory = $false)]
        [switch]$noClipboard
    )
    begin {
        # the juice
        $emojiMap = @{
            # the classics
            'dunno'      = '¯\_(ツ)_/¯'
            'shrug'      = '¯\_(ツ)_/¯'
            'tableflip'  = '(╯°□°)╯︵ ┻━┻'
            'unflip'     = '┬─┬ノ( º _ ºノ)'
            'lenny'      = '( ͡° ͜ʖ ͡°)'
            'disapprove' = 'ಠ_ಠ'
            'dealwithit' = '(⌐■_■)'
            'rage'       = '(ノಠ益ಠ)ノ彡┻━┻'
            'happy'      = '(◕‿◕)'
            'sad'        = '(╥﹏╥)'
            'dead'       = '(✖╭╮✖)'
            'think'      = '(¬‿¬)'
            # the mojis
            'beer'       = '🍺'
            'surfsup'    = '🤙'
            'fire'       = '🔥'
            'skull'      = '💀'
            'eyes'       = '👀'
            'point'      = '👉'
            'clap'       = '👏'
            'pray'       = '🙏'
            'facepalm'   = '🤦'
            'knife'      = '🔪'
            'gun'        = '🔫'
            'bomb'       = '💣'
            # the unwelcomed
            'sus'        = 'ඞ'
            'based'      = '🗿'
            'cringe'     = '😬'
            'yeet'       = '༼ つ ◕_◕ ༽つ'
            'oof'        = 'F'
            'bruh'       = '🤨'
            'fr'         = '💯'
            'nocap'      = '🧢'
            'ratio'      = '📉'
        }
        # rando
        if ($emoji -eq 'random') {
            $randomKey = $emojiMap.Keys | Where-Object { $_ -ne 'random' } | Get-Random
            $output = $emojiMap[$randomKey]
            $emoji = $randomKey  # for display purposes
        }
        else {
            $output = $emojiMap[$emoji]
        }
    }
    process {
        # no bitches?
    }
    end {
        if ( -not $noClipboard ) {
            Set-Clipboard -Value $output
            Write-Host "$($output) | " -NoNewline
            Write-Host "plastered to clipboard" -ForegroundColor Green -NoNewline
            Write-Host " [$emoji]" -ForegroundColor DarkGray
        }
        else {
            Write-Host $output
        }
    }
}
function Get-MemeTemplatePaths {
    <#
        .SYNOPSIS
            Returns all available meme template FileInfo objects
        .DESCRIPTION
            Merges templates from the installed module folder and the user-local
            ~/.memeshell/templates/pictures/ folder. Module templates win on
            BaseName conflict. Silently skips folders that don't exist.
        .PARAMETER modulePath
            Path to the module root (defaults to the installed module's base directory).
        .PARAMETER userTemplatePath
            Path to the user-local templates folder (defaults to ~/.memeshell/templates/pictures).
        .EXAMPLE
            PS > Get-MemeTemplatePaths
            Returns all available templates from both locations
    #>

    [CmdletBinding()]
    param (
        [string]$modulePath = ((Get-Module -Name 'trymsae.memeshell' -ErrorAction SilentlyContinue).ModuleBase),
        [string]$userTemplatePath = (Join-Path $env:USERPROFILE ".memeshell\templates\pictures")
    )

    $results = [System.Collections.Generic.List[System.IO.FileInfo]]::new()

    # Bundled module templates (these are the OG ones, they win all conflicts fr fr)
    if ($modulePath) {
        $bundledPath = Join-Path $modulePath "templates\pictures"
        if (Test-Path $bundledPath) {
            Get-ChildItem -Path $bundledPath -Recurse -ErrorAction SilentlyContinue |
                Where-Object { $_.Extension -in @('.jpg', '.jpeg', '.png', '.bmp') } |
                ForEach-Object { $results.Add($_) }
        }
    }

    # Build a hashtable of module BaseName → true for O(1) dedup lookups, no cap
    $moduleBaseNames = @{}
    $results | ForEach-Object { $moduleBaseNames[$_.BaseName] = $true }

    # User-local templates (deduplicate by BaseName — module wins, no cap)
    if ($userTemplatePath -and (Test-Path $userTemplatePath)) {
        Get-ChildItem -Path $userTemplatePath -Recurse -ErrorAction SilentlyContinue |
            Where-Object { $_.Extension -in @('.jpg', '.jpeg', '.png', '.bmp') } |
            ForEach-Object {
                $userFile = $_
                if (-not $moduleBaseNames.ContainsKey($userFile.BaseName)) {
                    $results.Add($userFile)
                }
            }
    }

    return $results.ToArray()
}
function Import-Meme {
    <#
        .SYNOPSIS
            Imports a custom meme template into MemeShell
        .DESCRIPTION
            Copies an image into MemeShell's templates folder, resizing it proportionally
            if it exceeds maxSize, and normalizing the filename to kebab-case.
            By default imports to the user-local folder (~/.memeshell/templates/pictures/).
            Use -toSource to import directly into the repo's source templates (for contributors).
            Accepts a local file path or an HTTP/HTTPS URL — auto-detected from -path.
        .PARAMETER path
            Path to the source image file (jpg, jpeg, png, bmp) or an HTTP/HTTPS URL to an image.
        .PARAMETER name
            Override the output filename. Will be kebab-cased automatically. Omit to use source filename.
        .PARAMETER maxSize
            Maximum dimension (width or height) in pixels. Default 800. Only resizes down, never up.
        .PARAMETER toSource
            Import to the repo's source templates folder instead of the user-local folder.
            Only works when running the module from the development repo.
        .EXAMPLE
            PS > Import-Meme -path "C:\memes\Funny Cat.jpg"
            Imports as funny-cat.png to ~/.memeshell/templates/pictures/
        .EXAMPLE
            PS > Import-Meme -path "C:\memes\drake2.png" -name "drake-v2" -toSource
            Imports as drake-v2.png to the repo source templates (for the next release)
        .EXAMPLE
            PS > Import-Meme -path "https://example.com/distracted-boyfriend.jpg"
            Downloads and imports as distracted-boyfriend.png to ~/.memeshell/templates/pictures/
        .EXAMPLE
            PS > Import-Meme -path "https://cdn.example.com/img?id=42" -name "big-brain"
            Downloads from URL with a meaningless path segment; uses -name override.
    #>

    [CmdletBinding()]
    param (
        [Parameter(Position = 0, Mandatory = $true)]
        [string]$path,

        [Parameter(Position = 1, Mandatory = $false)]
        [string]$name,

        [Parameter(Mandatory = $false)]
        [ValidateRange(100, 4000)]
        [int]$maxSize = 800,

        [Parameter(Mandatory = $false)]
        [switch]$toSource
    )
    begin {
        Add-Type -AssemblyName System.Drawing
        $supportedExtensions = @('.jpg', '.jpeg', '.png', '.bmp')
    }
    process {
        $bytes    = $null
        $baseName = $null
        $isUrl    = $path -match '^https?://'

        if ($isUrl) {
            $uri     = [System.Uri]$path
            $segment = [System.Uri]::UnescapeDataString($uri.Segments[-1]).TrimEnd('/')
            $segExt  = [System.IO.Path]::GetExtension($segment).ToLower()
            $segBase = [System.IO.Path]::GetFileNameWithoutExtension($segment)

            if ([string]::IsNullOrWhiteSpace($name)) {
                $preKebab    = $segBase.ToLower() -replace '[\s_]+', '-' -replace '[^a-z0-9\-]', '' -replace '-+', '-'
                $preKebab    = $preKebab.Trim('-')
                $blocklist   = @('img','image','images','photo','photos','download','file','meme','get','view','thumb','thumbnail','pic','pics')
                $hasImageExt = $segExt -in @('.jpg','.jpeg','.png','.bmp','.gif','.webp')
                if (($preKebab -in $blocklist -or $preKebab.Length -lt 3) -and -not $hasImageExt) {
                    Write-Error "Can't derive a usable filename from URL '$path' — add -name to sort it out fr"
                    return
                }
                $baseName = $segBase
            }
            else {
                $baseName = $name
            }

            $response    = Invoke-WebRequest -Uri $path -UseBasicParsing
            $contentType = ($response.Headers['Content-Type'] -split ';')[0].Trim()
            if ($contentType -notin @('image/jpeg','image/png','image/bmp','image/gif','image/webp')) {
                Write-Error "URL did not return an image (got '$contentType') — link's cooked fr"
                return
            }
            $bytes = $response.Content
        }
        else {
            # Validate source file exists
            if (-not (Test-Path $path)) {
                Write-Error "File not found: $path (you sure that's real cuh?)"
                return
            }

            $sourceFile = Get-Item $path

            # Validate format
            if ($sourceFile.Extension.ToLower() -notin $supportedExtensions) {
                Write-Error "Unsupported format: '$($sourceFile.Extension)'. Valid formats: jpg, jpeg, png, bmp (no bitch format)"
                return
            }

            $baseName = if (-not [string]::IsNullOrWhiteSpace($name)) { $name } else { $sourceFile.BaseName }
            $bytes    = [System.IO.File]::ReadAllBytes($sourceFile.FullName)
        }

        # Derive kebab-case output filename
        $kebabName  = $baseName.ToLower() -replace '[\s_]+', '-' -replace '[^a-z0-9\-]', '' -replace '-+', '-'
        $kebabName  = $kebabName.Trim('-')
        $outputName = "$kebabName.png"

        if ([string]::IsNullOrWhiteSpace($kebabName)) {
            Write-Error "Could not derive a valid filename from '$baseName' (skill issue — use -name to specify one fr)"
            return
        }

        # Determine destination directory
        if ($toSource) {
            $destDir = Join-Path (Split-Path $PSScriptRoot -Parent) "templates\pictures"
            if (-not (Test-Path $destDir)) {
                Write-Error "Source templates folder not found at: $destDir`nThe -toSource switch only works when running from the development repo, not from an installed PSGallery version (no cap)"
                return
            }
        }
        else {
            $destDir = Join-Path $env:USERPROFILE ".memeshell\templates\pictures"
            if (-not (Test-Path $destDir)) {
                New-Item -ItemType Directory -Path $destDir -Force | Out-Null
                Write-Host "Created user-local templates folder: $destDir" -ForegroundColor Cyan
            }
        }

        $destPath = Join-Path $destDir $outputName

        # Check for filename collision — prompt for a new name rather than hard-erroring
        while (Test-Path $destPath) {
            $newName = Read-Host "Template '$outputName' already exists — what do you want to call it? (empty to cancel)"
            if ([string]::IsNullOrWhiteSpace($newName)) {
                Write-Error "Import cancelled — '$outputName' already exists (ratio + L)"
                return
            }
            $kebabName  = $newName.ToLower() -replace '[\s_]+', '-' -replace '[^a-z0-9\-]', '' -replace '-+', '-'
            $kebabName  = $kebabName.Trim('-')
            if ([string]::IsNullOrWhiteSpace($kebabName)) {
                Write-Error "Could not derive a valid filename from '$newName' (skill issue fr)"
                return
            }
            $outputName = "$kebabName.png"
            $destPath   = Join-Path $destDir $outputName
        }

        # Load, optionally resize, save as PNG
        $graphics = $null
        $bitmap   = $null
        $img      = $null
        $ms       = $null
        try {
            $ms  = New-Object System.IO.MemoryStream(,$bytes)
            $img = [System.Drawing.Image]::FromStream($ms)

            $finalWidth  = $img.Width
            $finalHeight = $img.Height

            if ($img.Width -gt $maxSize -or $img.Height -gt $maxSize) {
                $ratio       = [Math]::Min($maxSize / $img.Width, $maxSize / $img.Height)
                $finalWidth  = [int]($img.Width  * $ratio)
                $finalHeight = [int]($img.Height * $ratio)

                $bitmap   = New-Object System.Drawing.Bitmap($finalWidth, $finalHeight)
                $graphics = [System.Drawing.Graphics]::FromImage($bitmap)
                $graphics.InterpolationMode    = [System.Drawing.Drawing2D.InterpolationMode]::HighQualityBicubic
                $graphics.SmoothingMode        = [System.Drawing.Drawing2D.SmoothingMode]::AntiAlias
                $graphics.CompositingQuality   = [System.Drawing.Drawing2D.CompositingQuality]::HighQuality
                $graphics.DrawImage($img, 0, 0, $finalWidth, $finalHeight)
            }
            else {
                $bitmap = New-Object System.Drawing.Bitmap($img)
            }

            $bitmap.Save($destPath, [System.Drawing.Imaging.ImageFormat]::Png)

            $dest = if ($toSource) { "source templates (next release included fr)" } else { "user-local templates" }
            Write-Host "Meme imported " -NoNewline
            Write-Host "no cap" -ForegroundColor Green -NoNewline
            Write-Host " 🔥 → " -NoNewline
            Write-Host $outputName -ForegroundColor Cyan -NoNewline
            Write-Host " (${finalWidth}x${finalHeight}px) → $dest" -ForegroundColor Gray
            Write-Host "Saved to: $destPath" -ForegroundColor Gray
        }
        catch {
            # Clean up partial file so the collision guard doesn't block retries
            if ($destPath -and (Test-Path -LiteralPath $destPath)) {
                Remove-Item -LiteralPath $destPath -Force -ErrorAction SilentlyContinue
            }
            Write-Error "Failed to process image: $($_.Exception.Message) (catch these hands)"
        }
        finally {
            if ($null -ne $graphics) { $graphics.Dispose() }
            if ($null -ne $bitmap)   { $bitmap.Dispose() }
            if ($null -ne $img)      { $img.Dispose() }
            if ($null -ne $ms)       { $ms.Dispose() }
        }
    }
}
function New-Meme {
    <#
        .SYNOPSIS
            Generates dank memes from local templates
        .DESCRIPTION
            Creates memes by adding text to local image templates. Classic top/bottom text format or use -manual for GUI mode.
        .PARAMETER template
            Template image name (without extension) from templates\pictures folder
        .PARAMETER topText
            Text for the top of the meme (classic format)
        .PARAMETER bottomText
            Text for the bottom of the meme (classic format)
        .PARAMETER manual
            Opens GUI window for manual meme creation (more control, less speed)
        .PARAMETER noClipboard
            Don't copy the result to clipboard (just save it)
        .PARAMETER textCase
            Text casing: Upper (default, classic meme), Lower, or Original (keep as typed)
        .EXAMPLE
            PS > New-Meme -template "drake" -topText "Using APIs" -bottomText "Local images with PowerShell"
            Meme created and mogged to clipboard
        .EXAMPLE
            PS > New-Meme -template "drake" -topText "Shouting" -bottomText "whispering" -textCase Original
            Renders with exact casing as typed
        .EXAMPLE
            PS > meme -manual
            Opens GUI for manual meme crafting
    #>

    [alias('meme')]
    [CmdletBinding()]
    param (
        [parameter(Position = 0, Mandatory = $false)]
        [ArgumentCompleter({
            param($commandName, $parameterName, $wordToComplete, $commandAst, $fakeBoundParameters)
            $modulePath = (Get-Module -Name 'trymsae.memeshell' -ErrorAction SilentlyContinue).ModuleBase
            if (-not $modulePath) { return }
            Get-MemeTemplatePaths -modulePath $modulePath |
                Where-Object { $_.BaseName -like "$wordToComplete*" } |
                ForEach-Object { $_.BaseName }
        })]
        [string]$template,
        [parameter(Position = 1, Mandatory = $false)]
        [string]$topText,
        [parameter(Position = 2, Mandatory = $false)]
        [string]$bottomText,
        [parameter(Position = 3, Mandatory = $false)]
        [switch]$manual,
        [parameter(Position = 4, Mandatory = $false)]
        [switch]$noClipboard,
        [parameter(Position = 5, Mandatory = $false)]
        [ValidateSet('Upper', 'Lower', 'Original')]
        [string]$textCase = 'Original',
        [parameter(Position = 6, Mandatory = $false)]
        [ValidateRange(2, 6)]
        [int]$TextLines = 2
    )
    begin {
        # Load the juice (System.Drawing for image manipulation)
        Add-Type -AssemblyName System.Drawing
        Add-Type -AssemblyName System.Windows.Forms

        $script:newMemeInitOk = $false

        # grab all image files from module and user-local templates (fr fr)
        # Note: $PSScriptRoot = release/ dir when installed, src/ when dot-sourced in dev (module templates won't load in dev, user-local still works)
        $availableTemplates = Get-MemeTemplatePaths -modulePath $PSScriptRoot

        if ($availableTemplates.Count -eq 0) {
            Write-Error "No templates found fr — check module install and ~/.memeshell/templates/pictures/"
            return
        }

        # temp output path
        $tempPath = [System.IO.Path]::GetTempPath()
        $outputFile = Join-Path $tempPath "meme_$(Get-Date -Format 'yyyyMMdd_HHmmss').png"

        $script:newMemeInitOk = $true
    }
    process {
        # bail if begin failed (return in begin doesn't stop process, no cap)
        if (-not $script:newMemeInitOk) { return }

        $useMultiLineMode = $false

        # Manual mode (GUI goes hard)
        if ($manual) {
            # Calculate form height based on text lines (dynamic sizing fr fr)
            $textLineHeight = 80
            $previewStartY = 90 + ($TextLines * $textLineHeight) + 20
            $previewHeight = 250
            $buttonY = $previewStartY + $previewHeight + 20
            $formHeight = $buttonY + 80

            # Create the form (dynamic height based on text lines, wider for X controls)
            $form = New-Object System.Windows.Forms.Form
            $form.Text = "MemeShell - Manual Mode 🔥 ($TextLines lines)"
            $form.Size = New-Object System.Drawing.Size 780, $formHeight
            $form.StartPosition = "CenterScreen"
            $form.FormBorderStyle = "FixedDialog"
            $form.MaximizeBox = $false

            # Template dropdown
            $labelTemplate = New-Object System.Windows.Forms.Label
            $labelTemplate.Location = New-Object System.Drawing.Point 10, 20
            $labelTemplate.Size = New-Object System.Drawing.Size 280, 20
            $labelTemplate.Text = "Select Template:"
            $form.Controls.Add($labelTemplate)

            $comboTemplate = New-Object System.Windows.Forms.ComboBox
            $comboTemplate.Location = New-Object System.Drawing.Point 10, 45
            $comboTemplate.Size = New-Object System.Drawing.Size 740, 25
            $comboTemplate.DropDownStyle = "DropDownList"
            foreach ($tmpl in $availableTemplates) {
                $comboTemplate.Items.Add($tmpl.BaseName) | Out-Null
            }
            if ($comboTemplate.Items.Count -gt 0) {
                # Pre-select the template passed via -template, otherwise fall back to first (no more always-bernie bug)
                $preSelectedIdx = $comboTemplate.Items.IndexOf($template)
                $comboTemplate.SelectedIndex = if ($preSelectedIdx -ge 0) { $preSelectedIdx } else { 0 }
            }
            $form.Controls.Add($comboTemplate)

            # Create dynamic text line controls (array-based for flexibility)
            $textBoxes = @()
            $numericXControls = @()
            $numericYControls = @()
            $checkboxWrapControls = @()
            $defaultYPositions = @(50, 200, 350, 500, 650, 800)  # Default Y positions for up to 6 lines

            for ($i = 0; $i -lt $TextLines; $i++) {
                $yPos = 90 + ($i * $textLineHeight)
                $lineNum = $i + 1

                # Text label
                $labelText = New-Object System.Windows.Forms.Label
                $labelText.Location = New-Object System.Drawing.Point 10, $yPos
                $labelText.Size = New-Object System.Drawing.Size 280, 20
                $labelText.Text = "Text Line $($lineNum):"
                $form.Controls.Add($labelText)

                # Text input
                $textBox = New-Object System.Windows.Forms.TextBox
                $textBox.Location = New-Object System.Drawing.Point 10, ($yPos + 25)
                $textBox.Size = New-Object System.Drawing.Size 380, 25
                $textBox.Font = New-Object System.Drawing.Font("Arial", 10)
                $textBox.Tag = $i  # Store index for later reference
                $form.Controls.Add($textBox)
                $textBoxes += $textBox

                # X position label
                $labelX = New-Object System.Windows.Forms.Label
                $labelX.Location = New-Object System.Drawing.Point 400, ($yPos + 5)
                $labelX.Size = New-Object System.Drawing.Size 70, 20
                $labelX.Text = "X Position:"
                $form.Controls.Add($labelX)

                # X position control (-1 = center, 0+ = absolute position)
                $numericX = New-Object System.Windows.Forms.NumericUpDown
                $numericX.Location = New-Object System.Drawing.Point 400, ($yPos + 25)
                $numericX.Size = New-Object System.Drawing.Size 80, 25
                $numericX.Minimum = -1
                $numericX.Maximum = 2000
                $numericX.Value = -1  # -1 means "center" (auto-calculated)
                $numericX.Increment = 5
                $numericX.Tag = $i
                $form.Controls.Add($numericX)
                $numericXControls += $numericX

                # Y position label
                $labelY = New-Object System.Windows.Forms.Label
                $labelY.Location = New-Object System.Drawing.Point 490, ($yPos + 5)
                $labelY.Size = New-Object System.Drawing.Size 70, 20
                $labelY.Text = "Y Position:"
                $form.Controls.Add($labelY)

                # Y position control
                $numericY = New-Object System.Windows.Forms.NumericUpDown
                $numericY.Location = New-Object System.Drawing.Point 490, ($yPos + 25)
                $numericY.Size = New-Object System.Drawing.Size 80, 25
                $numericY.Minimum = 0
                $numericY.Maximum = 2000
                $numericY.Value = $defaultYPositions[$i]
                $numericY.Increment = 5
                $numericY.Tag = $i  # Store index for later reference
                $form.Controls.Add($numericY)
                $numericYControls += $numericY

                # Center X button (quick reset to center)
                $buttonCenterX = New-Object System.Windows.Forms.Button
                $buttonCenterX.Location = New-Object System.Drawing.Point 580, ($yPos + 24)
                $buttonCenterX.Size = New-Object System.Drawing.Size 75, 25
                $buttonCenterX.Text = "Center X"
                $buttonCenterX.Tag = $i
                $buttonCenterX.Add_Click({
                    param($btnSender, $btnEvent)
                    $index = $btnSender.Tag
                    $numericXControls[$index].Value = -1
                })
                $form.Controls.Add($buttonCenterX)

                # Auto-wrap checkbox
                $checkboxWrap = New-Object System.Windows.Forms.CheckBox
                $checkboxWrap.Location = New-Object System.Drawing.Point 665, ($yPos + 26)
                $checkboxWrap.Size = New-Object System.Drawing.Size 100, 20
                $checkboxWrap.Text = "Auto-wrap"
                $checkboxWrap.Checked = $true
                $checkboxWrap.Tag = $i
                $form.Controls.Add($checkboxWrap)
                $checkboxWrapControls += $checkboxWrap
            }

            # Set initial text for first two lines if provided (backward compatibility)
            if ($textBoxes.Count -ge 1 -and -not [string]::IsNullOrWhiteSpace($topText)) {
                $textBoxes[0].Text = $topText
            }
            if ($textBoxes.Count -ge 2 -and -not [string]::IsNullOrWhiteSpace($bottomText)) {
                $textBoxes[1].Text = $bottomText
            }

            # Preview box
            $pictureBox = New-Object System.Windows.Forms.PictureBox
            $pictureBox.Location = New-Object System.Drawing.Point 10, $previewStartY
            $pictureBox.Size = New-Object System.Drawing.Size 740, $previewHeight
            $pictureBox.SizeMode = "Zoom"
            $pictureBox.BorderStyle = "FixedSingle"
            $form.Controls.Add($pictureBox)

            # Live preview update function (the sauce)
            $updatePreview = {
                $selectedTemplate = $availableTemplates | Where-Object { $_.BaseName -eq $comboTemplate.SelectedItem }

                if (-not $selectedTemplate) { return }

                try {
                    # Dispose previous image
                    if ($pictureBox.Image) {
                        $pictureBox.Image.Dispose()
                    }

                    # Load base template
                    $previewImage = [System.Drawing.Image]::FromFile($selectedTemplate.FullName)
                    $previewBitmap = New-Object System.Drawing.Bitmap($previewImage.Width, $previewImage.Height)
                    $previewGraphics = [System.Drawing.Graphics]::FromImage($previewBitmap)
                    $previewGraphics.DrawImage($previewImage, 0, 0, $previewImage.Width, $previewImage.Height)

                    # Set up text rendering (MAXIMUM QUALITY MODE - matches final output)
                    $previewGraphics.TextRenderingHint = [System.Drawing.Text.TextRenderingHint]::AntiAliasGridFit
                    $previewGraphics.SmoothingMode = [System.Drawing.Drawing2D.SmoothingMode]::AntiAlias
                    $previewGraphics.CompositingQuality = [System.Drawing.Drawing2D.CompositingQuality]::HighQuality
                    $previewGraphics.InterpolationMode = [System.Drawing.Drawing2D.InterpolationMode]::HighQualityBicubic

                    # Font setup
                    $previewFontSize = [Math]::Max(24, $previewImage.Width / 15)
                    $previewFont = New-Object System.Drawing.Font("Impact", $previewFontSize, [System.Drawing.FontStyle]::Bold)
                    $previewBrushWhite = [System.Drawing.Brushes]::White
                    $previewPenBlack = New-Object System.Drawing.Pen([System.Drawing.Color]::Black, ($previewFontSize / 12))
                    $previewPenBlack.LineJoin = [System.Drawing.Drawing2D.LineJoin]::Round  # Smooth corners, no artifacts

                    # Helper to draw text with outline and wrapping support (chef's kiss)
                    $drawPreviewText = {
                        param($text, $x, $y, $useXCenter, $enableWrap)

                        if ([string]::IsNullOrWhiteSpace($text)) { return }

                        $formattedText = switch ($textCase) {
                            'Lower'    { $text.ToLower() }
                            'Original' { $text }
                            default    { $text.ToUpper() }
                        }

                        # Check if wrapping is needed
                        $textWidth = $previewGraphics.MeasureString($formattedText, $previewFont).Width
                        $maxWidth = $previewImage.Width * 0.9  # Leave 10% margin

                        if ($enableWrap -and $textWidth -gt $maxWidth) {
                            # Split text into words and wrap (the wrapping sauce)
                            $words = $formattedText -split ' '
                            $lines = @()
                            $currentLine = ""

                            foreach ($word in $words) {
                                $testLine = if ($currentLine) { "$currentLine $word" } else { $word }
                                $testWidth = $previewGraphics.MeasureString($testLine, $previewFont).Width

                                if ($testWidth -gt $maxWidth -and $currentLine) {
                                    $lines += $currentLine
                                    $currentLine = $word
                                } else {
                                    $currentLine = $testLine
                                }
                            }
                            if ($currentLine) { $lines += $currentLine }

                            # Draw each line with offset
                            $lineHeight = $previewFontSize * 1.2
                            $startY = $y - (($lines.Count - 1) * $lineHeight / 2)

                            for ($lineIdx = 0; $lineIdx -lt $lines.Count; $lineIdx++) {
                                $lineY = $startY + ($lineIdx * $lineHeight)
                                $lineX = if ($useXCenter) { $previewImage.Width / 2 } else { $x }

                                $format = New-Object System.Drawing.StringFormat
                                $format.Alignment = if ($useXCenter) { [System.Drawing.StringAlignment]::Center } else { [System.Drawing.StringAlignment]::Near }
                                $format.LineAlignment = [System.Drawing.StringAlignment]::Center

                                $path = New-Object System.Drawing.Drawing2D.GraphicsPath
                                $path.AddString(
                                    $lines[$lineIdx],
                                    $previewFont.FontFamily,
                                    [int]$previewFont.Style,
                                    $previewFontSize,
                                    (New-Object System.Drawing.PointF($lineX, $lineY)),
                                    $format
                                )

                                $previewGraphics.DrawPath($previewPenBlack, $path)
                                $previewGraphics.FillPath($previewBrushWhite, $path)

                                $path.Dispose()
                                $format.Dispose()
                            }
                        }
                        else {
                            # Single line rendering (classic mode)
                            $finalX = if ($useXCenter) { $previewImage.Width / 2 } else { $x }

                            $format = New-Object System.Drawing.StringFormat
                            $format.Alignment = if ($useXCenter) { [System.Drawing.StringAlignment]::Center } else { [System.Drawing.StringAlignment]::Near }
                            $format.LineAlignment = [System.Drawing.StringAlignment]::Center

                            $path = New-Object System.Drawing.Drawing2D.GraphicsPath
                            $path.AddString(
                                $formattedText,
                                $previewFont.FontFamily,
                                [int]$previewFont.Style,
                                $previewFontSize,
                                (New-Object System.Drawing.PointF($finalX, $y)),
                                $format
                            )

                            $previewGraphics.DrawPath($previewPenBlack, $path)
                            $previewGraphics.FillPath($previewBrushWhite, $path)

                            $path.Dispose()
                            $format.Dispose()
                        }
                    }

                    # Draw all text lines dynamically with X position and wrapping (no cap we looping fr)
                    for ($j = 0; $j -lt $textBoxes.Count; $j++) {
                        if (-not [string]::IsNullOrWhiteSpace($textBoxes[$j].Text)) {
                            $textX = $numericXControls[$j].Value
                            $textY = $numericYControls[$j].Value
                            $useCenter = ($textX -eq -1)
                            $actualX = if ($useCenter) { $previewImage.Width / 2 } else { $textX }
                            $wrapEnabled = $checkboxWrapControls[$j].Checked

                            & $drawPreviewText $textBoxes[$j].Text $actualX $textY $useCenter $wrapEnabled
                        }
                    }

                    # Cleanup and set preview
                    $previewGraphics.Dispose()
                    $previewFont.Dispose()
                    $previewPenBlack.Dispose()
                    $previewImage.Dispose()

                    $pictureBox.Image = $previewBitmap
                }
                catch {
                    # catch these hands (fallback to template only)
                    Write-Verbose "Preview update failed: $($_.Exception.Message)"
                }
            }

            # Load initial preview
            & $updatePreview

            # Hook up all the events to update preview (live preview goes hard)
            $comboTemplate.Add_SelectedIndexChanged({ & $updatePreview })

            # Hook up events for all text lines dynamically (array gang)
            for ($k = 0; $k -lt $textBoxes.Count; $k++) {
                $textBoxes[$k].Add_TextChanged({ & $updatePreview })
                $numericXControls[$k].Add_ValueChanged({ & $updatePreview })
                $numericYControls[$k].Add_ValueChanged({ & $updatePreview })
                $checkboxWrapControls[$k].Add_CheckedChanged({ & $updatePreview })
            }

            # Mouse drag functionality (absolute madness but it works fr fr)
            $script:isDragging = $false
            $script:dragTargetIndex = -1  # Index of text line being dragged
            $script:lastMouseY = 0

            $pictureBox.Add_MouseDown({
                param($clickSender, $clickEvent)

                if (-not $pictureBox.Image) { return }

                # Convert click coordinates to image coordinates (account for zoom)
                $displayRect = $pictureBox.ClientRectangle

                # Calculate actual image position in the control (centered with zoom)
                $imageAspect = $pictureBox.Image.Width / $pictureBox.Image.Height
                $controlAspect = $displayRect.Width / $displayRect.Height

                if ($imageAspect -gt $controlAspect) {
                    # Image is wider - fit to width
                    $displayWidth = $displayRect.Width
                    $displayHeight = [int]($displayRect.Width / $imageAspect)
                    $displayX = 0
                    $displayY = [int](($displayRect.Height - $displayHeight) / 2)
                }
                else {
                    # Image is taller - fit to height
                    $displayHeight = $displayRect.Height
                    $displayWidth = [int]($displayRect.Height * $imageAspect)
                    $displayX = [int](($displayRect.Width - $displayWidth) / 2)
                    $displayY = 0
                }

                # Convert mouse coordinates to image coordinates
                if ($clickEvent.X -lt $displayX -or $clickEvent.X -gt ($displayX + $displayWidth) -or
                    $clickEvent.Y -lt $displayY -or $clickEvent.Y -gt ($displayY + $displayHeight)) {
                    return  # Click outside image
                }

                $imageY = [int](($clickEvent.Y - $displayY) / $displayHeight * $pictureBox.Image.Height)

                # Check if clicking near any text line (50px tolerance, find closest)
                $closestIndex = -1
                $closestDistance = 999999

                for ($m = 0; $m -lt $textBoxes.Count; $m++) {
                    # Only check lines with text
                    if ([string]::IsNullOrWhiteSpace($textBoxes[$m].Text)) { continue }

                    $lineY = $numericYControls[$m].Value
                    $distance = [Math]::Abs($imageY - $lineY)

                    if ($distance -lt $closestDistance -and $distance -lt 50) {
                        $closestDistance = $distance
                        $closestIndex = $m
                    }
                }

                # Start dragging if we found a nearby text line (that's the vibe - now with X and Y drag support)
                if ($closestIndex -ge 0) {
                    $script:isDragging = $true
                    $script:dragTargetIndex = $closestIndex
                    $script:lastMouseY = $imageY
                    $pictureBox.Cursor = [System.Windows.Forms.Cursors]::SizeAll

                    # If X is centered (-1), unlock it to the actual center pixel so dragging works immediately (no more 1px workaround)
                    if ($numericXControls[$closestIndex].Value -eq -1) {
                        $numericXControls[$closestIndex].Value = [int]($pictureBox.Image.Width / 2)
                    }
                }
            })

            $pictureBox.Add_MouseMove({
                param($moveSender, $moveEvent)

                if (-not $script:isDragging -or -not $pictureBox.Image) { return }

                # Convert to image coordinates (same logic as MouseDown)
                $displayRect = $pictureBox.ClientRectangle
                $imageAspect = $pictureBox.Image.Width / $pictureBox.Image.Height
                $controlAspect = $displayRect.Width / $displayRect.Height

                if ($imageAspect -gt $controlAspect) {
                    $displayWidth = $displayRect.Width
                    $displayHeight = [int]($displayRect.Width / $imageAspect)
                    $displayX = 0
                    $displayY = [int](($displayRect.Height - $displayHeight) / 2)
                }
                else {
                    $displayHeight = $displayRect.Height
                    $displayWidth = [int]($displayRect.Height * $imageAspect)
                    $displayX = [int](($displayRect.Width - $displayWidth) / 2)
                    $displayY = 0
                }

                $imageX = [int](($moveEvent.X - $displayX) / $displayWidth * $pictureBox.Image.Width)
                $imageY = [int](($moveEvent.Y - $displayY) / $displayHeight * $pictureBox.Image.Height)

                # Clamp to image bounds
                $imageX = [Math]::Max(0, [Math]::Min($imageX, $pictureBox.Image.Width))
                $imageY = [Math]::Max(0, [Math]::Min($imageY, $pictureBox.Image.Height))

                # Update both X and Y numeric controls (this triggers preview update)
                if ($script:dragTargetIndex -ge 0 -and $script:dragTargetIndex -lt $numericYControls.Count) {
                    # Only update X if it's not set to center (-1)
                    if ($numericXControls[$script:dragTargetIndex].Value -ne -1) {
                        $numericXControls[$script:dragTargetIndex].Value = $imageX
                    }
                    $numericYControls[$script:dragTargetIndex].Value = $imageY
                }
            })

            $pictureBox.Add_MouseUp({
                param($upSender, $upEvent)

                if ($script:isDragging) {
                    $script:isDragging = $false
                    $script:dragTargetIndex = -1
                    $pictureBox.Cursor = [System.Windows.Forms.Cursors]::Default
                }
            })

            # Generate button
            $buttonGenerate = New-Object System.Windows.Forms.Button
            $buttonGenerate.Location = New-Object System.Drawing.Point 10, $buttonY
            $buttonGenerate.Size = New-Object System.Drawing.Size 740, 40
            $buttonGenerate.Text = "Generate Meme"
            $buttonGenerate.Font = New-Object System.Drawing.Font("Arial", 11, [System.Drawing.FontStyle]::Bold)
            $buttonGenerate.Add_Click({
                $script:manualTemplate = $comboTemplate.SelectedItem

                # Collect all text lines, positions, and settings directly to arrays (no ArrayList nonsense)
                $script:manualTextLines = @()
                $script:manualXPositions = @()
                $script:manualYPositions = @()
                $script:manualWrapSettings = @()

                for ($n = 0; $n -lt $textBoxes.Count; $n++) {
                    $script:manualTextLines += [string]$textBoxes[$n].Text
                    $script:manualXPositions += [int]$numericXControls[$n].Value
                    $script:manualYPositions += [int]$numericYControls[$n].Value
                    $script:manualWrapSettings += [bool]$checkboxWrapControls[$n].Checked
                }

                $form.DialogResult = [System.Windows.Forms.DialogResult]::OK
                $form.Close()
            })
            $form.Controls.Add($buttonGenerate)

            # Show form
            $result = $form.ShowDialog()

            # Cleanup
            if ($pictureBox.Image) {
                $pictureBox.Image.Dispose()
            }
            $form.Dispose()

            if ($result -eq [System.Windows.Forms.DialogResult]::OK) {
                $template = $script:manualTemplate

                # Use script-level arrays directly (no conversion nonsense that breaks everything)
                # Check if we have any non-blank text (filter out empties)
                $hasNonBlankText = $false
                for ($idx = 0; $idx -lt $script:manualTextLines.Count; $idx++) {
                    if (-not [string]::IsNullOrWhiteSpace($script:manualTextLines[$idx])) {
                        $hasNonBlankText = $true
                        break
                    }
                }

                # Set flags for later use
                $useMultiLineMode = $hasNonBlankText

                # Backward compatibility: set topText/bottomText from first 2 lines for classic fallback
                if ($script:manualTextLines.Count -ge 1 -and -not [string]::IsNullOrWhiteSpace($script:manualTextLines[0])) {
                    $topText = $script:manualTextLines[0]
                    $topTextY = $script:manualYPositions[0]
                }
                if ($script:manualTextLines.Count -ge 2 -and -not [string]::IsNullOrWhiteSpace($script:manualTextLines[1])) {
                    $bottomText = $script:manualTextLines[1]
                    $bottomTextY = $script:manualYPositions[1]
                }
            }
            else {
                Write-Host "Meme generation cancelled (skill issue)" -ForegroundColor Yellow
                return
            }
        }

        # Validate template
        if ([string]::IsNullOrWhiteSpace($template)) {
            Write-Error "No template specified. Use -template or -manual (bruh)"
            Write-Host "Available templates:" -ForegroundColor Cyan
            $availableTemplates | ForEach-Object { Write-Host " - $($_.BaseName)" -ForegroundColor Gray }
            return
        }

        # Find the template file
        $templateFile = $availableTemplates | Where-Object { $_.BaseName -eq $template } | Select-Object -First 1

        if (-not $templateFile) {
            Write-Error "Template '$template' not found (L + ratio)"
            Write-Host "Available templates:" -ForegroundColor Cyan
            $availableTemplates | ForEach-Object { Write-Host " - $($_.BaseName)" -ForegroundColor Gray }
            return
        }

        # Load image
        try {
            $image = [System.Drawing.Image]::FromFile($templateFile.FullName)
            $bitmap = New-Object System.Drawing.Bitmap($image.Width, $image.Height)
            $graphics = [System.Drawing.Graphics]::FromImage($bitmap)
            $graphics.DrawImage($image, 0, 0, $image.Width, $image.Height)

            # Set up text rendering (MAXIMUM QUALITY MODE - no more grimy text)
            $graphics.TextRenderingHint = [System.Drawing.Text.TextRenderingHint]::AntiAliasGridFit
            $graphics.SmoothingMode = [System.Drawing.Drawing2D.SmoothingMode]::AntiAlias
            $graphics.CompositingQuality = [System.Drawing.Drawing2D.CompositingQuality]::HighQuality
            $graphics.InterpolationMode = [System.Drawing.Drawing2D.InterpolationMode]::HighQualityBicubic

            # Font setup (Impact-style, bold and thicc)
            $fontSize = [Math]::Max(24, $image.Width / 15)
            $font = New-Object System.Drawing.Font("Impact", $fontSize, [System.Drawing.FontStyle]::Bold)
            $brushWhite = [System.Drawing.Brushes]::White
            $penBlack = New-Object System.Drawing.Pen([System.Drawing.Color]::Black, ($fontSize / 12))
            $penBlack.LineJoin = [System.Drawing.Drawing2D.LineJoin]::Round  # Smooth corners, no black streaks

            # Helper function to draw outlined text with wrapping support (the sauce with extra sauce)
            $drawOutlinedText = {
                param($text, $x, $y, $alignment, $enableWrap)

                if ([string]::IsNullOrWhiteSpace($text)) { return }

                $formattedText = switch ($textCase) {
                    'Lower'    { $text.ToLower() }
                    'Original' { $text }
                    default    { $text.ToUpper() }
                }

                # Check if wrapping is needed
                $textWidth = $graphics.MeasureString($formattedText, $font).Width
                $maxWidth = $image.Width * 0.9  # Leave 10% margin

                if ($enableWrap -and $textWidth -gt $maxWidth) {
                    # Split text into words and wrap (wrapping goes hard fr)
                    $words = $formattedText -split ' '
                    $lines = @()
                    $currentLine = ""

                    foreach ($word in $words) {
                        $testLine = if ($currentLine) { "$currentLine $word" } else { $word }
                        $testWidth = $graphics.MeasureString($testLine, $font).Width

                        if ($testWidth -gt $maxWidth -and $currentLine) {
                            $lines += $currentLine
                            $currentLine = $word
                        } else {
                            $currentLine = $testLine
                        }
                    }
                    if ($currentLine) { $lines += $currentLine }

                    # Draw each line with offset (multi-line rendering)
                    $lineHeight = $fontSize * 1.2
                    $startY = $y - (($lines.Count - 1) * $lineHeight / 2)

                    for ($lineIdx = 0; $lineIdx -lt $lines.Count; $lineIdx++) {
                        $lineY = $startY + ($lineIdx * $lineHeight)

                        $format = New-Object System.Drawing.StringFormat
                        $format.Alignment = $alignment
                        $format.LineAlignment = [System.Drawing.StringAlignment]::Center

                        $path = New-Object System.Drawing.Drawing2D.GraphicsPath
                        $path.AddString(
                            $lines[$lineIdx],
                            $font.FontFamily,
                            [int]$font.Style,
                            $fontSize,
                            (New-Object System.Drawing.PointF($x, $lineY)),
                            $format
                        )

                        $graphics.DrawPath($penBlack, $path)
                        $graphics.FillPath($brushWhite, $path)

                        $path.Dispose()
                        $format.Dispose()
                    }
                }
                else {
                    # Single line rendering (classic mode)
                    $format = New-Object System.Drawing.StringFormat
                    $format.Alignment = $alignment
                    $format.LineAlignment = [System.Drawing.StringAlignment]::Center

                    $path = New-Object System.Drawing.Drawing2D.GraphicsPath
                    $path.AddString(
                        $formattedText,
                        $font.FontFamily,
                        [int]$font.Style,
                        $fontSize,
                        (New-Object System.Drawing.PointF($x, $y)),
                        $format
                    )

                    # Draw black outline (thicc)
                    $graphics.DrawPath($penBlack, $path)
                    # Draw white fill
                    $graphics.FillPath($brushWhite, $path)

                    $path.Dispose()
                    $format.Dispose()
                }
            }

            # Draw text lines (multi-line support with X positioning and wrapping fr fr)
            if ($useMultiLineMode -and $script:manualTextLines -and $script:manualTextLines.Count -gt 0) {
                # Manual mode with multiple text lines (using script arrays directly, no conversion bs)
                for ($p = 0; $p -lt $script:manualTextLines.Count; $p++) {
                    $currentText = $script:manualTextLines[$p]
                    if (-not [string]::IsNullOrWhiteSpace($currentText)) {
                        $textX = $script:manualXPositions[$p]
                        $textY = $script:manualYPositions[$p]
                        $wrapEnabled = $script:manualWrapSettings[$p]

                        # Determine X position and alignment
                        $useCenter = ($textX -eq -1)
                        $finalX = if ($useCenter) { $image.Width / 2 } else { $textX }
                        $alignment = if ($useCenter) { [System.Drawing.StringAlignment]::Center } else { [System.Drawing.StringAlignment]::Near }

                        & $drawOutlinedText $currentText $finalX $textY $alignment $wrapEnabled
                    }
                }
            }
            else {
                # Classic mode with just top/bottom text (backward compatibility with wrapping enabled)
                if (-not [string]::IsNullOrWhiteSpace($topText)) {
                    # Use custom Y position if provided (from manual mode), otherwise default
                    # NOTE: use $null -ne check, not if ($topTextY), because 0 is falsy fr fr
                    if ($null -ne $topTextY) {
                        $topY = $topTextY
                    }
                    else {
                        $topY = $fontSize
                    }
                    & $drawOutlinedText $topText ($image.Width / 2) $topY ([System.Drawing.StringAlignment]::Center) $true
                }

                if (-not [string]::IsNullOrWhiteSpace($bottomText)) {
                    # Use custom Y position if provided (from manual mode), otherwise default
                    # NOTE: use $null -ne check, not if ($bottomTextY), because 0 is falsy fr fr
                    if ($null -ne $bottomTextY) {
                        $bottomY = $bottomTextY
                    }
                    else {
                        $bottomY = $image.Height - ($fontSize * 1.5)
                    }
                    & $drawOutlinedText $bottomText ($image.Width / 2) $bottomY ([System.Drawing.StringAlignment]::Center) $true
                }
            }

            # Save the masterpiece
            $bitmap.Save($outputFile, [System.Drawing.Imaging.ImageFormat]::Png)

            # Cleanup (no memory leaks in this house)
            $graphics.Dispose()
            $bitmap.Dispose()
            $image.Dispose()
            $font.Dispose()
            $penBlack.Dispose()

        }
        catch {
            Write-Error "Meme generation failed: $($_.Exception.Message) (catch these hands)"
            return
        }
    }
    end {
        if (-not $script:newMemeInitOk) { return }
        if (Test-Path $outputFile) {
            # Copy to clipboard (based)
            if (-not $noClipboard) {
                try {
                    $image = [System.Drawing.Image]::FromFile($outputFile)
                    [System.Windows.Forms.Clipboard]::SetImage($image)
                    $image.Dispose()
                    Write-Host "Meme created and " -NoNewline
                    Write-Host "mogged to clipboard" -ForegroundColor Green -NoNewline
                    Write-Host " 🔥"
                }
                catch {
                    Write-Warning "Clipboard copy failed (skill issue): $($_.Exception.Message)"
                }
            }

            Write-Host "Saved to: " -NoNewline -ForegroundColor Cyan
            Write-Host $outputFile -ForegroundColor Gray

            # Open the meme (optional flex)
            try {
                Start-Process $outputFile
            }
            catch {
                # lmao whatever
            }
        }
        else {
            Write-Error "Output file not created (something broke but we ship anyway)"
        }
    }
}
function New-YoloCommit {
    <#
        .SYNOPSIS
            Generates unhinged but technically valid commit messages
        .DESCRIPTION
            Just send a commit message, bro.
        .PARAMETER type
            Commit type: feat, fix, chore, refactor, docs, style, test, perf, ci, build
        .PARAMETER module
            Module name for monorepo scope (default: trymsae.memeshell)
        .PARAMETER major
            major in ur mom.
        .PARAMETER noClipboard
            Just for display.
        .EXAMPLE
            PS > New-YoloCommit -type feat
            feat(trymsae.memeshell): yeet the deprecated code into the sun | mogged to clipboard
        .EXAMPLE
            PS > yolo fix
            fix(trymsae.memeshell): turns out the bug was a feature all along | mogged to clipboard
        .EXAMPLE
            PS > yolo feat -major
            feat(trymsae.memeshell)!: this changes everything
        .EXAMPLE
            PS > yolo feat -module "ur.mom"
            feat(ur.mom): built different fr fr | mogged to clipboard
    #>

    [alias('yolo')]
    [CmdletBinding()]
    param (
        [parameter(Position = 0, Mandatory = $true)]
        [ValidateSet('feat', 'fix', 'chore', 'refactor', 'docs', 'style', 'test', 'perf', 'ci', 'build')]
        [string]$type,
        [parameter(Position = 1, Mandatory = $false)]
        [string]$module = "trymsae.memeshell",
        [parameter(Position = 2, Mandatory = $false)]
        [switch]$major,
        [parameter(Position = 3, Mandatory = $false)]
        [switch]$noClipboard
    )
    begin {
        # get the sauce
        $messagesFile = Join-Path $PSScriptRoot "templates\texts\commit-messages.b64"
        if (Test-Path $messagesFile) {
            try {
                $base64Content = Get-Content $messagesFile -Raw
                $bytes = [Convert]::FromBase64String($base64Content.Trim())
                $decodedText = [System.Text.Encoding]::UTF8.GetString($bytes)
                $commitMessages = $decodedText -split "`r?`n" | Where-Object { $_.Trim() -ne "" }
            }
            catch {
                # catch these hands
                $commitMessages = @("something broke but we ship anyway")
            }
        }
        else {
            # if shit breaks, lmao.
            $commitMessages = @("yolo commit message generator broke lmao")
        }
    }
    process {
        # yeet a random message
        $randomMessage = $commitMessages | Get-Random
        # handle majors
        if ($major) {
            # breaking change vibes
            $output = "$($type)($($module))!: $($randomMessage)"
        }
        else {
            # what else?
            $output = "$($type)($($module)): $($randomMessage)"
        }
    }
    end {
        if ( -not $noClipboard ) {
            Set-Clipboard -Value $output
            Write-Host "$($output) | " -NoNewline
            Write-Host "mogged to clipboard" -ForegroundColor Green
        }
        else {
            Write-Host $output
        }
    }
}
function Start-MatrixRain {
    <#
        .SYNOPSIS
            Full-screen Matrix rain screensaver. Ctrl+C to exit.
        .DESCRIPTION
            Renders falling katakana characters in your terminal. Runs until you press Ctrl+C.
            Saves and restores all console state on exit (no escape from the sauce tho).
        .PARAMETER speed
            Rain speed: Slow, Normal (default), or Fast
        .PARAMETER color
            Rain color: Green (default), Cyan, or Red
        .EXAMPLE
            PS > Start-MatrixRain
            *green rain descends*
        .EXAMPLE
            PS > matrix -speed Fast -color Red
            *red chaos ensues*
    #>

    [alias('matrix')]
    [CmdletBinding()]
    param (
        [parameter(Position = 0, Mandatory = $false)]
        [ValidateSet('Slow', 'Normal', 'Fast')]
        [string]$speed = 'Normal',
        [parameter(Position = 1, Mandatory = $false)]
        [ValidateSet('Green', 'Cyan', 'Red')]
        [string]$color = 'Green'
    )

    # Color layers: head (bright), trail (normal), fade (dim)
    $colorMap = @{
        Green = @{ head = [ConsoleColor]::White; trail = [ConsoleColor]::Green;   fade = [ConsoleColor]::DarkGreen }
        Cyan  = @{ head = [ConsoleColor]::White; trail = [ConsoleColor]::Cyan;    fade = [ConsoleColor]::DarkCyan  }
        Red   = @{ head = [ConsoleColor]::White; trail = [ConsoleColor]::Red;     fade = [ConsoleColor]::DarkRed   }
    }
    $colors = $colorMap[$color]

    $tickMs = switch ($speed) {
        'Slow' { 80 }
        'Fast' { 20 }
        default { 40 }
    }

    # Half-width katakana (0xFF66-0xFF9F) + digits for that authentic look
    $charPool = [char[]](0xFF66..0xFF9F) + [char[]](0x30..0x39)

    # Save console state so we can restore it on exit (no memory leaks in this house)
    $originalCursorVisible  = [Console]::CursorVisible
    $originalForeground     = [Console]::ForegroundColor
    $originalBackground     = [Console]::BackgroundColor
    $originalTreatCtrlC     = [Console]::TreatControlCAsInput
    $originalOutputEncoding = [Console]::OutputEncoding
    [Console]::TreatControlCAsInput = $true
    [Console]::OutputEncoding = [System.Text.Encoding]::UTF8

    $initColumns = {
        param($w, $h)
        if ($w -le 0 -or $h -le 0) { return @() }
        0..($w - 1) | ForEach-Object {
            @{
                x           = $_
                headY       = Get-Random -Minimum (-$h) -Maximum 0
                trailLength = Get-Random -Minimum 5 -Maximum 22
                speedDiv    = Get-Random -Minimum 1 -Maximum 4  # per-column speed variation
                tickCount   = 0
                chars       = @{}  # y -> char drawn at that position
            }
        }
    }

    try {
        [Console]::CursorVisible  = $false
        [Console]::BackgroundColor = [ConsoleColor]::Black
        [Console]::Clear()

        $width   = [Console]::WindowWidth
        $height  = [Console]::WindowHeight
        $columns = & $initColumns $width $height

        while ($true) {
            # Ctrl+C check (TreatControlCAsInput means it won't kill the process directly)
            if ([Console]::KeyAvailable) {
                $key = [Console]::ReadKey($true)
                if ($key.Key -eq [ConsoleKey]::C -and ($key.Modifiers -band [ConsoleModifiers]::Control)) { break }
            }

            # Handle terminal resize gracefully
            $newW = [Console]::WindowWidth
            $newH = [Console]::WindowHeight
            if ($newW -ne $width -or $newH -ne $height) {
                $width  = $newW
                $height = $newH
                [Console]::Clear()
                $columns = & $initColumns $width $height
            }

            foreach ($col in $columns) {
                try {
                    $col.tickCount++
                    if ($col.tickCount -lt $col.speedDiv) { continue }
                    $col.tickCount = 0

                    $x     = $col.x
                    $headY = $col.headY

                    # Erase the position behind the tail end
                    $eraseY = $headY - $col.trailLength - 1
                    if ($eraseY -ge 0 -and $eraseY -lt $height) {
                        [Console]::SetCursorPosition($x, $eraseY)
                        [Console]::ForegroundColor = [ConsoleColor]::Black
                        [Console]::Write(' ')
                        $col.chars.Remove($eraseY)
                    }

                    # Dim the tail end (dark color)
                    $fadeY = $headY - $col.trailLength
                    if ($fadeY -ge 0 -and $fadeY -lt $height) {
                        if (-not $col.chars.ContainsKey($fadeY)) { $col.chars[$fadeY] = $charPool[(Get-Random -Maximum $charPool.Count)] }
                        [Console]::SetCursorPosition($x, $fadeY)
                        [Console]::ForegroundColor = $colors.fade
                        [Console]::Write($col.chars[$fadeY])
                    }

                    # Trail (normal color, one step behind head)
                    $trailY = $headY - 1
                    if ($trailY -ge 0 -and $trailY -lt $height) {
                        if (-not $col.chars.ContainsKey($trailY)) { $col.chars[$trailY] = $charPool[(Get-Random -Maximum $charPool.Count)] }
                        [Console]::SetCursorPosition($x, $trailY)
                        [Console]::ForegroundColor = $colors.trail
                        [Console]::Write($col.chars[$trailY])
                    }

                    # Head (bright color, randomly changing char)
                    if ($headY -ge 0 -and $headY -lt $height) {
                        $headChar = $charPool[(Get-Random -Maximum $charPool.Count)]
                        $col.chars[$headY] = $headChar
                        [Console]::SetCursorPosition($x, $headY)
                        [Console]::ForegroundColor = $colors.head
                        [Console]::Write($headChar)
                    }

                    $col.headY++

                    # Reset column once tail has fully scrolled off bottom
                    if (($col.headY - $col.trailLength - 1) -ge $height) {
                        $col.headY       = Get-Random -Minimum ([int](-$height / 2)) -Maximum 0
                        $col.trailLength = Get-Random -Minimum 5 -Maximum 22
                        $col.speedDiv    = Get-Random -Minimum 1 -Maximum 4
                        $col.chars       = @{}
                    }
                } catch [System.ArgumentOutOfRangeException] {
                    continue
                }
            }

            Start-Sleep -Milliseconds $tickMs
        }
    }
    finally {
        # Restore all state no matter what (try/finally goes hard)
        [Console]::TreatControlCAsInput = $originalTreatCtrlC
        [Console]::CursorVisible        = $originalCursorVisible
        [Console]::ForegroundColor      = $originalForeground
        [Console]::BackgroundColor      = $originalBackground
        [Console]::OutputEncoding       = $originalOutputEncoding
        [Console]::Clear()
        [Console]::SetCursorPosition(0, 0)
    }
}