#define AppId "winscp3"
#define AppMutex "WinSCP"
#define ParentRegistryKey "Software\Martin Prikryl"
#define RegistryKey ParentRegistryKey+"\WinSCP 2"
#define DefaultLang "en"
#define WebRoot "https://winscp.net/"
#define WebForum WebRoot+"forum/"
#define WebDocumentation WebRoot+"eng/docs/"
#define WebReport "https://winscp.net/install.php"
#define Year 2023
#define EnglishLang "English"
#define SetupTypeData "SetupType"
#define InnoSetupReg "Software\Microsoft\Windows\CurrentVersion\Uninstall\" + AppId + "_is1"
#define InnoSetupAppPathReg "Inno Setup: App Path"

#ifndef CompletenessThreshold
  #define CompletenessThreshold 100
#else
  #define CompletenessThreshold Int(CompletenessThreshold)
#endif

#ifndef InclusionThreshold
  #define InclusionThreshold 100
#else
  #define InclusionThreshold Int(InclusionThreshold)
#endif

#ifndef PuttySourceDir
  #if DirExists("c:\Program Files (x86)\PuTTY")
    #define PuttySourceDir "c:\Program Files (x86)\PuTTY"
  #else
    #define PuttySourceDir "c:\Program Files\PuTTY"
  #endif
#endif
#ifndef Status
  #define Status "unofficial"
#endif
#ifndef SourceDir
  #define SourceDir "..\source"
#endif
#ifndef BinariesDir
  #define BinariesDir SourceDir + "\Win32\Release"
#endif
#ifndef BinariesDir64
  #define BinariesDir64 SourceDir + "\Win64\Release"
#endif
#ifndef BinariesDirAssembly
  #define BinariesDirAssembly "..\dotnet\Win32\Release"
#endif
#ifndef AllTranslations
  #define AllTranslations
#endif

#define TranslationDir "translations"
#define OutputDir "."

#define TranslationFileMask "WinSCP.???"
#define MainFileName "WinSCP.exe"
#define MainFileSource BinariesDir+"\"+MainFileName
#define ShellExtFileName "DragExt.dll"
#define ShellExtFileSource BinariesDir+"\"+ShellExtFileName
#define ShellExt64FileName "DragExt64.dll"
#define ShellExt64FileSource BinariesDir64+"\"+ShellExt64FileName
#define ConsoleFileSource BinariesDir+"\WinSCP.com"
#define MapFileSource BinariesDir+"\WinSCP.map"
#define AssemblyFileSource BinariesDirAssembly+"\WinSCPnet.dll"

#ifdef Donations
#define PayPalCardImage "PayPalCard.bmp"
#endif

#define Major
#define Minor
#define Rev
#define Build
#expr GetVersionComponents(MainFileSource, Major, Minor, Rev, Build)
#define VersionOnly Str(Major)+"."+Str(Minor)+(Rev > 0 ? "."+Str(Rev) : "")
#define Version VersionOnly+(Status != "" ? " "+Status : "")

#ifndef BaseFilename
  #define FTag VersionOnly+(Status != "" ? "."+Status : "")
  #define BaseFilename "WinSCP-" + FTag + "-Setup"
#endif

#define WebArguments "ver=" +VersionOnly + "&lang={language}&utm_source=winscp&utm_medium=setup&utm_campaign=" + VersionOnly
#define WebGettingStarted WebRoot + "eng/installed.php?" + WebArguments + "&prevver="

#define MessagesPath(L) TranslationDir + "\" + "WinSCP." + L + ".islu"

#define ExplorerFileBase "Explorer"
#define CommanderFileBase "Commander"
#define SelectDirFileBase "Opened bookmark folder-stored session folder"

