/*
  ICS Multi File Transfer Demo - C++Builder translation of the Delphi demo
  OverbyteIcsXferTst1.pas (demos-delphi-vcl), ICS V9.8.

  Tests the multiple file transfer components:
  - TIcsFileCopy copies or deletes a single file, a directory tree or a whole volume, and
    cleans up (deletes or zips) old files by age or date range;
  - TIcsFtpMulti uploads and downloads files and directory trees by FTP, mirroring the
    directory structure, optionally only changed files, resuming failed transfers; the
    same transfers also run in background threads with TIcsFtpMultiThread;
  - TIcsHttpMulti downloads a list of URLs, or the files linked from HTML pages;
  - TIcsWebDAV lists, downloads and uploads files on a WebDAV server.
  Every transfer runs synchronously inside the button handler; the components pump
  messages, so the Abort buttons still work. Log lines are buffered and shown by a timer.

  How this C++ version differs from the Delphi original, apart from syntax:
  - The transfer components are local std::unique_ptr objects with no Owner, passed to the
    setup functions, instead of unit-level globals created with Create(Self).
  - The ini settings are read and written from one table of controls, so the two lists
    cannot drift apart; the ini file format is unchanged.
  - The FTP settings shared by TIcsFtpMulti and TIcsFtpMultiThread are applied by one
    template, where Delphi repeats them in SetFTPGen and SetFTPThreadGen.
  - The FTP threads are not kept in an array: nothing reads it after the thread starts.
  - Delphi demo bugs are fixed here; each fix has a one-line comment.
*/

#include <vcl.h>
#pragma hdrstop

#include <System.DateUtils.hpp>

#include <functional>
#include <initializer_list>
#include <memory>
#include <utility>
#include <vector>

#include "OverbyteIcsXferTst1.h"
#include "OverbyteIcsWSocket.hpp"
#include "OverbyteIcsHttpCCodZLib.hpp"
#include "OverbyteIcsIniFiles.hpp"
#include "OverbyteIcsUtils.hpp"
#include "OverbyteIcsSslBase.hpp"
#include "OverbyteIcsSslHttpRest.hpp"

#pragma package(smart_init)
#pragma resource "*.dfm"

TForm1 *Form1;

// Delphi links the gzip decoder by naming OverbyteIcsHttpCCodzlib in its uses clause; a C++
// #include links nothing, so this reference pulls the unit and its registration into the exe.
extern const TClass XferTstGzipCoder = __classid(THttpCCodzlib);

