; YMhut Box - WinUI 3 Windows installer (Stage B packaging)
; This installer ships the self-contained WinUI 3 publish output.
; Build the payload with:
;   dotnet publish src\box-winUI\YMhut.Box.WinUI.csproj -c Release -r win-x64 --self-contained -o publish\winui
; then run ISCC against this .iss.

#define MyAppName "YMhut Box"
#define MyAppPublisher "YMhut"
#define MyAppURL "https://ymhut.cn"
#define MyAppExeName "YMhutBox.exe"
#define MyAppId "cn.ymhut.box.winui"

#ifndef MyAppVersion
  #define MyAppVersion "2.0.2"
#endif
#ifndef MyAppBuild
  #define MyAppBuild "1"
#endif
#ifndef MyAppChannel
  #define MyAppChannel "stable"
#endif
#ifndef PayloadDir
  #define PayloadDir "..\latest"
#endif
#ifndef BundleVCRedist
  #define BundleVCRedist 0
#endif
#ifndef BundleWebView2
  #define BundleWebView2 0
#endif
#ifndef ChineseMessagesFile
  #define ChineseMessagesFile "compiler:Default.isl"
#endif
#ifndef WebView2BootstrapperUrl
  #define WebView2BootstrapperUrl "https://go.microsoft.com/fwlink/?LinkId=2124703"
#endif
#ifndef VCRedistUrl
  #define VCRedistUrl "https://aka.ms/vs/17/release/vc_redist.x64.exe"
#endif