[Setup]
AppId={#AppId}
AppName=WinSCP
AppPublisher=Martin Prikryl
AppPublisherURL={#WebRoot}
AppSupportURL={#WebForum}
AppUpdatesURL={#WebRoot}eng/download.php
VersionInfoCompany=Martin Prikryl
VersionInfoDescription=Setup for WinSCP {#Version} (SFTP, FTP, WebDAV and SCP client)
VersionInfoVersion={#Major}.{#Minor}.{#Rev}.{#Build}
VersionInfoTextVersion={#Version}
VersionInfoCopyright=(c) 2000-{#Year} Martin Prikryl
VersionInfoOriginalFileName={#BaseFilename}.exe
DefaultDirName={autopf}\WinSCP
LicenseFile=license.setup.txt
UninstallDisplayIcon={app}\WinSCP.exe
OutputDir={#OutputDir}
DisableStartupPrompt=yes
AppVersion={#Version}
AppVerName=WinSCP {#Version}
OutputBaseFilename={#BaseFilename}
SolidCompression=yes
#ifdef ImagesDir
WizardImageFile={#ImagesDir}\Tall *.bmp
WizardSmallImageFile={#ImagesDir}\Square *.bmp
#endif
ShowTasksTreeLines=yes
PrivilegesRequired=admin
PrivilegesRequiredOverridesAllowed=commandline dialog
ShowLanguageDialog=auto
UsePreviousLanguage=no
DisableProgramGroupPage=yes
SetupIconFile=winscpsetup.ico
DisableDirPage=no
WizardStyle=modern
; We do not want the Explorer restarts as that is not pleasant to the user
CloseApplications=no
UsedUserAreasWarning=no
#ifdef Sign
SignTool=sign $f "WinSCP Installer" https://winscp.net/eng/docs/installation
#endif

[Languages]
; English has to be first so that it is pre-selected
; on Setup Select Language window, when no translation matching
; Windows UI locale is available
Name: {#DefaultLang}; MessagesFile: {#MessagesPath(DefaultLang)}

#define FindHandle
#dim Languages[200]
#define LanguageCount 0
#define AnyLanguageComplete 0
#define LangI
; For some reason the variable cannot be defined near the code where we use it
#define AllTranslationsBuf

#sub ProcessTranslationFile

  #define FileName FindGetFileName(FindHandle)
  #define Lang Copy(FileName, Pos(".", FileName)+1)

  #define LangNameFull ReadIni(MessagesPath(Lang), "LangOptions", "LanguageName")
  #define Sep Pos(" -", LangNameFull)
  #if Sep > 0
    #define LangName Copy(LangNameFull, 1, Sep - 1)
  #else
    #define LangName LangNameFull
  #endif
  #define LangID ReadIni(MessagesPath(Lang), "LangOptions", "LanguageID")
  #define LangCompleteness Int(ReadIni(MessagesPath(Lang), "CustomOptions", "TranslationCompleteness"))

  #expr Languages[LanguageCount*4] = Lang
  ; Not used atm
  #expr Languages[LanguageCount*4+1] = LangName
  ; Not used atm
  #expr Languages[LanguageCount*4+2] = LangID
  #expr Languages[LanguageCount*4+3] = LangCompleteness
  #expr LanguageCount++

#if LangCompleteness >= CompletenessThreshold
Name: {#Lang}; MessagesFile: {#MessagesPath(Lang)}
  #expr AnyLanguageComplete = 1
#endif

#endsub /* sub ProcessTranslationFile */

#if FindHandle = FindFirst(TranslationDir + "\" + TranslationFileMask, 0)
  #define FResult 1
  #for {0; FResult; FResult = FindNext(FindHandle)} ProcessTranslationFile
  #expr FindClose(FindHandle)
#endif

; Types are not used anymore, they are preserved only to let setup
; detect previous installation type and decide between typical/custom setup
[Types]
Name: full; Description: "full"
Name: compact; Description: "compact"
Name: custom; Description: "custom"; Flags: iscustom

[Components]
Name: main; Description: {cm:ApplicationComponent}; \
  Types: full custom compact; Flags: fixed
; Because the files for the component have Check parameters, they are ignored for the size calculation
Name: shellext; Description: {cm:ShellExtComponent}; \
  ExtraDiskSpaceRequired: {#Max(FileSize(ShellExtFileSource), FileSize(ShellExt64FileSource))}; \
  Types: full compact;
Name: pageant; Description: {cm:PageantComponent}; \
  Types: full
Name: puttygen; Description: {cm:PuTTYgenComponent}; \
  Types: full
#if AnyLanguageComplete == 1
Name: transl; Description: {cm:TranslationsComponent}; \
  Types: full
#endif

[Tasks]
Name: enableupdates; Description: {cm:EnableUpdates}
Name: enableupdates\enablecollectusage; Description: {cm:EnableCollectUsage}
; Windows integration
Name: desktopicon; Description: {cm:DesktopIconTask}
Name: sendtohook; Description: {cm:SendToHookTask}
Name: urlhandler; Description: {cm:RegisterAsUrlHandlers}
Name: searchpath; Description: {cm:AddSearchPath}; \
  Flags: unchecked; Check: IsAdminInstallMode

[Icons]
Name: "{autoprograms}\WinSCP"; Filename: "{app}\WinSCP.exe"; Components: main; \
  Comment: "{cm:ProgramComment2}"
; This is created when desktopicon task is selected
Name: "{autodesktop}\WinSCP"; Filename: "{app}\WinSCP.exe"; \
  Tasks: desktopicon; Comment: "{cm:ProgramComment2}"
; This is created when sendtohook task is selected
Name: "{usersendto}\{cm:SendToHookNew}"; Filename: "{app}\WinSCP.exe"; \
  Parameters: "/upload"; Tasks: sendtohook

[InstallDelete]
; Remove pre-5.8.2 PuTTY help file
Type: files; Name: "{app}\PuTTY\putty.hlp"
; Remove pre-524 licence file (without .txt extension)
Type: files; Name: "{app}\license"
; Remove pre-520 start menu folders
Type: filesandordirs; Name: "{commonprograms}\WinSCP"
Type: filesandordirs; Name: "{userprograms}\WinSCP"; Check: HasUserPrograms

[Run]
Filename: "{app}\WinSCP.exe"; Parameters: "/RegisterForDefaultProtocols"; \
  StatusMsg: {cm:RegisteringAsUrlHandlers}; Tasks: urlhandler
Filename: "{app}\WinSCP.exe"; Parameters: "/AddSearchPath"; \
  StatusMsg: {cm:AddingSearchPath}; Tasks: searchpath
Filename: "{app}\WinSCP.exe"; Parameters: "/ImportSitesIfAny"; \
  StatusMsg: {cm:ImportSites}; Flags: skipifsilent

[UninstallDelete]
; These additional files are created by application
Type: files; Name: "{app}\WinSCP.ini"
Type: files; Name: "{app}\WinSCP.cgl"

[Files]
#ifdef ImagesDir
; Put these to the top as we extract them on demand and
; that can take long with solid compression enabled
Source: "{#ImagesDir}\{#ExplorerFileBase} *.bmp"; Flags: dontcopy
Source: "{#ImagesDir}\{#CommanderFileBase} *.bmp"; Flags: dontcopy
Source: "{#ImagesDir}\{#SelectDirFileBase} *.bmp"; Flags: dontcopy
#ifdef Donations
Source: "{#ImagesDir}\{#PayPalCardImage}"; Flags: dontcopy
#endif
#endif
Source: "{#MainFileSource}"; DestDir: "{app}"; \
  Components: main; Flags: ignoreversion
Source: "{#ConsoleFileSource}"; DestDir: "{app}"; \
  Components: main; Flags: ignoreversion
Source: "{#MapFileSource}"; DestDir: "{app}"; \
  Components: main; Flags: ignoreversion
Source: "{#AssemblyFileSource}"; DestDir: "{app}"; \
  Components: main; Flags: ignoreversion
Source: "license.txt"; DestDir: "{app}"; \
  Components: main; Flags: ignoreversion
; If the Check is ever removed, remove the ExtraDiskSpaceRequired parameter of the component too
Source: "{#ShellExtFileSource}"; DestDir: "{app}"; \
  Components: shellext; \
  Flags: regserver restartreplace uninsrestartdelete ignoreversion; \
  Check: not IsWin64 and ShouldInstallShellExt(ExpandConstant('{app}\{#ShellExtFileName}'), '{#GetVersionNumbersString(ShellExtFileSource)}')
Source: "{#ShellExt64FileSource}"; DestDir: "{app}"; \
  Components: shellext; \
  Flags: regserver restartreplace uninsrestartdelete ignoreversion; \
  Check: IsWin64 and ShouldInstallShellExt(ExpandConstant('{app}\{#ShellExt64FileName}'), '{#GetVersionNumbersString(ShellExt64FileSource)}')
Source: "{#PuttySourceDir}\LICENCE"; DestDir: "{app}\PuTTY"; \
  Components: pageant puttygen; Flags: ignoreversion
Source: "{#PuttySourceDir}\putty.chm"; DestDir: "{app}\PuTTY"; \
  Components: pageant puttygen; Flags: ignoreversion
Source: "{#PuttySourceDir}\pageant.exe"; DestDir: "{app}\PuTTY"; \
  Components: pageant; Flags: ignoreversion
Source: "{#PuttySourceDir}\puttygen.exe"; DestDir: "{app}\PuTTY"; \
  Components: puttygen; Flags: ignoreversion
#ifdef ExtensionsDir
Source: "{#ExtensionsDir}\*.*"; DestDir: "{app}\Extensions"
#endif
#ifdef Sponsor
Source: "{#Sponsor}\*.*"; Flags: dontcopy skipifsourcedoesntexist

  #define SponsorImages
  #if FindHandle = FindFirst(Sponsor + "\*.*", 0)
    #define FResult 1
    #for {0; FResult; FResult = FindNext(FindHandle)} SponsorImages = SponsorImages + FindGetFileName(FindHandle) + ","
    #expr FindClose(FindHandle)
    #expr SponsorImages = Copy(SponsorImages, 1, Len(SponsorImages) - 1)
  #endif
#endif

[Registry]
Root: HKCU; Subkey: "{#ParentRegistryKey}"; Flags: uninsdeletekeyifempty
Root: HKCU; Subkey: "{#RegistryKey}"; Flags: uninsdeletekeyifempty
; Norton Commander interface
Root: HKCU; SubKey: "{#RegistryKey}\Configuration\Interface"; ValueType: dword; \
  ValueName: "Interface"; ValueData: 0; Check: UserSettings(1)
Root: HKLM; SubKey: "{#RegistryKey}"; ValueType: dword; \
  ValueName: "DefaultInterfaceInterface"; ValueData: 0; \
  Check: UserSettings(1) and IsAdminInstallMode; Flags: noerror
; Explorer-like interface
Root: HKCU; SubKey: "{#RegistryKey}\Configuration\Interface"; ValueType: dword; \
  ValueName: "Interface"; ValueData: 1; Check: not UserSettings(1)
Root: HKLM; SubKey: "{#RegistryKey}"; ValueType: dword; \
  ValueName: "DefaultInterfaceInterface"; ValueData: 1; \
  Check: (not UserSettings(1)) and IsAdminInstallMode; Flags: noerror
; If installer enabled ddext, let it reset the settings on uninstall,
; so the default is used on the next run
Root: HKCU; SubKey: "{#RegistryKey}\Configuration\Interface"; ValueType: dword; \
  ValueName: "DDExtEnabled"; ValueData: 1; Components: shellext; \
  Flags: uninsdeletevalue
; Updates
Root: HKCU; SubKey: "{#RegistryKey}\Configuration\Interface\Updates"; \
  ValueType: dword; ValueName: "Period"; ValueData: 7; \
  Tasks: enableupdates; Check: not UpdatesEnabled
Root: HKLM; SubKey: "{#RegistryKey}"; \
  ValueType: dword; ValueName: "DefaultUpdatesPeriod"; ValueData: 7; \
  Tasks: enableupdates; Flags: noerror; Check: IsAdminInstallMode
Root: HKLM; SubKey: "{#RegistryKey}"; \
  ValueType: dword; ValueName: "DefaultCollectUsage"; ValueData: 1; \
  Tasks: enableupdates\enablecollectusage; Flags: noerror; Check: IsAdminInstallMode

#sub EmitLang

  #if Languages[LangI*4+3] >= InclusionThreshold

[Files]
Source: "{#TranslationDir}\WinSCP.{#Languages[LangI*4]}"; DestDir: "{app}\Translations"; \
  Components: transl; Flags: ignoreversion

  #endif

#endsub /* sub EmitLang */

#for {LangI = 0; LangI < LanguageCount; LangI++} EmitLang

; Delete translations from installation root folder (pre-5.10)
[InstallDelete]
#expr AllTranslationsBuf = AllTranslations + '-'

#sub DeleteRootTranslation
  #define P Pos('-', AllTranslationsBuf)
  #define Lang Copy(AllTranslationsBuf, 1, P - 1)
  #expr AllTranslationsBuf = Copy(AllTranslationsBuf, P + 1)
Type: files; Name: "{app}\WinSCP.{#Lang}"
#endsub

#for { 0; Len(AllTranslationsBuf) > 0; 0 } DeleteRootTranslation

[UninstallRun]
; Make sure no later uninstall task recreate the configuration
Filename: "{app}\WinSCP.exe"; Parameters: "/UninstallCleanup"; \
  RunOnceId: "UninstallCleanup"
Filename: "{app}\WinSCP.exe"; Parameters: "/RemoveSearchPath"; \
  RunOnceId: "RemoveSearchPath"
Filename: "{app}\WinSCP.exe"; Parameters: "/UnregisterForProtocols"; \
  RunOnceId: "UnregisterForProtocols"

[Code]
const
  NewLine = #13#10;

var
  TypicalTypeButton: TRadioButton;
  CustomTypeButton: TRadioButton;
  CommanderRadioButton: TRadioButton;
  ExplorerRadioButton: TRadioButton;
  LaunchCheckbox: TCheckbox;
  OpenGettingStartedCheckbox: TCheckbox;
  AreUpdatesEnabled: Boolean;
  AutomaticUpdate: Boolean;
  Upgrade: Boolean;
  PrevVersion: string;
  ShellExtNewerCacheFileName: string;
  ShellExtNewerCacheResult: Boolean;
  ShellExtNoRestart: Boolean;
#ifdef Donations
  DonationPanel: TPanel;
  AboutDonationCaption: TLabel;
#endif
  InstallationDone: Boolean;
  LicenseAccepted: Boolean;
  InitDir: string;
  InitComponents: string;
  InitTasks: string;
  InitInterface: Integer;
  Donated: Boolean;
  InterfacePage: TWizardPage;
  SetupTypePage: TWizardPage;
#ifdef Sponsor
  SponsorReq: Variant;
  SponsorPage: TWizardPage;
  Sponsor: string;
  SponsorStatus: string;
#endif

procedure ShowMessage(Text: string);
begin
  MsgBox(Text, mbInformation, MB_OK);
end;

#ifdef Sponsor
function IsWinVista: Boolean;
begin
  Result := (GetWindowsVersion >= $06000000);
end;
#endif

procedure CutVersionPart(var VersionString: string; var VersionPart: Word);
var
  P: Integer;
begin
  P := Pos('.', VersionString);
  if P > 0 then
  begin
    VersionPart := StrToIntDef(Copy(VersionString, 1, P - 1), 0);
    Delete(VersionString, 1, P);
  end
    else
  begin
    VersionPart := StrToIntDef(VersionString, 0);
    VersionString := '';
  end;
end;

function ShouldInstallShellExt(FileName: string; InstalledVersion: string): Boolean;
var
  ExistingMS, ExistingLS: Cardinal;
  ExistingMajor, ExistingMinor, ExistingRev, ExistingBuild: Cardinal;
  InstalledMajor, InstalledMinor, InstalledRev, InstalledBuild: Word;
begin
  if ShellExtNewerCacheFileName = FileName then
  begin
    if ShellExtNewerCacheResult then
    begin
      Log(Format('Allowing installation of shell extension %s as already decided', [FileName]));
      Result := True;
    end
      else
    begin
      Log(Format('Skipping installation of shell extension %s as already decided', [FileName]));
      Result := False;
    end;
    // Keeping ShellExtNoRestart value
  end
    else
  if not FileExists(FileName) then
  begin
    Log(Format('Shell extension %s does not exist yet, allowing installation', [FileName]));
    ShellExtNoRestart := False;
    Result := True;
  end
    else
  if not GetVersionNumbers(FileName, ExistingMS, ExistingLS) then
  begin
    Log(Format('Cannot retrieve version of existing shell extension %s, allowing installation', [FileName]));
    ShellExtNoRestart := False;
    Result := True;
  end
    else
  begin
    ExistingMajor := ExistingMS shr 16;
    ExistingMinor := ExistingMS and $FFFF;
    ExistingRev := ExistingLS shr 16;
    ExistingBuild := ExistingLS and $FFFF;
    Log(Format('Existing shell extension %s version: %d.%d.%d[.%d]', [FileName, ExistingMajor, ExistingMinor, ExistingRev, ExistingBuild]));

    Log(Format('Installed extension version string: %s', [InstalledVersion]));
    CutVersionPart(InstalledVersion, InstalledMajor);
    CutVersionPart(InstalledVersion, InstalledMinor);
    CutVersionPart(InstalledVersion, InstalledRev);
    CutVersionPart(InstalledVersion, InstalledBuild);
    Log(Format('Installed extension version: %d.%d.%d[.%d]', [InstalledMajor, InstalledMinor, InstalledRev, InstalledBuild]));

    if (InstalledMajor <> ExistingMajor) or
       ((ExistingMajor = 1) and (ExistingMinor <= 1)) then
    begin
      // Still on 1.x, so this won't be used when upgrading,
      // but it will be useful, if downgrading from future version with a different major version.
      if InstalledMajor <> ExistingMajor then
      begin
        Log('Existing extension has different major version, allowing installation, and will require restart, if it is locked.')
      end
        else
      begin
        // 1.1 uses Ansi encoding, and is incompatible with 1.2 and newer which uses Unicode
        Log('Existing extension is 1.1 or older, allowing installation, and will require restart, if it is locked.');
      end;

      Result := True;
      ShellExtNoRestart := False;
    end
      else
    if (InstalledMinor > ExistingMinor) or
       ((InstalledMinor = ExistingMinor) and (InstalledRev > ExistingRev)) then
    begin
      Log('Installed extension is newer than existing extension, but major version is the same, allowing installation, but we will delay replacing the extension until the next system start, if it is locked.');
      Result := True;
      ShellExtNoRestart := True;
    end
      else
    begin
      Log('Installed extension is same or older than existing extension (but the same major version), skipping installation');
      ShellExtNoRestart := False;
      Result := False;
    end;
  end;

  ShellExtNewerCacheFileName := FileName;
  ShellExtNewerCacheResult := Result;
end;

function UpdatesEnabled: Boolean;
begin
  Result := AreUpdatesEnabled;
end;

function UserSettings(Settings: Integer): Boolean;
begin
  case Settings of
    1: Result := CommanderRadioButton.Checked;
    else Result := False;
  end;
end;

function LanguageCompleteness(Lang: string): Integer;
begin
  #sub EmitLang4
  if (Lang = '{#Languages[LangI*4]}') then
  begin
    Result := {#Languages[LangI*4+3]};
  end
    else
  #endsub /* sub EmitLang4 */

  #for {LangI = 0; LangI < LanguageCount; LangI++} EmitLang4

  // used also for the default language
  Result := 100;
end;

procedure OpenBrowser(Url: string);
var
  ErrorCode: Integer;
begin
  ShellExec('open', Url, '', '', SW_SHOWNORMAL, ewNoWait, ErrorCode);
end;

function IsRestartPage: Boolean;
begin
  Result := WizardForm.YesRadio.Visible;
end;

#ifdef Sponsor
var
  SponsoringClicked: Boolean;

procedure SponsoringLinkLabelClick(Sender: TObject);
begin
  SponsoringClicked := True;
  OpenBrowser('{#WebReport}?mode=sponsoring' + Format('&sponsor=%s&', [Sponsor]) + ExpandConstant('{#WebArguments}'));
end;
#endif

procedure OpenHelp;
var
  HelpKeyword: string;
begin
  HelpKeyword := 'ui_installer'; // default

  case WizardForm.CurPageID of
    wpLicense:
      HelpKeyword := 'ui_installer_license';

    wpSelectDir:
      HelpKeyword := 'ui_installer_selectdir';

    wpSelectComponents:
      HelpKeyword := 'ui_installer_selectcomponents';

    wpSelectTasks:
      HelpKeyword := 'ui_installer_selecttasks';

    wpReady:
      HelpKeyword := 'ui_installer_ready';

    wpFinished:
      HelpKeyword := 'ui_installer_finished';

    SetupTypePage.ID:
      HelpKeyword := 'ui_installer_setuptype';

    InterfacePage.ID:
      HelpKeyword := 'ui_installer_interface';

#ifdef Sponsor
    SponsorPage.ID:
      begin
        SponsoringLinkLabelClick(nil);
        Exit;
      end;
#endif
  end;

  OpenBrowser('{#WebDocumentation}' + HelpKeyword + '?' + ExpandConstant('{#WebArguments}'));
end;

procedure HelpButtonClick(Sender: TObject);
begin
  OpenHelp;
end;

procedure FormKeyDown(Sender: TObject; var Key: Word; Shift: TShiftState);
begin
  if Key = 112 { VK_F1 } then
  begin
    OpenHelp;
    Key := 0;
  end;
end;

procedure CaptionClick(Sender: TObject);
begin
  WizardForm.ActiveControl := TLabel(Sender).FocusControl;
end;

procedure ImageClick(Sender: TObject);
begin
  WizardForm.ActiveControl := TWinControl(TControl(Sender).Tag);
end;

function WillRestart: Boolean;
begin
  Result := WizardForm.YesRadio.Visible and WizardForm.YesRadio.Checked;
end;

procedure UpdatePostInstallRunCheckboxes(Sender: TObject);
begin
  LaunchCheckbox.Enabled := not WillRestart;
  OpenGettingStartedCheckbox.Enabled :=
    LaunchCheckbox.Enabled
end;

procedure LinkLabel(Control: TLabel);
begin
  Control.ParentFont := True;
  Control.Font.Style := Control.Font.Style + [fsUnderline];
  Control.Font.Color := clBlue;
  Control.Cursor := crHand;
end;

function IsTypicalInstallation: Boolean;
begin
  Result := TypicalTypeButton.Checked;
end;

#ifdef Donations

procedure AboutDonationsLinkClick(Sender: TObject);
begin
  OpenBrowser('{#WebRoot}eng/donate.php?' + ExpandConstant('{#WebArguments}'));
  Donated := true;
end;

procedure DonateLinkClick(Sender: TObject);
var
  Control: TControl;
begin
  Control := TControl(Sender);
  OpenBrowser('{#WebRoot}eng/donate.php?amount=' + IntToStr(Control.Tag) + '&currency=' + CustomMessage('Currency') + '&' + ExpandConstant('{#WebArguments}'));
  Donated := true;
end;

procedure CreateDonateLink(Amount: Integer; var Top: Integer);
var
  Caption: TLabel;
begin
  Caption := TLabel.Create(DonationPanel);
  Caption.Left := 0;
  Caption.Top := Top;
  Caption.Tag := Amount;
  Caption.Parent := DonationPanel;
  Caption.Caption := Format(CustomMessage('Donate'), ['$' + IntToStr(Amount)]);
  Caption.OnClick := @DonateLinkClick;
  LinkLabel(Caption);
  Top := Top + ScaleY(16);
end;

#endif

procedure LoadBitmap(Image: TBitmapImage; FileName: string);
var
  Bitmap: TBitmap;
begin
  Bitmap := TBitmap.Create();
  Bitmap.AlphaFormat := afDefined;
  Bitmap.LoadFromFile(FileName);
  Image.Bitmap := Bitmap;
  Bitmap.Free;
end;

procedure LoadEmbededBitmap(Image: TBitmapImage; Name: string);
var
  FileName: string;
begin
  ExtractTemporaryFile(Name);
  FileName := ExpandConstant('{tmp}\' + Name);
  LoadBitmap(Image, FileName);
  // we won't need this anymore
  DeleteFile(FileName);
end;

function GetScalingFactor: Integer;
begin
  if WizardForm.Font.PixelsPerInch >= 192 then Result := 200
    else
  if WizardForm.Font.PixelsPerInch >= 144 then Result := 150
    else
  if WizardForm.Font.PixelsPerInch >= 120 then Result := 125
    else Result := 100;
end;

procedure LoadEmbededScaledIcon(Image: TBitmapImage; NameBase: string; SizeBase: Integer);
var
  Name: String;
begin
  Name := Format('%s %d.bmp', [NameBase, SizeBase * GetScalingFactor div 100]);
  LoadEmbededBitmap(Image, Name);
  Image.AutoSize := True;
end;

// WORKAROUND
// Checkboxes and Radio buttons created on runtime do
// not scale their height automatically
procedure ScaleFixedHeightControl(Control: TButtonControl);
begin
  Control.Height := ScaleY(Control.Height);
end;

function GetBottom(Control: TControl): Integer;
begin
  Result := Control.Top + Control.Height;
end;

function GetRight(Control: TControl): Integer;
begin
  Result := Control.Left + Control.Width;
end;

function CmdLineParamExists(const Value: string): Boolean;
var
  I: Integer;
begin
  Result := False;
  for I := 1 to ParamCount do
  begin
    if CompareText(ParamStr(I), Value) = 0 then
    begin
      Result := True;
      Exit;
    end;
  end;
end;

function InitializeSetup: Boolean;
var
  WaitInterval: Integer;
  Wait: Integer;
begin
  AutomaticUpdate := CmdLineParamExists('/AutomaticUpdate');
  if AutomaticUpdate then
  begin
    Log('Automatic update');

    Wait := 10000;
  end
    else
  begin
    Wait := 0;
  end;

  WaitInterval := 250;
  while (Wait > 0) and CheckForMutexes('{#AppMutex}') do
  begin
    Log('Application is still running, waiting');
    Sleep(WaitInterval);
    Wait := Wait - WaitInterval;
  end;

  while CheckForMutexes('{#AppMutex}') do
  begin
    if MsgBox(
         FmtMessage(SetupMessage(msgSetupAppRunningError), ['WinSCP']),
         mbError, MB_OKCANCEL) <> IDOK then
    begin
      Abort;
    end;
  end;

  if IsMsiProductInstalled('{029F9450-CFEF-4408-A2BB-B69ECE29EB18}', 0) and
     (not CmdLineParamExists('/OverrideMsi')) then
  begin
    MsgBox(CustomMessage('MsiInstallation'), mbError, MB_OK);
    Abort;
  end;

  Result := True;
end;

// Keep in sync with similar function on Preferences dialog
function Bullet(S: string): string;
begin
  if Copy(S, 1, 1) = '-' then S := #$2022'  ' + Trim(Copy(S, 2, Length(S) - 1));
  Result := S;
end;

procedure InitializeWizard;
var
  UserInterface: Cardinal;
  UpdatesPeriod: Cardinal;
  Caption: TLabel;
#ifdef ImagesDir
  Image: TBitmapImage;
#endif
  HelpButton: TButton;
#ifdef Donations
  P: Integer;
#ifdef ImagesDir
  P2: Integer;
#endif
#endif
  S: string;
  Completeness: Integer;
begin
  InstallationDone := False;
  LicenseAccepted := False;
  InitInterface := -1;

  Upgrade :=
    // We may want to use HKA to work correctly with side-by-side installations.
    // But as Updade is really used for changing REGISTRY configuration options only,
    // which are shared between all-users and current-user installations, it does not really matter.
    RegQueryStringValue(HKLM, '{#InnoSetupReg}', '{#InnoSetupAppPathReg}', S) or
    RegQueryStringValue(HKCU, '{#InnoSetupReg}', '{#InnoSetupAppPathReg}', S);

  if Upgrade then
  begin
    Log('Upgrade');
  end
    else
  begin
    Log('New installation');
  end;

  if Upgrade and GetVersionNumbersString(AddBackslash(WizardDirValue) + '{#MainFileName}', PrevVersion) and
     (PrevVersion[2] = '.') and (PrevVersion[4] = '.') and (PrevVersion[6] = '.') then
  begin
    PrevVersion := Copy(PrevVersion, 1, 5);
  end;

  WizardForm.KeyPreview := True;
  WizardForm.OnKeyDown := @FormKeyDown;

  // allow installation without requiring user to accept license
  WizardForm.LicenseAcceptedRadio.Checked := True;
  WizardForm.LicenseAcceptedRadio.Visible := False;
  WizardForm.LicenseNotAcceptedRadio.Visible := False;
  WizardForm.LicenseMemo.Height :=
    GetBottom(WizardForm.LicenseNotAcceptedRadio) -
    WizardForm.LicenseMemo.Top - ScaleY(5);

  // hide installation types combo
  WizardForm.TypesCombo.Visible := False;
  WizardForm.ComponentsList.Height :=
    GetBottom(WizardForm.ComponentsList) -
    WizardForm.TypesCombo.Top;
  WizardForm.ComponentsList.Top := WizardForm.TypesCombo.Top;

  // add help button
  HelpButton := TButton.Create(WizardForm);
  HelpButton.Parent := WizardForm;
  HelpButton.Anchors := [akLeft, akBottom];
  HelpButton.Left := WizardForm.ClientWidth - GetRight(WizardForm.CancelButton);
  HelpButton.Top := WizardForm.CancelButton.Top;
  HelpButton.Width := WizardForm.CancelButton.Width;
  HelpButton.Height := WizardForm.CancelButton.Height;
  HelpButton.Caption := CustomMessage('HelpButton');
  HelpButton.OnClick := @HelpButtonClick;

  Completeness := LanguageCompleteness(ActiveLanguage);
  if (Completeness < 100) and (not WizardSilent) then
  begin
    ShowMessage(FmtMessage(CustomMessage('IncompleteTranslation'), [IntToStr(Completeness)]));
  end;

  // installation type page

  SetupTypePage := CreateCustomPage(wpLicense,
    CustomMessage('SetupTypeTitle'),
    CustomMessage('SetupTypePrompt'));

  TypicalTypeButton := TRadioButton.Create(SetupTypePage);
  if not Upgrade then
    S := CustomMessage('TypicalType')
  else
    S := CustomMessage('TypicalUpgradeType');
  TypicalTypeButton.Caption :=
    FmtMessage(CustomMessage('Recommended'), [S]);
  // check typical install, if typical install was installed before or
  // when version without setup type support was installed with
  // "full" installation or when there were no installation before
  // ("full" installation is default)
  TypicalTypeButton.Checked :=
    ((GetPreviousData('{#SetupTypeData}', '') = 'typical')) or
    ((GetPreviousData('{#SetupTypeData}', '') = '') and
     (WizardSetupType(False) = 'full'));
  TypicalTypeButton.Left := ScaleX(4);
  TypicalTypeButton.Width := SetupTypePage.SurfaceWidth -
    TypicalTypeButton.Left;
  ScaleFixedHeightControl(TypicalTypeButton);
  TypicalTypeButton.Parent := SetupTypePage.Surface;

  Caption := TLabel.Create(SetupTypePage);
  Caption.WordWrap := True;
  if not Upgrade then
  begin
    Caption.Caption :=
      Bullet(CustomMessage('TypicalType1')) + NewLine +
      Bullet(CustomMessage('TypicalType2')) + NewLine +
      Bullet(CustomMessage('TypicalType3'));
  end
    else
  begin
    Caption.Caption :=
      Bullet(CustomMessage('TypicalUpgradeType1'));
  end;
  Caption.Left := ScaleX(4) + ScaleX(20);
  Caption.Width := SetupTypePage.SurfaceWidth - Caption.Left;
  Caption.Top := GetBottom(TypicalTypeButton) + ScaleY(6);
  Caption.Parent := SetupTypePage.Surface;
  Caption.FocusControl := TypicalTypeButton;
  Caption.OnClick := @CaptionClick;

  CustomTypeButton := TRadioButton.Create(SetupTypePage);
  if not Upgrade then
    CustomTypeButton.Caption := CustomMessage('CustomType')
  else
    CustomTypeButton.Caption := CustomMessage('CustomUpgradeType');
  CustomTypeButton.Checked := (not TypicalTypeButton.Checked);
  CustomTypeButton.Left := ScaleX(4);
  CustomTypeButton.Width := SetupTypePage.SurfaceWidth -
    CustomTypeButton.Left;
  CustomTypeButton.Top := GetBottom(Caption) + ScaleY(10);
  ScaleFixedHeightControl(CustomTypeButton);
  CustomTypeButton.Parent := SetupTypePage.Surface;

  Caption := TLabel.Create(SetupTypePage);
  Caption.WordWrap := True;
  if not Upgrade then
  begin
    Caption.Caption :=
      Bullet(CustomMessage('CustomType1'));
  end
    else
  begin
    Caption.Caption :=
      Bullet(CustomMessage('CustomUpgradeType1')) + NewLine +
      Bullet(CustomMessage('CustomUpgradeType2'));
  end;
  Caption.Left := ScaleX(4) + ScaleX(20);
  Caption.Width := SetupTypePage.SurfaceWidth - Caption.Left;
  Caption.Top := GetBottom(CustomTypeButton) + ScaleY(6);
  Caption.Parent := SetupTypePage.Surface;
  Caption.FocusControl := CustomTypeButton;
  Caption.OnClick := @CaptionClick;

  // interface page

  InterfacePage := CreateCustomPage(wpSelectTasks,
    CustomMessage('UserSettingsTitle'),
    CustomMessage('UserSettingsPrompt'));

  UpdatesPeriod := 0;
  RegQueryDWordValue(HKCU, '{#RegistryKey}\Configuration\Interface\Updates',
    'Period', UpdatesPeriod);
  AreUpdatesEnabled := (UpdatesPeriod <> 0);

  UserInterface := 0; { default is commander }
  RegQueryDWordValue(HKCU, '{#RegistryKey}\Configuration\Interface',
    'Interface', UserInterface);

  Caption := TLabel.Create(InterfacePage);
  Caption.Caption := CustomMessage('UserInterfaceStyle');
  Caption.Width := InterfacePage.SurfaceWidth;
  Caption.Parent := InterfacePage.Surface;

  CommanderRadioButton := TRadioButton.Create(InterfacePage);
  CommanderRadioButton.Caption := CustomMessage('NortonCommanderInterfaceC');
  CommanderRadioButton.Checked := (UserInterface = 0);
  CommanderRadioButton.Left := ScaleX(4);
  CommanderRadioButton.Width := ScaleX(116);
  CommanderRadioButton.Top := GetBottom(Caption) + ScaleY(6);
  ScaleFixedHeightControl(CommanderRadioButton);
  CommanderRadioButton.Parent := InterfacePage.Surface;

#ifdef ImagesDir
  Image := TBitmapImage.Create(InterfacePage);
  Image.Top := GetBottom(CommanderRadioButton) + ScaleY(6);
  Image.Left := CommanderRadioButton.Left + ScaleX(45);
  Image.Parent := InterfacePage.Surface;
  LoadEmbededScaledIcon(Image, '{#CommanderFileBase}', 32);
  Image.OnClick := @ImageClick;
  Image.Tag := Integer(CommanderRadioButton);
#endif

  Caption := TLabel.Create(InterfacePage);
  Caption.WordWrap := True;
  Caption.Caption :=
      Bullet(CustomMessage('NortonCommanderInterface1')) + NewLine +
      Bullet(CustomMessage('NortonCommanderInterface2')) + NewLine +
      Bullet(CustomMessage('NortonCommanderInterface3'));
  Caption.Anchors := [akLeft, akTop, akRight];
  Caption.Left := GetRight(CommanderRadioButton);
  Caption.Width := InterfacePage.SurfaceWidth - Caption.Left;
  Caption.Top := CommanderRadioButton.Top;
  Caption.Parent := InterfacePage.Surface;
  Caption.FocusControl := CommanderRadioButton;
  Caption.OnClick := @CaptionClick;

  ExplorerRadioButton := TRadioButton.Create(InterfacePage);
  ExplorerRadioButton.Caption := CustomMessage('ExplorerInterfaceC');
  ExplorerRadioButton.Checked := (UserInterface <> 0);
  ExplorerRadioButton.Left := ScaleX(4);
  ExplorerRadioButton.Width := CommanderRadioButton.Width;
  ExplorerRadioButton.Top := GetBottom(Caption) + ScaleY(10);
  ScaleFixedHeightControl(ExplorerRadioButton);
  ExplorerRadioButton.Parent := InterfacePage.Surface;

#ifdef ImagesDir
  Image := TBitmapImage.Create(InterfacePage);
  Image.Top := GetBottom(ExplorerRadioButton) + ScaleY(6);
  Image.Left := ExplorerRadioButton.Left + ScaleX(45);
  Image.Parent := InterfacePage.Surface;
  LoadEmbededScaledIcon(Image, '{#ExplorerFileBase}', 32);
  Image.OnClick := @ImageClick;
  Image.Tag := Integer(ExplorerRadioButton);
#endif

  Caption := TLabel.Create(InterfacePage);
  Caption.WordWrap := True;
  Caption.Caption :=
      Bullet(CustomMessage('ExplorerInterface1')) + NewLine +
      Bullet(CustomMessage('ExplorerInterface2')) + NewLine +
      Bullet(CustomMessage('ExplorerInterface3'));
  Caption.Anchors := [akLeft, akTop, akRight];
  Caption.Left := GetRight(ExplorerRadioButton);
  Caption.Width := InterfacePage.SurfaceWidth - Caption.Left;
  Caption.Top := ExplorerRadioButton.Top;
  Caption.Parent := InterfacePage.Surface;
  Caption.FocusControl := ExplorerRadioButton;
  Caption.OnClick := @CaptionClick;

  // run checkbox
  LaunchCheckbox := TCheckbox.Create(WizardForm.FinishedPage);
  LaunchCheckbox.Caption := CustomMessage('Launch');
  LaunchCheckbox.Checked := True;
  LaunchCheckbox.Left := WizardForm.YesRadio.Left;
  LaunchCheckbox.Width := WizardForm.YesRadio.Width;
  ScaleFixedHeightControl(LaunchCheckbox);
  LaunchCheckbox.Parent := WizardForm.FinishedPage;
  OpenGettingStartedCheckbox := TCheckbox.Create(WizardForm.FinishedPage);
  OpenGettingStartedCheckbox.Caption := CustomMessage('OpenGettingStarted');
  OpenGettingStartedCheckbox.Checked := True;
  OpenGettingStartedCheckbox.Left := WizardForm.YesRadio.Left;
  OpenGettingStartedCheckbox.Width := WizardForm.YesRadio.Width;
  ScaleFixedHeightControl(OpenGettingStartedCheckbox);
  OpenGettingStartedCheckbox.Parent := WizardForm.FinishedPage;

#ifdef Donations

  DonationPanel := TPanel.Create(WizardForm.FinishedPage);
  DonationPanel.Left := WizardForm.YesRadio.Left;
  DonationPanel.Width := WizardForm.YesRadio.Width;
  DonationPanel.Parent := WizardForm.FinishedPage;
  DonationPanel.BevelInner := bvNone;
  DonationPanel.BevelOuter := bvNone;
  DonationPanel.Color := WizardForm.FinishedPage.Color;

  Caption := TLabel.Create(DonationPanel);
  Caption.WordWrap := True;
  Caption.Caption := CustomMessage('PleaseDonate');
  Caption.Anchors := [akLeft, akTop, akRight];
  Caption.Left := 0;
  Caption.Top := 0;
  Caption.Width := DonationPanel.Width;
  Caption.Parent := DonationPanel;

  P := GetBottom(Caption) + ScaleY(12);
#ifdef ImagesDir
  P2 := P;
#endif

  CreateDonateLink( 9, P);
  CreateDonateLink(19, P);
  CreateDonateLink(49, P);

  AboutDonationCaption := TLabel.Create(DonationPanel);
  AboutDonationCaption.Left := 0;
  AboutDonationCaption.Top := P;
  AboutDonationCaption.Parent := DonationPanel;
  AboutDonationCaption.Caption := CustomMessage('AboutDonations');
  AboutDonationCaption.OnClick := @AboutDonationsLinkClick;
  LinkLabel(AboutDonationCaption);

#ifdef ImagesDir
  Image := TBitmapImage.Create(DonationPanel);
  LoadEmbededBitmap(Image, '{#PayPalCardImage}');
  Image.AutoSize := True;
  Image.Cursor := crHand;
  Image.Parent := DonationPanel;
  Image.Left := ScaleX(108);
  Image.Top := P2 + ScaleX(8);
  Image.Hint := CustomMessage('AboutDonations');
  Image.ShowHint := True;
  Image.OnClick := @AboutDonationsLinkClick;
#endif

  DonationPanel.Height := GetBottom(AboutDonationCaption);

#endif

  WizardForm.YesRadio.OnClick := @UpdatePostInstallRunCheckboxes;
  WizardForm.NoRadio.OnClick := @UpdatePostInstallRunCheckboxes;
  UpdatePostInstallRunCheckboxes(nil);

#ifdef ImagesDir
  // Text does not scale as quick as with DPI,
  // so the icon may overlap the labels. Shift them.
  P := WizardForm.SelectDirBitmapImage.Width;
  LoadEmbededScaledIcon(WizardForm.SelectDirBitmapImage, '{#SelectDirFileBase}', 32);
  P := (WizardForm.SelectDirBitmapImage.Width - P);
  // Vertical change should be the same as horizontal
  WizardForm.SelectDirLabel.Left := WizardForm.SelectDirLabel.Left + P;
  WizardForm.SelectDirBrowseLabel.Top := WizardForm.SelectDirBrowseLabel.Top + P;
  WizardForm.DirEdit.Top := WizardForm.DirEdit.Top + P;
  WizardForm.DirBrowseButton.Top := WizardForm.DirBrowseButton.Top + P;
#endif
end;

procedure RegisterPreviousData(PreviousDataKey: Integer);
var
  S: string;
begin
  if IsTypicalInstallation then S := 'typical'
    else S := 'custom';

  SetPreviousData(PreviousDataKey, '{#SetupTypeData}', S);
end;

function SaveCheckListBoxState(ListBox: TNewCheckListBox): string;
var
  I: Integer;
begin
  for I := 0 to ListBox.Items.Count - 1 do
  begin
    Result := Result + IntToStr(Integer(ListBox.State[I]));
  end;
end;

procedure CurPageChanged(CurPageID: Integer);
var
  Delta: Integer;
  LineHeight: Integer;
  LaunchCheckboxTop: Integer;
  S: string;
#ifdef Sponsor
  SponsorQueryUrl: string;
  PreferredSponsor: string;
#endif
begin
  if CurPageID = wpLicense then
  begin
    WizardForm.NextButton.Caption := CustomMessage('AcceptButton')
  end;

  if CurPageID = wpSelectDir then
  begin
    if InitDir = '' then
      InitDir := WizardForm.DirEdit.Text;
  end
    else
  if CurPageID = wpSelectComponents then
  begin
    if InitComponents = '' then
      InitComponents := SaveCheckListBoxState(WizardForm.ComponentsList);
  end
    else
  if CurPageID = wpSelectTasks then
  begin
    if InitTasks = '' then
      InitTasks := SaveCheckListBoxState(WizardForm.TasksList);
  end
    else
  if CurPageID = InterfacePage.ID then
  begin
    if InitInterface < 0 then
      InitInterface := Integer(CommanderRadioButton.Checked);
  end
    else
  if CurPageID = wpFinished then
  begin
    LineHeight := (WizardForm.NoRadio.Top - WizardForm.YesRadio.Top);

    // Are we at the "Restart?" screen
    // Note that it's not possible to get to the "finished" page more than once,
    // so the code below does not expect re-entry
    if IsRestartPage then
    begin
      if ShellExtNoRestart then
      begin
        Log('Hiding restart page as it''s not critical to replace the shell extension');
        WizardForm.YesRadio.Visible := False;
        WizardForm.NoRadio.Visible := False;
        WizardForm.NoRadio.Checked := True;

        S := SetupMessage(msgFinishedLabel);
        StringChange(S, '[name]', 'WinSCP');
        WizardForm.FinishedLabel.Caption :=
          S + NewLine + NewLine +
          // The additional new line is a padding for the "launch check box",
          // as the same padding is there for the YesRadio too.
          SetupMessage(msgClickFinish) + NewLine;
        Log(WizardForm.FinishedLabel.Caption);
        Delta := WizardForm.AdjustLabelHeight(WizardForm.FinishedLabel);
        LaunchCheckboxTop := WizardForm.YesRadio.Top + Delta;
      end
        else
      begin
        WizardForm.FinishedLabel.Caption :=
          CustomMessage('FinishedRestartDragExtLabel') + NewLine;

        Delta := WizardForm.AdjustLabelHeight(WizardForm.FinishedLabel);
        WizardForm.YesRadio.Top := WizardForm.YesRadio.Top + Delta;
        WizardForm.NoRadio.Top := WizardForm.NoRadio.Top + Delta;

        LaunchCheckboxTop := WizardForm.NoRadio.Top + LineHeight;
#ifdef Donations
        DonationPanel.Visible := False;
#endif
      end;
    end
      else
    begin
      LaunchCheckboxTop := WizardForm.RunList.Top;
    end;

    LaunchCheckbox.Top := LaunchCheckboxTop;
    OpenGettingStartedCheckbox.Top := LaunchCheckbox.Top + LineHeight;

    UpdatePostInstallRunCheckboxes(nil);

#ifdef Donations
    if DonationPanel.Visible then
    begin
      DonationPanel.Top := GetBottom(OpenGettingStartedCheckbox) + ScaleY(12);

      // Hide "about donations" if it does not fit nicely
      // (happens on "long" languages, as German)
      // Should not happen anymore with "modern" style of IS6.
      if (DonationPanel.Top + GetBottom(AboutDonationCaption)) >
         (WizardForm.FinishedPage.Height - ScaleY(8)) then
      begin
        AboutDonationCaption.Visible := False;
      end;
    end;
#endif
  end
    else
  if CurPageID = SetupTypePage.ID then
  begin
    Log('License accepted');
    LicenseAccepted := True;

#ifdef Sponsor
    if VarIsEmpty(SponsorReq) then
    begin
      // Need Vista for SHCONTCH_* constants
      if WizardSilent or CmdLineParamExists('/NoSponsor') or (not IsWinVista) then
      begin
        Log('Skipping sponsor query request');
        SponsorStatus := 'N';
      end
        else
      begin
        SponsorPage :=
          CreateCustomPage(wpInstalling, 'Release sponsor', 'Please read a message from the sponsor of this release.');

        SponsorQueryUrl :=
          '{#WebReport}?' +
          Format('mode=sponsorrequest&ver=%s&lang=%s&prevver=%s&scale=%d&images=%s', [
            '{#VersionOnly}', ActiveLanguage, PrevVersion, GetScalingFactor, '{#SponsorImages}']);
        PreferredSponsor := ExpandConstant('{param:Sponsor}');
        if PreferredSponsor <> '' then
        begin
          SponsorQueryUrl := SponsorQueryUrl + Format('&sponsor=%s', [PreferredSponsor]);
        end;

        Log('Sending sponsor query request: ' + SponsorQueryUrl);

        SponsorReq := CreateOleObject('WinHttp.WinHttpRequest.5.1');

        SponsorReq.Open('GET', SponsorQueryUrl, True);
        SponsorReq.Send('');
      end;
    end;
#endif
  end;
end;

function AskedRestart: Boolean;
begin
  Result := IsRestartPage;
end;

procedure DeinitializeSetup;
var
  WinHttpReq: Variant;
  ReportUrl: string;
  ReportData: string;
begin
  // cannot send report, unless user already accepted license
  // (with privacy policy)
  if LicenseAccepted then
  begin
    Log('Preparing installation report');

    ReportData := Format(
      'mode=report&installed=%d&silent=%d&ver=%s&lang=%s&prevver=%s&', [
       Integer(InstallationDone), Integer(WizardSilent),
       '{#VersionOnly}', ActiveLanguage,
       PrevVersion]);
#ifdef Sponsor
    ReportData := ReportData +
      Format('sponsorstatus=%s&sponsor=%s&sponsoringclicked=%d&', [SponsorStatus, Sponsor, Integer(SponsoringClicked)]);
#endif

    try
      ReportUrl := '{#WebReport}?' + ReportData;

      Log('Sending installation report: ' + ReportUrl);

      WinHttpReq := CreateOleObject('WinHttp.WinHttpRequest.5.1');
      WinHttpReq.Open('GET', ReportUrl, False);
      WinHttpReq.Send('');

      Log('Installation report send result: ' + IntToStr(WinHttpReq.Status) + ' ' + WinHttpReq.StatusText);
    except
      Log('Error sending installation report: ' + GetExceptionMessage);
    end;
  end;
end;

procedure ExecApp(Params: string; ShowCmd: Integer; Wait: TExecWait);
var
  Path: string;
  ErrorCode: Integer;
begin
  Path := ExpandConstant('{app}\{#MainFileName}');
  ExecAsOriginalUser(Path, Params, '', ShowCmd, Wait, ErrorCode)
end;

procedure OpenBrowserGettingStarted;
var
  WebGettingStarted: string;
begin
  WebGettingStarted :=
    ExpandConstant('{#WebGettingStarted}') + PrevVersion +
    '&automatic=' + IntToStr(Integer(AutomaticUpdate));
  Log('Opening getting started page: ' + WebGettingStarted);
  OpenBrowser(WebGettingStarted);
end;

procedure CurStepChanged(CurStep: TSetupStep);
var
  ShowCmd: Integer;
  OpenGettingStarted: Boolean;
  UsageData: string;
  CanPostInstallRuns: Boolean;
  Installations: Cardinal;
begin
  if CurStep = ssPostInstall then
  begin
    Log('Post install');
    InstallationDone := True;
  end
    else
  if CurStep = ssDone then
  begin
    Log('Done');
    // bug in InnoSetup causes it using ssDone even when
    // setup failed because machine was not restarted to complete previous
    // installation. double check that ssPostInstall was called
    if InstallationDone then
    begin
      CanPostInstallRuns := (not WizardSilent) and (not WillRestart);

      OpenGettingStarted :=
        OpenGettingStartedCheckbox.Enabled and
         OpenGettingStartedCheckbox.Checked;

      UsageData := '/Usage=';

      // old style counter
      UsageData := UsageData + Format('TypicalInstallation:%d,', [Integer(IsTypicalInstallation)]);

      UsageData := UsageData + 'InstallationsUser+,InstallationParentProcess@,';

      Installations := 0; // default, if the counter does not exist
      RegQueryDWordValue(HKEY_LOCAL_MACHINE, '{#RegistryKey}', 'Installations', Installations);
      Inc(Installations);
      if not RegWriteDWordValue(HKEY_LOCAL_MACHINE, '{#RegistryKey}', 'Installations', Installations) then
      begin
        Log('Cannot increment administrator installations counter, probably a non-elevated installation');
      end;

      // new style counters
      if not Upgrade then
      begin
        if IsTypicalInstallation then
          UsageData := UsageData + 'InstallationsFirstTypical+,'
        else
          UsageData := UsageData + 'InstallationsFirstCustom+,';
      end
        else
      begin
        if AutomaticUpdate then
          UsageData := UsageData + 'InstallationsUpgradeAutomatic+,'
        else if IsTypicalInstallation then
          UsageData := UsageData + 'InstallationsUpgradeTypical+,'
        else
          UsageData := UsageData + 'InstallationsUpgradeCustom+,';
      end;

      UsageData := UsageData + Format('LastInstallationAutomaticUpgrade:%d,', [Integer(AutomaticUpdate)]);

      if (InitDir <> '') and (InitDir <> WizardForm.DirEdit.Text) then
        UsageData := UsageData + 'InstallationsCustomDir+,';
      if (InitComponents <> '') and (InitComponents <> SaveCheckListBoxState(WizardForm.ComponentsList)) then
        UsageData := UsageData + 'InstallationsCustomComponents+,';
      if (InitTasks <> '') and (InitTasks <> SaveCheckListBoxState(WizardForm.TasksList)) then
        UsageData := UsageData + 'InstallationsCustomTasks+,';
      if (InitInterface >= 0) and (InitInterface <> Integer(CommanderRadioButton.Checked)) then
        UsageData := UsageData + 'InstallationsCustomInterface+,';
      if CanPostInstallRuns and OpenGettingStarted then
        UsageData := UsageData + 'InstallationsGettingStarted+,';
      if CanPostInstallRuns and LaunchCheckbox.Checked then
        UsageData := UsageData + 'InstallationsLaunch+,';
      if WizardSilent then
        UsageData := UsageData + 'InstallationsSilent+,';
      if AskedRestart then
        UsageData := UsageData + 'InstallationsNeedRestart+,';
      if WillRestart then
        UsageData := UsageData + 'InstallationsRestart+,';
      if Donated then
        UsageData := UsageData + 'InstallationsDonate+,';
      if not IsAdminInstallMode then
        UsageData := UsageData + 'InstallationsNonElevated+,';

      // have to do this before running WinSCP GUI instance below,
      // otherwise it loads the empty/previous counters and overwrites our changes,
      // when it's closed
      Log('Recording installer usage statistics: ' + UsageData);
      // make sure we write the counters using the "normal" account
      // (the account that will be used to report the counters)
      ExecApp(UsageData, SW_HIDE, ewWaitUntilTerminated);

      if AutomaticUpdate then
      begin
        Log('Launching WinSCP after automatic update');
        ExecApp('', SW_SHOWNORMAL, ewNoWait);

        if CmdLineParamExists('/OpenGettingStarted') then
        begin
          OpenBrowserGettingStarted;
        end;
      end
        else
      if CanPostInstallRuns then
      begin
        if OpenGettingStarted then
        begin
          OpenBrowserGettingStarted;
        end;

        if LaunchCheckbox.Checked then
        begin
          if OpenGettingStarted then
          begin
            Log('Will launch WinSCP minimized');
            ShowCmd := SW_SHOWMINIMIZED
          end
            else
          begin
            ShowCmd := SW_SHOWNORMAL;
          end;

          Log('Launching WinSCP');
          ExecApp('', ShowCmd, ewNoWait);
        end;
      end;
    end;
  end;
end;

#ifdef Sponsor
function CryptStringToBinary(
  sz: string; cch: LongWord; flags: LongWord; binary: string; var size: LongWord;
  skip: LongWord; flagsused: LongWord): Integer;
  external 'CryptStringToBinaryW@crypt32.dll stdcall';

const
  CRYPT_STRING_HEX = $04;
  SHCONTCH_NOPROGRESSBOX = 4;
  SHCONTCH_RESPONDYESTOALL = 16;

var
  ShowSponsor: Integer;
  SponsoringLinkLabel, SponsorLinkLabel: TLabel;
  SponsorImage: TBitmapImage;

procedure SponsorImageClick(Sender: TObject);
begin
  SponsorStatus := 'C';
  OpenBrowser('{#WebReport}?mode=sponsor' + Format('&sponsor=%s&', [Sponsor]) + ExpandConstant('{#WebArguments}'));
end;

function GetSponsorAreaHeight: Integer;
begin
  Result := SponsorPage.SurfaceHeight - SponsorLinkLabel.Height - ScaleY(12);
end;

procedure CenterSponsorImage;
begin
  if (Extended(SponsorImage.Bitmap.Width) / SponsorPage.SurfaceWidth) <
       (Extended(SponsorImage.Bitmap.Height) / GetSponsorAreaHeight())  then
  begin
    SponsorImage.Top := 0;
    SponsorImage.Height := GetSponsorAreaHeight();
    SponsorImage.Width :=
      Trunc((Extended(SponsorImage.Bitmap.Width) / SponsorImage.Bitmap.Height) * SponsorImage.Height);
    SponsorImage.Left := (SponsorPage.SurfaceWidth - SponsorImage.Width) div 2;
  end
    else
  begin
    SponsorImage.Left := 0;
    SponsorImage.Width := SponsorPage.SurfaceWidth;
    SponsorImage.Height :=
      Trunc((Extended(SponsorImage.Bitmap.Height) / SponsorImage.Bitmap.Width) * SponsorImage.Width);
    SponsorImage.Top := (GetSponsorAreaHeight() - SponsorImage.Height) div 2;
  end;
  SponsorLinkLabel.Left := SponsorImage.Left;
  SponsorLinkLabel.Top := GetBottom(SponsorImage) + ScaleX(6);
  SponsoringLinkLabel.Left := GetRight(SponsorImage) - SponsoringLinkLabel.Width;
  SponsoringLinkLabel.Top := SponsorLinkLabel.Top;
end;

procedure WizardFormResize(Sender: TObject);
begin
  CenterSponsorImage;
end;

function CheckSponsorReq: Boolean;
var
  R, Succeeded: Integer;
  Lines: TStrings;
  I, P: Integer;
  L, Key, Value: string;
  Stream: TStream;
  Buffer: string;
  Size: LongWord;
  ZipPath, TargetPath, ImagePath: string;
  Shell, ZipFile, TargetFolder: Variant;
  SponsorArea: TBitmapImage;
  ImageSize: Integer;
begin
  if ShowSponsor = 0 then
  begin
    SponsorImage := nil;

    Log('Checking for response to sponsor request');
    try
      Succeeded := 0;
      // Not testing return value, as it always returns -1 for some reason
      R := SponsorReq.WaitForResponse(1, Succeeded);
      if R = 0 then
      begin
        Log('Timed out waiting for a response to sponsor request');
        SponsorStatus := 'T';
        ShowSponsor := -1;
      end
        else
      if SponsorReq.Status <> 200 then
      begin
        Log('Sponsor request failed with HTTP error: ' + IntToStr(SponsorReq.Status) + ' ' + SponsorReq.StatusText);
        SponsorStatus := 'H';
        ShowSponsor := -1;
      end
        else
      begin
        Log('Sponsor request succeeded');

        if CmdLineParamExists('/SponsorArea') then
        begin
          SponsorArea := TBitmapImage.Create(SponsorPage);
          SponsorArea.Parent := SponsorPage.Surface;
          SponsorArea.Visible := CmdLineParamExists('/SponsorArea');
          SponsorArea.BackColor := clTeal;
          SponsorArea.Anchors := [akLeft, akTop, akRight, akBottom];
        end
          else
        begin
          SponsorArea := nil;
        end;

        SponsorLinkLabel := TLabel.Create(SponsorPage);
        SponsorLinkLabel.Parent := SponsorPage.Surface;
        SponsorLinkLabel.Caption := 'Visit release sponsor';
        SponsorLinkLabel.OnClick := @SponsorImageClick;
        LinkLabel(SponsorLinkLabel);

        SponsoringLinkLabel := TLabel.Create(SponsorPage);
        SponsoringLinkLabel.Parent := SponsorPage.Surface;
        SponsoringLinkLabel.Caption := 'Become next release sponsor';
        SponsoringLinkLabel.OnClick := @SponsoringLinkLabelClick;
        LinkLabel(SponsoringLinkLabel);

        Lines := TStringList.Create;
        try
          Lines.Text := SponsorReq.ResponseText;
          for I := 0 to Lines.Count - 1 do
          begin
            L := Lines[I];
            P := Pos('=', L);
            if P = 0 then
            begin
              Log('Malformed sponsor response directive: ' + L);
              SponsorStatus := 'P';
              ShowSponsor := -1;
              break;
            end
              else
            begin
              Key := Trim(Copy(L, 1, P - 1));
              Value := Trim(Copy(L, P + 1, Length(L) - P));

              if CompareText(Key, 'result') = 0 then
              begin
                if Value <> '-' then
                begin
                  Log('No sponsor returned');
                  SponsorStatus := Copy(Value, 1, 1);
                  ShowSponsor := -1;
                  break;
                end
                  else
                begin
                  Log('Sponsor returned');
                end;
              end
                else
              if (CompareText(Key, 'image') = 0) or
                 (CompareText(Key, 'localimage') = 0) then
              begin
                if CompareText(Key, 'localimage') = 0 then
                begin
                  Log(Format('Extracting embedded sponsor image (%s)', [Value]));
                  ExtractTemporaryFile(Value);
                  ImagePath := ExpandConstant('{tmp}\' + Value);
                end
                  else
                begin
                  Log(Format('Extracting returned sponsor image (%d bytes)', [Length(Value)]));

                  ZipPath := ExpandConstant('{tmp}\sponsor.zip');
                  Stream := TFileStream.Create(ZipPath, fmCreate);
                  try
                    SetLength(Buffer, (Length(Value) div 4) + 1);
                    Size := Length(Value) div 2;
                    if (CryptStringToBinary(Value, Length(Value), CRYPT_STRING_HEX, Buffer, Size, 0, 0) = 0) or
                       (Size <> Length(Value) div 2) then
                    begin
                      Log('Error decoding binary string');
                      SponsorStatus := 'P';
                      ShowSponsor := -1;
                      break;
                    end;

                    Stream.WriteBuffer(Buffer, Size);
                  finally
                    Stream.Free;
                  end;

                  Shell := CreateOleObject('Shell.Application');
                  ZipFile := Shell.NameSpace(ZipPath);
                  if VarIsClear(ZipFile) then
                  begin
                    RaiseException(Format('ZIP file "%s" does not exist or cannot be opened', [ZipPath]));
                  end
                    else
                  begin
                    TargetPath := ExpandConstant('{tmp}');
                    TargetFolder := Shell.NameSpace(TargetPath);
                    if VarIsClear(TargetFolder) then
                    begin
                      RaiseException(Format('Target path "%s" does not exist', [TargetPath]));
                    end
                      else
                    begin
                      TargetFolder.CopyHere(ZipFile.Items, SHCONTCH_NOPROGRESSBOX or SHCONTCH_RESPONDYESTOALL);

                      ImagePath := ExpandConstant('{tmp}\sponsor.bmp');
                    end;
                  end;
                end;

                SponsorImage := TBitmapImage.Create(SponsorPage);
                SponsorImage.Parent := SponsorPage.Surface;
                SponsorImage.Hint := SponsorLinkLabel.Caption;
                SponsorImage.ShowHint := True;
                SponsorImage.Stretch := True;
                try
                  LoadBitmap(SponsorImage, ImagePath);
                except
                  Log('Error loading sponsor image: ' + GetExceptionMessage);
                  SponsorStatus := 'I';
                  ShowSponsor := -1;
                end;

                if ShowSponsor = 0 then
                begin
                  if Assigned(SponsorArea) then
                  begin
                    SponsorArea.Left := 0;
                    SponsorArea.Top := 0;
                    SponsorArea.Width := SponsorPage.Surface.Width;
                    SponsorArea.Height := GetSponsorAreaHeight();
                    Log(Format('Sponsor area is %dx%d', [SponsorArea.Width, SponsorArea.Height]));
                  end;

                  CenterSponsorImage;

                  SponsorImage.Cursor := crHand;
                  SponsorImage.OnClick := @SponsorImageClick;

                  WizardForm.OnResize := @WizardFormResize;

                  FileSize(ImagePath, ImageSize);
                  Log(Format('Sponsor image loaded (%d bytes, %dx%d) and displayed (%dx%d)', [
                    Integer(ImageSize), SponsorImage.Bitmap.Width, SponsorImage.Bitmap.Height,
                    SponsorImage.Width, SponsorImage.Height]));
                end;
              end
                else
              if CompareText(Key, 'sponsor') = 0 then
              begin
                Sponsor := Value;
              end
                else
              if CompareText(Key, 'description') = 0 then
              begin
                SponsorPage.Description := Value;
                Log('Sponsor page description: ' + Value);
              end
                else
              if CompareText(Key, 'caption') = 0 then
              begin
                SponsorPage.Caption := Value;
                Log('Sponsor page caption: ' + Value);
              end
                else
              if CompareText(Key, 'sponsor_caption') = 0 then
              begin
                SponsorLinkLabel.Caption := Value;
                Log('Sponsor link caption: ' + Value);
              end
                else
              if CompareText(Key, 'sponsoring_caption') = 0 then
              begin
                if Value = '' then
                begin
                  SponsoringLinkLabel.Visible := False;
                  Log('Hiding sponsoring link');
                end
                  else
                begin
                  SponsoringLinkLabel.Caption := Value;
                  Log('Sponsoring link caption: ' + Value);
                end;
              end
                else
              begin
                Log('Unknown sponsor directive: ' + Key);
              end;
            end;
          end;
        finally
          Lines.Free;
        end;

        if ShowSponsor = 0 then
        begin
          if SponsorImage = nil then
          begin
            Log('Incomplete sponsor data');
            SponsorStatus := 'P';
            ShowSponsor := -1;
          end
            else
          begin
            SponsorStatus := 'S';
            ShowSponsor := 1;
          end;
        end;
      end;
    except
      Log('Error processing response to sponsor request: ' + GetExceptionMessage);
      SponsorStatus := 'E';
      ShowSponsor := -1;
    end;
  end;

  Result := (ShowSponsor > 0);
end;
#endif

function ShouldSkipPage(PageID: Integer): Boolean;
begin
  Result :=
    { Hide most pages during typical installation }
    (IsTypicalInstallation and
     ((PageID = wpSelectDir) or (PageID = wpSelectComponents) or
      (PageID = wpSelectTasks) or
      { Hide Interface page for upgrades only, show for fresh installs }
      ((PageID = InterfacePage.ID) and Upgrade)))
#ifdef Sponsor
    or
    ((SponsorPage <> nil) and (PageID = SponsorPage.ID) and (not CheckSponsorReq))
#endif
    ;
end;

function UpdateReadyMemo(Space, NewLine, MemoUserInfoInfo, MemoDirInfo,
  MemoTypeInfo, MemoComponentsInfo, MemoGroupInfo, MemoTasksInfo: string): string;
var
  S: string;
  S2: string;
begin
  S := '';

  S := S + MemoDirInfo + NewLine + NewLine;

  if not Upgrade then
  begin
    if IsTypicalInstallation then S2 := CustomMessage('TypicalType')
      else S2 := CustomMessage('CustomType');
  end
    else
  begin
    if IsTypicalInstallation then S2 := CustomMessage('TypicalUpgradeType')
      else S2 := CustomMessage('CustomUpgradeType');
  end;
  StringChange(S2, '&', '');
  S := S + SetupMessage(msgReadyMemoType) + NewLine + Space + S2 + NewLine + NewLine;

  S := S + MemoComponentsInfo + NewLine + NewLine;

  if Length(MemoGroupInfo) > 0 then
    S := S + MemoGroupInfo + NewLine + NewLine;

  if Length(MemoTasksInfo) > 0 then
    S := S + MemoTasksInfo + NewLine + NewLine;

  S := S + CustomMessage('UserSettingsOverview') + NewLine;
  S := S + Space;
  if CommanderRadioButton.Checked then S2 := CustomMessage('NortonCommanderInterfaceC')
    else S2 := CustomMessage('ExplorerInterfaceC');
  StringChange(S2, '&', '');
  S := S + S2;
  S := S + NewLine;

  Result := S;
end;

function InitializeUninstall: Boolean;
begin
  // let application know that we are running silent uninstall,
  // this turns UninstallCleanup to noop
  if UninstallSilent then
    CreateMutex('WinSCPSilentUninstall');
  Result := True;
end;

function HasUserPrograms: Boolean;
begin
  // To avoid the installer failing when the {userprograms}
  // cannot be resolved (when installing via system account or SCCM)
  try
    ExpandConstant('{userprograms}');
    Log('Have user programs');
    Result := True;
  except
    Log('Does not have user programs');
    Result := False;
  end;
end;

#expr SaveToFile(AddBackslash(SourcePath) + "Preprocessed.iss")