namespace {

const int MaxThreads = 20;

const int CleanupDeleteAge   = 0;
const int CleanupDeleteDates = 1;
const int CleanupZipAge      = 2;
const int CleanupZipDates    = 3;

const wchar_t *const SectionMainWindow = L"MainWindow";
const wchar_t *const SectionDemo       = L"demomain";
const wchar_t *const KeyTop            = L"Top";
const wchar_t *const KeyLeft           = L"Left";
const wchar_t *const KeyWidth          = L"Width";
const wchar_t *const KeyHeight         = L"Height";

// Runs a function however the scope ends (Delphi: try/finally). std::function holds whatever
// callable the caller passes, here lambdas that capture the form's this pointer.
class TOnExit
{
public:
    explicit TOnExit(std::function<void()> Func) : FFunc(std::move(Func)) {}
    ~TOnExit() { FFunc(); }
    TOnExit(const TOnExit &) = delete;
    TOnExit &operator=(const TOnExit &) = delete;
private:
    std::function<void()> FFunc;
};

// Enables the start buttons and disables the abort button when Idle, and the reverse when busy.
void SetIdle(std::initializer_list<TButton *> Starts, TButton *Abort, bool Idle)
{
    for (TButton *Btn : Starts)
        Btn->Enabled = Idle;
    if (Abort)
        Abort->Enabled = !Idle;
}

// Every saved control with the ini key it is stored under. The key is the name plus a suffix
// (_Text, _Checked, _ItemIndex, _Items, _Lines, _Date), as the Delphi demo wrote them.
template <class TCtrl>
using TSettingList = std::vector<std::pair<TCtrl *, const wchar_t *>>;

struct TSettingTables
{
    TSettingList<TStrings>        Lists;       // CommaText, before the combo texts
    TSettingList<TEdit>           Edits;       // _Text
    TSettingList<TComboBox>       ComboTexts;  // _Text
    TSettingList<TCheckBox>       Checks;      // _Checked, saved as 'True' / 'False'
    TSettingList<TComboBox>       ComboIndex;  // _ItemIndex
    TSettingList<TRadioGroup>     RadioIndex;  // _ItemIndex
    TSettingList<TDateTimePicker> Dates;       // _Date, saved as a float
};

TSettingTables SettingTables(TForm1 &F)
{
    TSettingTables T;
    T.Lists = {
        { F.HttpSrcDir->Lines, L"HttpSrcDir_Lines" },
        { F.FtpHost->Items,    L"FtpHost_Items" },
        { F.WebdavHost->Items, L"WebdavHost_Items" } };
    T.Edits = {
        { F.CopyIgnorePath, L"CopyIgnorePath" },   { F.CopySrcDir, L"CopySrcDir" },
        { F.CopySrcFile, L"CopySrcFile" },         { F.CopyTarDir, L"CopyTarDir" },
        { F.DirLogs, L"DirLogs" },                 { F.Ftp1LocDir, L"Ftp1LocDir" },
        { F.Ftp1Path, L"Ftp1Path" },               { F.Ftp1SrcName, L"Ftp1SrcName" },
        { F.Ftp1UpFile, L"Ftp1UpFile" },           { F.FtpBandWidth, L"FtpBandWidth" },
        { F.FtpIgnorePath, L"FtpIgnorePath" },     { F.FtpKeepAlive, L"FtpKeepAlive" },
        { F.FtpLocDir, L"FtpLocDir" },             { F.FtpPassword, L"FtpPassword" },
        { F.FtpPath, L"FtpPath" },                 { F.FtpPort, L"FtpPort" },
        { F.FtpPortSsl, L"FtpPortSsl" },           { F.FtpSrcFile, L"FtpSrcFile" },
        { F.FtpUsername, L"FtpUsername" },         { F.HttpBandWidth, L"HttpBandWidth" },
        { F.HttpTarDir, L"HttpTarDir" },           { F.NetLogon, L"NetLogon" },
        { F.NetPassword, L"NetPassword" },         { F.HttpParseLevels, L"HttpParseLevels" },
        { F.CleanupDays, L"CleanupDays" },         { F.CleanupFileMask, L"CleanupFileMask" },
        { F.CleanupPath, L"CleanupPath" },         { F.Copy1FileTar, L"Copy1FileTar" },
        { F.Copy1FileSrc, L"Copy1FileSrc" },       { F.UnzipDir, L"UnzipDir" },
        { F.WebdavAuthBearer, L"WebdavAuthBearer" }, { F.WebdavAuthLogin, L"WebdavAuthLogin" },
        { F.WebdavAuthPW, L"WebdavAuthPW" },       { F.WebdavExclDirs, L"WebdavExclDirs" },
        { F.WebdavMLocalDir, L"WebdavMLocalDir" }, { F.WebdavMRemPath, L"WebdavMRemPath" },
        { F.WebdavSDownDir, L"WebdavSDownDir" },   { F.WebdavSDownFile, L"WebdavSDownFile" },
        { F.WebdavSRemPath, L"WebdavSRemPath" },   { F.WebdavSUpPath, L"WebdavSUpPath" },
        { F.WebdavSrcFileMask, L"WebdavSrcFileMask" }, { F.WebdavPort, L"WebdavPort" } };
    T.ComboTexts = {
        { F.FtpHost, L"FtpHost" },       { F.HttpSrcFile, L"HttpSrcFile" },
        { F.WebdavHost, L"WebdavHost" }, { F.WebdavCliCertLoc, L"WebdavCliCertLoc" } };
    T.Checks = {
        { F.CopyEmptyDirs, L"CopyEmptyDirs" },       { F.CopySubdirs, L"CopySubdirs" },
        { F.CopyWow64Disable, L"CopyWow64Disable" }, { F.FtpCopyAllDir, L"FtpCopyAllDir" },
        { F.FtpDelDone, L"FtpDelDone" },             { F.FtpDelOldTar, L"FtpDelOldTar" },
        { F.FtpEmptyDirs, L"FtpEmptyDirs" },         { F.ftpIgnoreUtf8, L"FtpIgnoreUtf8" },
        { F.FtpNoCrc, L"FtpNoCrc" },                 { F.FtpNoFeatCmd, L"FtpNoFeatCmd" },
        { F.ftpNoHost, L"FtpNoHost" },               { F.FtpNoMd5, L"FtpNoMd5" },
        { F.FtpNoTmpFile, L"FtpNoTmpFile" },         { F.FtpNoUtf8, L"FtpNoUtf8" },
        { F.FtpNoZlib, L"FtpNoZlib" },               { F.Ftp1DelDone, L"Ftp1DelDone" },
        { F.FtpPassive, L"FtpPassive" },             { F.FtpSubdirs, L"FtpSubdirs" },
        { F.ReportChain, L"ReportChain" },           { F.RevokeCheck, L"RevokeCheck" },
        { F.ShowDiagsHigh, L"ShowDiagsHigh" },       { F.ShowDiagsLow, L"ShowDiagsLow" },
        { F.ShowDiagsSSL, L"ShowDiagsSSL" },         { F.ShowXProgesss, L"ShowXProgesss" },
        { F.FtpFixPassiveLanIP, L"FtpFixPassiveLanIP" }, { F.HttpParsePage, L"HttpParsePage" },
        { F.CleanupSubDirs, L"CleanupSubDirs" },     { F.CopyUnzip, L"CopyUnzip" },
        { F.CopyZip, L"CopyZip" },                   { F.Ftp1Unzip, L"Ftp1Unzip" },
        { F.Ftp1Zip, L"Ftp1Zip" },                   { F.FtpMultiUnzip, L"FtpMultiUnzip" },
        { F.FtpMultiZip, L"FtpMultiZip" },           { F.HttpUnzip, L"HttpUnzip" },
        { F.UnzipDownDel, L"UnzipDownDel" },         { F.NoSSL, L"NoSSL" },
        { F.WebdavCopyAllDir, L"WebdavCopyAllDir" }, { F.WebdavCopyEmptyDir, L"WebdavCopyEmptyDir" },
        { F.WebdavDelAfterXfer, L"WebdavDelAfterXfer" }, { F.WebdavDelTarFiles, L"WebdavDelTarFiles" },
        { F.WebdavJsonPropFind, L"WebdavJsonPropFind" }, { F.WebdavSDelAfter, L"WebdavSDelAfter" },
        { F.WebdavSubDirs, L"WebdavSubDirs" },       { F.WebdavSafeopy, L"WebdavSafeopy" },
        { F.WebdavCompress, L"WebdavCompress" } };
    T.ComboIndex = {
        { F.CopyFileRepl, L"CopyFileRepl" },     { F.FtpMultiFileRepl, L"FtpMultiFileRepl" },
        { F.Ftp1FileRepl, L"Ftp1FileRepl" },     { F.FtpServerType, L"FtpServerType" },
        { F.HttpFileRepl, L"HttpFileRepl" },     { F.SslSecurity, L"SslSecurity" },
        { F.Copy1Replc, L"Copy1Replc" },         { F.WebdavMFileRepl, L"WebdavMFileRepl" },
        { F.WebdavSFileRepl, L"WebdavSFileRepl" }, { F.WebdavCliCertLoc, L"WebdavCliCertLoc" } };
    T.RadioIndex = {
        { F.VerifyCertMode, L"VerifyCertMode" }, { F.XferSockFamily, L"XferSockFamily" },
        { F.UnzipZipPath, L"UnzipZipPath" },     { F.ZippingExtFmt, L"ZippingExtFmt" },
        { F.ZippingType, L"ZippingType" },       { F.WebdavAuthType, L"WebdavAuthType" } };
    T.Dates = {
        { F.CleanupDateFrom, L"CleanupDateFrom" }, { F.CleanupDateTo, L"CleanupDateTo" } };
    return T;
}

// The FTP settings common to TIcsFtpMulti and TIcsFtpMultiThread. The two classes share no
// base class but have the same property names, so a template is compiled once for each.
template <class TFtp>
void ApplyFtpSettings(TForm1 &F, TFtp *Ftp)
{
    Ftp->LocalHost = String(Overbyteicswsocket::LocalHostName());
    Ftp->HostName1 = F.FtpHost->Text;
    Ftp->HostName2 = "";
    Ftp->SocketFamily = static_cast<TSocketFamily>(F.XferSockFamily->ItemIndex);
    Ftp->SocketErrs = wsErrFriendly;
    Ftp->UserName = F.FtpUsername->Text;
    Ftp->PassWord = F.FtpPassword->Text;
    Ftp->FtpType = static_cast<TFtpType>(F.FtpServerType->ItemIndex);
    Ftp->Port = F.FtpPort->Text;
    Ftp->AttemptDelay = 5;
    Ftp->FailRepeat = 3;                                           // retries for failed xfers
    Ftp->NoopIntervalMins = Overbyteicsutils::atoi(F.FtpKeepAlive->Text);
    // ConnectionType: ftpDirect, ftpProxy, ftpSocks4, ftpSocks4A, ftpSocks5
    Ftp->ConnectionType = ftpDirect;
    Ftp->SocksPort = "";
    Ftp->SocksServer = "";
    Ftp->ProxyPort = "";
    Ftp->ProxyServer = "";
    Ftp->SocksUserCode = "";
    Ftp->SocksPassword = "";
    if (Ftp->ConnectionType == ftpProxy)
        Ftp->ProxyPort = F.FtpPort->Text;
    else if (Ftp->ConnectionType >= ftpSocks4) {
        Ftp->SocksPort = "1080";
        Ftp->SocksServer = "192.168.1.66";
    }
    Ftp->PassiveX = F.FtpPassive->Checked;                         // must be after connection type
    Ftp->HostType = FTPTYPE_NONE;
    Ftp->XferMode = XferModeBinary;
    Ftp->CaseFile = FileLowerCase;
    Ftp->DiffStampMins = 2;
    Ftp->Timeout = 600;                                            // 60 secs was too slow for MD5Sum
    Ftp->DispLog = true;
    Ftp->DispFiles = true;
    Ftp->DispRDir = true;
    Ftp->DispLDir = false;
    Ftp->MinResSize = 65535;                                       // also used for resume overlap
    Ftp->UpArchDir = "";
    Ftp->UpArchive = false;
    Ftp->ResFailed = true;
    Ftp->UseCompression = true;

    const std::pair<TCheckBox *, TIcsFtpMultiOpt> OptBoxes[] = {
        { F.FtpNoFeatCmd, magftpNoFeat },   { F.FtpNoZlib, magftpNoZlib },
        { F.FtpNoTmpFile, magftpNoTmpFile }, { F.FtpNoUtf8, magftpNoUtf8 },
        { F.ftpNoHost, magftpNoHost },      { F.ftpIgnoreUtf8, magftpIgnoreUtf8 },
        { F.FtpNoMd5, magftpNoMd5 },        { F.FtpNoCrc, magftpNoCrc } };
    TIcsFtpMultiOpts Opts = Ftp->MagFtpOpts;
    for (const auto &Box : OptBoxes) {
        if (Box.first->Checked)
            Opts << Box.second;
        else
            Opts >> Box.second;
    }
    Ftp->MagFtpOpts = Opts;

    Ftp->ZlibNoCompExt = ".zip;.rar;.7z;.cab;.lzh;.gz;.avi;.wmv;.mpg;.mp3;.jpg;.png;";
    Ftp->ZlibMaxSize = 500000000;
    Ftp->MaxResumeAttempts = 10;
    TFtpOptions Options = Ftp->Options;
    const int Bandwidth = Overbyteicsutils::atoi(F.FtpBandWidth->Text);
    if (Bandwidth > 0) {
        Ftp->BandwidthLimit = Bandwidth * 1024;
        Options << ftpBandwidthControl;
    }
    else
        Options >> ftpBandwidthControl;
    if (F.FtpFixPassiveLanIP->Checked)
        Options << ftpFixPasvLanIP;
    else
        Options >> ftpFixPasvLanIP;
    Ftp->Options = Options;
    Ftp->FtpSslPort = F.FtpPortSsl->Text;
    Ftp->SslSessCache = true;
    Ftp->FtpSslVerMethod = static_cast<TFtpSslVerifyMethod>(F.VerifyCertMode->ItemIndex);
    Ftp->FtpSslRevocation = F.RevokeCheck->Checked;
    Ftp->FtpSslReportChain = F.ReportChain->Checked;
    // Delphi's thread setup never sets FtpSslCliSecurity, so threads ignored the SSL security box.
    Ftp->FtpSslCliSecurity = static_cast<TSslCliSecurity>(F.SslSecurity->ItemIndex);
    Ftp->DispRemList = true;
}

} // namespace

__fastcall TForm1::TForm1(TComponent *Owner)
    : TForm(Owner)
{
}

void TForm1::GetSettings()
{
    // Delphi exits when there is no ini file, skipping the defaults below, so a first run sent
    // SslSecurity's ItemIndex of -1 to the components as a TSslCliSecurity.
    if (FileExists(FIniFileName)) {
        std::unique_ptr<TIcsIniFile> IniFile;
        try {
            IniFile.reset(new TIcsIniFile(FIniFileName, true));
        }
        catch (Exception &E) {
            AddLogText("Cannot open INI file: " + E.Message);   // Delphi exits silently
        }
        if (IniFile) {
            Width  = IniFile->ReadInteger(SectionMainWindow, KeyWidth, Width);
            Height = IniFile->ReadInteger(SectionMainWindow, KeyHeight, Height);
            Top    = IniFile->ReadInteger(SectionMainWindow, KeyTop, (Screen->Height - Height) / 2);
            Left   = IniFile->ReadInteger(SectionMainWindow, KeyLeft, (Screen->Width - Width) / 2);

            const TSettingTables T = SettingTables(*this);
            const String Key(SectionDemo);
            for (const auto &S : T.Lists)
                S.first->CommaText = IniFile->ReadString(Key, S.second, S.first->CommaText);
            for (const auto &S : T.Edits)
                S.first->Text = IniFile->ReadString(Key, String(S.second) + "_Text", S.first->Text);
            for (const auto &S : T.ComboTexts)
                S.first->Text = IniFile->ReadString(Key, String(S.second) + "_Text", S.first->Text);
            for (const auto &S : T.Checks)
                S.first->Checked = IniFile->ReadString(Key, String(S.second) + "_Checked", "False") == "True";
            for (const auto &S : T.ComboIndex)
                S.first->ItemIndex = IniFile->ReadInteger(Key, String(S.second) + "_ItemIndex", S.first->ItemIndex);
            for (const auto &S : T.RadioIndex)
                S.first->ItemIndex = IniFile->ReadInteger(Key, String(S.second) + "_ItemIndex", S.first->ItemIndex);
            for (const auto &S : T.Dates)
                S.first->Date = IniFile->ReadFloat(Key, String(S.second) + "_Date", S.first->Date);
        }
    }

    if (SslSecurity->ItemIndex <= 0)
        SslSecurity->ItemIndex = sslCliSecDefault;
    for (TComboBox *Combo : { FtpServerType, WebdavMFileRepl, Ftp1FileRepl, FtpMultiFileRepl,
                              HttpFileRepl, CopyFileRepl }) {
        if (Combo->ItemIndex < 0)
            Combo->ItemIndex = 0;
    }
}

