#define MyAppName "Crystal"
#define VersionFile FileOpen("portable\src\VERSION")
#define MyAppVersion FileRead(VersionFile)
#define MyAppVersionNum StringChange(MyAppVersion, "-dev", "")
#expr FileClose(VersionFile)
#define MyAppPublisher "Manas Technology Solutions"
#define MyAppURL "https://crystal-lang.org/"
#define MyAppExeName "crystal.exe"
#define MyAppCopyright GetFileCopyright("portable\" + MyAppExeName)
#define MyAppAssocName MyAppName + " Source File"
#define MyAppAssocExt ".cr"
#define MyAppAssocKey StringChange(MyAppAssocName, " ", "") + MyAppAssocExt

#define MajorNum 0
#define MinorNum 0
#expr UnpackVersionComponents(StrToVersion(MyAppVersionNum), MajorNum, MinorNum, null, null)

[Setup]
AppId={{7C307DDF-447E-46C5-BB3B-47A6F652D7C8}
AppName={#MyAppName} x86_64-windows-msvc
AppVersion={#MyAppVersion}
AppCopyright={#MyAppCopyright}
VersionInfoVersion={#MyAppVersionNum}
AppPublisher={#MyAppPublisher}
AppPublisherURL={#MyAppURL}
AppSupportURL={#MyAppURL}
AppUpdatesURL={#MyAppURL}
ArchitecturesAllowed=x64
ArchitecturesInstallIn64BitMode=x64
DefaultDirName={autopf}\{#MyAppName}
OutputBaseFilename=crystal-setup
LicenseFile=portable\LICENSE.txt

ChangesEnvironment=yes
ChangesAssociations=yes

DefaultGroupName={#MyAppName} {#MyAppVersion}
AllowNoIcons=yes

PrivilegesRequired=lowest
PrivilegesRequiredOverridesAllowed=dialog

Compression=lzma
SolidCompression=yes

WizardStyle=modern
WizardImageFile=crystal.bmp
WizardSmallImageFile=crystal_small.bmp
DisableWelcomePage=no

SetupMutex={#MyAppName}SetupMutex

[Languages]
Name: "english"; MessagesFile: "compiler:Default.isl"

[Types]
Name: "full"; Description: "Full installation"
Name: "minimal"; Description: "Minimal installation"
Name: "custom"; Description: "Custom installation"; Flags: iscustom

[Components]
Name: "main"; Description: "Crystal compiler"; Types: full minimal custom; Flags: fixed
Name: "shards"; Description: "Shards dependency manager"; Types: full
Name: "pdb"; Description: "Debug symbols"; Types: full
Name: "samples"; Description: "Sample programs"; Types: full
Name: "docs"; Description: "Offline standard library documentation"; Types: full

[Tasks]
Name: addtopath; Description: "Add Crystal's directory to the &PATH environment variable"; Flags: checkedonce
Name: association; Description: "{cm:AssocFileExtension,Crystal,.cr}"; Flags: unchecked

[Files]
Source: "portable\{#MyAppExeName}"; DestDir: "{app}"; Flags: ignoreversion; Components: main
Source: "portable\lib\*"; DestDir: "{app}\lib"; Flags: ignoreversion; Components: main
Source: "portable\*.dll"; DestDir: "{app}"; Flags: ignoreversion; Components: main
Source: "portable\src\*"; DestDir: "{app}\src"; Flags: ignoreversion recursesubdirs createallsubdirs; Components: main
Source: "portable\README.md"; DestDir: "{app}"; Flags: ignoreversion isreadme; Components: main
Source: "portable\LICENSE.txt"; DestDir: "{app}"; Flags: ignoreversion; Components: main

Source: "portable\shards.exe"; DestDir: "{app}"; Flags: ignoreversion; Components: shards

Source: "portable\crystal.pdb"; DestDir: "{app}"; Flags: ignoreversion; Components: pdb
Source: "portable\shards.pdb"; DestDir: "{app}"; Flags: ignoreversion; Components: pdb and shards

Source: "portable\examples\*"; DestDir: "{app}\examples"; Flags: ignoreversion recursesubdirs createallsubdirs; Components: samples

Source: "portable\docs\*"; DestDir: "{app}\docs"; Flags: ignoreversion recursesubdirs createallsubdirs; Components: docs

[UninstallDelete]
Type: filesandordirs; Name: "{localappdata}\crystal\cache"

[Registry]
Root: HKA; Subkey: "Software\Classes\{#MyAppAssocExt}"; ValueType: string; ValueName: "PerceivedType"; ValueData: "text"; Flags: uninsdeletevalue
Root: HKA; Subkey: "Software\Classes\{#MyAppAssocExt}\OpenWithProgids"; ValueType: string; ValueName: "{#MyAppAssocKey}"; ValueData: ""; Flags: uninsdeletevalue

Root: HKA; Subkey: "Software\Classes\{#MyAppAssocKey}"; ValueType: string; ValueName: ""; ValueData: "{#MyAppAssocName}"; Flags: uninsdeletekey
Root: HKA; Subkey: "Software\Classes\{#MyAppAssocKey}\DefaultIcon"; ValueType: string; ValueName: ""; ValueData: "{app}\{#MyAppExeName},0"; Flags: uninsdeletekey
Root: HKA; Subkey: "Software\Classes\{#MyAppAssocKey}\shell\open\command"; ValueType: string; ValueName: ""; ValueData: """{app}\{#MyAppExeName}"" ""%1"""; Flags: uninsdeletekey

Root: HKA; Subkey: "Software\Classes\Applications\{#MyAppExeName}\DefaultIcon"; ValueType: string; ValueName: ""; ValueData: "{app}\{#MyAppExeName},0"; Flags: uninsdeletekey
Root: HKA; Subkey: "Software\Classes\Applications\{#MyAppExeName}\SupportedTypes"; ValueType: string; ValueName: ".cr"; ValueData: ""; Flags: uninsdeletekey

[Icons]
Name: "{group}\Crystal Book"; Filename: "https://crystal-lang.org/reference/{#MajorNum}.{#MinorNum}/index.html"
Name: "{group}\Crystal Standard Library API"; Filename: "{app}\docs\index.html"; Components: docs
Name: "{group}\Crystal Standard Library API"; Filename: "https://crystal-lang.org/api/{#MyAppVersion}/index.html"; Components: not docs
Name: "{group}\Official Website"; Filename: "https://crystal-lang.org/"
Name: "{group}\GitHub Repository"; Filename: "https://github.com/crystal-lang/crystal"

[Code]
const CLSID_SetupConfiguration = '{177F0C4A-1CD3-4DE7-A32C-71DBBB9FA36D}';
const Win10SDK64 = 'SOFTWARE\WOW6432Node\Microsoft\Microsoft SDKs\Windows\v10.0';
const Win10SDK32 = 'SOFTWARE\Microsoft\Microsoft SDKs\Windows\v10.0';

type
  ISetupInstance = interface(IUnknown) '{B41463C3-8866-43B5-BC33-2B0676F7F42E}'
    procedure GetInstanceId;
    procedure GetInstallDate;
    procedure GetInstallationName;
    function GetInstallationPath(out installationPath: WideString): HResult;
    procedure GetInstallationVersion;
    procedure GetDisplayName;
    procedure GetDescription;
    procedure ResolvePath;
  end;

  IEnumSetupInstances = interface(IUnknown) '{6380BCFF-41D3-4B2E-8B2E-BF8A6810C848}'
    function Next(celt: DWord; out rgelt: ISetupInstance; out pceltFetched: DWord): HResult;
    procedure Skip;
    procedure Reset;
    procedure Clone;
  end;

  ISetupConfiguration = interface(IUnknown) '{42843719-DB4C-46C2-8E7C-64F1816EFD5B}'
    function EnumInstances(out enumInstances: IEnumSetupInstances): HResult;
  end;

  IClassFactory = interface(IUnknown) '{00000001-0000-0000-C000-000000000046}'
    function CreateInstance(unkOuter: IUnknown; riid: TGUID; out object: IUnknown): HResult;
    procedure LockServer;
  end;

function HasMSVC: Boolean;
var
  config: ISetupConfiguration;
  enumSetup: IEnumSetupInstances;
  count: DWord;
  setup: ISetupInstance;
  setupPath: WideString;
  msvcVersion: AnsiString;
  hresult: HResult;
begin
  result := False;

  try
    config := ISetupConfiguration(CreateComObject(StringToGuid(CLSID_SetupConfiguration)));
  except
    exit;
  end;

  OleCheck(config.EnumInstances(enumSetup));
  While True do
  begin
    hresult := enumSetup.Next(1, setup, count);
    OleCheck(hresult);
    if hresult = 1 then
      Break;
    hresult := setup.GetInstallationPath(setupPath);
    if hresult <> 0 then
      Continue;
    if not LoadStringFromFile(setupPath + '\VC\Auxiliary\Build\Microsoft.VCToolsVersion.default.txt', msvcVersion) then
      Continue;
    if not FileExists(setupPath + '\VC\Tools\MSVC\' + TrimRight(msvcVersion) + '\bin\Hostx64\x64\cl.exe') then
      Continue;
    Log('MSVC location: ' + setupPath + '\VC\Tools\MSVC\' + TrimRight(msvcVersion));
    result := True;
    exit;
  end;
end;

function HasWinSDKAt(const rootKey: Integer; const subKey: String): Boolean;
var
  installationFolder: String;
  productVersion: String;
begin
  result := False;
  if RegQueryStringValue(rootKey, subKey, 'InstallationFolder', installationFolder) then
    if RegQueryStringValue(rootKey, subKey, 'ProductVersion', productVersion) then
      if FileExists(installationFolder + '\Include\' + productVersion + '.0\um\winsdkver.h') then
      begin
        Log('Windows SDK location: ' + installationFolder + '\Lib\' + productVersion);
        result := True;
      end;
end;

function HasWinSDK: Boolean;
begin
  result := HasWinSDKAt(HKEY_LOCAL_MACHINE, Win10SDK64) or
    HasWinSDKAt(HKEY_LOCAL_MACHINE, Win10SDK32) or
    HasWinSDKAt(HKEY_CURRENT_USER, Win10SDK64) or
    HasWinSDKAt(HKEY_CURRENT_USER, Win10SDK32);
end;

function HasVCRedist: Boolean;
var
  regValue: Cardinal;
begin
  result := False;
  if RegQueryDWordValue(HKEY_LOCAL_MACHINE, 'SOFTWARE\Microsoft\VisualStudio\14.0\VC\Runtimes\X64', 'Installed', regValue) then
    if regValue <> 0 then
      result := True;
end;

{ Adopted from https://stackoverflow.com/a/46609047 }
procedure EnvAddPath(Path: string; IsSystem: Boolean);
var
  Paths: string;
  Status: Boolean;
begin
  if IsSystem then
    Status := RegQueryStringValue(HKEY_LOCAL_MACHINE, 'SYSTEM\CurrentControlSet\Control\Session Manager\Environment', 'Path', Paths)
  else
    Status := RegQueryStringValue(HKEY_CURRENT_USER, 'Environment', 'Path', Paths);

  if not Status then
    Paths := '';

  if Pos(';' + Uppercase(Path) + ';', ';' + Uppercase(Paths) + ';') > 0 then
    exit;

  Paths := Paths + ';' + Path + ';';

  if IsSystem then
    RegWriteStringValue(HKEY_LOCAL_MACHINE, 'SYSTEM\CurrentControlSet\Control\Session Manager\Environment', 'Path', Paths)
  else
    RegWriteStringValue(HKEY_CURRENT_USER, 'Environment', 'Path', Paths);
end;

procedure EnvRemovePath(Path: string; IsSystem: Boolean);
var
  Paths: string;
  Status: Boolean;
  P: Integer;
begin
  if IsSystem then
    Status := RegQueryStringValue(HKEY_LOCAL_MACHINE, 'SYSTEM\CurrentControlSet\Control\Session Manager\Environment', 'Path', Paths)
  else
    Status := RegQueryStringValue(HKEY_CURRENT_USER, 'Environment', 'Path', Paths);

  if not Status then
    exit;

  P := Pos(';' + Uppercase(Path) + ';', ';' + Uppercase(Paths) + ';');
  if P = 0 then
    exit;

  Delete(Paths, P - 1, Length(Path) + 1);

  if IsSystem then
    RegWriteStringValue(HKEY_LOCAL_MACHINE, 'SYSTEM\CurrentControlSet\Control\Session Manager\Environment', 'Path', Paths)
  else
    RegWriteStringValue(HKEY_CURRENT_USER, 'Environment', 'Path', Paths);
end;

function GetUninstallString(): String;
var
  sUnInstPath: String;
  sUnInstallString: String;
begin
  sUnInstPath := ExpandConstant('Software\Microsoft\Windows\CurrentVersion\Uninstall\{#emit SetupSetting("AppId")}_is1');
  sUnInstallString := '';
  if not RegQueryStringValue(HKLM, sUnInstPath, 'UninstallString', sUnInstallString) then
    RegQueryStringValue(HKCU, sUnInstPath, 'UninstallString', sUnInstallString);
  result := sUnInstallString;
end;

procedure InitializeWizard;
var
  updatingPage: TOutputMsgWizardPage;
begin
  if GetUninstallString() <> '' then
    updatingPage := CreateOutputMsgPage(
      wpSelectTasks,
      'Pre-Install Checks',
      'A previous Crystal installation already exists.',
      'Setup has detected a previous installation of Crystal; it will be uninstalled before the new version is installed. ' +
      'This ensures that requiring Crystal files will not pick up any leftover files from the previous version.'#13#13 +
      'To use multiple Crystal installations side-by-side, the portable packages for the extra versions must be downloaded manually.');
end;

procedure CurStepChanged(CurStep: TSetupStep);
var
  uninstallString: String;
  exitCode: Integer;
begin
  case CurStep of
  ssInstall:
    begin
      uninstallString := GetUninstallString();
      if uninstallString <> '' then
        if not Exec(RemoveQuotes(uninstallString), '/VERYSILENT /NORESTART /SUPPRESSMSGBOXES', '', SW_HIDE, ewWaitUntilTerminated, exitCode) then
        begin
          SuppressibleMsgBox('Failed to remove the previous Crystal installation. Setup will now exit.', mbCriticalError, MB_OK, IDOK);
          Abort;
        end;
    end;
  ssPostInstall:
    begin
      if (not HasVCRedist) and (IDYES = SuppressibleTaskDialogMsgBox(
        'Install Visual C++ Redistributable',
        'Setup is unable to detect a copy of the Visual C++ 2015 Redistributable (x64) or newer on this machine. ' +
          'The runtime libraries are needed by dynamically linked executables, including the compiler itself. ' +
          'If you select "Agree", the installer will proceed to download: '#13#10#13#10 +
          'https://aka.ms/vs/17/release/vc_redist.x64.exe'#13#10#13#10 +
          'and then run:'#13#10#13#10 +
          'VC_redist.x64.exe /passive /norestart'#13#10#13#10 +
          'Would you like to install the Visual C++ Redistributable now?',
        mbInformation, MB_YESNO, ['Agree', 'Decline'], IDYES, IDYES)) then
      begin
        DownloadTemporaryFile('https://aka.ms/vs/17/release/vc_redist.x64.exe', 'VC_redist.x64.exe', '', nil);
        if not Exec(ExpandConstant('{tmp}\VC_redist.x64.exe'), '/passive /norestart', '', SW_SHOW, ewWaitUntilTerminated, exitCode) then
          SuppressibleMsgBox('Failed to install the Visual C++ Redistributable.', mbError, MB_OK, IDOK);
      end;

      if (not HasMSVC) and (IDYES = SuppressibleTaskDialogMsgBox(
        'Install MSVC Build Tools',
        'Setup is unable to detect a copy of the Build Tools for Visual Studio 2017 or newer on this machine. ' +
          'The MSVC Build Tools are required to link Crystal programs into Windows executables. ' +
          'If you select "Agree", the installer will proceed to download: '#13#10#13#10 +
          'https://aka.ms/vs/17/release/vs_BuildTools.exe'#13#10#13#10 +
          'and then run:'#13#10#13#10 +
          'vs_BuildTools.exe --passive --norestart --wait --add Microsoft.VisualStudio.Workload.VCTools --add Microsoft.VisualStudio.Component.VC.Tools.x86.x64'#13#10#13#10 +
          'Would you like to install the MSVC Build Tools now? ' +
          'Note that you may modify your installation later using the Visual Studio Installer.',
        mbInformation, MB_YESNO, ['Agree', 'Decline'], IDYES, IDYES)) then
      begin
        DownloadTemporaryFile('https://aka.ms/vs/17/release/vs_BuildTools.exe', 'vs_BuildTools.exe', '', nil);
        if not Exec(
          ExpandConstant('{tmp}\vs_BuildTools.exe'),
          '--passive --norestart --wait --add Microsoft.VisualStudio.Workload.VCTools --add Microsoft.VisualStudio.Component.VC.Tools.x86.x64',
          '', SW_SHOW, ewWaitUntilTerminated, exitCode
        ) then
          SuppressibleMsgBox('Failed to install the Build Tools for Visual Studio.', mbError, MB_OK, IDOK);
      end;

      if (not HasWinSDK) and (IDYES = SuppressibleTaskDialogMsgBox(
        'Install Windows SDK',
        'Setup is unable to detect a copy of the Windows 10 SDK or newer on this machine. ' +
          'The Crystal runtime and standard library rely on the Win32 libraries to interface with the Windows system. ' +
          'If you select "Agree", the installer will proceed to download: '#13#10#13#10 +
          'https://aka.ms/vs/17/release/vs_BuildTools.exe'#13#10#13#10 +
          'and then run:'#13#10#13#10 +
          'vs_BuildTools.exe --passive --norestart --wait --add Microsoft.VisualStudio.Component.Windows11SDK.22621'#13#10#13#10 +
          'Would you like to install the Windows SDK now? ' +
          'Note that you may modify your installation later using the Visual Studio Installer.',
        mbInformation, MB_YESNO, ['Agree', 'Decline'], IDYES, IDYES)) then
      begin
        DownloadTemporaryFile('https://aka.ms/vs/17/release/vs_BuildTools.exe', 'vs_BuildTools.exe', '', nil);
        if not Exec(
          ExpandConstant('{tmp}\vs_BuildTools.exe'),
          '--passive --norestart --wait --add Microsoft.VisualStudio.Component.Windows11SDK.22621',
          '', SW_SHOW, ewWaitUntilTerminated, exitCode
        ) then
          SuppressibleMsgBox('Failed to install the Windows SDK.', mbError, MB_OK, IDOK);
      end;

      if WizardIsTaskSelected('addtopath') then
        EnvAddPath(ExpandConstant('{app}'), IsAdminInstallMode());
    end;
  end;
end;

procedure CurUninstallStepChanged(CurUninstallStep: TUninstallStep);
begin
  if CurUninstallStep = usPostUninstall then
    EnvRemovePath(ExpandConstant('{app}'), IsAdminInstallMode());
end;