[Setup]
AppId={#MyAppId}
AppName={#MyAppName}
AppVersion={#MyAppVersion}
AppVerName={#MyAppName} {#MyAppVersion}
AppPublisher={#MyAppPublisher}
AppPublisherURL={#MyAppURL}
DefaultDirName={code:GetDefaultInstallDir}
DefaultGroupName={#MyAppName}
OutputDir=..\installer_output
OutputBaseFilename=YMhut_Box_WinUI_Setup_{#MyAppVersion}
Compression=lzma2
CompressionThreads=auto
LZMADictionarySize=1048576
LZMAUseSeparateProcess=yes
SolidCompression=yes
WizardStyle=modern
ShowLanguageDialog=yes
LanguageDetectionMethod=uilanguage
PrivilegesRequired=lowest
PrivilegesRequiredOverridesAllowed=commandline
UsePreviousPrivileges=no
ArchitecturesAllowed=x64compatible
ArchitecturesInstallIn64BitMode=x64compatible
UsePreviousAppDir=no
AllowNoIcons=yes
SetupLogging=yes
CloseApplications=yes
CloseApplicationsFilter={#MyAppExeName}
RestartApplications=no
UninstallDisplayIcon={app}\{#MyAppExeName}

[Languages]
Name: "chinesesimp"; MessagesFile: "{#ChineseMessagesFile}"
Name: "english"; MessagesFile: "compiler:Default.isl"

[CustomMessages]
english.DesktopIcon=Create a desktop shortcut
english.StartMenuIcon=Create a Start menu shortcut
english.ShortcutGroup=Shortcuts
english.AutoStart=Start automatically after sign-in
english.LaunchApp=Launch {#MyAppName} after setup finishes
english.StartupGroup=Startup
english.MaintenanceGroup=Maintenance
english.InstallSummaryTitle=Install output summary
english.InstallSummaryDescription=Review the install directories and selected output before copying files.
english.InstallSummaryVersion=Version
english.InstallSummaryInstallDir=Install directory
english.InstallSummaryPayload=Payload source
english.InstallSummaryUserData=User data
english.InstallSummaryLogs=Logs
english.InstallSummaryTasks=Selected tasks
english.InstallOutputInitial=Ready to extract files.
english.InstallOutputClean=Cleaning old application files.
english.InstallOutputExtract=Extracting new application payload.
english.InstallOutputFile=Extracting:
english.InstallOutputPostInstall=Finalizing installation.
english.InstallOutputDone=Installation files are ready.
english.ProtectedInstallNeedsAdmin=The existing installation is in a protected system directory. Restart setup as administrator or choose a user-writable folder.
english.NonSystemInstallRequired=Please choose a writable folder on a non-system drive. YMhut Box no longer installs to the Windows system drive by default.
chinesesimp.NonSystemInstallRequired=请选择一个非系统盘上的可写目录。YMhut Box 不再默认安装到 Windows 系统盘。
english.RepairRuntimes=Check and install system prerequisites (VC++ / WebView2)
english.MigrateLegacyData=Import legacy data
english.CleanCache=Clean legacy cache folders
english.WebView2RuntimeName=WebView2 Runtime
english.VCRuntimeName=VC++ Runtime
english.DependencyDownloadCancelled=%1 download was canceled.
english.DependencyDownloadFailed=%1 download failed: %2
english.DependencyStartFailed=%1 installer could not be started.
english.DependencyInstallFailed=%1 installation failed with exit code %2.
english.WebView2StillMissing=WebView2 Runtime is still not detected after installation. Check the network connection or system policy, then retry.
english.VCRuntimeStillMissing=VC++ Runtime is still not detected after installation. Check the network connection or system policy, then retry.
chinesesimp.DesktopIcon=创建桌面快捷方式
chinesesimp.StartMenuIcon=创建开始菜单快捷方式
chinesesimp.ShortcutGroup=快捷方式
chinesesimp.AutoStart=开机后自动启动
chinesesimp.LaunchApp=安装完成后立即启动 {#MyAppName}
chinesesimp.StartupGroup=启动
chinesesimp.MaintenanceGroup=维护
chinesesimp.InstallSummaryTitle=安装输出摘要
chinesesimp.InstallSummaryDescription=复制文件前确认安装目录、输出位置和已选任务。
chinesesimp.InstallSummaryVersion=版本
chinesesimp.InstallSummaryInstallDir=安装目录
chinesesimp.InstallSummaryPayload=载荷来源
chinesesimp.InstallSummaryUserData=用户数据
chinesesimp.InstallSummaryLogs=日志
chinesesimp.InstallSummaryTasks=已选任务
chinesesimp.InstallOutputInitial=准备提取文件。
chinesesimp.InstallOutputClean=正在清理旧版本程序文件。
chinesesimp.InstallOutputExtract=正在提取新版本程序文件。
chinesesimp.InstallOutputFile=正在提取：
chinesesimp.InstallOutputPostInstall=正在完成安装收尾。
chinesesimp.InstallOutputDone=安装文件已准备完成。
chinesesimp.ProtectedInstallNeedsAdmin=检测到旧版本安装在受保护的系统目录。请以管理员身份重新运行安装器修复，或选择当前用户可写的安装目录。
chinesesimp.RepairRuntimes=检查并安装系统依赖（VC++ / WebView2）
chinesesimp.MigrateLegacyData=导入旧版数据
chinesesimp.CleanCache=清理旧版缓存目录
chinesesimp.WebView2RuntimeName=WebView2 Runtime
chinesesimp.VCRuntimeName=VC++ Runtime
chinesesimp.DependencyDownloadCancelled=%1 下载已取消。
chinesesimp.DependencyDownloadFailed=%1 下载失败：%2
chinesesimp.DependencyStartFailed=%1 安装器启动失败。
chinesesimp.DependencyInstallFailed=%1 安装失败，退出码：%2。
chinesesimp.WebView2StillMissing=WebView2 Runtime 安装后仍未检测到，请检查网络连接或系统策略后重试。
chinesesimp.VCRuntimeStillMissing=VC++ Runtime 安装后仍未检测到，请检查网络连接或系统策略后重试。

[Tasks]
Name: "desktopicon"; Description: "{cm:DesktopIcon}"; GroupDescription: "{cm:ShortcutGroup}"; Flags: checkedonce
Name: "startmenuicon"; Description: "{cm:StartMenuIcon}"; GroupDescription: "{cm:ShortcutGroup}"; Flags: unchecked
Name: "autostart"; Description: "{cm:AutoStart}"; GroupDescription: "{cm:StartupGroup}"; Flags: unchecked
Name: "launchapp"; Description: "{cm:LaunchApp}"; GroupDescription: "{cm:StartupGroup}"; Flags: checkedonce

[Files]
Source: "{#PayloadDir}\*"; DestDir: "{app}"; Flags: ignoreversion recursesubdirs createallsubdirs; BeforeInstall: LogCurrentExtractFile
#if Int(BundleVCRedist) == 1
Source: "{#PayloadDir}\prereqs\vc_redist.x64.exe"; Flags: dontcopy
#endif
#if Int(BundleWebView2) == 1
Source: "{#PayloadDir}\prereqs\MicrosoftEdgeWebView2RuntimeInstallerX64.exe"; Flags: dontcopy
#endif

[Dirs]
Name: "{app}\data"
Name: "{app}\FeedbackPackages"; Permissions: users-modify
Name: "{app}\data\legacy-imports"

[InstallDelete]
Type: filesandordirs; Name: "{app}\resources\lang"
Type: filesandordirs; Name: "{app}\lang"
Type: filesandordirs; Name: "{localappdata}\YMhut Box\WinUI\Runtime"
Type: filesandordirs; Name: "{localappdata}\YMhut Box\WinUI\runtime"
Type: filesandordirs; Name: "{localappdata}\YMhut Box\WinUI\Runtimes"
Type: filesandordirs; Name: "{localappdata}\YMhut Box\WinUI\runtimes"
Type: filesandordirs; Name: "{localappdata}\YMhut Box\WinUI\Tools"
Type: filesandordirs; Name: "{localappdata}\YMhut Box\WinUI\Metadata"
Type: filesandordirs; Name: "{localappdata}\YMhut Box\Runtime"
Type: filesandordirs; Name: "{localappdata}\YMhut Box\runtime"
Type: filesandordirs; Name: "{localappdata}\YMhut Box\Tools"
Type: filesandordirs; Name: "{localappdata}\YMhut Box\Metadata"
Type: filesandordirs; Name: "{localappdata}\ymhut_box\Runtime"
Type: filesandordirs; Name: "{localappdata}\ymhut_box\runtime"
Type: filesandordirs; Name: "{localappdata}\ymhut_box\Tools"
Type: filesandordirs; Name: "{localappdata}\ymhut_box\Metadata"
Type: filesandordirs; Name: "{localappdata}\YMhut Box\cache"; Check: ShouldCleanCache
Type: filesandordirs; Name: "{userappdata}\YMhut Box\cache"; Check: ShouldCleanCache
Type: filesandordirs; Name: "{localappdata}\ymhut_box\cache"; Check: ShouldCleanCache
Type: filesandordirs; Name: "{userappdata}\ymhut_box\cache"; Check: ShouldCleanCache
Type: files; Name: "{app}\*.winmd"
Type: files; Name: "{app}\*.pdb"
Type: files; Name: "{app}\workloads*.json"
Type: files; Name: "{app}\installer-layout.txt"
Type: files; Name: "{app}\config\installer-layout.dat"
Type: files; Name: "{app}\config\installer-required-files.dat"
Type: files; Name: "{app}\Assets\data\reference-data.dat"
Type: files; Name: "{app}\YMhut.Box.Worker.exe"
Type: files; Name: "{app}\YMhut.Box.Worker.dll"
Type: files; Name: "{app}\YMhut.Box.Worker.deps.json"
Type: files; Name: "{app}\YMhut.Box.Worker.runtimeconfig.json"
Type: files; Name: "{app}\YMhut.Box.PluginHost.exe"
Type: files; Name: "{app}\YMhut.Box.PluginHost.dll"
Type: files; Name: "{app}\YMhut.Box.PluginHost.deps.json"
Type: files; Name: "{app}\YMhut.Box.PluginHost.runtimeconfig.json"
Type: filesandordirs; Name: "{app}\updater"
Type: files; Name: "{app}\YMhut.Box.Updater.exe"
Type: files; Name: "{app}\YMhut.Box.Updater.dll"
Type: files; Name: "{app}\YMhut.Box.Updater.deps.json"
Type: files; Name: "{app}\YMhut.Box.Updater.runtimeconfig.json"

[Icons]
Name: "{userprograms}\{#MyAppName}"; Filename: "{app}\{#MyAppExeName}"; Tasks: startmenuicon
Name: "{userdesktop}\{#MyAppName}"; Filename: "{app}\{#MyAppExeName}"; Tasks: desktopicon

[Registry]
Root: HKCU; Subkey: "Software\Microsoft\Windows\CurrentVersion\Run"; ValueType: string; ValueName: "{#MyAppName}"; ValueData: """{app}\{#MyAppExeName}"""; Flags: uninsdeletevalue; Tasks: autostart

[Run]
Filename: "{app}\{#MyAppExeName}"; Description: "{cm:LaunchApp}"; Flags: nowait postinstall skipifsilent; Tasks: launchapp

[UninstallDelete]
Type: files; Name: "{userdesktop}\{#MyAppName}.lnk"
Type: files; Name: "{userprograms}\{#MyAppName}.lnk"

[Code]
const
  DirectoryAttribute = $10;
  InstallOutputFlushLines = 16;
  InstallOutputTrimCharacters = 65000;
  InstallOutputKeepCharacters = 48000;
  BundledVCRedistIncluded = {#BundleVCRedist};
  BundledWebView2Included = {#BundleWebView2};
  WebView2ClientKey = 'SOFTWARE\Microsoft\EdgeUpdate\Clients\{F3017226-FE2A-4295-8BDF-00C3A9A7E4C5}';
  WebView2ClientKeyWow64 = 'SOFTWARE\WOW6432Node\Microsoft\EdgeUpdate\Clients\{F3017226-FE2A-4295-8BDF-00C3A9A7E4C5}';
  WebView2ClientStateKey = 'SOFTWARE\Microsoft\EdgeUpdate\ClientState\{F3017226-FE2A-4295-8BDF-00C3A9A7E4C5}';
  WebView2ClientStateKeyWow64 = 'SOFTWARE\WOW6432Node\Microsoft\EdgeUpdate\ClientState\{F3017226-FE2A-4295-8BDF-00C3A9A7E4C5}';
  VCRedistKey = 'SOFTWARE\Microsoft\VisualStudio\14.0\VC\Runtimes\x64';

var
  DownloadPage: TDownloadWizardPage;
  SummaryPage: TWizardPage;
  SummaryMemo: TNewMemo;
  InstallOutputMemo: TNewMemo;
  ExistingInstallDir: string;
  LastInstallProgressPercent: Integer;
  InstallOutputLineCount: Integer;
  InstallOutputPendingLineCount: Integer;
  InstallOutputBuffer: string;

function NormalizePathPrefix(const Value: string): string;
begin
  Result := Lowercase(AddBackslash(ExpandConstant(Value)));
end;

function PathStartsWith(const Path, Prefix: string): Boolean;
begin
  Result := Pos(Lowercase(Prefix), Lowercase(AddBackslash(Path))) = 1;
end;

function IsProtectedInstallPath(const DirName: string): Boolean;
begin
  Result :=
    PathStartsWith(DirName, NormalizePathPrefix('{autopf}')) or
    PathStartsWith(DirName, NormalizePathPrefix('{pf}')) or
    PathStartsWith(DirName, NormalizePathPrefix('{pf32}')) or
    PathStartsWith(DirName, NormalizePathPrefix('{commonpf}')) or
    PathStartsWith(DirName, NormalizePathPrefix('{commonpf32}'));
end;

function GetSystemDrivePrefix(): string;
begin
  Result := Uppercase(Copy(ExpandConstant('{win}'), 1, 2));
end;

function IsSystemDrivePath(const DirName: string): Boolean;
begin
  Result := Uppercase(Copy(ExpandConstant(DirName), 1, 2)) = GetSystemDrivePrefix();
end;

function IsWritableDirectory(const DirName: string): Boolean;
var
  ProbePath: string;
begin
  Result := False;
  try
    if not ForceDirectories(DirName) then
      Exit;

    ProbePath := AddBackslash(DirName) + '.ymhut-write-test.tmp';
    if SaveStringToFile(ProbePath, 'ok', False) then
    begin
      DeleteFile(ProbePath);
      Result := True;
    end;
  except
    Result := False;
  end;
end;

function IsNonSystemWritableInstallDir(const DirName: string): Boolean;
begin
  Result := (DirName <> '') and
    (not IsSystemDrivePath(DirName)) and
    (not IsProtectedInstallPath(DirName)) and
    IsWritableDirectory(DirName);
end;

function FindDefaultNonSystemInstallDir(): string;
var
  Index: Integer;
  DriveLetter: string;
  Candidate: string;
begin
  Result := '';
  for Index := 1 to Length('DEFGHIJKLMNOPQRSTUVWXYZ') do
  begin
    DriveLetter := Copy('DEFGHIJKLMNOPQRSTUVWXYZ', Index, 1);
    if DriveLetter + ':' = GetSystemDrivePrefix() then
      Continue;

    Candidate := DriveLetter + ':\Programs\YMhut Box';
    if DirExists(DriveLetter + ':\') and IsWritableDirectory(Candidate) then
    begin
      Result := Candidate;
      Exit;
    end;
  end;
end;

function GetDefaultInstallDir(Param: string): string;
var
  DefaultDir: string;
begin
  if (ExistingInstallDir <> '') and ((not IsProtectedInstallPath(ExistingInstallDir)) or IsAdminInstallMode) then
    Result := ExistingInstallDir
  else
  begin
    DefaultDir := FindDefaultNonSystemInstallDir();
    if DefaultDir <> '' then
      Result := DefaultDir
    else
      Result := 'D:\Programs\YMhut Box';
  end;
end;

function ShouldCleanCache(): Boolean;
begin
  Result := True;
end;

function DetectExistingInstallDir(): string;
var
  UninstallKey: string;
  InstallLocation: string;
begin
  Result := '';
  UninstallKey := 'Software\Microsoft\Windows\CurrentVersion\Uninstall\' + '{#MyAppId}' + '_is1';
  if RegQueryStringValue(HKLM64, UninstallKey, 'InstallLocation', InstallLocation) and
    FileExists(AddBackslash(InstallLocation) + '{#MyAppExeName}') then
  begin
    Result := InstallLocation;
    Exit;
  end;

  if RegQueryStringValue(HKLM, UninstallKey, 'InstallLocation', InstallLocation) and
    FileExists(AddBackslash(InstallLocation) + '{#MyAppExeName}') then
  begin
    Result := InstallLocation;
    Exit;
  end;

  if RegQueryStringValue(HKCU, UninstallKey, 'InstallLocation', InstallLocation) and
    FileExists(AddBackslash(InstallLocation) + '{#MyAppExeName}') then
  begin
    Result := InstallLocation;
    Exit;
  end;
end;

function InitializeSetup(): Boolean;
begin
  ExistingInstallDir := DetectExistingInstallDir();
  if (ExistingInstallDir <> '') and IsProtectedInstallPath(ExistingInstallDir) and (not IsAdminInstallMode) then
  begin
    MsgBox(CustomMessage('ProtectedInstallNeedsAdmin'), mbError, MB_OK);
    Result := False;
    Exit;
  end;

  Result := True;
end;

procedure LayoutInstallOutputMemo(); forward;
procedure ScrollInstallOutputToBottom(); forward;

procedure InitializeWizard;
begin
  DownloadPage := CreateDownloadPage(SetupMessage(msgWizardPreparing), SetupMessage(msgPreparingDesc), nil);
  DownloadPage.ShowBaseNameInsteadOfUrl := True;
  ExistingInstallDir := DetectExistingInstallDir();

  SummaryPage := CreateCustomPage(wpSelectTasks, CustomMessage('InstallSummaryTitle'), CustomMessage('InstallSummaryDescription'));
  SummaryMemo := TNewMemo.Create(SummaryPage);
  SummaryMemo.Parent := SummaryPage.Surface;
  SummaryMemo.Left := 0;
  SummaryMemo.Top := 0;
  SummaryMemo.Width := SummaryPage.SurfaceWidth;
  SummaryMemo.Height := SummaryPage.SurfaceHeight;
  SummaryMemo.ReadOnly := True;
  SummaryMemo.WordWrap := False;
  SummaryMemo.ScrollBars := ssVertical;

  InstallOutputMemo := TNewMemo.Create(WizardForm.InstallingPage);
  InstallOutputMemo.Parent := WizardForm.InstallingPage;
  InstallOutputMemo.ReadOnly := True;
  InstallOutputMemo.WordWrap := False;
  InstallOutputMemo.ScrollBars := ssVertical;
  InstallOutputMemo.Text := CustomMessage('InstallOutputInitial') + #13#10;
  InstallOutputLineCount := 1;
  InstallOutputPendingLineCount := 0;
  InstallOutputBuffer := '';
  LayoutInstallOutputMemo();
  ScrollInstallOutputToBottom();
end;

procedure LayoutInstallOutputMemo();
var
  AvailableWidth: Integer;
  AvailableHeight: Integer;
begin
  if InstallOutputMemo = nil then
    Exit;

  InstallOutputMemo.Left := WizardForm.ProgressGauge.Left;
  InstallOutputMemo.Top := WizardForm.ProgressGauge.Top + WizardForm.ProgressGauge.Height + ScaleY(10);

  AvailableWidth := WizardForm.InstallingPage.Width - (InstallOutputMemo.Left * 2);
  if AvailableWidth < WizardForm.ProgressGauge.Width then
    AvailableWidth := WizardForm.ProgressGauge.Width;
  InstallOutputMemo.Width := AvailableWidth;

  AvailableHeight := WizardForm.InstallingPage.Height - InstallOutputMemo.Top - ScaleY(6);
  if AvailableHeight < ScaleY(96) then
    AvailableHeight := ScaleY(96);
  InstallOutputMemo.Height := AvailableHeight;
end;

procedure ScrollInstallOutputToBottom();
begin
  if InstallOutputMemo = nil then
    Exit;

  InstallOutputMemo.SelStart := Length(InstallOutputMemo.Text);
end;

procedure FlushInstallOutput();
var
  TextValue: string;
begin
  if InstallOutputMemo = nil then
  begin
    InstallOutputBuffer := '';
    InstallOutputPendingLineCount := 0;
    Exit;
  end;

  if InstallOutputBuffer = '' then
  begin
    ScrollInstallOutputToBottom();
    Exit;
  end;

  TextValue := InstallOutputMemo.Text + InstallOutputBuffer;
  InstallOutputLineCount := InstallOutputLineCount + InstallOutputPendingLineCount;

  if Length(TextValue) > InstallOutputTrimCharacters then
  begin
    TextValue := Copy(TextValue, Length(TextValue) - InstallOutputKeepCharacters + 1, InstallOutputKeepCharacters);
    InstallOutputLineCount := 500;
  end;

  InstallOutputMemo.Text := TextValue;
  InstallOutputBuffer := '';
  InstallOutputPendingLineCount := 0;
  ScrollInstallOutputToBottom();
end;

procedure QueueInstallOutput(const Text: string; const ForceFlush, WriteSetupLog: Boolean);
begin
  if WriteSetupLog then
    Log(Text);
  if InstallOutputMemo = nil then
    Exit;

  InstallOutputBuffer := InstallOutputBuffer + Text + #13#10;
  InstallOutputPendingLineCount := InstallOutputPendingLineCount + 1;

  if ForceFlush or (InstallOutputPendingLineCount >= InstallOutputFlushLines) then
    FlushInstallOutput();
end;

procedure AppendInstallOutput(const Text: string);
begin
  QueueInstallOutput(Text, True, True);
end;

procedure LogCurrentExtractFile();
var
  FileName: string;
begin
  FileName := ExpandConstant(CurrentFileName);
  if FileName <> '' then
    QueueInstallOutput(CustomMessage('InstallOutputFile') + ' ' + FileName, False, False);
end;

function SelectedTaskText(const TaskName, LabelText: string): string;
begin
  if WizardIsTaskSelected(TaskName) then
    Result := '  - ' + LabelText + #13#10
  else
    Result := '';
end;

function BuildInstallSummary(): string;
var
  Tasks: string;
begin
  Tasks :=
    SelectedTaskText('desktopicon', CustomMessage('DesktopIcon')) +
    SelectedTaskText('startmenuicon', CustomMessage('StartMenuIcon')) +
    SelectedTaskText('autostart', CustomMessage('AutoStart')) +
    SelectedTaskText('launchapp', CustomMessage('LaunchApp'));
  if Tasks = '' then
    Tasks := '  - (none)' + #13#10;

  Result :=
    CustomMessage('InstallSummaryVersion') + ': {#MyAppVersion}' + #13#10 +
    CustomMessage('InstallSummaryInstallDir') + ': ' + WizardDirValue + #13#10 +
    CustomMessage('InstallSummaryPayload') + ': {#PayloadDir}' + #13#10 +
    '' + #13#10 +
    CustomMessage('InstallSummaryTasks') + ':' + #13#10 +
    Tasks;
end;

procedure UpdateInstallSummary();
begin
  if SummaryMemo <> nil then
    SummaryMemo.Text := BuildInstallSummary();
end;

procedure CurPageChanged(CurPageID: Integer);
begin
  if (SummaryPage <> nil) and (CurPageID = SummaryPage.ID) then
    UpdateInstallSummary();
  if CurPageID = wpInstalling then
  begin
    LayoutInstallOutputMemo();
    FlushInstallOutput();
  end;
end;

function NextButtonClick(CurPageID: Integer): Boolean;
begin
  Result := True;
  if (CurPageID = wpSelectDir) and
    ((ExistingInstallDir = '') or (IsProtectedInstallPath(ExistingInstallDir) and (not IsAdminInstallMode))) then
  begin
    if not IsNonSystemWritableInstallDir(WizardDirValue) then
    begin
      MsgBox(CustomMessage('NonSystemInstallRequired'), mbError, MB_OK);
      Result := False;
    end;
  end;
end;

function DirectoryHasEntries(const DirName: string): Boolean;
var
  FindRec: TFindRec;
begin
  Result := False;
  if not DirExists(DirName) then
    Exit;

  if FindFirst(AddBackslash(DirName) + '*', FindRec) then
  begin
    try
      repeat
        if (FindRec.Name <> '.') and (FindRec.Name <> '..') then
        begin
          Result := True;
          Exit;
        end;
      until not FindNext(FindRec);
    finally
      FindClose(FindRec);
    end;
  end;
end;

function PathLooksLikeUpgradeInstall(const DirName: string): Boolean;
begin
  Result :=
    FileExists(AddBackslash(DirName) + '{#MyAppExeName}') or
    FileExists(AddBackslash(DirName) + 'YMhutBoxDesktop.exe') or
    FileExists(AddBackslash(DirName) + 'installer-layout.txt') or
    FileExists(AddBackslash(DirName) + 'config\installer-layout.dat') or
    FileExists(AddBackslash(DirName) + 'config\installer-required-files.dat') or
    FileExists(AddBackslash(DirName) + 'config\install-manifest.ini');
end;

function IsVCRedistInstalled(): Boolean;
var
  Version: string;
  Installed: Cardinal;
begin
  Result :=
    (RegQueryDWordValue(HKLM64, VCRedistKey, 'Installed', Installed) and (Installed = 1) and RegQueryStringValue(HKLM64, VCRedistKey, 'Version', Version) and (Version <> '') and (Version <> '0.0.0.0')) or
    (RegQueryDWordValue(HKLM, VCRedistKey, 'Installed', Installed) and (Installed = 1) and RegQueryStringValue(HKLM, VCRedistKey, 'Version', Version) and (Version <> '') and (Version <> '0.0.0.0')) or
    (RegQueryDWordValue(HKLM32, VCRedistKey, 'Installed', Installed) and (Installed = 1) and RegQueryStringValue(HKLM32, VCRedistKey, 'Version', Version) and (Version <> '') and (Version <> '0.0.0.0'));
end;

function HasWebView2RuntimeExecutable(const Root: string): Boolean;
var
  FindRec: TFindRec;
begin
  Result := False;
  if not DirExists(Root) then
    Exit;

  if FileExists(AddBackslash(Root) + 'msedgewebview2.exe') then
  begin
    Result := True;
    Exit;
  end;

  if FindFirst(AddBackslash(Root) + '*', FindRec) then
  begin
    try
      repeat
        if ((FindRec.Attributes and DirectoryAttribute) <> 0) and
          FileExists(AddBackslash(Root) + FindRec.Name + '\msedgewebview2.exe') then
        begin
          Result := True;
          Exit;
        end;
      until not FindNext(FindRec);
    finally
      FindClose(FindRec);
    end;
  end;
end;

function IsWebView2RuntimeVersionInstalled(const RootKey: Integer; const SubKey: string): Boolean;
var
  Version: string;
begin
  Result :=
    (RegQueryStringValue(RootKey, SubKey, 'pv', Version) or
     RegQueryStringValue(RootKey, SubKey, 'Version', Version)) and
    (Version <> '') and
    (Version <> '0.0.0.0');
end;

function IsWebView2Installed(): Boolean;
begin
  Result :=
    IsWebView2RuntimeVersionInstalled(HKLM64, WebView2ClientKey) or
    IsWebView2RuntimeVersionInstalled(HKLM, WebView2ClientKey) or
    IsWebView2RuntimeVersionInstalled(HKLM32, WebView2ClientKey) or
    IsWebView2RuntimeVersionInstalled(HKCU, WebView2ClientKey) or
    IsWebView2RuntimeVersionInstalled(HKLM64, WebView2ClientKeyWow64) or
    IsWebView2RuntimeVersionInstalled(HKLM, WebView2ClientKeyWow64) or
    IsWebView2RuntimeVersionInstalled(HKLM32, WebView2ClientKeyWow64) or
    IsWebView2RuntimeVersionInstalled(HKCU, WebView2ClientKeyWow64) or
    IsWebView2RuntimeVersionInstalled(HKLM64, WebView2ClientStateKey) or
    IsWebView2RuntimeVersionInstalled(HKLM, WebView2ClientStateKey) or
    IsWebView2RuntimeVersionInstalled(HKLM32, WebView2ClientStateKey) or
    IsWebView2RuntimeVersionInstalled(HKCU, WebView2ClientStateKey) or
    IsWebView2RuntimeVersionInstalled(HKLM64, WebView2ClientStateKeyWow64) or
    IsWebView2RuntimeVersionInstalled(HKLM, WebView2ClientStateKeyWow64) or
    IsWebView2RuntimeVersionInstalled(HKLM32, WebView2ClientStateKeyWow64) or
    IsWebView2RuntimeVersionInstalled(HKCU, WebView2ClientStateKeyWow64) or
    HasWebView2RuntimeExecutable(ExpandConstant('{localappdata}\Microsoft\EdgeWebView\Application')) or
    HasWebView2RuntimeExecutable(ExpandConstant('{pf}\Microsoft\EdgeWebView\Application')) or
    HasWebView2RuntimeExecutable(ExpandConstant('{pf32}\Microsoft\EdgeWebView\Application')) or
    HasWebView2RuntimeExecutable(ExpandConstant('{localappdata}\Microsoft\EdgeCore')) or
    HasWebView2RuntimeExecutable(ExpandConstant('{pf}\Microsoft\EdgeCore')) or
    HasWebView2RuntimeExecutable(ExpandConstant('{pf32}\Microsoft\EdgeCore')) or
    HasWebView2RuntimeExecutable(ExpandConstant('{localappdata}\Microsoft\Edge\Application')) or
    HasWebView2RuntimeExecutable(ExpandConstant('{pf}\Microsoft\Edge\Application')) or
    HasWebView2RuntimeExecutable(ExpandConstant('{pf32}\Microsoft\Edge\Application'));
end;

function WaitForWebView2Installed(const Seconds: Integer): Boolean;
var
  Index: Integer;
begin
  for Index := 1 to Seconds do
  begin
    if IsWebView2Installed() then
    begin
      Result := True;
      Exit;
    end;
    Sleep(1000);
  end;

  Result := IsWebView2Installed();
end;

function WaitForVCRedistInstalled(const Seconds: Integer): Boolean;
var
  Index: Integer;
begin
  for Index := 1 to Seconds do
  begin
    if IsVCRedistInstalled() then
    begin
      Result := True;
      Exit;
    end;
    Sleep(1000);
  end;

  Result := IsVCRedistInstalled();
end;

function GetCustomMessageValue(const Key: string): string;
begin
  Result := CustomMessage(Key);
end;

function FormatCustomMessage(const Key, Value: string): string;
begin
  Result := FmtMessage(CustomMessage(Key), [Value]);
end;

function FormatCustomMessage2(const Key, Value1, Value2: string): string;
begin
  Result := FmtMessage(CustomMessage(Key), [Value1, Value2]);
end;

function DownloadAndInstallPrerequisite(const Url, FileName, Arguments, FriendlyName: string; var NeedsRestart: Boolean): Boolean;
var
  ResultCode: Integer;
  ExecutablePath: string;
begin
  Result := False;
  ExecutablePath := ExpandConstant('{tmp}\' + FileName);
  DownloadPage.Clear;
  DownloadPage.Add(Url, FileName, '');
  if not WizardSilent then
    DownloadPage.Show;
  try
    DownloadPage.Download;
  except
    if DownloadPage.AbortedByUser then
      RaiseException(FormatCustomMessage('DependencyDownloadCancelled', FriendlyName))
    else
      RaiseException(FormatCustomMessage2('DependencyDownloadFailed', FriendlyName, GetExceptionMessage));
  end;
  if not WizardSilent then
    DownloadPage.Hide;

  if not Exec(ExecutablePath, Arguments, '', SW_HIDE, ewWaitUntilTerminated, ResultCode) then
    RaiseException(FormatCustomMessage('DependencyStartFailed', FriendlyName));

  if (ResultCode <> 0) and (ResultCode <> 1638) and (ResultCode <> 3010) and (ResultCode <> 1641) then
    RaiseException(FormatCustomMessage2('DependencyInstallFailed', FriendlyName, IntToStr(ResultCode)));

  if (ResultCode = 3010) or (ResultCode = 1641) then
    NeedsRestart := True;

  Result := True;
end;

procedure CopyIfExists(const Source, TargetDir: string);
begin
  if FileExists(Source) then
  begin
    ForceDirectories(TargetDir);
    FileCopy(Source, AddBackslash(TargetDir) + ExtractFileName(Source), False);
  end;
end;

procedure MigrateLegacyData();
var
  TargetDir: string;
begin
  TargetDir := ExpandConstant('{app}\data\legacy-imports');
  CopyIfExists(ExpandConstant('{localappdata}\YMhut Box\settings.json'), TargetDir);
  CopyIfExists(ExpandConstant('{localappdata}\YMhut Box\shared_preferences.json'), TargetDir);
  CopyIfExists(ExpandConstant('{localappdata}\YMhut Box\app_state.db'), TargetDir);
  CopyIfExists(ExpandConstant('{userappdata}\ymhut_box\settings.json'), TargetDir);
  CopyIfExists(ExpandConstant('{userappdata}\ymhut_box\shared_preferences.json'), TargetDir);
  CopyIfExists(ExpandConstant('{userappdata}\ymhut_box\app_state.db'), TargetDir);
end;

procedure DeleteFilesByPattern(const DirName, Pattern: string; const Recursive: Boolean);
var
  FindRec: TFindRec;
  Child: string;
begin
  if not DirExists(DirName) then
    Exit;

  if FindFirst(AddBackslash(DirName) + Pattern, FindRec) then
  begin
    try
      repeat
        if ((FindRec.Attributes and DirectoryAttribute) = 0) then
          DeleteFile(AddBackslash(DirName) + FindRec.Name);
      until not FindNext(FindRec);
    finally
      FindClose(FindRec);
    end;
  end;

  if not Recursive then
    Exit;

  if FindFirst(AddBackslash(DirName) + '*', FindRec) then
  begin
    try
      repeat
        if (FindRec.Name <> '.') and (FindRec.Name <> '..') and
          ((FindRec.Attributes and DirectoryAttribute) <> 0) then
        begin
          Child := AddBackslash(DirName) + FindRec.Name;
          DeleteFilesByPattern(Child, Pattern, True);
        end;
      until not FindNext(FindRec);
    finally
      FindClose(FindRec);
    end;
  end;
end;

procedure CleanCultureFolders(const DirName: string);
var
  FindRec: TFindRec;
  CultureDir: string;
begin
  if not DirExists(DirName) then
    Exit;

  if FindFirst(AddBackslash(DirName) + '*', FindRec) then
  begin
    try
      repeat
        if (FindRec.Name <> '.') and (FindRec.Name <> '..') and
          ((FindRec.Attributes and DirectoryAttribute) <> 0) then
        begin
          CultureDir := AddBackslash(DirName) + FindRec.Name;
          if FileExists(AddBackslash(CultureDir) + 'Microsoft.ui.xaml.dll.mui') or
             FileExists(AddBackslash(CultureDir) + 'Microsoft.UI.Xaml.Phone.dll.mui') then
            DelTree(CultureDir, True, True, True);
        end;
      until not FindNext(FindRec);
    finally
      FindClose(FindRec);
    end;
  end;
end;

function ShouldPreserveAppEntry(const Name: string): Boolean;
var
  LowerName: string;
begin
  LowerName := Lowercase(Name);
  Result :=
    (LowerName = 'data') or
    (LowerName = 'feedbackpackages') or
    (Pos('unins', LowerName) = 1);
end;

procedure CleanOldApplicationPayload(const DirName: string);
var
  FindRec: TFindRec;
  EntryPath: string;
begin
  if not DirExists(DirName) then
    Exit;

  if FindFirst(AddBackslash(DirName) + '*', FindRec) then
  begin
    try
      repeat
        if (FindRec.Name <> '.') and (FindRec.Name <> '..') and
          (not ShouldPreserveAppEntry(FindRec.Name)) then
        begin
          EntryPath := AddBackslash(DirName) + FindRec.Name;
          if ((FindRec.Attributes and DirectoryAttribute) <> 0) then
            DelTree(EntryPath, True, True, True)
          else
            DeleteFile(EntryPath);
        end;
      until not FindNext(FindRec);
    finally
      FindClose(FindRec);
    end;
  end;
end;

procedure CleanLegacyInstallLayout();
var
  AppDir: string;
begin
  AppDir := ExpandConstant('{app}');
  CleanOldApplicationPayload(AppDir);
  DeleteFilesByPattern(AppDir, '*.winmd', True);
  DeleteFilesByPattern(AppDir, '*.pdb', True);
  DeleteFilesByPattern(AppDir, 'workloads*.json', True);
  DeleteFilesByPattern(AddBackslash(AppDir) + 'Assets\data', '*.json', True);
  DeleteFile(AddBackslash(AppDir) + 'installer-layout.txt');
  DeleteFile(AddBackslash(AppDir) + 'config\installer-layout.dat');
  DeleteFile(AddBackslash(AppDir) + 'config\installer-required-files.dat');
  DeleteFile(AddBackslash(AppDir) + 'Assets\data\reference-data.dat');
  CleanCultureFolders(AppDir);
end;

function PrepareToInstall(var NeedsRestart: Boolean): string;
var
  ResultCode: Integer;
begin
  AppendInstallOutput('Prepare system prerequisites.');
  Result := '';
  if (ExistingInstallDir <> '') and IsProtectedInstallPath(ExistingInstallDir) and (not IsAdminInstallMode) then
  begin
    Result := CustomMessage('ProtectedInstallNeedsAdmin');
    Exit;
  end;

  if IsProtectedInstallPath(WizardDirValue) and (not IsAdminInstallMode) then
  begin
    Result := CustomMessage('ProtectedInstallNeedsAdmin');
    Exit;
  end;

  try
    if not IsWebView2Installed() then
    begin
      DownloadAndInstallPrerequisite('{#WebView2BootstrapperUrl}', 'MicrosoftEdgeWebView2Setup.exe', '/silent /install', GetCustomMessageValue('WebView2RuntimeName'), NeedsRestart);

      if not WaitForWebView2Installed(60) then
      begin
        Result := GetCustomMessageValue('WebView2StillMissing');
        Exit;
      end;
    end;

    if (not IsVCRedistInstalled()) then
    begin
      DownloadAndInstallPrerequisite('{#VCRedistUrl}', 'vc_redist.x64.exe', '/install /quiet /norestart', GetCustomMessageValue('VCRuntimeName'), NeedsRestart);

      if not WaitForVCRedistInstalled(10) then
      begin
        Result := GetCustomMessageValue('VCRuntimeStillMissing');
        Exit;
      end;
    end;
  except
    Result := GetExceptionMessage;
  end;
end;

function ShouldSkipPage(PageID: Integer): Boolean;
begin
  Result := (PageID = wpSelectDir) and (ExistingInstallDir <> '') and
    ((not IsProtectedInstallPath(ExistingInstallDir)) or IsAdminInstallMode);
end;

procedure CurStepChanged(CurStep: TSetupStep);
begin
  if CurStep = ssInstall then
  begin
    LastInstallProgressPercent := -1;
    AppendInstallOutput(CustomMessage('InstallOutputClean'));
    CleanLegacyInstallLayout();
    AppendInstallOutput(CustomMessage('InstallOutputExtract'));
  end
  else if CurStep = ssPostInstall then
  begin
    FlushInstallOutput();
    AppendInstallOutput(CustomMessage('InstallOutputPostInstall'));
    MigrateLegacyData();
    AppendInstallOutput(CustomMessage('InstallOutputDone'));
  end;
end;

procedure CurInstallProgressChanged(CurProgress, MaxProgress: Integer);
var
  ProgressPercent: Integer;
begin
  if (MaxProgress > 0) then
  begin
    if (CurProgress >= MaxProgress) then
    begin
      if LastInstallProgressPercent <> 100 then
      begin
        LastInstallProgressPercent := 100;
        FlushInstallOutput();
      end;
      Exit;
    end;

    if (CurProgress > 0) then
    begin
      ProgressPercent := CurProgress * 100 div MaxProgress;
      if ProgressPercent <> LastInstallProgressPercent then
      begin
        LastInstallProgressPercent := ProgressPercent;
        FlushInstallOutput();
      end;
    end;
  end;
end;