void TForm1::PutSettings()
{
    std::unique_ptr<TIcsIniFile> IniFile;
    try {
        IniFile.reset(new TIcsIniFile(FIniFileName, true));
    }
    catch (Exception &) {
        return;
    }
    try {
        IniFile->WriteInteger(SectionMainWindow, KeyTop, Top);
        IniFile->WriteInteger(SectionMainWindow, KeyLeft, Left);
        IniFile->WriteInteger(SectionMainWindow, KeyWidth, Width);
        IniFile->WriteInteger(SectionMainWindow, KeyHeight, Height);

        const TSettingTables T = SettingTables(*this);
        const String Key(SectionDemo);
        for (const auto &S : T.Lists)
            IniFile->WriteString(Key, S.second, S.first->CommaText);
        for (const auto &S : T.Edits)
            IniFile->WriteString(Key, String(S.second) + "_Text", S.first->Text);
        for (const auto &S : T.ComboTexts)
            IniFile->WriteString(Key, String(S.second) + "_Text", S.first->Text);
        for (const auto &S : T.Checks)
            IniFile->WriteString(Key, String(S.second) + "_Checked", S.first->Checked ? "True" : "False");
        for (const auto &S : T.ComboIndex)
            IniFile->WriteInteger(Key, String(S.second) + "_ItemIndex", S.first->ItemIndex);
        for (const auto &S : T.RadioIndex)
            IniFile->WriteInteger(Key, String(S.second) + "_ItemIndex", S.first->ItemIndex);
        for (const auto &S : T.Dates)
            IniFile->WriteFloat(Key, String(S.second) + "_Date", S.first->Date);
    }
    catch (...) {
        IniFile->UpdateFile();   // as Delphi's finally: keep the settings written before the failure
        throw;
    }
    IniFile->UpdateFile();
}

// Opens the log file, or moves it when the directory changed; the log is written as UTF-8.
void TForm1::OpenLogFile()
{
    if (DirLogs->Text.IsEmpty())
        return;
    const String FName = "\"" + IncludeTrailingPathDelimiter(DirLogs->Text) +
                         "ics-xfertst-\"yyyy-mm-dd\".log\"";
    if (!FLogStream)
        FLogStream.reset(new TIcsBuffLogStream(nullptr, FName, Caption + IcsCRLF, FileCPUtf8));
    else {
        if (FName == FLogStream->NameMask)
            return;
        if (FLogStream->LogSize > 0)
            FLogStream->FlushFile(true);   // changing log path, write old log first
        FLogStream->NameMask = FName;
    }
    AddLogText(String(IcsCRLF) + "Opened log file: " + FLogStream->FullName);
}

// Buffers a log line for TimerUpdatesTimer, and writes it to the log file.
void TForm1::AddLogText(String S)
{
    if (Application->Terminated)
        return;
    S = FormatDateTime(ISOLongTimeMask, Now()) + String(IcsSPACE) + S;
    FBuffDiags += S + IcsCRLF;
    if (DirLogs->Text.IsEmpty() || !FLogStream)
        return;
    try {
        FLogStream->WriteLine(S);
    }
    catch (Exception &) {
        // as Delphi: a failed log write must not stop the transfer; the line is still on screen
    }
}

void TForm1::AddLogDelim(const String &S)
{
    LogDelim->Lines->Add(S);   // delimited formatted xfer information
}

void __fastcall TForm1::TimerUpdatesTimer(TObject *Sender)
{
    const int DispLen = FBuffDiags.Length();
    if (DispLen == 0)
        return;
    try {
        FBuffDiags.SetLength(DispLen - 2);   // remove CRLF
        LogText->Lines->Add(FBuffDiags);
        SendMessage(LogText->Handle, WM_VSCROLL, SB_BOTTOM, 0);
    }
    catch (Exception &) {
        LabelProgress->Caption = "Error writing to diag log";
    }
    FBuffDiags = "";
}

void __fastcall TForm1::doAbortClick(TObject *Sender)
{
    FAbortFlag = true;
}

void __fastcall TForm1::doExitClick(TObject *Sender)
{
    FAbortFlag = true;
    Close();
}

void __fastcall TForm1::FormCreate(TObject *Sender)
{
    Caption = Caption + IcsVerLitSample;   // common ICS sample caption: version and date
    ReportMemoryLeaksOnShutdown = (DebugHook != 0);
    OnCloseQuery = FormCloseQuery;   // not in Delphi or the shared .dfm; stops running FTP threads
    FtpServerType->Items->Clear();
    for (int I = FtpTypeNone; I <= FtpTypeConnSslBoth; I++)
        FtpServerType->Items->Add(FtpTypeStrings[I]);
    FtpServerType->ItemIndex = 0;
    FIniFileName = GetIcsIniFileName();
    LabelSslState->Caption = "SSL Enabled, Unicode GUI";
    SslSecurity->Items->Clear();   // SSL client security levels from the ICS literals
    for (int Level = sslCliSecIgnore; Level <= sslCliSecHigh192; Level++)
        SslSecurity->Items->Add(SslCliSecurityNames[Level]);
    GetSettings();   // after SslSecurity is loaded
    LabelSslState->Caption = LabelSslState->Caption + IcsCRLF + IcsBuiltWithEx();
    AddLogText("INI File: " + FIniFileName + " VCL");
    AddLogText(String(IcsVerLitRelDate) + ", Compiler: " + IcsBuiltWithEx() + " VCL");

    // how InitSSL loads OpenSSL on first use
    GSSL_SignTest_Check = true;         // check digitally signed
    GSSL_SignTest_Certificate = true;   // check digital certificate
    GSSLEAY_LOAD_LEGACY = false;        // OpenSSL 3.0 legacy provider for old algorithms
}

void __fastcall TForm1::FormDestroy(TObject *Sender)
{
    PutSettings();
    FLogStream.reset();
}

void TForm1::InitSSL()
{
    if (NoSSL->Checked)
        return;
    IcsSslRootCAStore->Initialise();   // if OpenSSL and the internal root store are not loaded, do it
    AddLogText("SSL/TLS DLL: " + IcsReportOpenSSLVer(true));
}

void __fastcall TForm1::onCopyEvent(TIcsCopyLogLevel LogLevel, String Info, bool &Cancel)
{
    if (LogLevel == LogLevelInfo || LogLevel == LogLevelFile) {
        AddLogText(Info);
        LabelProgress->Caption = Info;
    }
    if (LogLevel == LogLevelProg)
        LabelProgress->Caption = Info.IsEmpty() ? String() : "Progress: " + Info;
    if (LogLevel == LogLevelDiag && (ShowDiagsLow->Checked || ShowDiagsHigh->Checked || ShowDiagsSSL->Checked))
        AddLogText(Info);
    if (LogLevel == LogLevelDelimFile || LogLevel == LogLevelDelimTot)
        AddLogDelim(Info);
    if (FAbortFlag)
        Cancel = true;
}

void __fastcall TForm1::onProgressEvent(TObject *Sender, const TIcsCopyProgress &CopyProgress, bool &Cancel)
{
    const TIcsCopyProgress &P = CopyProgress;
    if (P.LogLevel == LogLevelInfo) {
        AddLogText(P.Info);
        LabelProgress->Caption = P.Info;
    }
    if (P.LogLevel == LogLevelFile)
        AddLogText(P.Info);
    if (P.LogLevel == LogLevelProg) {
        String S = P.Info;
        if (P.CurFileBytes > 0) {
            S = "Copying File " + P.CurSrcName + " to " + P.CurTarName + ", Size " +
                IntToKbyte(P.CurFileBytes - P.CurFileStartPos) +   // partial file when resumed
                ", Done " + IntToStr(P.CurDonePercent) + "%";
            if (P.CurEstimateTicks > 5000)   // only if more than 5 secs
                S += ", Time Left " + IcsSecsToStr(static_cast<int>((P.CurEstimateTicks - P.CurDoneTicks) / 1000));
        }
        if (P.TotProcBytes > 0) {
            S += String(IcsCRLF) + "Totals: Copying " + IcsIntToCStr(P.TotDoneNr) + " of " +
                 IcsIntToCStr(P.TotProcFiles) + ", Total Size " + IntToKbyte(P.TotProcBytes) +
                 ", Done " + IntToStr(P.SessDonePercent) + "%";
            if (P.SessEstimateTicks > 5000) {
                const String S2 = "Time Left " +
                    IcsSecsToStr(static_cast<int>((P.SessEstimateTicks - P.SessDoneTicks) / 1000)) +
                    ", average speed " + IntToKbyte(P.SessAvSpeed) + "/sec";
                S += ", " + S2;
                if (FLogEstimate)   // log the session estimate once only
                    AddLogText("Session Estimated " + S2);
                FLogEstimate = false;
            }
        }
        LabelProgress->Caption = S;
    }
    if (P.LogLevel == LogLevelDiag && (ShowDiagsLow->Checked || ShowDiagsHigh->Checked || ShowDiagsSSL->Checked))
        AddLogText(P.Info);
    if (P.LogLevel == LogLevelDelimFile || P.LogLevel == LogLevelDelimTot)
        AddLogDelim(P.Info);
    if (FAbortFlag)
        Cancel = true;
}

