<#
================================================================================
  Build-ApplyScript.ps1  - generator for the distributable Apply script
  (Dev tool. Console messages here are Korean for the maintainer; everything it
   EMITS into Apply-EllesmereUI-Locale.ps1 and the locale files is English.)

  Input : Locale-Data.ps1  (localization definition = single source of truth)
  Output: Apply-EllesmereUI-Locale.ps1  (run by the addon author on a clean copy)

  Usage:  .\Build-ApplyScript.ps1
================================================================================
#>
param(
    [string]$DataFile = "E:\WOW_KR\_EUI_Localize\Locale-Data.ps1",
    [string]$OutFile  = "E:\WOW_KR\_EUI_Localize\Apply-EllesmereUI-Locale.ps1"
)
$ErrorActionPreference = "Stop"
$data    = & $DataFile
$WRAPPED = $data.WRAPPED
$TR      = $data.TR
$LMAP    = $data.LOCALE_MAP
$CTX     = $data.CTX; if (-not $CTX) { $CTX = @{} }   # per-module context overrides (e.g. Armor=방어구 in bags, 방어도 on char sheet)
$rx      = [regex]'EUI_L\["((?:[^"\\]|\\.)*)"\]'

# NOTE: keys/values here are ALREADY in Lua source-escaped form, exactly as they appear
# inside EllesmereUI.lua's  EUI_L["..."]  (e.g. \n is backslash+n, \" is backslash+quote).
# So we must NOT re-escape anything — re-escaping a " into \" turns an existing \" into \\"
# (this caused ruRU.lua parse errors). Output the string verbatim.
function LuaEsc($s) { return $s }

# from(trim) 역산 + file:line
#  EUI_L["str"]  -> "str"   (따옴표 키)
#  EUI_L[var]    -> var      (변수 인덱싱, 예: 탭 라벨 EUI_L[name])
$rxVar = [regex]'EUI_L\[([A-Za-z_][A-Za-z0-9_.]*)\]'
$entries = foreach ($w in $WRAPPED) {
    $toTrim   = $w.to.Trim()
    if (($w -is [hashtable]) -and $w.ContainsKey('from') -and $w.from) {
        # 명시적 from (to 역산으로 원본을 재구성 못하는 경우: 문자열 분할 래핑 등)
        $fromTrim = $w.from.Trim()
    } else {
        $fromTrim = $rx.Replace($toTrim, '"$1"')
        $fromTrim = $rxVar.Replace($fromTrim, '$1')
    }
    [pscustomobject]@{ File=$w.f; Line=$w.line; From=$fromTrim; To=$toTrim }
}

# key 등장 순서 + 모든 출처(file:line) 수집
$orderedKeys = New-Object System.Collections.Generic.List[string]
$keySrcAll = New-Object 'System.Collections.Generic.Dictionary[string,object]' ([System.StringComparer]::Ordinal)   # key -> List of "file:line" (case-sensitive: General vs GENERAL 구분)
foreach ($w in $WRAPPED) {
    foreach ($m in $rx.Matches($w.to)) {
        $k = $m.Groups[1].Value
        if (-not $keySrcAll.ContainsKey($k)) {
            $keySrcAll[$k] = New-Object System.Collections.Generic.List[string]
            $orderedKeys.Add($k)
        }
        $keySrcAll[$k].Add("$($w.f):$($w.line)")
    }
}
# ★ $TR/$__tr 에만 정의된 키도 포함($WRAPPED 에 EUI_L["키"] 따옴표 형태로 없는 것 —
#   예: 탭/포탈/모듈 desc 처럼 변수 인덱싱 EUI_L[name] 으로 쓰여 위에서 못 모은 키).
#   이 키들도 로케일 파일에 반드시 써야 게임에서 번역됨.
foreach ($k in $TR.Keys) {
    if (-not $keySrcAll.ContainsKey($k)) {
        $keySrcAll[$k] = New-Object System.Collections.Generic.List[string]
        $keySrcAll[$k].Add("(variable-indexed)")
        $orderedKeys.Add($k)
    }
}
# 주석용 문자열: 같은 키가 여러 곳이면 전부 나열
function SrcComment([string]$k){ return ($keySrcAll[$k] -join ", ") }

