<#
.SYNOPSIS
  Generate the punktfunk host installer branding assets (wizard BMPs + setup .ico).

.DESCRIPTION
  Renders the punktfunk brand mark - the two overlapping circles ("lens") from
  web/src/components/brand-mark.tsx (the canonical flattened geometry, shared with the Apple icon,
  the marketing site and the docs) - into the assets Inno Setup consumes:

    wizard-image-*.bmp   welcome/finish page side panel (164x314 base, 100..200% DPI variants);
                         dark violet gradient panel + the mark + the lowercase wordmark. The panel
                         is self-contained dark, so it reads correctly in BOTH the light and dark
                         (WizardStyle=dynamic) wizard appearances.
    wizard-small-*.bmp   header tile on the inner pages (55x55 base, 100..200% DPI variants);
                         the square brand tile (mark on #1C1530), matching the MSIX client tile.
    punktfunk.ico        multi-size icon (16..256, PNG-compressed entries - Vista+ format, we
                         require Windows 10) for SetupIconFile + the Apps & Features entry.

  Outputs are COMMITTED next to this script (like include/punktfunk_core.h, generated-but-checked-in);
  re-run only when the brand changes. Everything is drawn 4x supersampled and downscaled
  (System.Drawing regions/clips do not antialias), so edges stay clean at every size.

.EXAMPLE
  pwsh -File packaging/windows/branding/gen-branding.ps1
#>
[CmdletBinding()]
param([string]$OutDir = $PSScriptRoot)
$ErrorActionPreference = 'Stop'
Add-Type -AssemblyName System.Drawing

# --- brand constants (colors from brand-mark.tsx; tile background from the MSIX assets) -------
$colLight = [System.Drawing.Color]::FromArgb(255, 0xA7, 0x9F, 0xF8)   # large circle
$colDeep = [System.Drawing.Color]::FromArgb(255, 0x6C, 0x5B, 0xF3)   # small circle
$colHi = [System.Drawing.Color]::FromArgb(255, 0xD2, 0xC9, 0xFB)   # lens overlap highlight
$colTile = [System.Drawing.Color]::FromArgb(255, 0x1C, 0x15, 0x30)   # brand tile background
$colPanelTop = [System.Drawing.Color]::FromArgb(255, 0x27, 0x1E, 0x46)   # wizard panel gradient
$colPanelBot = [System.Drawing.Color]::FromArgb(255, 0x11, 0x0D, 0x1F)
$colText = [System.Drawing.Color]::FromArgb(255, 0xEA, 0xE6, 0xFB)   # wordmark on the panel

# Mark geometry in the 1000-unit viewbox of brand-mark.tsx: two r=194.41 circles at (403.04,597.26)
# (light, behind) and (597.81,402.85) (deep, in front), their intersection filled as the highlight.
$R = 194.41
$c1x = 403.037; $c1y = 597.262
$c2x = 597.8075; $c2y = 402.8525
# Mark bounding box -> center/span, so callers can place it by center + size.
$bbMinX = $c1x - $R; $bbMaxX = $c2x + $R
$bbMinY = $c2y - $R; $bbMaxY = $c1y + $R
$markCx = ($bbMinX + $bbMaxX) / 2.0
$markCy = ($bbMinY + $bbMaxY) / 2.0
$markSpan = $bbMaxX - $bbMinX   # == $bbMaxY - $bbMinY (the bbox is square)

# Draw the mark onto $g centered at ($cx,$cy) with bounding-box size $size (device pixels).
function Draw-Mark([System.Drawing.Graphics]$g, [double]$cx, [double]$cy, [double]$size) {
    $s = $size / $markSpan
    function ellRect([double]$ecx, [double]$ecy) {
        $r = $R * $s
        [System.Drawing.RectangleF]::new(
            [float]($cx + ($ecx - $markCx) * $s - $r), [float]($cy + ($ecy - $markCy) * $s - $r),
            [float](2 * $r), [float](2 * $r))
    }
    $r1 = ellRect $c1x $c1y
    $r2 = ellRect $c2x $c2y
    $b = New-Object System.Drawing.SolidBrush($colLight)
    $g.FillEllipse($b, $r1); $b.Dispose()
    $b = New-Object System.Drawing.SolidBrush($colDeep)
    $g.FillEllipse($b, $r2); $b.Dispose()
    # Highlight = intersection: clip to circle 1, fill circle 2. The clip edge is not antialiased,
    # but every caller renders 4x supersampled and downscales, which smooths it.
    $p1 = New-Object System.Drawing.Drawing2D.GraphicsPath
    $p1.AddEllipse($r1)
    $g.SetClip($p1)
    $b = New-Object System.Drawing.SolidBrush($colHi)
    $g.FillEllipse($b, $r2); $b.Dispose()
    $g.ResetClip(); $p1.Dispose()
}

# New 32bpp canvas + antialiased Graphics.
function New-Canvas([int]$w, [int]$h) {
    $bmp = New-Object System.Drawing.Bitmap($w, $h, [System.Drawing.Imaging.PixelFormat]::Format32bppArgb)
    $g = [System.Drawing.Graphics]::FromImage($bmp)
    $g.SmoothingMode = [System.Drawing.Drawing2D.SmoothingMode]::AntiAlias
    $g.TextRenderingHint = [System.Drawing.Text.TextRenderingHint]::AntiAlias
    @($bmp, $g)
}

# Downscale $src to $w x $h (high-quality bicubic) - the supersample resolve.
function Resize-Bitmap([System.Drawing.Bitmap]$src, [int]$w, [int]$h) {
    $dst = New-Object System.Drawing.Bitmap($w, $h, [System.Drawing.Imaging.PixelFormat]::Format32bppArgb)
    $g = [System.Drawing.Graphics]::FromImage($dst)
    $g.InterpolationMode = [System.Drawing.Drawing2D.InterpolationMode]::HighQualityBicubic
    $g.PixelOffsetMode = [System.Drawing.Drawing2D.PixelOffsetMode]::HighQuality
    $g.DrawImage($src, (New-Object System.Drawing.Rectangle(0, 0, $w, $h)),
        0, 0, $src.Width, $src.Height, [System.Drawing.GraphicsUnit]::Pixel)
    $g.Dispose()
    $dst
}

# Save as 24bpp BMP (opaque - what Inno's wizard image loader expects by default).
function Save-Bmp24([System.Drawing.Bitmap]$bmp, [string]$path) {
    $b24 = $bmp.Clone((New-Object System.Drawing.Rectangle(0, 0, $bmp.Width, $bmp.Height)),
        [System.Drawing.Imaging.PixelFormat]::Format24bppRgb)
    $b24.Save($path, [System.Drawing.Imaging.ImageFormat]::Bmp)
    $b24.Dispose()
    Write-Host "  wrote $path ($($bmp.Width)x$($bmp.Height))"
}

$SS = 4   # supersample factor

# --- wizard side panel (welcome/finish page): gradient + mark + wordmark ----------------------
# Base size 164x314 (Inno's classic canvas); DPI variants via the wizard-image-*.bmp wildcard.
foreach ($pct in 100, 125, 150, 175, 200) {
    $w = [int][Math]::Round(164 * $pct / 100.0)
    $h = [int][Math]::Round(314 * $pct / 100.0)
    $bmp, $g = New-Canvas ($w * $SS) ($h * $SS)
    $rect = New-Object System.Drawing.Rectangle(0, 0, ($w * $SS), ($h * $SS))
    $grad = New-Object System.Drawing.Drawing2D.LinearGradientBrush($rect, $colPanelTop, $colPanelBot, 90.0)
    $g.FillRectangle($grad, $rect); $grad.Dispose()
    # Mark: 58% of the panel width, centered horizontally, optical center at ~40% height.
    Draw-Mark $g ($w * $SS / 2.0) ($h * $SS * 0.40) ($w * $SS * 0.58)
    # Wordmark: lowercase brand name under the mark.
    $font = New-Object System.Drawing.Font('Segoe UI Semibold', [float](13.0 * $SS * $pct / 100.0), [System.Drawing.FontStyle]::Regular, [System.Drawing.GraphicsUnit]::Pixel)
    $tb = New-Object System.Drawing.SolidBrush($colText)
    $fmt = New-Object System.Drawing.StringFormat
    $fmt.Alignment = [System.Drawing.StringAlignment]::Center
    $g.DrawString('punktfunk', $font, $tb,
        (New-Object System.Drawing.PointF([float]($w * $SS / 2.0), [float]($h * $SS * 0.60))), $fmt)
    $fmt.Dispose(); $tb.Dispose(); $font.Dispose(); $g.Dispose()
    $out = Resize-Bitmap $bmp $w $h
    $bmp.Dispose()
    Save-Bmp24 $out (Join-Path $OutDir ("wizard-image-{0}.bmp" -f $pct))
    $out.Dispose()
}

# --- wizard header tile (inner pages): the square brand tile --------------------------------
# Base size 55x55; DPI variants via the wizard-small-*.bmp wildcard. Opaque square (BMP has no
# alpha here): the same full-bleed dark tile as the client's MSIX logo assets.
foreach ($pct in 100, 125, 150, 175, 200) {
    $sz = [int][Math]::Round(55 * $pct / 100.0)
    $bmp, $g = New-Canvas ($sz * $SS) ($sz * $SS)
    $b = New-Object System.Drawing.SolidBrush($colTile)
    $g.FillRectangle($b, 0, 0, ($sz * $SS), ($sz * $SS)); $b.Dispose()
    Draw-Mark $g ($sz * $SS / 2.0) ($sz * $SS / 2.0) ($sz * $SS * 0.74)
    $g.Dispose()
    $out = Resize-Bitmap $bmp $sz $sz
    $bmp.Dispose()
    Save-Bmp24 $out (Join-Path $OutDir ("wizard-small-{0}.bmp" -f $pct))
    $out.Dispose()
}

# --- punktfunk.ico: rounded brand tile at 16..256 --------------------------------------------
# Small sizes are classic 32bpp DIB entries (Inno's SetupIconFile resource updater and older shell
# consumers reject an all-PNG icon); only 128/256 use PNG compression (the standard Vista+ layout).
function New-IconTile([int]$sz) {
    $bmp, $g = New-Canvas ($sz * $SS) ($sz * $SS)
    # Rounded-rect tile (22% corner radius - the Windows 11 app-icon look).
    $S = $sz * $SS; $rad = [int]($S * 0.22)
    $path = New-Object System.Drawing.Drawing2D.GraphicsPath
    $path.AddArc(0, 0, 2 * $rad, 2 * $rad, 180, 90)
    $path.AddArc($S - 2 * $rad, 0, 2 * $rad, 2 * $rad, 270, 90)
    $path.AddArc($S - 2 * $rad, $S - 2 * $rad, 2 * $rad, 2 * $rad, 0, 90)
    $path.AddArc(0, $S - 2 * $rad, 2 * $rad, 2 * $rad, 90, 90)
    $path.CloseFigure()
    $b = New-Object System.Drawing.SolidBrush($colTile)
    $g.FillPath($b, $path); $b.Dispose(); $path.Dispose()
    Draw-Mark $g ($S / 2.0) ($S / 2.0) ($S * 0.74)
    $g.Dispose()
    $out = Resize-Bitmap $bmp $sz $sz
    $bmp.Dispose()
    $out
}

# PNG-compressed entry payload (used for the 128/256 entries). The leading comma keeps the byte[]
# a single pipeline object (PowerShell would otherwise unroll it into individual bytes).
function ConvertTo-IconPng([System.Drawing.Bitmap]$tile) {
    $ms = New-Object System.IO.MemoryStream
    $tile.Save($ms, [System.Drawing.Imaging.ImageFormat]::Png)
    return , $ms.ToArray()
}

# Classic ICO DIB entry payload: BITMAPINFOHEADER (height doubled) + bottom-up 32bpp BGRA XOR data
# + an all-zero 1bpp AND mask (32bpp icons carry transparency in the alpha channel).
function ConvertTo-IconDib([System.Drawing.Bitmap]$tile) {
    $s = $tile.Width
    $rect = New-Object System.Drawing.Rectangle(0, 0, $s, $s)
    $data = $tile.LockBits($rect, [System.Drawing.Imaging.ImageLockMode]::ReadOnly,
        [System.Drawing.Imaging.PixelFormat]::Format32bppArgb)
    $px = New-Object byte[] ($data.Stride * $s)
    [System.Runtime.InteropServices.Marshal]::Copy($data.Scan0, $px, 0, $px.Length)
    $tile.UnlockBits($data)
    $maskStride = [int][Math]::Ceiling($s / 32.0) * 4     # 1bpp rows padded to 32 bits
    $ms = New-Object System.IO.MemoryStream
    $w = New-Object System.IO.BinaryWriter($ms)
    $w.Write([uint32]40); $w.Write([int]$s); $w.Write([int]($s * 2))   # biSize, biWidth, biHeight (XOR+AND)
    $w.Write([uint16]1); $w.Write([uint16]32); $w.Write([uint32]0)     # biPlanes, biBitCount, BI_RGB
    $w.Write([uint32]($s * $s * 4 + $maskStride * $s))                 # biSizeImage
    $w.Write([int]0); $w.Write([int]0); $w.Write([uint32]0); $w.Write([uint32]0)
    for ($y = $s - 1; $y -ge 0; $y--) { $w.Write($px, $y * $data.Stride, $s * 4) }   # XOR, bottom-up
    $w.Write((New-Object byte[] ($maskStride * $s)))                   # AND mask: all opaque
    $w.Flush()
    $bytes = $ms.ToArray()
    $w.Dispose(); $ms.Dispose()
    return , $bytes   # leading comma: emit the byte[] as ONE object, not unrolled bytes
}

$icoSizes = 16, 20, 24, 32, 40, 48, 64, 128, 256
$pngs = @(foreach ($s in $icoSizes) {
    $tile = New-IconTile $s
    if ($s -ge 128) { ConvertTo-IconPng $tile } else { ConvertTo-IconDib $tile }
    $tile.Dispose()
})
$ico = New-Object System.IO.MemoryStream
$bw = New-Object System.IO.BinaryWriter($ico)
# ICONDIR
$bw.Write([uint16]0); $bw.Write([uint16]1); $bw.Write([uint16]$icoSizes.Count)
# ICONDIRENTRYs (width/height byte 0 means 256)
$offset = 6 + 16 * $icoSizes.Count
for ($i = 0; $i -lt $icoSizes.Count; $i++) {
    $s = $icoSizes[$i]
    $dim = if ($s -ge 256) { 0 } else { $s }
    $bw.Write([byte]$dim); $bw.Write([byte]$dim)          # width, height
    $bw.Write([byte]0); $bw.Write([byte]0)                # colors, reserved
    $bw.Write([uint16]1); $bw.Write([uint16]32)           # planes, bitcount
    $bw.Write([uint32]$pngs[$i].Length); $bw.Write([uint32]$offset)
    $offset += $pngs[$i].Length
}
foreach ($p in $pngs) { $bw.Write([byte[]]$p) }
$bw.Flush()
$icoPath = Join-Path $OutDir 'punktfunk.ico'
[IO.File]::WriteAllBytes($icoPath, $ico.ToArray())
$bw.Dispose(); $ico.Dispose()
# Self-check: a malformed container here would only surface later as ISCC "Icon file is invalid".
$probe = New-Object System.Drawing.Icon($icoPath)
$probe.Dispose()
Write-Host "  wrote $icoPath ($($icoSizes -join ',')) - verified loadable"
Write-Host "==> branding assets generated in $OutDir"