void __fastcall TForm1::doCopyListClick(TObject *Sender)
{
    OpenLogFile();
    AddLogText(DateToStr(Now()));
    FAbortFlag = false;
    SetIdle({ doCopyFiles, doCopyCheck, doCopyList }, doCopyAbort, false);
    TOnExit Done([this] {
        LabelProgress->Caption = "List Completed";
        SetIdle({ doCopyFiles, doCopyCheck, doCopyList }, doCopyAbort, true);
    });
    std::unique_ptr<TIcsFileCopy> FileCopy(new TIcsFileCopy(nullptr));
    FileCopy->CopyEvent = onCopyEvent;
    AddLogText(String(IcsCRLF) + "Directory: " + CopySrcDir->Text + IcsCRLF +
               FileCopy->DispLocFiles(CopySrcDir->Text, CopySrcFile->Text, FCTypeMaskDir,
                                      CopySubdirs->Checked, false, CopyEmptyDirs->Checked));
}

void __fastcall TForm1::CopyFiles(TObject *Sender)
{
    OpenLogFile();
    AddLogText(DateToStr(Now()));
    FAbortFlag = false;
    SetIdle({ doCopyFiles, doCopyCheck, doCopyList }, doCopyAbort, false);
    TOnExit Done([this] {   // declared first, so it runs after FileCopy is freed
        LabelProgress->Caption = "Copy Completed";
        SetIdle({ doCopyFiles, doCopyCheck, doCopyList }, doCopyAbort, true);
    });
    std::unique_ptr<TIcsFileCopy> FileCopy(new TIcsFileCopy(nullptr));
    const bool CheckFlag = (Sender == doCopyCheck);
    try {
        // MultiDir true copies several specific directories, passed as MultiDirList
        // (srcdir + tab + tardir + recsep, repeated) or as the SrcDirList / TarDirList pair
        FileCopy->MultiDir = false;
        FileCopy->SrcDir = CopySrcDir->Text;
        FileCopy->SrcFName = CopySrcFile->Text;
        FileCopy->TarDir = CopyTarDir->Text;
        FileCopy->IgnorePaths = CopyIgnorePath->Text;
        FileCopy->Wow64RedirDisable = CopyWow64Disable->Checked;
        // CopyType: FCTypeSingle, FCTypeMaskDir, FCTypeArchDir, FCTypeAllDir, FCTypeDates
        FileCopy->CopyType = FCTypeMaskDir;
        FileCopy->SubDirs = CopySubdirs->Checked;
        FileCopy->EmptyDirs = CopyEmptyDirs->Checked;
        FileCopy->DelDone = false;     // true delete source file after copy
        FileCopy->DelOldTar = false;   // true delete target files not in source directories
        FileCopy->Mask = false;        // true, allow date/time masked characters in SrcFName
        FileCopy->Prev = false;        // true, use yesterday's date for Mask
        FileCopy->Repl = static_cast<TIcsFileCopyRepl>(CopyFileRepl->ItemIndex);
        FileCopy->ReplRO = true;       // true, replace read only files
        FileCopy->Safe = false;        // true, copy file with TMP extension, rename when done
        FileCopy->LocalHost = String(Overbyteicswsocket::LocalHostName());
        if (ShowXProgesss->Checked)
            FileCopy->ProgressEvent = onProgressEvent;
        else
            FileCopy->CopyEvent = onCopyEvent;
        FileCopy->Zipped = CopyUnzip->Checked || CopyZip->Checked;
        FileCopy->ZipDownDel = UnzipDownDel->Checked;
        // ZipPath: PathNone, PathNew, PathOriginal, PathNewOrig, PathSpecific, PathSpecOrig
        // ZipType: TypeUnzip, TypeSrcAddX, TypeSrcReplX, TypeSrcDirs
        // Delphi tests the FTP tab's FtpMultiUnzip here; this tab's own box is CopyUnzip.
        if (CopyUnzip->Checked)
            FileCopy->ZipType = TypeUnzip;
        else
            FileCopy->ZipType = static_cast<TIcsZipType>(ZippingType->ItemIndex);
        FileCopy->ZipPath = static_cast<TIcsZipPath>(UnzipZipPath->ItemIndex);
        FileCopy->ZipDir = UnzipDir->Text;
        if (FileCopy->ZipPath >= PathSpecific && !DirectoryExists(FileCopy->ZipDir)) {
            AddLogText("Unzip Directory Not Found: " + FileCopy->ZipDir);
            return;
        }
        FileCopy->IgnoreFileExt = "tmp";
        FLogEstimate = true;
        const TIcsTaskResult TaskRes = FileCopy->SelCopyFiles(CheckFlag);   // main file copy function
        AddLogText("Task Result: " + IcsGetTaskResName(TaskRes));
        AddLogText(FileCopy->ReqResponse);
    }
    catch (Exception &E) {
        AddLogText("Copying Error - " + IcsGetExceptMess(&E));
    }
}

void __fastcall TForm1::DeleteFiles(TObject *Sender)
{
    OpenLogFile();
    AddLogText(DateToStr(Now()));
    FAbortFlag = false;
    SetIdle({ doDeleteFiles, doDeleteCheck }, doCopyAbort, false);
    TOnExit Done([this] {
        LabelProgress->Caption = "Delete Completed";
        SetIdle({ doDeleteFiles, doDeleteCheck }, doCopyAbort, true);
    });
    std::unique_ptr<TIcsFileCopy> FileCopy(new TIcsFileCopy(nullptr));
    const bool CheckFlag = (Sender == doDeleteCheck);
    try {
        FileCopy->SrcDir = CopyTarDir->Text;
        FileCopy->SrcFName = CopySrcFile->Text;
        FileCopy->MultiDir = false;
        FileCopy->SubDirs = CopySubdirs->Checked;
        FileCopy->EmptyDirs = CopyEmptyDirs->Checked;
        FileCopy->IgnorePaths = CopyIgnorePath->Text;
        FileCopy->Mask = false;
        FileCopy->Prev = false;
        FileCopy->CopyLoDT = 0;       // lower date range
        FileCopy->CopyHiDT = Now();   // higher date range
        if (ShowXProgesss->Checked)
            FileCopy->ProgressEvent = onProgressEvent;
        else
            FileCopy->CopyEvent = onCopyEvent;
        const TIcsTaskResult TaskRes = FileCopy->DeleteFiles(CheckFlag);   // main file delete function
        AddLogText("Task Result: " + IcsGetTaskResName(TaskRes));
        AddLogText(FileCopy->ReqResponse);
    }
    catch (Exception &E) {
        AddLogText("Copying Error - " + IcsGetExceptMess(&E));
    }
}

void __fastcall TForm1::HttpDownload(TObject *Sender)
{
    OpenLogFile();
    AddLogText(DateToStr(Now()));
    FAbortFlag = false;
    SetIdle({ doHttpDownCheck, doHttpDownFiles }, doHttpAbort, false);
    // Delphi calls InitSSL outside its try/finally, so an OpenSSL load failure left the buttons disabled.
    TOnExit Done([this] {
        LabelProgress->Caption = "HTTP Completed";
        SetIdle({ doHttpDownCheck, doHttpDownFiles }, doHttpAbort, true);
    });
    InitSSL();
    std::unique_ptr<TIcsHttpMulti> Http(new TIcsHttpMulti(nullptr));
    const bool CheckFlag = (Sender == doHttpDownCheck);
    try {
        Http->NoSSL = NoSSL->Checked;
        Http->UrlList = HttpSrcDir->Lines->Text;   // one or more source URLs separated by CRLF
        Http->SocketFamily = static_cast<TSocketFamily>(XferSockFamily->ItemIndex);
        Http->SrcMask = HttpSrcFile->Text;   // optional source file mask to restrict downloads
        Http->DownDir = HttpTarDir->Text;
        Http->KeepPath = false;              // if true, use HTTP path for subdirs
        Http->KeepHost = false;              // if true, use HTTP host for subdir
        Http->SocketErrs = wsErrFriendly;
        Http->MaxAttempts = 3;               // logon attempts
        Http->Repl = static_cast<TIcsFileCopyRepl>(HttpFileRepl->ItemIndex);
        Http->ReplRO = true;
        Http->LogFiles = true;               // log each file downloaded
        Http->LogProt = ShowDiagsHigh->Checked;
        Http->LogLDir = false;
        Http->LogRDir = true;
        THttpCliOptions Options = Http->Options;
        Options << httpoEnableContentCoding;
        const int Bandwidth = Overbyteicsutils::atoi(HttpBandWidth->Text);
        if (Bandwidth > 0) {
            Http->BandwidthLimit = Bandwidth * 1024;
            Options << httpoBandwidthControl;
        }
        else
            Options >> httpoBandwidthControl;
        Http->Options = Options;
        Http->ParseHTML = HttpParsePage->Checked;   // parse HTML pages for links to files
        Http->ParseLevels = Overbyteicsutils::atoi(HttpParseLevels->Text);
        Http->HttpSslVerMethod = static_cast<THttpSslVerifyMethod>(VerifyCertMode->ItemIndex);
        Http->SslRevocation = RevokeCheck->Checked;
        Http->SslReportChain = ReportChain->Checked;
        Http->SslCliSecurity = static_cast<TSslCliSecurity>(SslSecurity->ItemIndex);
        Http->SslSessCache = true;
        Http->CopyEvent = onCopyEvent;
        if (ShowDiagsLow->Checked)
            Http->WSDebugOptions = Http->WSDebugOptions << wsdlogHdr;          // HTTP headers
        if (ShowDiagsSSL->Checked)
            Http->WSDebugOptions = Http->WSDebugOptions + DefWSDebugDevel;     // all devel logging
        Http->Zipped = HttpUnzip->Checked;
        Http->ZipDownDel = UnzipDownDel->Checked;
        Http->ZipPath = static_cast<TIcsZipPath>(UnzipZipPath->ItemIndex);
        Http->ZipDir = UnzipDir->Text;
        if (Http->ZipPath >= PathSpecific && !DirectoryExists(Http->ZipDir)) {
            AddLogText("Unzip Directory Not Found: " + Http->ZipDir);
            return;
        }
        FLogEstimate = true;
        const TIcsTaskResult TaskRes = Http->Download(CheckFlag);   // main download function
        AddLogText("Task Result: " + IcsGetTaskResName(TaskRes));
        AddLogText(Http->ReqResponse);
    }
    catch (Exception &E) {
        AddLogText("Download Error - " + IcsGetExceptMess(&E));
    }
}