# ── locale file text (English comments) ──
function BuildLocaleText([string]$locale, [string]$langKey) {
    $isMaster = ($locale -eq "enUS")
    # count untranslated keys for this locale (master has none by definition)
    $missCount = 0
    if (-not $isMaster) {
        foreach ($key in $orderedKeys) {
            if (-not ($TR[$key] -and $TR[$key].ContainsKey($langKey) -and $TR[$key][$langKey])) { $missCount++ }
        }
    }
    $sb = New-Object System.Text.StringBuilder
    $w = { param($x) [void]$sb.AppendLine($x) }
    & $w "--------------------------------------------------------------------------------"
    if ($isMaster) {
        & $w "--  EllesmereUI / Locale/$locale.lua  -  English master (key = value)"
        & $w "--  Reference list of every key; fallback for any locale that omits a key."
    } else {
        & $w "--  EllesmereUI / Locale/$locale.lua  -  $locale"
        & $w "--  Applied only when GetLocale()==`"$locale`". Missing keys fall back to enUS."
        if ($missCount -gt 0) { & $w "--  $missCount string(s) still need translation (marked -- TODO below)." }
    }
    & $w "--  The [file:line] comment marks where the string is wrapped with EUI_L[...]."
    & $w "--------------------------------------------------------------------------------"
    if (-not $isMaster) { & $w "if GetLocale() ~= `"$locale`" then return end" }
    & $w "local L = EUI_L"
    & $w ""
    foreach ($key in $orderedKeys) {
        # context-sensitive key: emit EUI_L_CTX["key"]={["*"]=default,["Module"]=override} instead of L["key"]=...
        # so LocalCore's __index can resolve it per calling module. English source is untouched.
        if ($CTX.Contains($key)) {
            $def = if ($isMaster) { $key } elseif ($TR[$key] -and $TR[$key].ContainsKey($langKey) -and $TR[$key][$langKey]) { $TR[$key][$langKey] } else { $key }
            $parts = "[`"*`"] = `"" + (LuaEsc $def) + "`""
            foreach ($mod in $CTX[$key].Keys) {
                $mv = if ($isMaster) { $key } elseif ($CTX[$key][$mod].ContainsKey($langKey) -and $CTX[$key][$mod][$langKey]) { $CTX[$key][$mod][$langKey] } else { $def }
                $parts += ", [`"" + $mod + "`"] = `"" + (LuaEsc $mv) + "`""
            }
            & $w "-- [$(SrcComment $key)] (context-sensitive: per-module)"
            & $w ("EUI_L_CTX[`"" + (LuaEsc $key) + "`"] = { " + $parts + " }")
            continue
        }
        & $w "-- [$(SrcComment $key)]"
        if ($isMaster) {
            & $w ("L[`"" + (LuaEsc $key) + "`"] = `"" + (LuaEsc $key) + "`"")
        } else {
            $val = $null
            if ($TR[$key] -and $TR[$key].ContainsKey($langKey)) { $val = $TR[$key][$langKey] }
            if ($null -ne $val) {   # 빈 문자열("")도 유효한 번역(분할 래핑 A부분 등) → TODO 처리 안 함
                & $w ("L[`"" + (LuaEsc $key) + "`"] = `"" + (LuaEsc $val) + "`"")
            } else {
                # 미번역: 영문 원문으로 채워둠 (번역자가 이 줄의 값만 자기 언어로 교체하면 됨).
                & $w ("L[`"" + (LuaEsc $key) + "`"] = `"" + (LuaEsc $key) + "`"   -- TODO: translate")
            }
        }
    }
    return $sb.ToString()
}

$localCore = @'
--------------------------------------------------------------------------------
--  EllesmereUI / Locale/LocalCore.lua  -  localization engine (loads first)
--  Unknown keys return the key itself (English) via __index, so SetText(nil)
--  can never happen.
--  Usage:  in code   obj:SetText(EUI_L["English string"])
--          in locale EUI_L["English string"] = "translation"
--
--  Context-sensitive keys: a few English strings need a different translation
--  per module (e.g. "Armor" = the item category in bags vs the defense stat on
--  the character sheet). Locale files register these as
--      EUI_L_CTX["Armor"] = { ["*"] = <default>, ["EllesmereUIBags"] = <override> }
--  and do NOT set L["Armor"] directly, so __index resolves them by checking
--  which addon is calling (debugstack). The English source is never changed and
--  normal keys keep the fast direct-table path -- only context keys (very few)
--  pay the stack lookup.
--------------------------------------------------------------------------------
EUI_L_CTX = EUI_L_CTX or {}
EUI_L = EUI_L or setmetatable({}, { __index = function(_, k)
    local ctx = EUI_L_CTX[k]
    if ctx then
        local stack = debugstack(2, 12, 0)
        if stack then
            for mod, val in pairs(ctx) do
                if mod ~= "*" and stack:find(mod, 1, true) then return val end
            end
        end
        return ctx["*"] or k
    end
    return k
end })
'@

# ── assemble Apply script (all emitted text English) ──
$sb = New-Object System.Text.StringBuilder
function Emit($s) { [void]$sb.AppendLine($s) }

Emit '<#'
Emit '  Apply-EllesmereUI-Locale.ps1   (auto-generated by Build-ApplyScript.ps1)'
Emit '  Injects localization into a clean EllesmereUI install:'
Emit '    1) creates Locale folder + LocalCore.lua + one file per locale'
Emit '    2) inserts the Locale files into EllesmereUI.toc (after EllesmereUI_Lite.lua)'
Emit '    3) wraps the target strings in EllesmereUI.lua with EUI_L[...] (line-exact,'
Emit '       trimmed match; indentation preserved; idempotent; mismatches reported)'
Emit '  Self-contained; safe to run multiple times.'
Emit '  Place this script anywhere under Interface\AddOns (e.g. AddOns\_EUI_Localize)'
Emit '  and just run it; the EllesmereUI folder is located automatically. You can also'
Emit '  pass -AddonPath to point at the EllesmereUI folder explicitly.'
Emit '  Usage:  .\Apply-EllesmereUI-Locale.ps1   [ -AddonPath "...\AddOns\EllesmereUI" ]'
Emit '#>'
Emit 'param([string]$AddonPath = "", [switch]$Force, [switch]$Test)'
Emit '$__pauseAtEnd = -not $Force   # remember BEFORE the gate may flip $Force: pause only on interactive (right-click) runs, never on automated -Force runs'
Emit ''
Emit '# ---- message language: Korean if Windows UI/format locale is ko-*, or the WoW client is koKR (checked after path resolve) ----'
Emit '$script:KO = (((Get-UICulture).Name -like "ko*") -or ((Get-Culture).Name -like "ko*"))'
Emit 'function M([string]$en,[string]$ko){ if($script:KO){ return $ko } else { return $en } }'
Emit '$ErrorActionPreference = "Stop"'
Emit '$enc = New-Object System.Text.UTF8Encoding($false)'
Emit 'function WF($p,$c){ $d=Split-Path $p -Parent; if(-not(Test-Path $d)){New-Item -ItemType Directory -Force -Path $d|Out-Null}; [IO.File]::WriteAllText($p,$c,$enc); Write-Host "[WRITE] $p" -ForegroundColor Green }'
Emit ''
Emit '# ── locate the EllesmereUI folder (works from anywhere) ──'
Emit 'function Test-EUI([string]$dir){ return ($dir -and (Test-Path (Join-Path $dir "EllesmereUI.toc"))) }'
Emit 'function Resolve-EUI {'
Emit '  param([string]$hint)'
Emit '  # 1) explicit -AddonPath (accept either the EllesmereUI folder or an AddOns folder)'
Emit '  if($hint){'
Emit '    $f=[IO.Path]::GetFullPath($hint)'
Emit '    if(Test-EUI $f){ return $f }'
Emit '    if(Test-EUI (Join-Path $f "EllesmereUI")){ return ([IO.Path]::GetFullPath((Join-Path $f "EllesmereUI"))) }'
Emit '  }'
Emit '  # (WoW install is located FIRST below; the script-location walk-up is the LAST fallback)'
Emit '  # helper: given a "...\World of Warcraft" base, check each flavor for EllesmereUI'
Emit '  function FromWoWBase([string]$wow){'
Emit '    if(-not $wow){ return $null }'
Emit '    foreach($flavor in @("_retail_","_classic_","_classic_era_","_ptr_","_xptr_","_beta_")){'
Emit '      $c = Join-Path (Join-Path (Join-Path (Join-Path $wow $flavor) "Interface") "AddOns") "EllesmereUI"'
Emit '      if(Test-EUI $c){ return ([IO.Path]::GetFullPath($c)) }'
Emit '    }'
Emit '    return $null'
Emit '  }'
Emit '  # 2) running WoW process -> derive install path (fastest when the game is open)'
Emit '  try {'
Emit '    foreach($pn in @("Wow","WowClassic","WowT","WowB")){'
Emit '      $proc = Get-Process -Name $pn -ErrorAction SilentlyContinue | Select-Object -First 1'
Emit '      if($proc -and $proc.Path){'
Emit '        # ...\World of Warcraft\<flavor>\WowXXX.exe  -> parent of <flavor> is the WoW base'
Emit '        $flavorDir = Split-Path $proc.Path -Parent'
Emit '        $r = FromWoWBase (Split-Path $flavorDir -Parent)'
Emit '        if($r){ return $r }'
Emit '        $r = Join-Path (Join-Path $flavorDir "Interface\AddOns") "EllesmereUI"'
Emit '        if(Test-EUI $r){ return ([IO.Path]::GetFullPath($r)) }'
Emit '      }'
Emit '    }'
Emit '  } catch {}'
Emit '  # 3) registry (Blizzard records the install dir)'
Emit '  try {'
Emit '    $keys = @('
Emit '      "HKLM:\SOFTWARE\WOW6432Node\Blizzard Entertainment\World of Warcraft",'
Emit '      "HKLM:\SOFTWARE\Blizzard Entertainment\World of Warcraft",'
Emit '      "HKCU:\SOFTWARE\Blizzard Entertainment\World of Warcraft"'
Emit '    )'
Emit '    foreach($k in $keys){'
Emit '      $ip = (Get-ItemProperty -Path $k -ErrorAction SilentlyContinue).InstallPath'
Emit '      if($ip){'
Emit '        $r = Join-Path (Join-Path $ip "Interface\AddOns") "EllesmereUI"   # InstallPath usually points at a flavor dir'
Emit '        if(Test-EUI $r){ return ([IO.Path]::GetFullPath($r)) }'
Emit '        $r = FromWoWBase (Split-Path $ip -Parent)'
Emit '        if($r){ return $r }'
Emit '      }'
Emit '    }'
Emit '  } catch {}'
Emit '  # 4) common install roots (no recursion - direct path test, instant)'
Emit '  $roots = @($env:ProgramFiles, ${env:ProgramFiles(x86)}, "C:\Program Files (x86)", "C:\Program Files", "C:\Games", "D:\Games")'
Emit '  foreach($dv in ([IO.DriveInfo]::GetDrives() | Where-Object { $_.IsReady -and $_.DriveType -eq "Fixed" })){'
Emit '    $roots += $dv.RootDirectory.FullName'
Emit '    $roots += (Join-Path $dv.RootDirectory.FullName "Games")'
Emit '    $roots += (Join-Path $dv.RootDirectory.FullName "Program Files (x86)")'
Emit '  }'
Emit '  foreach($rt in ($roots | Select-Object -Unique)){'
Emit '    if($rt){ $r = FromWoWBase (Join-Path $rt "World of Warcraft"); if($r){ return $r } }'
Emit '  }'
Emit '  # 5) LAST fallback: walk up from the script location (AddOns\_EUI_Localize layout)'
Emit '  $dir = $PSScriptRoot'
Emit '  for($i=0; $i -lt 10 -and $dir; $i++){'
Emit '    if(Test-EUI (Join-Path $dir "EllesmereUI")){ return ([IO.Path]::GetFullPath((Join-Path $dir "EllesmereUI"))) }'
Emit '    $dir = Split-Path $dir -Parent'
Emit '  }'
Emit '  return $null'
Emit '}'
Emit '$AddonPath = Resolve-EUI $AddonPath'
Emit 'if(-not $AddonPath){ throw (M "Could not locate the EllesmereUI folder. Pass -AddonPath ""...\AddOns\EllesmereUI""." "EllesmereUI 폴더를 찾지 못했습니다. -AddonPath ""...\AddOns\EllesmereUI"" 로 직접 지정하세요.") }'
Emit 'Write-Host "[EUI ] $AddonPath" -ForegroundColor Cyan'
Emit '# ---- 권한: EUI 폴더에 쓸 수 없고 관리자도 아니면 관리자로 자동 재실행(UAC). 쓸 수 있으면 그냥 진행 ----'
Emit '$__isAdmin = ([Security.Principal.WindowsPrincipal][Security.Principal.WindowsIdentity]::GetCurrent()).IsInRole([Security.Principal.WindowsBuiltinRole]::Administrator)'
Emit '$__canWrite = $false; try { $__wt = Join-Path $AddonPath (".wtest_" + [IO.Path]::GetRandomFileName()); Set-Content -LiteralPath $__wt -Value "x" -ErrorAction Stop; Remove-Item -LiteralPath $__wt -Force; $__canWrite = $true } catch {}'
Emit 'if(-not $__canWrite -and -not $__isAdmin){'
Emit '  Write-Host (M "This game folder needs admin rights. A Windows prompt appears - click Yes." "게임 폴더에 쓰려면 관리자 권한이 필요합니다. 잠시 후 뜨는 [예]를 누르세요.") -ForegroundColor Yellow'
Emit '  try { Start-Process powershell -ArgumentList "-NoProfile -ExecutionPolicy Bypass -File `"$PSCommandPath`"" -Verb RunAs | Out-Null } catch { Write-Host (M "Cancelled - right-click the .bat and choose Run as administrator." "취소됨 - .bat 를 우클릭하고 관리자 권한으로 실행을 선택하세요.") -ForegroundColor Red }'
Emit '  return'
Emit '}'
Emit 'if(-not $script:KO){ try { $__cfg = Join-Path (Split-Path (Split-Path (Split-Path $AddonPath))) "WTF\Config.wtf"; if((Test-Path $__cfg) -and ((Get-Content $__cfg -Raw -ErrorAction SilentlyContinue) -match ''locale\s+"koKR"'')){ $script:KO = $true } } catch {} }'
Emit '$tocFile=Join-Path $AddonPath "EllesmereUI.toc"'
Emit 'if(-not(Test-Path $tocFile)){throw (M "EllesmereUI.toc not found: $tocFile" "EllesmereUI.toc 를 찾을 수 없습니다: $tocFile")}'
Emit ''
Emit '# ── prepare locale file contents (written later, after confirmation) ──'
Emit ('$L_LocalCore = @''' + "`r`n" + $localCore + "`r`n" + '''@')

$localeOrder = @($LMAP.Keys)
foreach ($loc in $localeOrder) {
    $txt = BuildLocaleText $loc $LMAP[$loc]
    $var = '$L_' + $loc
    Emit ($var + ' = @''' + "`r`n" + $txt.TrimEnd("`r","`n") + "`r`n" + '''@')
}
$tocAdd = @("# Localization (auto-injected)", "Locale\LocalCore.lua")
foreach ($loc in $localeOrder) { $tocAdd += "Locale\$loc.lua" }
$arr = "@(" + (($tocAdd | ForEach-Object { "'" + $_.Replace("'","''") + "'" }) -join ", ") + ")"
Emit ('$tocAdd = ' + $arr)
Emit ''
Emit '# ── 3) wrap strings in source (line-exact, trimmed match) ──'
Emit '$patches = @('
foreach ($e in $entries) {
    $fromEsc = $e.From.Replace("'", "''")
    $toEsc   = $e.To.Replace("'", "''")
    Emit ("    [pscustomobject]@{ file='" + $e.File + "'; line=" + $e.Line + "; from='" + $fromEsc + "'; to='" + $toEsc + "' }")
}
Emit ')'
Emit '$byFile = $patches | Group-Object file'
Emit ''
Emit '# classify each patch WITHOUT writing: ok(to apply) / done(already) / miss(not found)'
Emit '$cOk = New-Object System.Collections.Generic.List[object]'
Emit '$cDone = New-Object System.Collections.Generic.List[object]'
Emit '$cMiss = New-Object System.Collections.Generic.List[object]'
Emit 'foreach($g in $byFile){'
Emit '  $path = Join-Path $AddonPath $g.Name'
Emit '  $set = @{}'
Emit '  if(Test-Path $path -PathType Leaf){ $lines=[IO.File]::ReadAllLines($path); for($i=0;$i -lt $lines.Count;$i++){ $k=$lines[$i].Trim(); if(-not $set.ContainsKey($k)){$set[$k]=$true} } }'
Emit '  foreach($p in $g.Group){'
Emit '    if($set.ContainsKey($p.to)){ $cDone.Add($p) }'
Emit '    elseif($set.ContainsKey($p.from)){ $cOk.Add($p) }'
Emit '    else{ $cMiss.Add($p) }'
Emit '  }'
Emit '}'
Emit ''
Emit '# ── dry-run report ──'
Emit 'Write-Host ""'
Emit 'Write-Host (M "=== Scan result (no files changed yet) ===" "=== 검사 결과 (아직 파일 변경 없음) ===") -ForegroundColor Cyan'
Emit 'Write-Host ((M "  to apply   : {0}" "  적용 예정   : {0}") -f $cOk.Count)   -ForegroundColor Green'
Emit 'Write-Host ((M "  already    : {0}" "  이미 적용됨  : {0}") -f $cDone.Count) -ForegroundColor DarkGray'
Emit '$missColor = if($cMiss.Count -gt 0){"Red"}else{"DarkGray"}; Write-Host ((M "  NOT FOUND  : {0}" "  찾지 못함   : {0}") -f $cMiss.Count) -ForegroundColor $missColor'
Emit 'if($cMiss.Count -gt 0){ Write-Host (M "  -- these lines were not found (source may have changed):" "  -- 다음 줄을 찾지 못했습니다 (소스가 변경됐을 수 있음):") -ForegroundColor Red; $cMiss|ForEach-Object{ Write-Host ("     [{0}:{1}] {2}" -f $_.file,$_.line,$_.from) -ForegroundColor Red } }'
Emit 'Write-Host ""'
Emit 'if($cOk.Count -eq 0 -and $cMiss.Count -eq 0){ Write-Host (M "No source wraps to apply; refreshing locale files only." "적용할 소스 래핑이 없습니다. 로케일 파일만 새로 고칩니다.") -ForegroundColor Cyan; $Force = $true }'
Emit 'if($Force){ Write-Host (M "-Force given, applying without prompt." "-Force 지정됨, 확인 없이 적용합니다.") -ForegroundColor DarkGray }'
Emit 'else{'
Emit '  try { $ans = Read-Host (M "Press ENTER to apply, or type N to cancel" "적용하려면 ENTER, 취소하려면 N 입력") }'
Emit '  catch { Write-Host (M "Non-interactive shell: re-run with -Force to apply. No changes made." "비대화형 셸입니다. 적용하려면 -Force 로 다시 실행하세요. 변경 없음.") -ForegroundColor Yellow; return }'
Emit '  if($ans -and $ans.Trim() -match "^[nN]"){ Write-Host (M "Cancelled. No changes made." "취소됨. 변경 없음.") -ForegroundColor Yellow; return }'
Emit '}'
Emit ''
Emit '# ── apply (locale files, toc, replacements) ──'
# -Test: prepend [L] to every localized VALUE so the author can visually see which
#        strings are wrapped (English client → "[L]Reset" etc.). All locales marked.
$locNames = "'" + ($localeOrder -join "','") + "'"
Emit ('if($Test){')
Emit ('  Write-Host "[TEST] Prepending [L] marker to every localized value (coverage check)." -ForegroundColor Magenta')
Emit ('  foreach($__ln in @(' + $locNames + ')){')
Emit ('    $__v = Get-Variable -Name ("L_"+$__ln) -ValueOnly')
Emit ('    $__v = [regex]::Replace($__v, ''(?m)^(L\["(?:[^"\\]|\\.)*"\]\s*=\s*")'', ''${1}|cffff3cff[L]|r '')')
Emit ('    Set-Variable -Name ("L_"+$__ln) -Value $__v')
Emit ('  }')
Emit ('}')
Emit 'WF (Join-Path $AddonPath "Locale\LocalCore.lua") $L_LocalCore'
foreach ($loc in $localeOrder) { Emit ('WF (Join-Path $AddonPath "Locale\' + $loc + '.lua") $L_' + $loc) }
Emit '$tl=[IO.File]::ReadAllLines($tocFile)'
Emit 'if($tl -match "Locale\\LocalCore\.lua"){ Write-Host (M "[SKIP] toc already patched" "[SKIP] toc 이미 패치됨") -ForegroundColor Yellow }'
Emit 'else{'
Emit '  $o=New-Object System.Collections.Generic.List[string]; $ins=$false'
Emit '  foreach($ln in $tl){ $o.Add($ln); if(-not $ins -and $ln.Trim() -eq "EllesmereUI_Lite.lua"){ $o.Add(""); foreach($a in $tocAdd){ $o.Add($a) }; $ins=$true } }'
Emit '  if($ins){ [IO.File]::WriteAllLines($tocFile,$o,$enc); Write-Host (M "[TOC ] inserted" "[TOC ] 삽입됨") -ForegroundColor Green } else { Write-Host (M "[WARN] could not find EllesmereUI_Lite.lua line in toc" "[WARN] toc 에서 EllesmereUI_Lite.lua 줄을 찾지 못함") -ForegroundColor Red }'
Emit '}'
Emit '$skipped = New-Object System.Collections.Generic.List[string]'
Emit '$appliedList = New-Object System.Collections.Generic.List[string]'
Emit '$alreadyCount = 0'
Emit 'foreach($g in $byFile){'
Emit '  # file may be inside EllesmereUI (e.g. "EllesmereUI.lua") or a sibling module via'
Emit '  # a "..\Module\Core.lua" prefix; GetFullPath normalizes the .. so both resolve.'
Emit '  $path = [IO.Path]::GetFullPath((Join-Path $AddonPath $g.Name))'
Emit '  if(-not(Test-Path $path -PathType Leaf)){ foreach($p in $g.Group){ $skipped.Add("$($p.file):$($p.line)  $($p.from)") }; continue }'
Emit '  $lines = [IO.File]::ReadAllLines($path); $fileChanged=$false'
Emit '  foreach($p in $g.Group){'
Emit '    $applied=$false; $already=$false'
Emit '    # replace EVERY matching line (identical lines may appear more than once).'
Emit '    for($i=0;$i -lt $lines.Count;$i++){'
Emit '      $t=$lines[$i].Trim()'
Emit '      if($t -eq $p.to){ $already=$true; continue }   # 이 줄은 이미 적용됨 (멱등 재실행 시 건너뜀)'
Emit '      if($t -eq $p.from){ $m=[regex]::Match($lines[$i],"^(\s*)(.*?)(\s*)$"); $lines[$i]=$m.Groups[1].Value+$p.to+$m.Groups[3].Value; $applied=$true; $fileChanged=$true }'
Emit '    }'
Emit '    if($applied){ $appliedList.Add("$($p.file):$($p.line)  $($p.from)") } elseif($already){ $alreadyCount++ } else { $skipped.Add("$($p.file):$($p.line)  $($p.from)") }'
Emit '  }'
Emit '  if($fileChanged){ [IO.File]::WriteAllLines($path,$lines,$enc); Write-Host "[PATCH] $($g.Name)" -ForegroundColor Green }'
Emit '}'
Emit 'Write-Host ""'
Emit 'Write-Host (M ("== Summary: $($appliedList.Count) newly applied / $alreadyCount already up-to-date (skipped) / $($skipped.Count) not matched ==") ("== 요약: 새로 적용 $($appliedList.Count) / 이미 최신=건너뜀 $alreadyCount / 매칭 실패 $($skipped.Count) ==")) -ForegroundColor Cyan'
Emit 'if($appliedList.Count -gt 0){ Write-Host (M "-- newly applied this run --" "-- 이번에 새로 적용된 항목 --") -ForegroundColor Green; $appliedList|ForEach-Object{ Write-Host "  [NEW]  $_" -ForegroundColor Green } }'
Emit 'if($skipped.Count -gt 0){ Write-Host (M "-- NOT MATCHED (source changed / check these) --" "-- 매칭 실패 (소스 변경 / 확인 필요) --") -ForegroundColor Red; $skipped|ForEach-Object{ Write-Host "  [FAIL] $_" -ForegroundColor Red } }'
Emit '# ── Korean font (koKR only): download Wanted Sans (OFL) at apply time, install Bold as media\fonts\Expressway.TTF + OFL.txt, then delete temp. Not pre-bundled / not redistributed — each machine fetches from the official release. ──'
Emit 'if($script:KO){'
Emit '  Write-Host (M "[FONT] Korean client detected — replacing the UI font with a Korean font (Wanted Sans Bold, OFL)." "[FONT] 한글 환경이라 UI 폰트를 한글 폰트(Wanted Sans Bold, OFL)로 교체합니다.") -ForegroundColor Cyan'
Emit '  $__fontDst = Join-Path $AddonPath "media\fonts\Expressway.TTF"'
Emit '  $__ofl     = Join-Path $AddonPath "media\fonts\WantedSans-OFL.txt"'
Emit '  try { Get-ChildItem (Split-Path $__fontDst -Parent) -Filter "Expressway.TTF.old_*" -ErrorAction SilentlyContinue | Remove-Item -Force -ErrorAction SilentlyContinue } catch {}   # 이전 실행에서 잠겨 옆으로 치워둔 잔재 정리(게임 종료 후 삭제됨)'
Emit '  $__boldBytes = 2344596   # Wanted Sans v1.0.3 Bold 정확 바이트 (SemiBold=2352124 와 7.5KB 차이)'
Emit '  # 업데이트 재실행: 이미 *Bold*(정확 용량)가 깔렸으면 다운로드 건너뜀. SemiBold가 깔려 있으면 용량 불일치 → 자동으로 Bold 다시 받음(자가 치유).'
Emit '  if((Test-Path $__fontDst) -and ((Get-Item $__fontDst).Length -eq $__boldBytes)){ Write-Host (M "[FONT] Wanted Sans Bold already installed (size match) - skipping download" "[FONT] Wanted Sans Bold 이미 설치됨(용량 일치) - 다운로드 건너뜀") -ForegroundColor DarkGray }'
Emit '  else {'
Emit '    $__tmp = Join-Path ([IO.Path]::GetTempPath()) "wanted_ttf.zip"; $__exd = Join-Path ([IO.Path]::GetTempPath()) "wanted_ttf_ex"'
Emit '    try {'
Emit '      Write-Host (M "[FONT] downloading Wanted Sans (OFL, ~21MB)..." "[FONT] Wanted Sans 내려받는 중 (OFL, 약 21MB)...") -ForegroundColor Cyan'
Emit '      if(Test-Path $__exd){ Remove-Item $__exd -Recurse -Force }'
Emit '      Invoke-WebRequest -Uri "https://github.com/wanteddev/wanted-sans/releases/download/v1.0.3/WantedSans-1.0.3.zip" -OutFile $__tmp -UseBasicParsing'
Emit '      Expand-Archive -Path $__tmp -DestinationPath $__exd -Force'
Emit '      $__ttf = Get-ChildItem $__exd -Recurse -Filter "WantedSans-Bold.ttf" | Select-Object -First 1'
Emit '      if($__ttf){'
Emit '        # 강제 복사: WoW 실행 중이어도 시도. ① 직접 덮어쓰기 → ② 잠겨 실패하면 기존을 옆으로 치우고 새로 쓰기. (새 폰트는 게임 재시작 후 보임 — 복사 자체는 실행 중에도 됨.)'
Emit '        $__done = $false'
Emit '        try { Copy-Item $__ttf.FullName $__fontDst -Force -ErrorAction Stop; $__done = $true } catch { try { $__old = "$__fontDst.old_" + [IO.Path]::GetRandomFileName(); Rename-Item -LiteralPath $__fontDst -NewName (Split-Path $__old -Leaf) -Force -ErrorAction Stop; Copy-Item $__ttf.FullName $__fontDst -Force -ErrorAction Stop; $__done = $true; Remove-Item -LiteralPath $__old -Force -ErrorAction SilentlyContinue } catch {} }'
Emit '        if($__done){ try { Invoke-WebRequest -Uri "https://raw.githubusercontent.com/wanteddev/wanted-sans/main/OFL.txt" -OutFile $__ofl -UseBasicParsing } catch {}; $__kb=[int]((Get-Item $__fontDst).Length/1KB); Write-Host (M "[FONT] installed -> media\fonts\Expressway.TTF ($__kb KB, Wanted Sans Bold, OFL) - shows after you restart WoW" "[FONT] 설치됨 -> media\fonts\Expressway.TTF ($__kb KB, Wanted Sans Bold, OFL) - 게임을 재시작하면 보입니다") -ForegroundColor Green }'
Emit '        else { Write-Host (M "[FONT] could not replace now - it will apply next time you restart WoW (or re-run)" "[FONT] 지금은 교체 못 함 - 다음에 게임을 재시작하면 적용됩니다(또는 다시 실행)") -ForegroundColor Yellow }'
Emit '      }'
Emit '      else { Write-Host (M "[FONT] Bold TTF not found in archive (skip)" "[FONT] 압축에서 Bold TTF 못 찾음 (건너뜀)") -ForegroundColor Yellow }'
Emit '    } catch { Write-Host (M "[FONT] download/extract failed ($_)" "[FONT] 내려받기/압축해제 실패 ($_)") -ForegroundColor Yellow }'
Emit '    finally { if($__tmp){ Remove-Item $__tmp,$__exd -Recurse -Force -ErrorAction SilentlyContinue } }'
Emit '  }'
Emit '}'
Emit 'Write-Host (M "Done. Type /reload in game to verify." "완료. 게임에서 /reload 를 입력해 확인하세요.") -ForegroundColor Cyan'
Emit 'if($__pauseAtEnd){ Write-Host ""; Write-Host (M "Press any key to close this window..." "아무 키나 누르면 창이 닫힙니다...") -ForegroundColor Cyan; try { $Host.UI.RawUI.FlushInputBuffer(); $null = $Host.UI.RawUI.ReadKey("NoEcho,IncludeKeyDown") } catch { try { $null = Read-Host } catch {} } }'

[IO.File]::WriteAllText($OutFile, $sb.ToString(), (New-Object System.Text.UTF8Encoding($true)))
Write-Host "Generated: $OutFile" -ForegroundColor Green
Write-Host "patches=$($entries.Count)  keys=$($orderedKeys.Count)  locales=$($localeOrder.Count)" -ForegroundColor Cyan