// Adds the host to the top of the combo's history, keeping about 50.
void TForm1::AddHostToList(TComboBox *Hosts)
{
    if (Hosts->Items->IndexOf(Hosts->Text) >= 0)
        return;
    if (Hosts->Items->Count > 50)
        Hosts->Items->Delete(Hosts->Items->Count - 1);
    Hosts->Items->Insert(0, Hosts->Text);
}

// Sets up an FTP client from the form; returns true when setup failed (as Delphi).
bool TForm1::SetFTPGen(TIcsFtpMulti *Ftp)
{
    InitSSL();
    OpenLogFile();
    AddLogText(DateToStr(Now()));
    FAbortFlag = false;
    FLogEstimate = true;
    AddHostToList(FtpHost);
    try {
        Ftp->NoSSL = NoSSL->Checked;
        ApplyFtpSettings(*this, Ftp);
        Ftp->MaxAttempts = 3;   // logon attempts
        if (ShowXProgesss->Checked)
            Ftp->ProgressEvent = onProgressEvent;
        else
            Ftp->CopyEvent = onCopyEvent;
        if (ShowDiagsSSL->Checked)
            Ftp->ControlSocket->WSDebugOptions = Ftp->ControlSocket->WSDebugOptions + DefWSDebugDevel;
        Ftp->Zipped = FtpMultiUnzip->Checked || FtpMultiZip->Checked;
        Ftp->ZipDownDel = UnzipDownDel->Checked;
        // ZipExtFmt: ExtnAdd, ExtnReplace
        Ftp->ZipExtFmt = static_cast<TIcsZipExtFmt>(ZippingExtFmt->ItemIndex);
        Ftp->ZipPath = static_cast<TIcsZipPath>(UnzipZipPath->ItemIndex);
        Ftp->ZipDir = UnzipDir->Text;
        if (Ftp->ZipPath >= PathSpecific && !DirectoryExists(Ftp->ZipDir)) {
            AddLogText("Unzip Directory Not Found: " + Ftp->ZipDir);
            return true;   // Delphi exits here with Result still False, so the transfer went ahead
        }
    }
    catch (Exception &E) {
        AddLogText("FTP Error - " + IcsGetExceptMess(&E));
        return true;
    }
    return false;
}

void __fastcall TForm1::onThreadEvent(TIcsCopyLogLevel LogLevel, const String Id, const String Info, bool &Cancel)
{
    onCopyEvent(LogLevel, Id + ": " + Info, Cancel);
}

void __fastcall TForm1::OnThreadTerminate(TObject *Sender)
{
    TIcsFtpMultiThread *Thread = static_cast<TIcsFtpMultiThread *>(Sender);
    FCurThreads--;   // counted before any logging, so FormCloseQuery is never left waiting
    if (FCurThreads <= 0)
        doFtpAbort->Enabled = false;
    AddLogText("Terminated FTP: " + Thread->ID);
    AddLogText("Task Result: " + IcsGetTaskResName(Thread->TaskRes));
    // Delphi logs only the task result, which hides an exception raised inside the thread.
    if (Exception *E = dynamic_cast<Exception *>(Thread->FatalException))
        AddLogText("FTP Thread Exception - " + IcsGetExceptMess(E));
    if (Thread->TaskRes == TaskResOKNew && Thread->FtpThreadOpt == ftpthdList)
        AddLogText(Thread->DirListing);
}

// Creates a suspended FTP thread set up from the form, or returns nullptr when too many run.
// The thread frees itself when it finishes (FreeOnTerminate, set by its constructor).
TIcsFtpMultiThread *TForm1::SetFTPThreadGen()
{
    InitSSL();
    OpenLogFile();
    AddLogText(DateToStr(Now()));
    FAbortFlag = false;
    doFtpAbort->Enabled = true;
    FLogEstimate = true;
    AddHostToList(FtpHost);
    if (FCurThreads >= MaxThreads)
        return nullptr;
    TIcsFtpMultiThread *Thread = new TIcsFtpMultiThread();
    const int ThreadNr = FNextThread;
    FCurThreads++;
    FNextThread++;
    if (FNextThread >= MaxThreads)
        FNextThread = 0;
    try {
        Thread->FThreadEvent = onThreadEvent;
        Thread->OnTerminate = OnThreadTerminate;
        Thread->FreeOnTerminate = true;
        Thread->ID = "ThreadNr=" + IntToStr(ThreadNr);
        Thread->NoProgress = true;   // stop progress log events
        ApplyFtpSettings(*this, Thread);
        Thread->MaxAttempts = 2;     // logon attempts
    }
    catch (Exception &E) {
        DiscardThread(Thread, true);   // Delphi returns the half-configured thread and the caller starts it
        AddLogText("FTP Error - " + IcsGetExceptMess(&E));
        return nullptr;
    }
    return Thread;
}

// Drops a thread that never ran. TThread's destructor resumes it terminated, so Execute never
// runs, but OnTerminate would still fire and count the thread twice, hence it is cleared first.
// After a failed Start the destructor can wait forever for the thread, so it is left unfreed.
void TForm1::DiscardThread(TIcsFtpMultiThread *Thread, bool Free)
{
    Thread->OnTerminate = nullptr;
    Thread->FThreadEvent = nullptr;
    if (Free)
        delete Thread;
    FCurThreads--;
    if (FCurThreads <= 0)
        doFtpAbort->Enabled = false;
}

// Delphi closes with FTP threads still running, so the process exit kills them mid-file. Here the
// close asks them to abort and waits up to 30 seconds for them to finish; after that the operator
// is told and the window closes anyway, stopping what is left. With NoProgress a thread sees the
// abort only between files, so a long file usually runs out the 30 seconds.
void __fastcall TForm1::FormCloseQuery(TObject *Sender, bool &CanClose)
{
    if (FCurThreads <= 0)
        return;
    FAbortFlag = true;
    const String Waiting = "Waiting up to 30 seconds for " + IntToStr(FCurThreads) + " FTP transfer(s) to stop";
    AddLogText(Waiting);
    LabelProgress->Caption = Waiting;
    LabelProgress->Update();   // repaint now: no messages are processed during the wait
    Screen->Cursor = crHourGlass;
    const ULONGLONG Started = GetTickCount64();
    while (FCurThreads > 0 && GetTickCount64() - Started < 30000)
        CheckSynchronize(50);   // runs the threads' synchronized events, which see FAbortFlag, and OnTerminate
    Screen->Cursor = crDefault;
    if (FCurThreads > 0) {
        const String Stopped = IntToStr(FCurThreads) + " FTP transfer(s) did not stop within 30 seconds "
                               "and will be ended now; their files may be incomplete.";
        AddLogText(Stopped);
        Application->MessageBox(Stopped.c_str(), L"FTP transfers still running", MB_OK | MB_ICONWARNING);
    }
}

void TForm1::SetFtpButtons(bool Value)
{
    // Delphi leaves Upload One enabled, so it could start a second transfer inside a running one.
    SetIdle({ doFtpDownCheck, doFtpDownFiles, doFtpUpCheck, doFtpUpFiles, doFtpList, doFtpDown1, doFtpUp1 },
            doFtpAbort, Value);
    doFtpAbort1->Enabled = !Value;
}

void __fastcall TForm1::doFtpListClick(TObject *Sender)
{
    FAbortFlag = false;
    SetFtpButtons(false);
    TOnExit Done([this] {
        LabelProgress->Caption = "FTP Completed";
        SetFtpButtons(true);
    });
    std::unique_ptr<TIcsFtpMulti> Ftp(new TIcsFtpMulti(nullptr));
    if (SetFTPGen(Ftp.get()))
        return;
    try {
        Ftp->BulkMode = BulkModeDownload;
        Ftp->SrcDir = FtpPath->Text;
        Ftp->SubDirs = FtpSubdirs->Checked;
        Ftp->EmptyDirs = FtpEmptyDirs->Checked;
        String DirListing;
        const TIcsTaskResult TaskRes = Ftp->DispFtpDir(DirListing);   // main FTP function
        AddLogText("Task Result: " + IcsGetTaskResName(TaskRes));
        AddLogText(DirListing);
    }
    catch (Exception &E) {
        AddLogText("FTP Error - " + IcsGetExceptMess(&E));
    }
}

void __fastcall TForm1::doFtpListThreadClick(TObject *Sender)
{
    FAbortFlag = false;
    try {
        TIcsFtpMultiThread *Thread = SetFTPThreadGen();
        if (!Thread)
            return;
        const String Id = Thread->ID;   // Delphi reads ID after Start, when a FreeOnTerminate thread may be gone
        try {   // a thread that never started is not freed by FreeOnTerminate
            Thread->BulkMode = BulkModeDownload;
            Thread->SrcDir = FtpPath->Text;
            Thread->SubDirs = FtpSubdirs->Checked;
            Thread->EmptyDirs = FtpEmptyDirs->Checked;
            Thread->FtpThreadOpt = ftpthdList;
        }
        catch (Exception &) {
            DiscardThread(Thread, true);
            throw;
        }
        try {
            Thread->Start();
        }
        catch (Exception &) {
            DiscardThread(Thread, false);
            throw;
        }
        AddLogText("Created FTP: " + Id);
    }
    catch (Exception &E) {
        AddLogText("FTP Error - " + IcsGetExceptMess(&E));
    }
}

void __fastcall TForm1::FtpDownload(TObject *Sender)
{
    FAbortFlag = false;
    SetFtpButtons(false);
    TOnExit Done([this] {
        LabelProgress->Caption = "FTP Completed";
        SetFtpButtons(true);
    });
    std::unique_ptr<TIcsFtpMulti> Ftp(new TIcsFtpMulti(nullptr));
    if (SetFTPGen(Ftp.get()))
        return;
    const bool CheckFlag = (Sender == doFtpDownCheck);
    try {
        Ftp->BulkMode = BulkModeDownload;
        Ftp->SrcDir = FtpPath->Text;
        Ftp->TarDir = FtpLocDir->Text;
        Ftp->CopyType = FtpCopyAllDir->Checked ? FCTypeAllDir : FCTypeMaskDir;
        Ftp->DelDone = FtpDelDone->Checked;
        Ftp->DelOldTar = FtpDelOldTar->Checked;
        Ftp->SubDirs = FtpSubdirs->Checked;
        Ftp->EmptyDirs = FtpEmptyDirs->Checked;
        Ftp->IgnorePaths = FtpIgnorePath->Text;
        Ftp->SrcFName = FtpSrcFile->Text;   // may include masked directories if Mask, MaskLocDir, MaskRemDir
        Ftp->Mask = false;                  // true, allow date/time masked characters and directories in SrcFName
        Ftp->Prev = false;                  // true, use yesterday's date for Mask
        Ftp->MaskLocDir = false;
        Ftp->MaskRemDir = false;
        Ftp->Repl = static_cast<TIcsFileCopyRepl>(FtpMultiFileRepl->ItemIndex);
        Ftp->ReplRO = true;
        Ftp->Safe = false;                  // true, copy file with TMP extension, rename when done
        Ftp->IgnoreFileExt = "tmp;ftp";
        const TIcsTaskResult TaskRes = Ftp->FtpDownload(CheckFlag);   // main FTP function
        AddLogText("Task Result: " + IcsGetTaskResName(TaskRes));
        AddLogText(Ftp->ReqResponse);
    }
    catch (Exception &E) {
        AddLogText("FTP Error - " + IcsGetExceptMess(&E));
    }
}

void __fastcall TForm1::FtpUpload(TObject *Sender)
{
    FAbortFlag = false;
    SetFtpButtons(false);
    TOnExit Done([this] {
        LabelProgress->Caption = "FTP Completed";
        SetFtpButtons(true);
    });
    std::unique_ptr<TIcsFtpMulti> Ftp(new TIcsFtpMulti(nullptr));
    if (SetFTPGen(Ftp.get()))
        return;
    const bool CheckFlag = (Sender == doFtpUpCheck);
    try {
        Ftp->BulkMode = BulkModeUpload;
        Ftp->SrcDir = FtpLocDir->Text;
        Ftp->TarDir = FtpPath->Text;
        Ftp->CopyType = FtpCopyAllDir->Checked ? FCTypeAllDir : FCTypeMaskDir;
        Ftp->DelDone = FtpDelDone->Checked;
        Ftp->DelOldTar = FtpDelOldTar->Checked;
        Ftp->SubDirs = FtpSubdirs->Checked;
        Ftp->EmptyDirs = FtpEmptyDirs->Checked;
        Ftp->IgnorePaths = FtpIgnorePath->Text;
        Ftp->SrcFName = FtpSrcFile->Text;
        Ftp->Mask = false;
        Ftp->Prev = false;
        Ftp->MaskLocDir = false;
        Ftp->MaskRemDir = false;
        Ftp->Repl = static_cast<TIcsFileCopyRepl>(FtpMultiFileRepl->ItemIndex);
        Ftp->Safe = true;        // ignored for uploads
        Ftp->IgnoreFileExt = "tmp";
        Ftp->TimeStamp = false;  // update local file time stamp to match remote
        const TIcsTaskResult TaskRes = Ftp->FtpUpload(CheckFlag);   // main FTP function
        AddLogText("Task Result: " + IcsGetTaskResName(TaskRes));
        AddLogText(Ftp->ReqResponse);
    }
    catch (Exception &E) {
        AddLogText("FTP Error - " + IcsGetExceptMess(&E));
    }
}

void __fastcall TForm1::doFtpDown1Click(TObject *Sender)
{
    FAbortFlag = false;
    SetFtpButtons(false);
    TOnExit Done([this] {
        LabelProgress->Caption = "FTP Completed";
        SetFtpButtons(true);
    });
    std::unique_ptr<TIcsFtpMulti> Ftp(new TIcsFtpMulti(nullptr));
    TOnExit Logoff([&Ftp] { Ftp->FtpLogoff(); });   // runs before Ftp is freed
    if (SetFTPGen(Ftp.get()))
        return;
    try {
        Ftp->SrcDir = "/";
        Ftp->BulkMode = BulkModeDownload;
        // Ftp1DelDone is not applied: DelFile is pending a TIcsFtpMulti fix
        const TIcsFileCopyRepl Replace = static_cast<TIcsFileCopyRepl>(Ftp1FileRepl->ItemIndex);
        TIcsTaskResult TaskRes = Ftp->FtpLogon();
        if (TaskRes == TaskResOKNew)
            TaskRes = Ftp->FtpDownOneFile(Ftp1Path->Text, Ftp1SrcName->Text,
                                          Ftp1LocDir->Text + "\\" + Ftp1SrcName->Text, Replace);
        AddLogText("Task Result: " + IcsGetTaskResName(TaskRes));
        AddLogText(Ftp->ReqResponse);
    }
    catch (Exception &E) {
        AddLogText("FTP Error - " + IcsGetExceptMess(&E));
    }
}

void __fastcall TForm1::doFtpUp1Click(TObject *Sender)
{
    FAbortFlag = false;
    SetFtpButtons(false);
    TOnExit Done([this] {
        LabelProgress->Caption = "FTP Completed";
        SetFtpButtons(true);
    });
    std::unique_ptr<TIcsFtpMulti> Ftp(new TIcsFtpMulti(nullptr));
    TOnExit Logoff([&Ftp] { Ftp->FtpLogoff(); });   // runs before Ftp is freed
    if (SetFTPGen(Ftp.get()))
        return;
    try {
        Ftp->TarDir = "/";
        Ftp->BulkMode = BulkModeUpload;
        // Ftp1DelDone is not applied: DelFile is pending a TIcsFtpMulti fix
        const TIcsFileCopyRepl Replace = static_cast<TIcsFileCopyRepl>(Ftp1FileRepl->ItemIndex);
        TIcsTaskResult TaskRes = Ftp->FtpLogon();
        if (TaskRes == TaskResOKNew)
            TaskRes = Ftp->FtpUpOneFile(Ftp1UpFile->Text, Ftp1Path->Text, Ftp1SrcName->Text, Replace);
        AddLogText("Task Result: " + IcsGetTaskResName(TaskRes));
        AddLogText(Ftp->ReqResponse);
    }
    catch (Exception &E) {
        AddLogText("FTP Error - " + IcsGetExceptMess(&E));
    }
}

void __fastcall TForm1::doFtpDownThreadClick(TObject *Sender)
{
    FAbortFlag = false;
    try {
        TIcsFtpMultiThread *Thread = SetFTPThreadGen();
        if (!Thread)
            return;
        const String Id = Thread->ID;   // Delphi reads ID after Start, when a FreeOnTerminate thread may be gone
        try {   // a thread that never started is not freed by FreeOnTerminate
            Thread->BulkMode = BulkModeDownload;
            Thread->SrcDir = FtpPath->Text;
            Thread->TarDir = FtpLocDir->Text;
            Thread->CopyType = FtpCopyAllDir->Checked ? FCTypeAllDir : FCTypeMaskDir;
            Thread->DelDone = FtpDelDone->Checked;
            Thread->DelOldTar = FtpDelOldTar->Checked;
            Thread->SubDirs = FtpSubdirs->Checked;
            Thread->EmptyDirs = FtpEmptyDirs->Checked;
            Thread->IgnorePaths = FtpIgnorePath->Text;   // commented out in Delphi, so threads ignored the box
            Thread->SrcFName = FtpSrcFile->Text;
            Thread->Mask = false;
            Thread->Prev = false;
            Thread->MaskLocDir = false;
            Thread->MaskRemDir = false;
            Thread->Repl = static_cast<TIcsFileCopyRepl>(FtpMultiFileRepl->ItemIndex);
            Thread->ReplRO = true;
            Thread->Safe = false;
            Thread->IgnoreFileExt = "tmp;ftp";
            Thread->FtpThreadOpt = ftpthdDownFiles;
        }
        catch (Exception &) {
            DiscardThread(Thread, true);
            throw;
        }
        try {
            Thread->Start();
        }
        catch (Exception &) {
            DiscardThread(Thread, false);
            throw;
        }
        AddLogText("Created FTP: " + Id);
    }
    catch (Exception &E) {
        AddLogText("FTP Error - " + IcsGetExceptMess(&E));
    }
}

void __fastcall TForm1::doFtpUpThreadClick(TObject *Sender)
{
    FAbortFlag = false;
    try {
        TIcsFtpMultiThread *Thread = SetFTPThreadGen();
        if (!Thread)
            return;
        const String Id = Thread->ID;   // Delphi reads ID after Start, when a FreeOnTerminate thread may be gone
        try {   // a thread that never started is not freed by FreeOnTerminate
            Thread->BulkMode = BulkModeUpload;
            Thread->SrcDir = FtpLocDir->Text;
            Thread->TarDir = FtpPath->Text;
            Thread->CopyType = FtpCopyAllDir->Checked ? FCTypeAllDir : FCTypeMaskDir;
            Thread->DelDone = FtpDelDone->Checked;
            Thread->DelOldTar = FtpDelOldTar->Checked;
            Thread->SubDirs = FtpSubdirs->Checked;
            Thread->EmptyDirs = FtpEmptyDirs->Checked;
            Thread->IgnorePaths = FtpIgnorePath->Text;   // commented out in Delphi, so threads ignored the box
            Thread->SrcFName = FtpSrcFile->Text;
            Thread->Mask = false;
            Thread->Prev = false;
            Thread->MaskLocDir = false;
            Thread->MaskRemDir = false;
            Thread->Repl = static_cast<TIcsFileCopyRepl>(FtpMultiFileRepl->ItemIndex);
            Thread->Safe = true;        // ignored for uploads
            Thread->IgnoreFileExt = "tmp";
            Thread->TimeStamp = false;
            Thread->FtpThreadOpt = ftpthdUpFiles;
        }
        catch (Exception &) {
            DiscardThread(Thread, true);
            throw;
        }
        try {
            Thread->Start();
        }
        catch (Exception &) {
            DiscardThread(Thread, false);
            throw;
        }
        AddLogText("Created FTP: " + Id);
    }
    catch (Exception &E) {
        AddLogText("FTP Error - " + IcsGetExceptMess(&E));
    }
}

bool TForm1::ValidCleanupMask(const String &Mask)
{
    const String Extn = LowerCase(ExtractFileExt(Mask));
    const String FName = IcsExtractNameOnly(Mask);
    if (Mask.Length() < 2 || Extn.Length() < 2) {
        AddLogText("File Extension Required: " + Mask);
        return false;
    }
    if (Extn.Pos("*") > 0) {
        AddLogText("Wildcard Not Allowed in File Extension: " + Extn);
        return false;
    }
    if (Extn == ".exe" || Extn == ".dll" || Extn == ".sys" || Extn == ".ini") {
        AddLogText("Unable to Clean-up this File Type: " + Extn);
        return false;
    }
    if (FName.Pos("*") == 0) {
        AddLogText("Wildcard Required in File Name: " + FName);
        return false;
    }
    return true;
}

bool TForm1::SetCleanupDates()
{
    const double FiveSecs = 5 * OneSecond;
    if (CleanupType->ItemIndex == CleanupDeleteAge || CleanupType->ItemIndex == CleanupZipAge) {
        const int Days = Overbyteicsutils::atoi(CleanupDays->Text);
        if (Days < 7) {
            AddLogText("At Least Seven Days Old");
            return false;
        }
        FCleanLoDT = 1;
        FCleanHiDT = DateOf(Date()).Val - Days + 1 - FiveSecs;
    }
    else {
        FCleanLoDT = DateOf(CleanupDateFrom->Date);
        FCleanHiDT = DateOf(CleanupDateTo->Date).Val + 1 - FiveSecs;
        if (FCleanLoDT.Val >= FCleanHiDT.Val) {
            AddLogText("Invalid Date Range");
            return false;
        }
    }
    return true;
}

void __fastcall TForm1::CleanupTypeClick(TObject *Sender)
{
    const int Type = CleanupType->ItemIndex;
    BoxCleanupDays->Visible = (Type == CleanupDeleteAge || Type == CleanupZipAge);
    BoxCleanupDates->Visible = (Type == CleanupDeleteDates || Type == CleanupZipDates);
}

void TForm1::doCleanup(bool CheckFiles)
{
    if (CleanupPath->Text.IsEmpty()) {
        AddLogText("No Clean-up Path Specified");
        return;
    }
    if (!ValidCleanupMask(CleanupFileMask->Text) || !SetCleanupDates())
        return;
    SetIdle({ doCleanupCheck, doCleanupStart }, nullptr, false);
    TOnExit Done([this] { SetIdle({ doCleanupCheck, doCleanupStart }, nullptr, true); });
    std::unique_ptr<TIcsFileCopy> FileCopy(new TIcsFileCopy(nullptr));
    FileCopy->SrcFName = CleanupFileMask->Text;
    FileCopy->SrcDir = CleanupPath->Text;
    FileCopy->CopyType = FCTypeDates;
    FileCopy->SubDirs = CleanupSubDirs->Checked;
    FileCopy->Zipped = (CleanupType->ItemIndex == CleanupZipDates || CleanupType->ItemIndex == CleanupZipAge);
    FileCopy->EmptyDirs = false;
    FileCopy->CopyLoDT = FCleanLoDT;
    FileCopy->CopyHiDT = FCleanHiDT;
    FileCopy->ProgressEvent = onProgressEvent;
    AddLogText("Clean-up Path: " + CleanupPath->Text + "\\" + CleanupFileMask->Text);
    const TIcsTaskResult TaskRes = FileCopy->DeleteFiles(CheckFiles);
    AddLogText("Task Result: " + IcsGetTaskResName(TaskRes));
    AddLogText(FileCopy->ReqResponse);
    AddLogText("Clean-up, Total Files: " + IcsIntToCStr(FileCopy->TotSrcFiles));
}

void __fastcall TForm1::doCleanupCheckClick(TObject *Sender)
{
    doCleanup(true);
}

void __fastcall TForm1::doCleanupStartClick(TObject *Sender)
{
    doCleanup(false);
}

void __fastcall TForm1::doClearClick(TObject *Sender)
{
    LogDelim->Lines->Clear();
    LogText->Lines->Clear();
}

void __fastcall TForm1::doCopyOneFileClick(TObject *Sender)
{
    OpenLogFile();
    AddLogText(DateToStr(Now()));
    FAbortFlag = false;
    doCopyOneFile->Enabled = false;
    TOnExit Done([this] {
        LabelProgress->Caption = "";
        doCopyOneFile->Enabled = true;
    });
    std::unique_ptr<TIcsFileCopy> FileCopy(new TIcsFileCopy(nullptr));
    try {
        FileCopy->ProgressEvent = onProgressEvent;
        __int64 NewFSize = 0;
        const TIcsTaskResult TaskRes = FileCopy->CopyOneFile(Copy1FileSrc->Text, Copy1FileTar->Text,
            static_cast<TIcsFileCopyRepl>(Copy1Replc->ItemIndex), true, NewFSize);
        AddLogText("Task Result: " + IcsGetTaskResName(TaskRes));
        AddLogText(FileCopy->ReqResponse);
    }
    catch (Exception &E) {
        AddLogText("Copying Error - " + IcsGetExceptMess(&E));
    }
}

void __fastcall TForm1::WebDAVRestProg(TObject *Sender, TLogOption LogOption, const String Msg)
{
    if (LogOption == loProgress) {
        LabelProgress->Caption = Msg;
        LabelProgress->Refresh();
    }
    else
        AddLogText(Msg);
}

// Sets up a WebDAV client from the form; returns true when setup failed (as Delphi).
bool TForm1::SetWebDAVGen(TIcsWebDAV *WebDAV)
{
    InitSSL();
    OpenLogFile();
    AddLogText(DateToStr(Now()));
    FAbortFlag = false;
    FLogEstimate = true;
    AddHostToList(WebdavHost);   // Delphi adds to the FTP host list here; WebdavHost_Items is the saved WebDAV list
    try {
        WebDAV->OnHttpRestProg = WebDAVRestProg;   // main logging
        WebDAV->DebugLevel = Overbyteicstypes::DebugConn;
        if (ShowDiagsSSL->Checked)
            WebDAV->DebugLevel = Overbyteicstypes::DebugSsl;
        if (ShowDiagsLow->Checked)
            WebDAV->DebugLevel = Overbyteicstypes::DebugHdr;
        WebDAV->SslCertVerMethod = static_cast<Overbyteicstypes::TCertVerMethod>(VerifyCertMode->ItemIndex);
        WebDAV->SslRevocation = RevokeCheck->Checked;
        WebDAV->SslReportChain = ReportChain->Checked;
        WebDAV->SslCliSecurity = static_cast<TSslCliSecurity>(SslSecurity->ItemIndex);
        const String CertFile = WebdavCliCertFile->Text;
        if (WebdavCliCertLoc->ItemIndex == CliCertLocFile && !CertFile.IsEmpty()) {
            if (FileExists(CertFile)) {
                try {
                    String Errs;
                    // the private key could also be loaded separately with PrivateKeyLoadFromPemFile
                    WebDAV->SslCliCert->LoadFromFileEx(CertFile, croYes, croTry, "password", Errs);
                    if (!Errs.IsEmpty())
                        AddLogText(Errs);
                    if (!WebDAV->SslCliCert->CheckCertAndPKey())
                        AddLogText("SSL client certificate ignored, invalid: " + CertFile);
                    else
                        AddLogText("SSL client certificate loaded OK: " + CertFile);
                }
                catch (Exception &E) {
                    AddLogText("Failed to load SSL client certificate: " + E.Message + " - " + CertFile);
                }
            }
            else
                AddLogText("SSL client certificate ignored, not found: " + CertFile);
        }
        WebDAV->WDHostName1 = WebdavHost->Text;
        WebDAV->WDHostName2 = "";
        WebDAV->WDPort = WebdavPort->Text;
        WebDAV->ServerAuth = static_cast<THttpAuthType>(WebdavAuthType->ItemIndex);
        WebDAV->Username = WebdavAuthLogin->Text;
        WebDAV->Password = WebdavAuthPW->Text;
        WebDAV->AuthBearerToken = WebdavAuthBearer->Text;
        WebDAV->SocketFamily = static_cast<TSocketFamily>(XferSockFamily->ItemIndex);
        WebDAV->UseCompression = WebdavCompress->Checked;   // currently downloads only
        WebDAV->ShowProgress = true;
        WebDAV->Timeout = 60;
        WebDAV->SyncMaxSecs = 0;   // large files may take hours
        WebDAV->CacheType = httpCacheNone;
        WebDAV->FollowRelocation = false;
        WebDAV->Connection = "Keep-Alive";
        WebDAV->LocalHost = String(Overbyteicswsocket::LocalHostName());
        WebDAV->AttemptDelay = 5;
        WebDAV->MaxAttempts = 1;   // logon attempts
        WebDAV->FailRepeat = 0;    // retries for failed xfers
        WebDAV->DiffStampMins = 2;
        WebDAV->DispLog = true;
        WebDAV->DispRDir = true;
        WebDAV->DispLDir = true;
        if (ShowXProgesss->Checked)
            WebDAV->ProgressEvent = onProgressEvent;   // progress and delim file logging, allows Cancel
        else
            WebDAV->CopyEvent = onCopyEvent;
        WebDAV->UpArchDir = "";
        WebDAV->UpArchive = false;
        WebDAV->ResFailed = true;
        WebDAV->MaxResumeAttempts = 10;
        WebDAV->DispRemList = true;
    }
    catch (Exception &E) {
        AddLogText("WebDAV Error - " + IcsGetExceptMess(&E));
        return true;
    }
    return false;
}

void TForm1::SetWebDAVButtons(bool Value)
{
    SetIdle({ doWebdavMDown, doWebdavMCheckDown, doWebdavMUp, doWebdavMCheckUp, doWebdavMList },
            doWebdavMAbort, Value);
}

void __fastcall TForm1::doWebdavMListClick(TObject *Sender)
{
    FAbortFlag = false;
    SetWebDAVButtons(false);
    TOnExit Done([this] { SetWebDAVButtons(true); });
    std::unique_ptr<TIcsWebDAV> WebDAV(new TIcsWebDAV(nullptr));
    if (SetWebDAVGen(WebDAV.get()))
        return;
    try {
        WebDAV->RemoteDir = WebdavMRemPath->Text;
        WebDAV->SubDirs = WebdavSubDirs->Checked;
        WebDAV->EmptyDirs = WebdavCopyEmptyDir->Checked;
        WebDAV->JsonPropFind = WebdavJsonPropFind->Checked;
        String DirListing;
        const TIcsTaskResult TaskRes = WebDAV->DispRemDir(DirListing);   // main WebDAV remote directory function
        AddLogText("Task Result: " + IcsGetTaskResName(TaskRes));
        AddLogText(DirListing);
    }
    catch (Exception &E) {
        AddLogText("WebDAV Error - " + IcsGetExceptMess(&E));
    }
}

// Bound to both the Check Download and Download buttons.
void __fastcall TForm1::doWebdavMDownFiles(TObject *Sender)
{
    FAbortFlag = false;
    SetWebDAVButtons(false);
    TOnExit Done([this] { SetWebDAVButtons(true); });
    const bool CheckFlag = (Sender != doWebdavMDown);
    std::unique_ptr<TIcsWebDAV> WebDAV(new TIcsWebDAV(nullptr));
    if (SetWebDAVGen(WebDAV.get()))
        return;
    try {
        WebDAV->RemoteDir = WebdavMRemPath->Text;
        WebDAV->LocalDir = WebdavMLocalDir->Text;
        WebDAV->SubDirs = WebdavSubDirs->Checked;
        WebDAV->EmptyDirs = WebdavCopyEmptyDir->Checked;
        WebDAV->JsonPropFind = WebdavJsonPropFind->Checked;
        // CopyType: FCTypeSingle, FCTypeMaskDir, FCTypeArchDir, FCTypeAllDir, FCTypeDates
        WebDAV->CopyType = WebdavCopyAllDir->Checked ? FCTypeAllDir : FCTypeMaskDir;
        WebDAV->DelDone = WebdavDelAfterXfer->Checked;
        WebDAV->DelOldTar = WebdavDelTarFiles->Checked;
        WebDAV->IgnorePaths = WebdavExclDirs->Text;
        WebDAV->CopyFName = WebdavSrcFileMask->Text;   // may include masked directories if Mask, MaskLocDir, MaskRemDir
        WebDAV->Mask = false;
        WebDAV->Prev = false;
        WebDAV->MaskLocDir = false;
        WebDAV->MaskRemDir = false;
        WebDAV->Repl = static_cast<TIcsFileCopyRepl>(WebdavMFileRepl->ItemIndex);
        WebDAV->ReplRO = true;
        WebDAV->Safe = WebdavSafeopy->Checked;   // true, copy file with TMP extension, rename when done
        WebDAV->IgnoreFileExt = "tmp;ftp";
        const TIcsTaskResult TaskRes = WebDAV->RemDownload(CheckFlag);   // main WebDAV download files function
        AddLogText("Task Result: " + IcsGetTaskResName(TaskRes));
    }
    catch (Exception &E) {
        AddLogText("WebDAV Error - " + IcsGetExceptMess(&E));
    }
}

// Bound to both the Check Upload and Upload buttons.
void __fastcall TForm1::doWebdavMUpClick(TObject *Sender)
{
    FAbortFlag = false;
    SetWebDAVButtons(false);
    TOnExit Done([this] { SetWebDAVButtons(true); });
    const bool CheckFlag = (Sender != doWebdavMUp);
    std::unique_ptr<TIcsWebDAV> WebDAV(new TIcsWebDAV(nullptr));
    if (SetWebDAVGen(WebDAV.get()))
        return;
    try {
        WebDAV->RemoteDir = WebdavMRemPath->Text;
        WebDAV->LocalDir = WebdavMLocalDir->Text;
        WebDAV->SubDirs = WebdavSubDirs->Checked;
        WebDAV->EmptyDirs = WebdavCopyEmptyDir->Checked;
        WebDAV->JsonPropFind = WebdavJsonPropFind->Checked;
        WebDAV->CopyType = WebdavCopyAllDir->Checked ? FCTypeAllDir : FCTypeMaskDir;
        WebDAV->DelDone = WebdavDelAfterXfer->Checked;
        WebDAV->DelOldTar = WebdavDelTarFiles->Checked;
        WebDAV->IgnorePaths = WebdavExclDirs->Text;
        WebDAV->CopyFName = WebdavSrcFileMask->Text;
        WebDAV->Mask = false;
        WebDAV->Prev = false;
        WebDAV->MaskLocDir = false;
        WebDAV->MaskRemDir = false;
        WebDAV->Repl = static_cast<TIcsFileCopyRepl>(WebdavMFileRepl->ItemIndex);
        WebDAV->ReplRO = true;
        WebDAV->Safe = WebdavSafeopy->Checked;
        WebDAV->IgnoreFileExt = "tmp;ftp";
        const TIcsTaskResult TaskRes = WebDAV->RemUpload(CheckFlag);   // main WebDAV upload files function
        AddLogText("Task Result: " + IcsGetTaskResName(TaskRes));
    }
    catch (Exception &E) {
        AddLogText("WebDAV Error - " + IcsGetExceptMess(&E));
    }
}

// The single file WebDAV tab is not implemented in the Delphi demo yet; the .dfm binds these.
void __fastcall TForm1::doWebdavSAbortClick(TObject *Sender)
{
}

void __fastcall TForm1::doWebdavSDownClick(TObject *Sender)
{
}

void __fastcall TForm1::doWebdavSUpClick(TObject *Sender)
{
}
