/*
  ICS HTTPS REST and OAuth Demo - C++Builder translation of the Delphi demo
  OverbyteIcsHttpRestTst1.pas (demos-delphi-vcl), ICS V9.8.

  Tests the TSslHttpRest component with GET, QUERY, POST, HEAD, PUT, DELETE, PATCH,
  OPTIONS, TRACE and the WebDAV verbs, REST parameters from a grid, Json and XML
  response parsing, file download and upload, and the related components: TRestOAuth
  (OAuth2 logins), TDnsQueryHttps (DNS over HTTPS), TIcsSMS (SMS bureaus), TIcsTwitter,
  TIcsRestEmail (Gmail and Microsoft 365 REST email), TIcsInetAlive (internet
  connectivity) and TSslWebSocketCli (WebSocket client). Json responses are shown in the
  list view; a double click opens a nested object in the Json Object window
  (OverbyteIcsHttpRestTst2). Settings are kept in the ICS ini file.

  How this C++ version differs from the Delphi original, apart from syntax:
  - A client certificate from the Windows certificate store is loaded only when the ICS
    C++ library has OverbyteIcsMsSslUtils.hpp (ICS_HAVE_MSCRYPT, set below the includes).
    The standard ICS build undefines MSCRYPT_Tools for C++Builder and has no TMsCertTools;
    then choosing a store location logs that instead.
  - Components created at run time have no Owner and are held in std::unique_ptr.
  - The unit globals (log buffer, SMS ids, raw email, Json array lines, OAuth accounts,
    WebSocket line counter) are members of the form.
  - The ini settings are saved and restored from tables of controls, with the key derived
    from the control name, instead of one hand-written line per control; the keys are the
    ones the Delphi demo writes.
  - Delphi demo bugs are fixed here; each fix has a one-line comment.
*/

#include <vcl.h>
#pragma hdrstop

#include <algorithm>
#include <iterator>
#include <memory>
#include <vector>

#include "OverbyteIcsHttpRestTst1.h"
#include "OverbyteIcsHttpRestTst2.h"
#include "OverbyteIcsLogin.h"
#include "OverbyteIcsUtils.hpp"
#include "OverbyteIcsIniFiles.hpp"
#include "OverbyteIcsWSocket.hpp"
#include "OverbyteIcsUrl.hpp"
#include "OverbyteIcsMimeUtils.hpp"
#include "OverbyteIcsHtmlUtils.hpp"
#include "OverbyteIcsSslBase.hpp"
#include "OverbyteIcsLIBEAY.hpp"
#include "OverbyteIcsDnsQuery.hpp"
#include "OverbyteIcsFileCopy.hpp"
#include "OverbyteIcsHttpMulti.hpp"
#include "OverbyteIcsSuperXMLParser.hpp"
#if __has_include(<OverbyteIcsMsSslUtils.hpp>)
#define ICS_HAVE_MSCRYPT 1
#include "OverbyteIcsMsSslUtils.hpp"
#endif

#pragma package(smart_init)
#pragma link "OverbyteIcsDnsQuery"
#pragma link "OverbyteIcsHttpProt"
#pragma link "OverbyteIcsSslBase"
#pragma link "OverbyteIcsWndControl"
#pragma resource "*.dfm"

THttpRestForm *HttpRestForm;

namespace {

const wchar_t *const SectionMainWindow = L"MainWindow";
const wchar_t *const SectionObjWindow  = L"ObjWindow";
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";
const wchar_t *const SectionData       = L"Data";
const wchar_t *const KeyRestParams     = L"RestParams";

// Disables a button while an operation runs and re-enables it however the scope ends
// (Delphi: try/finally).
class TButtonBusy
{
public:
    explicit TButtonBusy(TButton *Btn) : FBtn(Btn) { FBtn->Enabled = false; }
    ~TButtonBusy() { FBtn->Enabled = true; }
    TButtonBusy(const TButtonBusy &) = delete;
    TButtonBusy &operator=(const TButtonBusy &) = delete;
private:
    TButton *FBtn;
};

// Authentication types that use a login and password, so a 401 shows the login window.
bool UsesLogin(THttpAuthType Auth)
{
    return Auth == httpAuthBasic || Auth == httpAuthNtlm || Auth == httpAuthDigest || Auth == httpAuthDigest2;
}

// Converts a literal parameter type from the grid into the type, default string.
TRParamType FindPType(const String &PType)
{
    for (int PT = RPTypeStr; PT <= RPTypeFile; PT++) {
        if (RParamTypeLits[PT] == PType)
            return static_cast<TRParamType>(PT);
    }
    return RPTypeStr;
}

// Delphi indexes DnsRCodeTable (codes 0 to 5) with the server's response code, which can be
// up to 15 on the wire and higher in Json, reading past the table.
String RCodeName(int RCode)
{
    if (RCode >= 0 && RCode < DnsRCodeTable.size())
        return DnsRCodeTable[RCode];
    return "Response Code " + IntToStr(RCode);
}

void AddDnsHeaderRows(TListView *List)
{
    List->Items->Add()->Caption = "";   // blank line
    TListItem *Item = List->Items->Add();
    Item->Caption = "Query Name";
    Item->SubItems->Add("Type");
    Item->SubItems->Add("Data");
    Item->SubItems->Add("TTL");
}

// The array in a server reply, or nullptr when the field is missing or not an array.
// Delphi calls AsArray unchecked on replies, and AsArray returns nil for a non-array.
TSuperArray *JsonArrayOf(const _di_ISuperObject &Json)
{
    return (Json && Json->DataType == stArray) ? Json->AsArray() : nullptr;
}

// Formats the Microsoft Graph "emailAddress" object, or an array of them, as
// "name" <address> separated by commas.
String JsonEmailAddresses(_di_ISuperObject HdrJson)
{
    String Result;
    if (!HdrJson)
        return Result;
    if (HdrJson->DataType == stArray) {
        TSuperArray *Arr = HdrJson->AsArray();
        for (int I = 0; I < Arr->Length; I++) {
            _di_ISuperObject EmailJson = Arr->O[I]->O["emailAddress"];
            if (!EmailJson)
                continue;
            // Delphi assigns instead of appending here, so only the last recipient is shown.
            if (!Result.IsEmpty())
                Result += ", ";
            Result += "\"" + EmailJson->S["name"] + "\" <" + EmailJson->S["address"] + ">";
        }
    }
    else {
        _di_ISuperObject EmailJson = HdrJson->O["emailAddress"];
        if (EmailJson)
            Result = "\"" + EmailJson->S["name"] + "\" <" + EmailJson->S["address"] + ">";
    }
    return Result;
}

// {"emailAddress": {"address": ..., "name": ...}} from a friendly email "Name <address>".
_di_ISuperObject EmailAddressJson(const String &Friendly)
{
    String Name;
    const String Address = ParseEmail(Trim(Friendly), Name);
    _di_ISuperObject Addr = SO();
    Addr->S["address"] = Address;
    Addr->S["name"] = Name;
    _di_ISuperObject Result = SO();
    Result->O["emailAddress"] = Addr;
    return Result;
}

} // namespace

__fastcall THttpRestForm::THttpRestForm(TComponent *Owner)
    : TForm(Owner)
{
}

THttpDebugLevel THttpRestForm::SelectedDebugLevel()
{
    return static_cast<THttpDebugLevel>(DebugLogging->ItemIndex);
}

void __fastcall THttpRestForm::FormCreate(TObject *Sender)
{
    Caption = Caption + IcsVerLitSample;   // common ICS sample caption: version and date
    ReportMemoryLeaksOnShutdown = (DebugHook != 0);
    FIniFileName = GetIcsIniFileName();
    FCookieFileName = ChangeFileExt(FIniFileName, ".cookie");
    LogWin->Lines->Add("INI File: " + FIniFileName);

    // Ignore OpenSSL 4 and use OpenSSL 3, only if OpenSSL_Resource_Files and
    // OpenSSL_AutoLoad_CA_Bundle are not defined.
    GSSLEAY_DLL_IgnoreNew = true;
    GSSL_SignTest_Check = true;         // check OpenSSL DLLs are digitally signed
    GSSL_SignTest_Certificate = true;   // and check the signing certificate

    if (ICS_OPENSSL_VERSION_NUMBER > 0)   // loaded at startup; report it, including load errors
        LogWin->Lines->Add(String(IcsVerLitRelDate) + ", SSL/TLS " + IcsReportOpenSSLVer(true));
    LogWin->Lines->Add("Built With " + IcsBuiltWithEx());
    GridParams->Cells[0][0] = "Name";
    GridParams->Cells[1][0] = "Value";
    GridParams->Cells[2][0] = "Data Type";

    // Delphi leaves the client certificate controls unbound, so an open keep-alive connection
    // would go on using the previous certificate; drop it, as the other settings do. The name
    // uses OnExit, not OnChange, so typing does not abort a transfer at every keystroke.
    SslClientCertLoc->OnChange = SettingsChange;
    SslClientCertFile->OnExit = SettingsChange;
}

// Bound in the .dfm; the Delphi handler is empty too./* Ini keys are "<control name>_Text", "_Checked", "_ItemIndex" or "_Lines" in [Data], the   same keys the Delphi demo writes, so both versions share one ini file layout. */
void THttpRestForm::LoadSettings()
{
	std::unique_ptr<TIcsIniFile> IniFile(new TIcsIniFile(FIniFileName, true));
    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);
    FormObject->Width = IniFile->ReadInteger(SectionObjWindow, KeyWidth, FormObject->Width);
    FormObject->Height = IniFile->ReadInteger(SectionObjWindow, KeyHeight, FormObject->Height);
    FormObject->Top = IniFile->ReadInteger(SectionObjWindow, KeyTop, (Screen->Height - FormObject->Height) / 2);
    FormObject->Left = IniFile->ReadInteger(SectionObjWindow, KeyLeft, (Screen->Width - FormObject->Width) / 2);

    // The grid is saved as one '|' delimited list, row by row.
    std::unique_ptr<TStringList> SL(new TStringList);
    SL->Delimiter = L'|';
    SL->DelimitedText = IniFile->ReadString(SectionData, KeyRestParams, "");
    int K = 0;
    for (int I = 0; I < GridParams->RowCount; I++) {
        for (int J = 0; J < GridParams->ColCount; J++, K++) {
            // Delphi assigns SL[K] even past the end of the saved list, which raises "List index
            // out of bounds" on a first run with no ini, and the rest of FormShow is skipped.
            if (K >= SL->Count)
                continue;
            if (J == 2 && SL->Strings[K].Length() < 3 && !GridParams->Cells[0][I].IsEmpty())
                SL->Strings[K] = RParamTypeLits[RPTypeStr];   // old y/n values become RPTypeStr
            GridParams->Cells[J][I] = SL->Strings[K];
        }
    }
    GridParams->Cells[2][0] = "Data Type";

    // An edit's default is its current .dfm text.
    for (TEdit *Edit : { AuthBearer, AuthLogin, AuthPassword, OAuthScope, OAuthRefrMins,
            OAuthAccToken, OAuthAppUrl, OAuthClientId, OAuthClientSecret, OAuthExpire,
            OAuthRedirectUrl, OAuthRefToken, OAuthTokenUrl, OAuthWebPort, ProxyURL, RawParams,
            SslClientCertFile, DirLogs, KapowAccName, KapowAccPw, SmsAccSender, AlpnProtos,
            OAuthPrompt, TwitAccSecret, TwitAccToken, TwitApiKey, TwitApiSecret, TwitIdList,
            TwitQuery, TwitScrnId, TwitScrnName, OAuthConsoleUrl, EmailClientId, EmailAccToken,
            EmailRefrToken, EmailAccount, EmailTokenAccnt, EmailClientSecret, EmailFrom, EmailCC,
            EmailSubject, EmailAccExpiry, EmailAccountHint, RestDownFile, HttpUploadFile,
            OAuthEdgeCache, SmsWorksJWT, OAuthLoginHint, WebsocketPingSecs, EmailFileAttach,
            SyncReqTimeout, SyncReqMaxSecs, CacheSeconds })
        Edit->Text = IniFile->ReadString(SectionData, Edit->Name + "_Text", Edit->Text);
    // These default to empty and are filled from the component defaults in FormShow.
    for (TEdit *Edit : { UserAgent, SslGroupsEdit, SslCipherEdit, SslCipher13Edit })
        Edit->Text = IniFile->ReadString(SectionData, Edit->Name + "_Text", "");
    for (TComboBox *Combo : { OAuthWebIP, RestURL, DnsHttpsUrl, DnsDomainName, ReqAccept,
            OAuthUserAuth, EmailUserAuth, WebSockUrl, WebsockMessage })
        Combo->Text = IniFile->ReadString(SectionData, Combo->Name + "_Text", Combo->Text);
    for (TComboBox *Combo : { DnsQueryType, EmailRestType, SslClientCertLoc })
        Combo->ItemIndex = IniFile->ReadInteger(SectionData, Combo->Name + "_ItemIndex", Combo->ItemIndex);
    for (TRadioGroup *Radio : { AuthType, CertVerMethod, DebugLogging, OAuthAuthType, ParamContent,
            ReqMode, ReqType, SslSecurity, IpSockFamily, AliveMethod, AliveNets, ReqMemStrategy,
            HttpUploadStrat, EmailAuthType, TwitAuthType, ParamJsonFmt, SslSniType, CacheControl,
            ReqPropFind })
        Radio->ItemIndex = IniFile->ReadInteger(SectionData, Radio->Name + "_ItemIndex", Radio->ItemIndex);
    for (TCheckBox *Check : { OAuthAutoRefresh, OAuthOptNoRedir, ReportCertChain, DnsDnssec,
            DnsNoValidation, OAuthAccess, TwitForceLogin, EmailForceLogin, ReqReplFile, CertRevoke,
            OAuthOptBasic, WebsocketAllHeaders, EmailRawHdrs, FormDataUtf8, NoSSL, SaveNonTextual,
            OAuthOptNoSecret, GzipCompress, FollowRelocations, ReqGetRaw, ReqPFindSome })
        Check->Checked = (IniFile->ReadString(SectionData, Check->Name + "_Checked", "False") == "True");
    for (TMemo *Memo : { ExtraHeaders, SmsMsgText, SmsDestNums, TwitMsg, EmailRecipList, EmailMessage })
        Memo->Lines->CommaText = IniFile->ReadString(SectionData, Memo->Name + "_Lines", "");
    WebsockMessLines->Lines->CommaText = IniFile->ReadString(SectionData, "WebsockMessLines_Lines",
                                                             WebsockMessLines->Lines->CommaText);
}

void THttpRestForm::SaveSettings()
{
    std::unique_ptr<TIcsIniFile> IniFile(new TIcsIniFile(FIniFileName, true));
    IniFile->WriteInteger(SectionMainWindow, KeyTop, Top);
    IniFile->WriteInteger(SectionMainWindow, KeyLeft, Left);
    IniFile->WriteInteger(SectionMainWindow, KeyWidth, Width);
    IniFile->WriteInteger(SectionMainWindow, KeyHeight, Height);

    std::unique_ptr<TStringList> SL(new TStringList);
    SL->Delimiter = L'|';
    for (int I = 0; I < GridParams->RowCount; I++) {
        for (int J = 0; J < GridParams->ColCount; J++)
            SL->Add(GridParams->Cells[J][I]);
    }
    IniFile->WriteString(SectionData, KeyRestParams, SL->DelimitedText);

    for (TEdit *Edit : { AuthBearer, AuthLogin, AuthPassword, OAuthScope, OAuthRefrMins,
            OAuthAccToken, OAuthAppUrl, OAuthClientId, OAuthClientSecret, OAuthExpire,
            OAuthRedirectUrl, OAuthRefToken, OAuthTokenUrl, OAuthWebPort, ProxyURL, RawParams,
            SslClientCertFile, DirLogs, KapowAccName, KapowAccPw, SmsAccSender, AlpnProtos,
            OAuthPrompt, TwitAccSecret, TwitAccToken, TwitApiKey, TwitApiSecret, TwitIdList,
            TwitQuery, TwitScrnId, TwitScrnName, OAuthConsoleUrl, EmailClientId, EmailAccToken,
            EmailRefrToken, EmailAccount, EmailTokenAccnt, EmailClientSecret, EmailFrom, EmailCC,
            EmailSubject, EmailAccExpiry, EmailAccountHint, RestDownFile, HttpUploadFile,
            OAuthEdgeCache, SmsWorksJWT, OAuthLoginHint, WebsocketPingSecs, EmailFileAttach,
            SyncReqTimeout, SyncReqMaxSecs, CacheSeconds,
            UserAgent, SslGroupsEdit, SslCipherEdit, SslCipher13Edit })
        IniFile->WriteString(SectionData, Edit->Name + "_Text", Edit->Text);
    for (TComboBox *Combo : { OAuthWebIP, RestURL, DnsHttpsUrl, DnsDomainName, ReqAccept,
            OAuthUserAuth, EmailUserAuth, WebSockUrl, WebsockMessage })
        IniFile->WriteString(SectionData, Combo->Name + "_Text", Combo->Text);
    for (TComboBox *Combo : { DnsQueryType, EmailRestType, SslClientCertLoc })
        IniFile->WriteInteger(SectionData, Combo->Name + "_ItemIndex", Combo->ItemIndex);
    for (TRadioGroup *Radio : { AuthType, CertVerMethod, DebugLogging, OAuthAuthType, ParamContent,
            ReqMode, ReqType, SslSecurity, IpSockFamily, AliveMethod, AliveNets, ReqMemStrategy,
            HttpUploadStrat, EmailAuthType, TwitAuthType, ParamJsonFmt, SslSniType, CacheControl,
            ReqPropFind })
        IniFile->WriteInteger(SectionData, Radio->Name + "_ItemIndex", Radio->ItemIndex);
    for (TCheckBox *Check : { OAuthAutoRefresh, OAuthOptNoRedir, ReportCertChain, DnsDnssec,
            DnsNoValidation, OAuthAccess, TwitForceLogin, EmailForceLogin, ReqReplFile, CertRevoke,
            OAuthOptBasic, WebsocketAllHeaders, EmailRawHdrs, FormDataUtf8, NoSSL, SaveNonTextual,
            OAuthOptNoSecret, GzipCompress, FollowRelocations, ReqGetRaw, ReqPFindSome })
        IniFile->WriteString(SectionData, Check->Name + "_Checked", Check->Checked ? "True" : "False");
    for (TMemo *Memo : { ExtraHeaders, SmsMsgText, SmsDestNums, TwitMsg, EmailRecipList, EmailMessage,
            WebsockMessLines })
        IniFile->WriteString(SectionData, Memo->Name + "_Lines", Memo->Lines->CommaText);
    IniFile->UpdateFile();
}

void __fastcall THttpRestForm::FormShow(TObject *Sender)
{
    if (!FInitialized) {
        FInitialized = true;

        // replace the .dfm list items with the ICS literals
        SslSecurity->Items->Clear();
        for (int Level = sslCliSecIgnore; Level <= sslCliSecHigh192; Level++)
            SslSecurity->Items->Add(SslCliSecurityNames[Level]);
        DnsQueryType->Items->Clear();
        for (int I = 0; I < DnsReqTable.size(); I++)
            DnsQueryType->Items->Add(DnsReqTable[I].Asc + " [" + DnsReqTable[I].Desc + "]");
        DnsHttpsUrl->Items->Clear();
        for (int I = 0; I < DnsPublicHttpsTable.size(); I++)
            DnsHttpsUrl->Items->Add(DnsPublicHttpsTable[I]);
        AuthType->Items->Clear();
        for (int Auth = httpAuthNone; Auth <= httpAuthDigest2; Auth++)
            AuthType->Items->Add(HttpCliAuthNames[Auth]);
        EmailRestType->Items->Clear();
        for (int RestEmail = RestEmailNone; RestEmail <= RestEmailMSSmtp; RestEmail++)
            EmailRestType->Items->Add(RestEmailTypeLits[RestEmail]);
        ParamJsonFmt->Items->Clear();
        for (int Fmt = RPJFmtNestObj; Fmt <= RPJFmtArrayObj; Fmt++)
            ParamJsonFmt->Items->Add(RPJsonFmtLits[Fmt]);
        ParamJsonFmt->ItemIndex = 0;
        ParamContent->Items->Clear();
        for (int PContent = PContNone; PContent <= PContCommaList; PContent++)
            ParamContent->Items->Add(PContentLits[PContent]);
        ParamContent->ItemIndex = 0;

        LoadSettings();
    }

    if (DnsQueryType->ItemIndex < 0)
        DnsQueryType->ItemIndex = 0;
    for (TRadioGroup *Radio : { AuthType, CertVerMethod, DebugLogging, OAuthAuthType, ParamContent,
            SslSecurity, ReqMode, ReqType, IpSockFamily }) {
        if (Radio->ItemIndex < 0)
            Radio->ItemIndex = 0;
    }
    if (CacheControl->ItemIndex < 0)
        CacheControl->ItemIndex = httpCacheMaxAge;
    if (CacheSeconds->Text.IsEmpty())
        CacheSeconds->Text = "600";
    if (UserAgent->Text.IsEmpty())
        UserAgent->Text = HttpRest1->Agent;
    if (SslGroupsEdit->Text.IsEmpty())
        SslGroupsEdit->Text = sslCryptoGroups13;
    if (SslCipherEdit->Text.IsEmpty())
        SslCipherEdit->Text = sslCiphersNormal;
    if (SslCipher13Edit->Text.IsEmpty())
        SslCipher13Edit->Text = sslCipherSuitesTLS13;

    HttpRest1->RestCookies->LoadFromFile(FCookieFileName);
    OAuthWebIP->Items->Assign(LocalIPList());
    OAuthWebIP->Items->Insert(0, ICS_LOCAL_HOST_NAME);   // localhost means both IPv4 and IPv6
    OAuthWebIP->Items->Insert(1, ICS_LOCAL_HOST_V4);
    OAuthWebIP->Items->Insert(2, ICS_LOCAL_HOST_V6);

    // OAuth2 Account Title list, prefilling the details of common accounts
    FOAuthUris = { OAuthUriNone, OAuthUriGoogle, OAuthUriMSRest, OAuthUriMSSmtp,
                   OAuthUriTwitterOA2, OAuthUriSipgate };
    OAuthAccTitle->Items->Clear();
    for (const TOAuthUri &Uri : FOAuthUris)
        OAuthAccTitle->Items->Add(Uri.CAccName);
    if (OAuthUserAuth->Text.IsEmpty())
        OAuthUserAuth->Text = OAuthMsUserAuthDef;
    if (EmailUserAuth->Text.IsEmpty())
        EmailUserAuth->Text = OAuthMsUserAuthDef;
}

// Checks the parameter type column holds a known literal, or sets the default.
void __fastcall THttpRestForm::GridParamsSelectCell(TObject *Sender, int ACol, int ARow, bool &CanSelect)
{
    if (ACol != 2 || GridParams->Cells[0][ARow].IsEmpty())
        return;
    for (int PT = RPTypeStr; PT <= RPTypeFile; PT++) {
        if (RParamTypeLits[PT] == GridParams->Cells[ACol][ARow])
            return;
    }
    GridParams->Cells[ACol][ARow] = RParamTypeLits[RPTypeStr];
}

void __fastcall THttpRestForm::FormClose(TObject *Sender, TCloseAction &Action)
{
    HttpRest1->RestCookies->SaveToFile(FCookieFileName);
    FIcsBuffLogStream.reset();   // writes the log file
    SaveSettings();
}

void __fastcall THttpRestForm::doExitClick(TObject *Sender)
{
    Close();
}

// Log lines are buffered and shown by TimerLog, so heavy logging does not slow requests.
void THttpRestForm::AddLog(const String &S)
{
    FBuffLogLines += TimeToStr(Time()) + " " + S + IcsCRLF;
    if (DirLogs->Text.IsEmpty() || !FIcsBuffLogStream)
        return;
    try {
        FIcsBuffLogStream->WriteLine(S);
    }
    catch (Exception &) {
        // as in Delphi a log file write error is ignored: reporting it would log again
    }
}

void THttpRestForm::FlushLog()
{
    if (DirLogs->Text.IsEmpty() || !FIcsBuffLogStream)
        return;
    try {
        FIcsBuffLogStream->FlushFile();
    }
    catch (Exception &) {
        // as in Delphi a log file write error is ignored: reporting it would log again
    }
}

void __fastcall THttpRestForm::TimerLogTimer(TObject *Sender)
{
    const int DispLen = FBuffLogLines.Length();
    if (DispLen == 0)
        return;
    FBuffLogLines.SetLength(DispLen - 2);   // remove the last CRLF
    LogWin->Lines->Add(FBuffLogLines);
    SendMessage(LogWin->Handle, EM_LINESCROLL, 0, 999999);
    FBuffLogLines = "";
}

void __fastcall THttpRestForm::HttpRest1HttpRestProg(TObject *Sender, TLogOption LogOption, const String Msg)
{
    if (LogOption == loProgress) {
        LabelProgress->Caption = Msg;
        LabelProgress->Refresh();
    }
    else
        AddLog(Msg);
}

/* Opens the log file, one per day, written as UTF-8, or changes its name if the log
   directory was changed. */
void THttpRestForm::OpenLogFile()
{
    if (DirLogs->Text.IsEmpty())
        return;
    const String FName = "\"" + IncludeTrailingPathDelimiter(DirLogs->Text) + "ics-httprest-\"yyyy-mm-dd\".log\"";
    if (!FIcsBuffLogStream)
        FIcsBuffLogStream.reset(new TIcsBuffLogStream(nullptr, FName, Caption + IcsCRLF, FileCPUtf8));
    else {
        if (FName == FIcsBuffLogStream->NameMask)
            return;
        if (FIcsBuffLogStream->LogSize > 0)
            FIcsBuffLogStream->FlushFile(true);   // changing log path, write the old log first
        FIcsBuffLogStream->NameMask = FName;
    }
    AddLog(String(IcsCRLF) + "Opened log file: " + FIcsBuffLogStream->FullName);
}

// Delphi clears the list without forgetting a previous Json array, so a double click on a
// later DNS or other row opens a stale array element, or reads past the end of the array.
void THttpRestForm::ClearRespList()
{
    RespList->Items->Clear();
    FJArrayItems.clear();
}

void __fastcall THttpRestForm::doAbortClick(TObject *Sender)
{
    if (HttpRest1->State > httpReady || HttpRest1->Connected) {
        AddLog("Aborting operation");
        HttpRest1->Abort();
    }
    // the response stream is not freed, so resume can be tested
    doStartReq->Enabled = true;
}

void __fastcall THttpRestForm::doClearClick(TObject *Sender)
{
    LogWin->Lines->Clear();
    ClearRespList();
    HttpRest1->ClearResp();   // frees the response stream
}

void __fastcall THttpRestForm::doClearParamsClick(TObject *Sender)
{
    // Delphi runs to row RowCount, one past the last row.
    for (int Row = 1; Row < GridParams->RowCount; Row++) {
        for (int Col = 0; Col < GridParams->ColCount; Col++)
            GridParams->Cells[Col][Row] = "";
    }
}

void __fastcall THttpRestForm::SelDirDownClick(TObject *Sender)
{
    OpenDirDiag->InitialDir = ExtractFilePath(RestDownFile->Text);
    if (OpenDirDiag->Execute())
        RestDownFile->Text = OpenDirDiag->FileName;
}

void __fastcall THttpRestForm::SelDirEmailFileClick(TObject *Sender)
{
    OpenDirDiag->InitialDir = ExtractFilePath(EmailFileAttach->Text);
    if (OpenDirDiag->Execute())
        EmailFileAttach->Text = OpenDirDiag->FileName;
}

void __fastcall THttpRestForm::SelDirLogsClick(TObject *Sender)
{
    OpenDirDiag->InitialDir = DirLogs->Text;
    if (OpenDirDiag->Execute())
        DirLogs->Text = ExtractFilePath(OpenDirDiag->FileName);
}

void __fastcall THttpRestForm::SelDirUploadClick(TObject *Sender)
{
    OpenDirDiag->InitialDir = ExtractFilePath(HttpUploadFile->Text);
    if (OpenDirDiag->Execute())
        HttpUploadFile->Text = OpenDirDiag->FileName;
}

void __fastcall THttpRestForm::SettingsChange(TObject *Sender)
{
    if (HttpRest1->Connected)
        HttpRest1->Abort();   // close the socket so the new settings are used
}

// Loads the optional SSL client certificate from a PEM or PFX file (password 'password') into
// an already cleared SslCliCert. Returns false, with SslCliCert cleared, if a file is named but
// no certificate with a matching private key could be loaded.
// The private key could also be loaded separately with PrivateKeyLoadFromPemFile.
bool THttpRestForm::LoadClientCert(TX509Base *SslCliCert)
{
    const String FileName = SslClientCertFile->Text;
    if (FileName.IsEmpty())
        return true;
    if (!FileExists(FileName)) {
        AddLog("SSL client certificate not found: " + FileName);
        return false;
    }
    try {
        String Errs;
        SslCliCert->LoadFromFileEx(FileName, croYes, croTry, "password", Errs);
        if (!Errs.IsEmpty())
            AddLog(Errs);
        if (SslCliCert->CheckCertAndPKey()) {
            AddLog("SSL client certificate loaded OK: " + FileName);
            return true;
        }
        AddLog("SSL client certificate invalid, or no matching private key: " + FileName);
    }
    catch (Exception &E) {
        AddLog("Failed to load SSL client certificate: " + E.Message + " - " + FileName);
    }
    SslCliCert->ClearAll();
    return false;
}

// Loads the SSL client certificate named in the file edit from the Windows store into an
// already cleared SslCliCert. Returns false, with SslCliCert cleared, if it could not be loaded.
bool THttpRestForm::LoadClientCertFromStore(TX509Base *SslCliCert)
{
    if (SslClientCertFile->Text.IsEmpty())
        return true;
#ifdef ICS_HAVE_MSCRYPT
    // The certificate name is searched for in the store; the local machine store needs
    // administrator rights to read private keys.
    const TMsCertLocation MsCertLoc =
        (SslClientCertLoc->ItemIndex == CliCertLocMachine) ? MsLocMachine : MsLocCurUser;
    std::unique_ptr<TMsCertTools> MsCertTools(new TMsCertTools(nullptr));
    try {
        const String Errs = MsCertTools->LoadOneFromStore(MsCertLoc, SslClientCertFile->Text, true);
        if (Errs.IsEmpty()) {
            SslCliCert->Assign(MsCertTools.get());
            AddLog("Loaded SSL client certificate from Windows Store: " + MsCertTools->CertName);
            return true;
        }
        AddLog("Error loading SSL client certificate from Windows Store: " + Errs);
    }
    catch (Exception &E) {
        AddLog("Error loading SSL client certificate from Windows Store: " + E.Message);
    }
    SslCliCert->ClearAll();
    return false;
#else
    // No TMsCertTools in this ICS C++ library (see the top of this file), so the requested
    // certificate cannot be loaded and the request is refused.
    AddLog("SSL client certificate from the Windows Store is not available in this C++Builder "
           "build of ICS: " + SslClientCertFile->Text);
    return false;
#endif
}

// Optional HTTP parameters; all have defaults, so can be ignored if not needed.
// Returns false if OpenSSL failed to load or a requested SSL client certificate could not be
// loaded; a request must not be started then.
bool THttpRestForm::CommonRestSettings()
{
    bool Result = true;
    // Delphi keeps the previous client certificate when the new selection is None, empty or
    // fails to load, so the next request would authenticate as the old identity.
    HttpRest1->SslCliCert->ClearAll();
    HttpRest1->DebugLevel = SelectedDebugLevel();
    HttpRest1->NoSSL = NoSSL->Checked;
    if (!HttpRest1->NoSSL) {
        if (ICS_OPENSSL_VERSION_NUMBER == 0) {   // OpenSSL and the internal root bundle not loaded yet
            IcsSslRootCAStore->Initialise();
            LogWin->Lines->Add("SSL/TLS " + IcsReportOpenSSLVer(true));
        }
        if (ICS_OPENSSL_VERSION_NUMBER == 999) {   // this or an earlier load failed
            AddLog("Failed to load SSL/TLS: " + GSSL_LOAD_ERRS);
            return false;
        }
        HttpRest1->CertVerMethod = static_cast<TCertVerMethod>(CertVerMethod->ItemIndex);
        HttpRest1->SslCliSecurity = static_cast<TSslCliSecurity>(SslSecurity->ItemIndex);
        HttpRest1->SslReportChain = ReportCertChain->Checked;
        HttpRest1->SslRevocation = CertRevoke->Checked;

        if (SslClientCertLoc->ItemIndex == CliCertLocFile)
            Result = LoadClientCert(HttpRest1->SslCliCert);
        else if (SslClientCertLoc->ItemIndex >= CliCertLocUser)
            Result = LoadClientCertFromStore(HttpRest1->SslCliCert);
    }
    HttpRest1->ServerAuth = static_cast<THttpAuthType>(AuthType->ItemIndex);
    HttpRest1->Username = AuthLogin->Text;
    HttpRest1->Password = AuthPassword->Text;
    HttpRest1->AuthBearerToken = AuthBearer->Text;
    HttpRest1->ProxyURL = ProxyURL->Text;
    HttpRest1->AlpnProtocols->CommaText = AlpnProtos->Text;
    HttpRest1->ExtraHeaders = ExtraHeaders->Lines;
    HttpRest1->SocketFamily = static_cast<TSocketFamily>(IpSockFamily->ItemIndex);
    HttpRest1->Accept = ReqAccept->Text;
    HttpRest1->HttpMemStrategy = HttpStratMem;
    HttpRest1->ShowProgress = true;
    HttpRest1->Options = THttpCliOptions() << httpoGetContent << httpoEnableContentCoding;
    HttpRest1->SslSniType = static_cast<TSslSniType>(SslSniType->ItemIndex);
    HttpRest1->Timeout = Icsatoi(SyncReqTimeout->Text);
    HttpRest1->SyncMaxSecs = Icsatoi(SyncReqMaxSecs->Text);
    HttpRest1->CacheType = static_cast<THttpCacheType>(CacheControl->ItemIndex);
    HttpRest1->CacheAgeSecs = Icsatoi(CacheSeconds->Text);
    if (GzipCompress->Checked)
        HttpRest1->Options = HttpRest1->Options << httpoEnableContentCoding;
    else
        HttpRest1->Options = HttpRest1->Options >> httpoEnableContentCoding;
    HttpRest1->FollowRelocation = FollowRelocations->Checked;
    HttpRest1->AcceptLanguage = "en, fr";
    HttpRest1->Connection = "Keep-Alive";
    HttpRest1->Agent = UserAgent->Text;
    // PROPFIND Depth header. Delphi indexes its list with ItemIndex unchecked; none selected sends 0.
    const wchar_t *const PropsList[] = { L"0", L"1", L"infinity" };
    HttpRest1->ReqDepth = PropsList[std::min(std::max(ReqPropFind->ItemIndex, 0), 2)];
    HttpRest1->ReqTranslate = ReqGetRaw->Checked ? "F" : "";   // GET WebDAV raw file and PROPFIND

    // the cipher settings need the SslContext initialised
    if (!HttpRest1->NoSSL) {
        HttpRest1->InitSsl();
        HttpRest1->SslContext->SslCipherList = Trim(SslCipherEdit->Text);
        HttpRest1->SslContext->SslCipherList13 = Trim(SslCipher13Edit->Text);
        HttpRest1->SslContext->SslCryptoGroups = Trim(SslGroupsEdit->Text);
    }
    LabelProgress->Caption = "";
    return Result;
}

void __fastcall THttpRestForm::doStartReqClick(TObject *Sender)
{
    const THttpRequest ReqList[] = { httpGET, httpQUERY, httpPOST, httpHEAD, httpPUT, httpDELETE,
        httpPATCH, httpOPTIONS, httpTRACE, httpPROPFIND, httpPROPPATCH, httpMKCOL };
    // Delphi makes this check after disabling Start Request, and leaves it disabled.
    const int ReqIndex = ReqType->ItemIndex;
    if (ReqIndex < 0 || ReqIndex >= static_cast<int>(std::size(ReqList)))
        return;
    const THttpRequest Req = ReqList[ReqIndex];
    const bool Async = (ReqMode->ItemIndex == 1);

    doStartReq->Enabled = false;
    ClearRespList();
    OpenLogFile();
    if (!CommonRestSettings()) {
        AddLog("REST request not started");
        doStartReq->Enabled = true;
        System::Sysutils::Beep();
        return;
    }
    HttpRest1->HttpMemStrategy = static_cast<THttpMemStrategy>(ReqMemStrategy->ItemIndex);
    HttpRest1->HttpDownFileName = RestDownFile->Text;
    HttpRest1->HttpDownReplace = ReqReplFile->Checked;
    HttpRest1->HttpUploadStrat = static_cast<THttpUploadStrat>(HttpUploadStrat->ItemIndex);
    HttpRest1->HttpUploadFile = Trim(HttpUploadFile->Text);

    // read the grid and build the REST parameters
    TRestParams *Params = HttpRest1->RestParams;
    Params->Clear();
    String ErrStr;
    Params->PContent = static_cast<TPContent>(ParamContent->ItemIndex);
    Params->RPJsonFmt = static_cast<TRPJsonFmt>(ParamJsonFmt->ItemIndex);
    String MyParams = RawParams->Text;
    if (Params->PContent > PContNone) {
        Params->FormDataUtf8 = FormDataUtf8->Checked;
        // Delphi runs to row RowCount, one past the last row.
        for (int Row = 1; Row < GridParams->RowCount; Row++) {
            GridParams->Cells[0][Row] = IcsStrRemCntls(Trim(GridParams->Cells[0][Row]), false);   // remove CRLF etc
            const String PN = GridParams->Cells[0][Row];
            if (PN.IsEmpty())
                continue;
            GridParams->Cells[1][Row] = IcsStrRemCntls(Trim(GridParams->Cells[1][Row]), false);
            const String PV = GridParams->Cells[1][Row];
            const TRParamType PT = FindPType(GridParams->Cells[2][Row]);
            if (PT != RPTypeFile)
                Params->AddItem(PN, PV, PT);
            else if (Params->PContent != PContFormData)   // binary files are form-data uploads
                ErrStr = "Files need POST Form-Data Content Type";
            else {
                const __int64 FSize = IcsGetFileSize(PV);
                if (FSize < 0)
                    ErrStr = "File not found: " + PV;
                else
                    Params->AddItemFile(PN, PV, FSize);
            }
        }
        MyParams = "";   // no raw parameters if REST parameters are specified
    }

    // some data validation checks
    const TPContent PC = Params->PContent;
    if (HttpRest1->HttpUploadStrat > HttpUploadNone &&
            (PC == PContBodyJson || PC == PContBodyUrlEn || PC == PContBodyXML || PC == PContFormData))
        ErrStr = "Can not upload file with POST body parameters";
    if (Req == httpQUERY && MyParams.IsEmpty() && Params->Count == 0)
        ErrStr = "QUERY requires some content";
    if (!ErrStr.IsEmpty()) {
        AddLog(ErrStr);
        doStartReq->Enabled = true;
        System::Sysutils::Beep();
        return;
    }

    // WebDAV PROPFIND needs XML content
    if (Req == httpPROPFIND) {
        Params->PContent = PContBodyXML;
        MyParams = ReqPFindSome->Checked ? XmlProfindSome : XmlProfindAll;
        AddLog(MyParams);
    }

    // make the HTTP request; RestParams are ignored if MyParams is not blank
    FLastRawParams = MyParams;
    FlushLog();
    TimerLogTimer(this);   // update the log window, the request may take a long time
    const int StatCode = HttpRest1->RestRequest(Req, RestURL->Text, Async, MyParams);
    if (StatCode < 0) {   // the request never started
        AddLog("REST request failed: " + HttpRest1->ReasonPhrase);
        doStartReq->Enabled = true;
        System::Sysutils::Beep();
        TimerLogTimer(this);
    }
    else if (Async)
        AddLog("Async REST request started, Please Wait");
    else {
        AddLog("Sync REST request completed, Status " + IntToStr(StatCode));
        AddLog("");
        // for sync the result could be processed here instead of in HttpRest1RestRequestDone
        doStartReq->Enabled = true;
        if (StatCode == 401 && UsesLogin(HttpRest1->ServerAuth))
            PostMessage(Handle, WM_401REPEAT, 0, 0);
    }
}

void __fastcall THttpRestForm::DebugLoggingClick(TObject *Sender)
{
    HttpRest1->WSDebugOptions = TWSDebugOptions();   // clear the old debug log options
}

// Parses a Json or XML response into the response list.
void THttpRestForm::DisplayJson(_di_ISuperObject RespObj)
{
    try {
        JsonToListView(RespList, RespObj, FJArrayItems);
    }
    catch (Exception &E) {
        AddLog("Error parsing Json: " + E.Message);
    }
}

// Shows the login window after a 401 response, then repeats the request.
void __fastcall THttpRestForm::WM401Repeat(TMessage &Msg)
{
    const bool Async = (ReqMode->ItemIndex == 1);
    std::unique_ptr<TFormLogin> FormLogin(new TFormLogin(nullptr));
    FormLogin->Top = Top + 20;
    FormLogin->Left = Left + 20;

    // default to the login used by the request; a real application would store logins per path
    FormLogin->AuthUsername->Text = HttpRest1->Username;
    FormLogin->AuthPassword->Text = HttpRest1->Password;

    // the authentication types the server offered
    const TWWWAuthInfos AuthInfos = HttpRest1->WWWAuthInfos;
    if (AuthInfos.Length > 0) {
        FormLogin->ListMethods->Items->Clear();
        FormLogin->LabelPageURL->Caption = "Login to page: " + AuthInfos[0].Uri;
        for (int I = 0; I < AuthInfos.Length; I++)
            FormLogin->ListMethods->Items->Add("Type: " + String(HttpCliAuthNames[AuthInfos[I].AuthType]) +
                                               ", Realm: " + AuthInfos[I].Realm);
        FormLogin->ListMethods->ItemIndex = FormLogin->ListMethods->Items->Count - 1;   // select last
    }
    if (FormLogin->ShowModal() != mrOk)
        return;
    HttpRest1->Username = FormLogin->AuthUsername->Text;
    HttpRest1->Password = FormLogin->AuthPassword->Text;
    const int I = FormLogin->ListMethods->ItemIndex;
    if (AuthInfos.Length > 0 && I >= 0)
        HttpRest1->ServerAuth = AuthInfos[I].AuthType;
    else
        HttpRest1->ServerAuth = httpAuthBasic;
    AddLog("Authentication Details Entered Manually, Login: " + HttpRest1->Username);

    // restart the HTTP request
    doStartReq->Enabled = false;
    AddLog(DateTimeToStr(Now()) + " Restarting REST request for URL: " + RestURL->Text);
    // Delphi repeats with the Raw Parameters box, not the parameters the request was sent with
    // (it clears them when the grid is used, and PROPFIND sends its XML).
    FlushLog();
    TimerLogTimer(this);   // update the log window, the request may take a long time
    const int StatCode = HttpRest1->RestRequest(HttpRest1->RequestType, RestURL->Text, Async, FLastRawParams);
    if (StatCode < 0) {   // never started; Delphi reports it as started and leaves the button disabled
        AddLog("REST request failed: " + HttpRest1->ReasonPhrase);
        doStartReq->Enabled = true;
        System::Sysutils::Beep();
        TimerLogTimer(this);
    }
    else if (Async)
        AddLog("Async REST request started");
    else {
        AddLog("Sync REST request completed, Status " + IntToStr(StatCode));
        AddLog("");
        doStartReq->Enabled = true;
    }
}

void __fastcall THttpRestForm::HttpRest1RestRequestDone(TObject *Sender, THttpRequest RqType, Word ErrCode)
{
    const bool Async = (ReqMode->ItemIndex == 1);
    doStartReq->Enabled = true;
    if (!HttpRest1->GetAlpnProtocol().IsEmpty())
        AddLog("ALPN Requested by Server: " + HttpRest1->GetAlpnProtocol());
    if (ErrCode != 0) {
        // TSslHttpRest has already logged this; repeated to show what information is available
        AddLog("Request failed: Error: " + HttpRest1->RequestDoneErrorStr + " - " +
               IntToStr(HttpRest1->StatusCode) + IcsSPACE + HttpRest1->ReasonPhrase);
        return;
    }
    AddLog("Request done, StatusCode " + IntToStr(HttpRest1->StatusCode) + ", Response Size: " +
           IntToKbyte(HttpRest1->ResponseStream->Size));
    FlushLog();

    // need authentication
    if (HttpRest1->StatusCode == 401 && Async && UsesLogin(HttpRest1->ServerAuth))
        PostMessage(Handle, WM_401REPEAT, 0, 0);

    // only display textual content in the log
    if (!HttpRest1->RespAttachment && IcsMimeIsTextual(HttpRest1->RespMimeType))
        AddLog(HttpRest1->ResponseRaw);
    else if (HttpRest1->StatusCode == 200 && HttpRest1->ResponseSize > 0) {
        // save an attachment as a file if there was a Content-Disposition: attachment header, or requested
        if (HttpRest1->RespAttachment && SaveNonTextual->Checked) {
            String FName = HttpRest1->RespFileName;   // from the response header
            if (FName.IsEmpty()) {                    // or try to find it from the URL
                String Proto, User, Pass, Host, Port, Dirs, Section, Query;
                IcsParseExURL(HttpRest1->URL, Proto, User, Pass, Host, Port, Dirs, FName, Section, Query);
            }
            AddLog("Response is File Attachment: " + FName + " (Size: " + IntToKbyte(HttpRest1->ResponseSize) + ")");
            SaveDialog1->Title = "Save Downloaded File As";
            SaveDialog1->FileName = FName;
            SaveDialog1->InitialDir = DirLogs->Text;
            if (SaveDialog1->Execute()) {
                const String SaveName = SaveDialog1->FileName;
                try {
                    if (FileExists(SaveName))
                        IcsDeleteFile(SaveName, true);
                    {
                        // Delphi casts ResponseStream to TMemoryStream, which raises an invalid cast
                        // when the Memory Strategy saves to a file; any stream is copied here.
                        std::unique_ptr<TFileStream> OutFile(new TFileStream(SaveName, fmCreate));
                        OutFile->CopyFrom(HttpRest1->ResponseStream, 0);
                    }
                    if (FileExists(SaveName))
                        AddLog("Saved Response OK as: " + SaveName);
                    else
                        AddLog("Failed to Saved Response as: " + SaveName);
                }
                catch (Exception &E) {
                    AddLog("Failed to Save File: " + SaveName + " - " + E.Message);
                }
            }
            else
                AddLog("Cancelled Save File");
        }
        else
            AddLog("<Non-textual content received: " + HttpRest1->RespMimeType + ">");
    }

    // look for a Json response
    if (HttpRest1->ResponseRaw.Pos("{") > 0 || HttpRest1->RespMimeType.Pos("json") > 0) {
        _di_ISuperObject Json = HttpRest1->ResponseJson;
        if (Json) {
            AddLog("Json main content type: " + SuperTypeName(Json->DataType));
            DisplayJson(Json);
        }
    }

    // look for an XML response
    if (HttpRest1->ResponseRaw.Pos("<?xml version=") > 0 || HttpRest1->RespMimeType.Pos("xml") > 0) {
        if (HttpRest1->RequestType == httpPROPFIND) {
            // a WebDAV PROPFIND directory listing, converted into ICS file records
            int TotFiles = 0;
            TIcsFDirRecs RemFiles;
            AddLog(IcsUnpackXMLFDir(HttpRest1->ResponseStream, HttpRest1->URL, "", TotFiles, RemFiles));
            if (TotFiles > 0) {
                AddLog("WebDAV PROPFIND Files: " + String(IcsCRLF) + IcsFmtFileDir(RemFiles, false));
                AddLog("WebDAV URLs");
                for (int I = 0; I < TotFiles; I++)
                    AddLog(RemFiles[I].FrLinks);
            }
        }
        else {
            // general XML, converted to Json and shown in the list
            try {
                _di_ISuperObject XmlObj = XMLParseStream(HttpRest1->ResponseStream, true);   // packed for easier display
                if (XmlObj) {
                    AddLog("XML content as Json: " + XmlObj->AsString());
                    DisplayJson(XmlObj);
                }
            }
            catch (Exception &E) {
                AddLog("Error parsing XML: " + E.Message);
            }
        }
    }
    AddLog("");
    FlushLog();
    TimerLogTimer(this);
}

/* Called during the SSL handshake when SSL Certificate Check is Custom Check
   (CertVerOwnEvent): accepts a chain with a certificate issued by ICS. A real check
   should also check the root, or compare X509PubKeyTB with a known key. */
void __fastcall THttpRestForm::HttpRest1SslCertVerifyEvent(TObject *Sender, TX509List *CertChain,
    int &VerifyCode, String &VerifyInfo, bool &Disconnect)
{
    AddLog("SslCertVerifyEvent: " + VerifyInfo);
    if (!CertChain)
        return;
    for (int I = 0; I < CertChain->Count; I++) {
        if (CertChain->Items[I]->IssuerCName.Pos("ICS") > 0) {
            VerifyCode = X509_V_OK;
            VerifyInfo = "ICS Certificate Matched OK";
            return;
        }
    }
    VerifyCode = X509_V_ERR_UNABLE_TO_GET_ISSUER_CERT;
    VerifyInfo = "Failed to Verify Public Key";
}

// Opens the Json Object window with the selected array element, or a nested object or array.
void __fastcall THttpRestForm::RespListDblClick(TObject *Sender)
{
    if (!FormObject)
        return;
    const int Index = RespList->ItemIndex;
    if (Index < 0)
        return;
    FormObject->SubRespList->Items->Clear();
    if (!FJArrayItems.empty()) {
        if (Index < static_cast<int>(FJArrayItems.size()))
            FormObject->DispJson(FJArrayItems[Index]);
        return;
    }
    TListItem *Item = RespList->Items->Item[Index];
    TStrings *SubItems = Item->SubItems;
    if (SubItems->Count >= 2 && (SubItems->Strings[0] == "stArray" || SubItems->Strings[0] == "stObject"))
        FormObject->DispJson(SubItems->Strings[1]);
    // an array may have a Json object in any column; only the first is shown
    else if (StartsJson(Item->Caption))
        FormObject->DispJson(Item->Caption);
    else {
        for (int I = 0; I < SubItems->Count; I++) {
            if (StartsJson(SubItems->Strings[I])) {
                FormObject->DispJson(SubItems->Strings[I]);
                break;
            }
        }
    }
    FormObject->BringToFront();
}

void __fastcall THttpRestForm::doListCertStoreClick(TObject *Sender)
{
    TButtonBusy Busy(doListCertStore);
    OpenLogFile();
    // Only an OpenSSL load failure stops the listing; the client certificate is not used here.
    if (!CommonRestSettings() && ICS_OPENSSL_VERSION_NUMBER == 999)
        return;
    if (HttpRest1->NoSSL) {   // Delphi goes on, and there is no SSL context to list
        AddLog("No SSL is ticked, so there is no SSL context certificate store to list");
        return;
    }
    std::unique_ptr<TX509List> CertList(new TX509List(nullptr, true));
    HttpRest1->InitSsl();
    if (X509_STORE_get0_objects)
        HttpRest1->SslContext->SslGetAllCerts(CertList.get());
    else {
        // ICS SslGetAllCerts calls X509_STORE_get0_objects, which ICS V9.8 does not load for
        // OpenSSL 4 (nil, access violation, Delphi too); list the store as ICS SslGetCerts does.
        // get1 returns a stack of up-referenced certs and Add makes its own copy (X509_dup).
        // unique_ptr<void, ...> holds the raw stack and calls OSSL_STACK_OF_X509_free at scope
        // exit, releasing the stack and its references even if Add raises.
        auto FreeStack = [](PSTACK S) { OSSL_STACK_OF_X509_free(S); };
        std::unique_ptr<void, decltype(FreeStack)> Stack(
            X509_STORE_get1_all_certs(HttpRest1->SslContext->GetX509Store()), FreeStack);
        if (Stack) {
            const int Num = OPENSSL_sk_num(Stack.get());
            for (int I = 0; I < Num; I++)
                CertList->Add(OPENSSL_sk_value(Stack.get(), I));
        }
    }
    // SslGetAllCerts returns the count of all store objects, CRLs included, but adds only
    // certificates; Delphi indexes the list by that count.
    const int Tot = CertList->Count;
    if (Tot <= 0) {
        AddLog("! SSL context certificate store empty");
        return;
    }
    CertList->SortChain(xsrtIssuerFirst);
    String Info = "! SSL context contains " + IntToStr(Tot) + " certificate in store\r\n";
    for (int I = 0; I < Tot; I++) {
        TX509Base *Cert = CertList->Items[I];
        Info += "#" + IntToStr(I + 1) + " ";
        if (!Cert->SubAltNameDNS.IsEmpty())
            Info += IcsUnwrapNames(Cert->SubAltNameDNS);
        else if (!Cert->SubjectCName.IsEmpty())   // some roots have no common name
            Info += IcsUnwrapNames(Cert->SubjectCName);
        else
            Info += IcsUnwrapNames(Cert->SubjectOName);
        Info += " (" + IcsUnwrapNames(Cert->SubjectOName) + ")";
        if (!Cert->SubjectOUName.IsEmpty())
            Info += " OU: " + IcsUnwrapNames(Cert->SubjectOUName);
        Info += "\r\n";
    }
    AddLog(Info);
}

// OAuth2 settings only
void THttpRestForm::RestOAuthSetup()
{
    LabelResult->Caption = "Result: Please Wait";
    OpenLogFile();
    RestOAuth1->DebugLevel = SelectedDebugLevel();
    RestOAuth1->ProtoType = OAuthv2;
    RestOAuth1->AuthCode = OAuthAuthCode->Text;
    RestOAuth1->AuthType = static_cast<TOAuthType>(OAuthAuthType->ItemIndex);
    RestOAuth1->AppUrl = Trim(OAuthAppUrl->Text);
    RestOAuth1->RedirectMsg = "App: " + OAuthAppUrl->Text;
    RestOAuth1->ClientId = Trim(OAuthClientId->Text);
    RestOAuth1->ClientSecret = Trim(OAuthClientSecret->Text);
    TOAuthOptions Options;
    if (OAuthOptNoRedir->Checked)
        Options << OAopAuthNoRedir;
    if (!OAuthPrompt->Text.IsEmpty()) {
        RestOAuth1->LoginPrompt = OAuthPrompt->Text;
        Options << OAopAuthPrompt;
    }
    if (OAuthAccess->Checked) {
        RestOAuth1->RefreshOffline = true;
        Options << OAopAuthAccess;
    }
    if (OAuthOptBasic->Checked)
        Options << OAopAuthBasic;
    if (OAuthOptNoSecret->Checked)
        Options << OAopAuthNoSecret;
    RestOAuth1->OAOptions = Options;
    RestOAuth1->RefreshAuto = OAuthAutoRefresh->Checked;
    RestOAuth1->RefrMinsPrior = Icsatoi(OAuthRefrMins->Text);
    RestOAuth1->RefreshToken = OAuthRefToken->Text;
    RestOAuth1->Scope = Trim(OAuthScope->Text);
    RestOAuth1->TokenUrl = Trim(OAuthTokenUrl->Text);
    RestOAuth1->StopSrv();
    RestOAuth1->RedirectUrl = Trim(OAuthRedirectUrl->Text);
    RestOAuth1->WebSrvIP = Trim(OAuthWebIP->Text);
    RestOAuth1->WebSrvPort = Trim(OAuthWebPort->Text);
    RestOAuth1->AccName = Trim(OAuthAccTitle->Text);
    RestOAuth1->ConsoleUrl = Trim(OAuthConsoleUrl->Text);
    RestOAuth1->MsUserAuthority = Trim(OAuthUserAuth->Text);
    RestOAuth1->EdgeCacheDir = OAuthEdgeCache->Text;
    RestOAuth1->LoginHint = OAuthLoginHint->Text;
    RestOAuth1->HttpRest->SocketFamily = static_cast<TSocketFamily>(IpSockFamily->ItemIndex);
}

void __fastcall THttpRestForm::RestOAuth1OAuthAuthUrl(TObject *Sender, const String URL)
{
    if (static_cast<TRestOAuth*>(Sender)->AuthType == OAuthTypeMan)
        AddLog("Please copy this URL and browse to it, then enter Auth Code: " + URL);
}

void __fastcall THttpRestForm::RestOAuth1OAuthEmbed(TObject *Sender, const String URL, bool &Success)
{
    Success = OAuthBrowser1->BrowserEmbedded(static_cast<TRestOAuth*>(Sender), this, URL);
}

void __fastcall THttpRestForm::RestOAuth1OAuthNewCode(TObject *Sender)
{
    OAuthAuthCode->Text = static_cast<TRestOAuth*>(Sender)->AuthCode;
    LabelResult->Caption = "Result: Got New Auth Code OK";   // an AuthCode usually expires in 10 minutes or less
}

void __fastcall THttpRestForm::RestOAuth1OAuthNewToken(TObject *Sender)
{
    TRestOAuth *OAuth = static_cast<TRestOAuth*>(Sender);
    OAuthAccToken->Text = OAuth->AccToken;
    OAuthRefToken->Text = OAuth->RefreshToken;
    OAuthExpire->Text = DateTimeToStr(OAuth->ExpireDT);
    AuthBearer->Text = OAuth->AccToken;
    LabelResult->Caption = "Result: Got New Token OK";
}

void __fastcall THttpRestForm::RestOAuth1OAuthProg(TObject *Sender, TLogOption LogOption, const String Msg)
{
    AddLog(Msg);
}

void __fastcall THttpRestForm::doTestRedirClick(TObject *Sender)
{
    RestOAuthSetup();
    RestOAuth1->TestRedirect();
}

void __fastcall THttpRestForm::doOAuthConsoleClick(TObject *Sender)
{
    RestOAuthSetup();
    RestOAuth1->LaunchConsole();
}

void __fastcall THttpRestForm::doOAuthLoginClick(TObject *Sender)
{
    RestOAuthSetup();
    if (RestOAuth1->StartAuthorization()) {
        if (RestOAuth1->AuthType != OAuthTypeEmbed)   // embedded returns with the codes
            LabelResult->Caption = "Result: Waiting for Auth Code";
    }
    else
        LabelResult->Caption = "Result: Failed - " + RestOAuth1->LastError;
}

void __fastcall THttpRestForm::doOAuthTokenClick(TObject *Sender)
{
    RestOAuthSetup();
    LabelResult->Caption = RestOAuth1->GrantAuthToken() ? String("Result: Got New Token OK")
                                                        : "Result: Failed - " + RestOAuth1->LastError;
}

void __fastcall THttpRestForm::doOAuthRefreshClick(TObject *Sender)
{
    RestOAuthSetup();
    LabelResult->Caption = RestOAuth1->GrantRefresh() ? String("Result: Got New Token OK")
                                                      : "Result: Failed - " + RestOAuth1->LastError;
}

void __fastcall THttpRestForm::doGrantCredClick(TObject *Sender)
{
    RestOAuthSetup();
    LabelResult->Caption = RestOAuth1->GrantAppToken() ? String("Result: Got New Token OK")
                                                       : "Result: Failed - " + RestOAuth1->LastError;
}

void __fastcall THttpRestForm::doGrantPasswordClick(TObject *Sender)
{
    RestOAuthSetup();
    LabelResult->Caption = RestOAuth1->GrantPasswordToken(Trim(AuthLogin->Text), Trim(AuthPassword->Text))
        ? String("Result: Got New Token OK") : "Result: Failed - " + RestOAuth1->LastError;
}

// Fills the OAuth2 details for common accounts.
void __fastcall THttpRestForm::OAuthAccTitleChange(TObject *Sender)
{
    RestOAuth1->StopSrv();
    const int Index = OAuthAccTitle->ItemIndex;
    if (Index <= 0 || Index >= static_cast<int>(FOAuthUris.size()))   // 0 is None
        return;
    const TOAuthUri &Uri = FOAuthUris[Index];
    OAuthAccTitle->Text = Uri.CAccName;
    OAuthConsoleUrl->Text = Uri.CConsoleUrl;
    OAuthAppUrl->Text = Uri.CAppUrl;
    OAuthRedirectUrl->Text = Uri.CRedirectUrl;
    OAuthTokenUrl->Text = Uri.CTokenUrl;
    OAuthScope->Text = Uri.CScope;
}

/* DNS over HTTPS with a Json response, which looks like:
   {"Status": 0, "TC": false, "RD": true, "RA": true, "AD": false, "CD": false,
    "Question": [{"name": "www.overbyte.eu.", "type": 1}],
    "Answer": [{"name": "www.overbyte.eu.", "type": 1, "TTL": 1426, "data": "91.183.89.111"}]}
   with "Authority" instead of "Answer" for some queries. */
void __fastcall THttpRestForm::doDNSJsonClick(TObject *Sender)
{
    TButtonBusy Busy(doDNSJson);
    std::unique_ptr<TStringList> ResultList(new TStringList);
    ClearRespList();
    OpenLogFile();
    if (!CommonRestSettings()) {
        AddLog("DNS request not started");
        return;
    }
    HttpRest1->Accept = MimeDnsJson;
    HttpRest1->NoCache = true;
    String QueryType = DnsQueryType->Text;
    const int I = QueryType.Pos(" [");
    if (I > 1)
        QueryType.SetLength(I - 1);

    // Json DNS parameters; the String() casts keep the literals off AddItem's bool overload
    TRestParams *Params = HttpRest1->RestParams;
    Params->Clear();
    Params->AddItem("name", IcsIDNAToASCII(Trim(DnsDomainName->Text)), true);
    Params->AddItem("type", QueryType, true);
    if (DnsDnssec->Checked)
        Params->AddItem("do", String("true"), true);
    if (DnsNoValidation->Checked)
        Params->AddItem("cd", String("true"), true);
    Params->PContent = PContUrlencoded;

    AddLog("Starting sync DNS request");
    const int StatCode = HttpRest1->RestRequest(httpGET, DnsHttpsUrl->Text, false);
    _di_ISuperObject Json;
    if (StatCode == 200)
        Json = HttpRest1->ResponseJson;
    if (!Json) {
        AddLog("DNS request failed: HTTP Failed " + HttpRest1->ResponseRaw);
        return;
    }

    // there may be multiple answers
    const int RCode = Json->I["Status"];
    if (RCode != DnsRCodeNoError) {
        AddLog("DNS request failed: " + RCodeName(RCode));
        return;
    }
    _di_ISuperObject ArrayJson = Json->O["Answer"];
    if (!ArrayJson)
        ArrayJson = Json->O["Authority"];
    TSuperArray *Answers = JsonArrayOf(ArrayJson);
    if (!Answers) {
        AddLog("DNS request no answer");
        return;
    }
    AddDnsHeaderRows(RespList);
    for (int J = 0; J < Answers->Length; J++) {
        _di_ISuperObject Answer = Answers->O[J];
        const int QType = Answer->I["type"];
        if (QType == DnsQueryA || QType == DnsQueryAAAA || QType == DnsQueryMX ||
                QType == DnsQueryNS || QType == DnsQueryCNAME)
            ResultList->Add(Answer->S["data"]);
        TListItem *Item = RespList->Items->Add();
        Item->Caption = Answer->S["name"];
        Item->SubItems->Add(FindDnsReqTypeName(QType));
        Item->SubItems->Add(Answer->S["data"]);
        Item->SubItems->Add(Answer->S["TTL"]);
    }
    if (ResultList->Count > 0)
        AddLog("DNS result: " + ResultList->CommaText);
}

// The settings shared by the three wire-format DNS buttons.
void THttpRestForm::SetupDnsQuery()
{
    ClearRespList();
    OpenLogFile();
    DnsQueryHttps1->DnsSrvUrl = DnsHttpsUrl->Text;
    DnsQueryHttps1->DebugLevel = SelectedDebugLevel();
    DnsQueryHttps1->HttpRest->CertVerMethod = static_cast<TCertVerMethod>(CertVerMethod->ItemIndex);
    DnsQueryHttps1->HttpRest->SslReportChain = ReportCertChain->Checked;
    DnsQueryHttps1->HttpRest->SocketFamily = static_cast<TSocketFamily>(IpSockFamily->ItemIndex);
}

void __fastcall THttpRestForm::doDnsQuery1Click(TObject *Sender)
{
    SetupDnsQuery();
    const int QType = DnsReqTable[DnsQueryType->ItemIndex].Num;
    AddLog(DnsQueryHttps1->DOHQueryAny(Trim(DnsDomainName->Text), QType) ? "Starting async DNS request"
                                                                         : "DNS request failed");
}

void __fastcall THttpRestForm::doDnsQueryAllClick(TObject *Sender)
{
    SetupDnsQuery();
    AddLog(DnsQueryHttps1->DOHQueryAll(Trim(DnsDomainName->Text)) ? "Starting async DNS request"
                                                                  : "DNS request failed");
}

void __fastcall THttpRestForm::doDnsQueryBothAClick(TObject *Sender)
{
    SetupDnsQuery();
    AddLog(DnsQueryHttps1->DOHQueryBothA(Trim(DnsDomainName->Text)) ? "Starting async DNS request"
                                                                    : "DNS request failed");
}

void __fastcall THttpRestForm::DnsQueryHttps1DnsProg(TObject *Sender, TLogOption LogOption, const String Msg)
{
    AddLog(Msg);
}

void __fastcall THttpRestForm::DnsQueryHttps1RequestDone(TObject *Sender, Word Error)
{
    TDnsQueryHttps *MyDnsQuery = static_cast<TDnsQueryHttps*>(Sender);
    if (Error != 0) {
        AddLog("Request failed, error #" + IntToStr(Error) + ". Status = " +
               IntToStr(MyDnsQuery->HttpRest->StatusCode) + " - " + MyDnsQuery->HttpRest->ReasonPhrase);
        return;
    }
    AddLog("Reponse Code: " + RCodeName(MyDnsQuery->ResponseCode) +
           (MyDnsQuery->ResponseAuthoritative ? ", Authoritative Server" : ", Not Authoritative") +
           ", Answer Records: " + IntToStr(MyDnsQuery->ResponseANCount) +
           ", NS Records: " + IntToStr(MyDnsQuery->ResponseNSCount) +
           ", Additional Records: " + IntToStr(MyDnsQuery->ResponseARCount));
    if (MyDnsQuery->AnswerTotal <= 0)
        return;
    AddDnsHeaderRows(RespList);
    for (int I = 0; I < MyDnsQuery->AnswerTotal; I++) {
        const TRRRecord Answer = MyDnsQuery->AnswerRecord[I];
        TListItem *Item = RespList->Items->Add();
        Item->Caption = String(Answer.RRName);
        Item->SubItems->Add(FindDnsReqTypeName(Answer.RRType));
        String S = String(Answer.RDData);
        if (Answer.RRType == DnsQueryMX)   // some records return more than a string
            S = IntToStr(Answer.MxPref) + " = " + S;
        Item->SubItems->Add(S);
        Item->SubItems->Add(IntToStr(static_cast<__int64>(Answer.TTL)));
    }
}

// The settings shared by the SMS buttons; Kapow logs in with a name and password, SMS Works with a JWT.
void THttpRestForm::SetupSms(TSmsProvider Provider)
{
    OpenLogFile();
    IcsSMS1->SmsProvider = Provider;
    IcsSMS1->DebugLevel = SelectedDebugLevel();
    if (Provider == SmsProvKapow) {
        IcsSMS1->AccountName = KapowAccName->Text;
        IcsSMS1->AccountPW = KapowAccPw->Text;
    }
    else
        IcsSMS1->AccountJwt = SmsWorksJWT->Text;
}

void __fastcall THttpRestForm::doKapowSendClick(TObject *Sender)
{
    SetupSms(SmsProvKapow);
    IcsSMS1->MsgSender = SmsAccSender->Text;   // premium feature
    if (!IcsSMS1->SendSMS(SmsDestNums->Lines->CommaText, SmsMsgText->Lines->Text))
        AddLog("Failed to Send SMS: " + IcsSMS1->LastError);
    else
        AddLog("SMS Send Started");
}

void __fastcall THttpRestForm::doKapowCheckClick(TObject *Sender)
{
    SetupSms(SmsProvKapow);
    if (!IcsSMS1->CheckSMS(FKapowSentId))
        AddLog("Failed to Delivery Check SMS: " + IcsSMS1->LastError);
    else
        AddLog("Delivery Check Started");
}

void __fastcall THttpRestForm::doKapowCreditClick(TObject *Sender)
{
    SetupSms(SmsProvKapow);
    if (!IcsSMS1->CheckCredit())
        AddLog("Failed to Check Credit: " + IcsSMS1->LastError);
    else
        AddLog("Credit Check Started");
}

void __fastcall THttpRestForm::doSmsWorksSendClick(TObject *Sender)
{
    SetupSms(SmsProvSmsWorks);
    IcsSMS1->MsgSender = SmsAccSender->Text;
    const String S = IcsSMS1->SendSMS(SmsDestNums->Lines->CommaText, SmsMsgText->Lines->Text)
        ? String("SMS Send Started") : "Failed to Send SMS: " + IcsSMS1->LastError;
    LabelSmsWorksResult->Caption = "Result: " + S;
    AddLog(S);
}

void __fastcall THttpRestForm::doSmsWorksCheckClick(TObject *Sender)
{
    SetupSms(SmsProvSmsWorks);
    if (!IcsSMS1->CheckSMS(FSmsWorksSentId, true, SmsDestNums->Lines->Count > 1))
        AddLog("Failed to Delivery Check SMS: " + IcsSMS1->LastError);
    else
        AddLog("Delivery Check Started");
}

void __fastcall THttpRestForm::doSmsWorksCreditClick(TObject *Sender)
{
    SetupSms(SmsProvSmsWorks);
    if (!IcsSMS1->CheckCredit())
        AddLog("Failed to Check Credit: " + IcsSMS1->LastError);
    else
        AddLog("Credit Check Started");
}

void __fastcall THttpRestForm::IcsSMS1SmsDone(TObject *Sender)
{
    AddLog("SMS Response:: " + IcsSMS1->LastResp);
    const bool Kapow = (IcsSMS1->SmsProvider == SmsProvKapow);
    String &SentId = Kapow ? FKapowSentId : FSmsWorksSentId;
    String S;
    if (IcsSMS1->LastError.IsEmpty()) {
        if (!IcsSMS1->SentID.IsEmpty()) {
            SentId = IcsSMS1->SentID;
            S = "SMS Queued for Delivery, Reference: " + SentId;
            (Kapow ? doKapowCheck : doSmsWorksCheck)->Enabled = true;
        }
        if (!IcsSMS1->Credits.IsEmpty()) {
            S = "Credits: " + IcsSMS1->Credits;
            (Kapow ? LabelKapowCredit : LabelSmsWorksCredits)->Caption = S;
        }
        if (!IcsSMS1->Delivery.IsEmpty())
            S = "SMS Delivery for " + SentId + ": " + IcsSMS1->Delivery;
    }
    else
        S = "Failed: " + IcsSMS1->LastError;
    (Kapow ? LabelKapowResult : LabelSmsWorksResult)->Caption = "Result: " + S;
    AddLog("SMS Result: " + S);
    System::Sysutils::Beep();
}

void __fastcall THttpRestForm::IcsSMS1SmsProg(TObject *Sender, TLogOption LogOption, const String Msg)
{
    AddLog(Msg);
}

void THttpRestForm::TwitSetup()
{
    OpenLogFile();
    TwitResultLabel->Caption = "Result: Pending";
    IcsTwitter1->DebugLevel = SelectedDebugLevel();
    IcsTwitter1->ConApiKey = TwitApiKey->Text;
    IcsTwitter1->ConApiSecret = TwitApiSecret->Text;
    IcsTwitter1->AccToken = TwitAccToken->Text;
    IcsTwitter1->AccTokSecret = TwitAccSecret->Text;
    IcsTwitter1->ForceLogin = TwitForceLogin->Checked;
    IcsTwitter1->OAAuthType = static_cast<TOAuthType>(TwitAuthType->ItemIndex);
    IcsTwitter1->OAEdgeCacheDir = OAuthEdgeCache->Text;
}

// Delphi logs ResponseJson.AsJson unchecked, an access violation when a failed request returned no Json.
void THttpRestForm::LogTwitterJson()
{
    if (IcsTwitter1->ResponseJson)
        AddLog(IcsTwitter1->ResponseJson->AsJSon(true, false));
}

// TIcsTwitter passes this event to its own TRestOAuth, which is the Sender.
void __fastcall THttpRestForm::IcsTwitter1TwitEmbed(TObject *Sender, const String URL, bool &Success)
{
    Success = OAuthBrowser1->BrowserEmbedded(static_cast<TRestOAuth*>(Sender), this, URL);
}

void __fastcall THttpRestForm::IcsTwitter1TwitNewToken(TObject *Sender)
{
    TIcsTwitter *Twitter = static_cast<TIcsTwitter*>(Sender);
    TwitAccToken->Text = Twitter->AccToken;
    TwitAccSecret->Text = Twitter->AccTokSecret;
    TwitScrnName->Text = Twitter->AccScreenName;
    TwitScrnId->Text = Twitter->AccUserId;
    TwitResultLabel->Caption = "Result: Got New Twitter Token OK for " + TwitScrnName->Text;
    AddLog(TwitResultLabel->Caption + IcsCRLF);
}

void __fastcall THttpRestForm::doTwitLoginClick(TObject *Sender)
{
    TwitSetup();
    if (IcsTwitter1->StartAuthorization())
        TwitResultLabel->Caption = "Result: Started Login Process";
    else {
        TwitResultLabel->Caption = "Result: " + IcsTwitter1->LastError;
        DisplayJson(IcsTwitter1->ResponseJson);
    }
    AddLog(TwitResultLabel->Caption);
    TimerLogTimer(this);
}

void __fastcall THttpRestForm::doTwitMsgClick(TObject *Sender)
{
    TwitSetup();
    if (IcsTwitter1->SendTweet(TwitMsg->Lines->Text)) {
        TwitScrnName->Text = IcsTwitter1->AccScreenName;
        TwitScrnId->Text = IcsTwitter1->AccUserId;
        TwitIdList->Text = IcsTwitter1->LastTweetId;
        TwitResultLabel->Caption = "Result: Sent Tweet OK for " + TwitScrnName->Text + " with ID " + TwitIdList->Text;
    }
    else
        TwitResultLabel->Caption = "Result: " + IcsTwitter1->LastError;
    AddLog(TwitResultLabel->Caption + IcsCRLF);
    DisplayJson(IcsTwitter1->ResponseJson);
    TimerLogTimer(this);
}

void __fastcall THttpRestForm::doTwitAccSettClick(TObject *Sender)
{
    TwitSetup();
    TwitResultLabel->Caption = IcsTwitter1->GetAccSett() ? String("Result: Found Account Settings")
                                                         : "Result: " + IcsTwitter1->LastError;
    AddLog(TwitResultLabel->Caption + IcsCRLF);
    DisplayJson(IcsTwitter1->ResponseJson);
    LogTwitterJson();
    TimerLogTimer(this);
}

void __fastcall THttpRestForm::doTwitQueryClick(TObject *Sender)
{
    TwitSetup();
    if (IcsTwitter1->SearchTweets(TwitQuery->Text)) {
        TwitResultLabel->Caption = "Result: Found Some Tweets";
        AddLog(TwitResultLabel->Caption + IcsCRLF);
        DisplayJson(IcsTwitter1->ResponseJson);
        LogTwitterJson();
        _di_ISuperObject StatusesJson;
        if (IcsTwitter1->ResponseJson)
            StatusesJson = IcsTwitter1->ResponseJson->O["statuses"];
        TSuperArray *Statuses = JsonArrayOf(StatusesJson);
        if (Statuses && Statuses->Length > 0) {
            String AllHdrs;
            for (int I = 0; I < Statuses->Length; I++) {
                _di_ISuperObject TweetJson = Statuses->O[I];
                AllHdrs += "TweetId: " + TweetJson->S["id_str"] + ", Text: " + TweetJson->S["text"] +
                           ", Created: " + TweetJson->S["created_at"] + ", Source: " + TweetJson->S["source"] + IcsCRLF;
            }
            AddLog(AllHdrs);
        }
    }
    else {
        TwitResultLabel->Caption = "Result: " + IcsTwitter1->LastError;
        AddLog(TwitResultLabel->Caption + IcsCRLF);
        DisplayJson(IcsTwitter1->ResponseJson);
    }
    TimerLogTimer(this);
}

void __fastcall THttpRestForm::doTwitGetIdClick(TObject *Sender)
{
    TwitSetup();
    TwitResultLabel->Caption = IcsTwitter1->ListTweets(TwitIdList->Text) ? String("Result: Found Some Tweets")
                                                                         : "Result: " + IcsTwitter1->LastError;
    AddLog(TwitResultLabel->Caption + IcsCRLF);
    DisplayJson(IcsTwitter1->ResponseJson);
    LogTwitterJson();
    TimerLogTimer(this);
}

void THttpRestForm::EmailSetup()
{
    OpenLogFile();
    EmailOAResultLabel->Caption = "Result: Pending";
    IcsRestEmail1->DebugLevel = SelectedDebugLevel();
    IcsRestEmail1->RestEmailType = static_cast<TRestEmailType>(EmailRestType->ItemIndex);
    IcsRestEmail1->ClientId = EmailClientId->Text;
    IcsRestEmail1->ClientSecret = EmailClientSecret->Text;
    IcsRestEmail1->AccToken = EmailAccToken->Text;
    IcsRestEmail1->RefrToken = EmailRefrToken->Text;
    IcsRestEmail1->AccExpireDT = RFC3339_StrToDate(EmailAccExpiry->Text);
    IcsRestEmail1->ForceLogin = EmailForceLogin->Checked;
    IcsRestEmail1->AccountHint = EmailAccountHint->Text;
    if (IcsRestEmail1->AccountHint.IsEmpty() && EmailAccount->Text != "me")
        IcsRestEmail1->AccountHint = EmailAccount->Text;
    IcsRestEmail1->MsUserAuth = EmailUserAuth->Text;
    IcsRestEmail1->OAAuthType = static_cast<TOAuthType>(EmailAuthType->ItemIndex);
    IcsRestEmail1->OAEdgeCacheDir = OAuthEdgeCache->Text;
}

void __fastcall THttpRestForm::IcsRestEmail1EmailNewToken(TObject *Sender)
{
    TIcsRestEmail *RestEmail = static_cast<TIcsRestEmail*>(Sender);
    EmailAccToken->Text = RestEmail->AccToken;
    if (!RestEmail->RefrToken.IsEmpty())
        EmailRefrToken->Text = RestEmail->RefrToken;
    const String S = RestEmail->NewAccName.IsEmpty() ? RestEmail->NewAccEmail
        : "\"" + RestEmail->NewAccName + "\" <" + RestEmail->NewAccEmail + ">";
    EmailTokenAccnt->Text = S;
    EmailFrom->Text = S;
    EmailAccExpiry->Text = RFC3339_DateToStr(RestEmail->AccExpireDT);
    DisplayJson(RestEmail->ResponseJson);   // profile
    EmailOAResultLabel->Caption = "Result: Got New Email Token OK for " + EmailTokenAccnt->Text;
    AddLog(EmailOAResultLabel->Caption + IcsCRLF);
}

// TIcsRestEmail passes this event to its own TRestOAuth, which is the Sender.
void __fastcall THttpRestForm::IcsRestEmail1OAEmbed(TObject *Sender, const String URL, bool &Success)
{
    Success = OAuthBrowser1->BrowserEmbedded(static_cast<TRestOAuth*>(Sender), this, URL);
}

void __fastcall THttpRestForm::doEmaiTstRedirClick(TObject *Sender)
{
    EmailSetup();
    IcsRestEmail1->TestRedirect();
}

void __fastcall THttpRestForm::doEmailOAConsClick(TObject *Sender)
{
    EmailSetup();
    IcsRestEmail1->LaunchConsole();
}

void __fastcall THttpRestForm::doEmailOALoginClick(TObject *Sender)
{
    EmailSetup();
    IcsRestEmail1->StartAuthorization();
}

void __fastcall THttpRestForm::doEmailOARefreshClick(TObject *Sender)
{
    EmailSetup();
    IcsRestEmail1->UpdateToken();
}

// Similar to the POP3 UIDL command, gets the identifier of each message in the mailbox.
void __fastcall THttpRestForm::doEmailListClick(TObject *Sender)
{
    EmailSetup();
    EmailReadMessId->Items->Clear();
    LabelEmailResult->Caption = "Getting Email ID List";
    IcsRestEmail1->GetNewToken(true);   // login to get a token
    // mailbox labels include INBOX, SENT, UNREAD, TRASH, CATEGORY_PERSONAL, IMPORTANT,
    // CATEGORY_UPDATES and CATEGORY_PROMOTIONS; none means all labels
    String MBIds;
    if (EmailHdrFolder->ItemIndex >= 0)
        MBIds = EmailHdrFolder->Items->Strings[EmailHdrFolder->ItemIndex];
    if (IcsRestEmail1->ListEmails("", MBIds, 100)) {
        DisplayJson(IcsRestEmail1->ResponseJson);
        _di_ISuperObject MsgJson = IcsRestEmail1->ResponseJson ? IcsRestEmail1->ResponseJson->O["messages"]
                                                               : _di_ISuperObject();
        if (TSuperArray *Messages = JsonArrayOf(MsgJson)) {
            // Delphi adds element 0's id once per message instead of each message's id.
            for (int I = 0; I < Messages->Length; I++)
                EmailReadMessId->Items->Add(Messages->O[I]->S["id"]);
        }
        LabelEmailResult->Caption = "Finished Getting Email ID List";
    }
    else {
        DisplayJson(IcsRestEmail1->ResponseJson);
        LabelEmailResult->Caption = "List IDs Failed: " + IcsRestEmail1->LastError;
        AddLog(LabelEmailResult->Caption);
    }
}

/* Like doEmailListClick, but also reads some headers of each message, similar to the
   POP3 TOP command. It takes a while for large mailboxes; save the headers if not
   deleting the email. */
void __fastcall THttpRestForm::doEmailHdrsClick(TObject *Sender)
{
    EmailSetup();
    EmailReadMessId->Items->Clear();
    ClearRespList();
    LabelEmailResult->Caption = "Getting Email ID List";
    IcsRestEmail1->GetNewToken(true);   // login to get a token
    String MBIds;
    if (EmailHdrFolder->ItemIndex >= 0)
        MBIds = EmailHdrFolder->Items->Strings[EmailHdrFolder->ItemIndex];
    const TRestEmailType EmailType = IcsRestEmail1->RestEmailType;
    if (EmailType == RestEmailGoogle) {
        IcsRestEmail1->HdrFieldList = "to,from,subject,date";   // the fields we want
        MBIds = UpperCase(MBIds);
    }
    else if (EmailType == RestEmailMSRest) {
        IcsRestEmail1->HdrFieldList = "toRecipients,from,subject,sentDateTime,bodyPreview";
        MBIds = LowerCase(MBIds);
    }

    if (!IcsRestEmail1->ListEmails("", MBIds, 100)) {   // only 100 headers
        DisplayJson(IcsRestEmail1->ResponseJson);
        LabelEmailResult->Caption = "List IDs Failed: " + IcsRestEmail1->LastError;
    }
    else {
        DisplayJson(IcsRestEmail1->ResponseJson);
        _di_ISuperObject ListJson = IcsRestEmail1->ResponseJson;
        if (EmailType == RestEmailGoogle) {
            _di_ISuperObject MsgJson = ListJson ? ListJson->O["messages"] : _di_ISuperObject();
            TSuperArray *Messages = JsonArrayOf(MsgJson);
            const int Tot = Messages ? Messages->Length : 0;
            if (Tot > 0) {
                AddLog("");
                String AllHdrs;
                int Errs = 0;
                // Google offers a batch API (one multipart/mixed request with many GETs);
                // this sample does one request per message.
                AddLog("Getting Headers for Total Messages: " + IntToStr(Tot));
                for (int I = 0; I < Tot; I++) {
                    LabelEmailResult->Caption = "Getting Header " + IntToStr(I + 1) + " of " + IntToStr(Tot);
                    const String MessId = Messages->O[I]->S["id"];
                    EmailReadMessId->Items->Add(MessId);
                    if (IcsRestEmail1->GetEmail(MessId, EmailFmtHdr)) {
                        _di_ISuperObject Resp = IcsRestEmail1->ResponseJson;
                        if (I == 0)
                            DisplayJson(Resp);   // just the first
                        // Delphi reads payload.headers without checking the response has a payload.
                        _di_ISuperObject PayloadJson = Resp ? Resp->O["payload"] : _di_ISuperObject();
                        _di_ISuperObject HdrsJson = PayloadJson ? PayloadJson->O["headers"] : _di_ISuperObject();
                        // headers are an array of objects with name and value for each header line
                        TSuperArray *Hdrs = JsonArrayOf(HdrsJson);
                        if (Hdrs && Hdrs->Length > 0) {
                            String HdrLine = "MessId: " + MessId;
                            for (int J = 0; J < Hdrs->Length; J++) {
                                const String HdrName = Hdrs->O[J]->S["name"];
                                if (HdrName == "To" || HdrName == "From" || HdrName == "Subject" || HdrName == "Date")
                                    HdrLine += ", " + HdrName + ": " + Hdrs->O[J]->S["value"];
                            }
                            HdrLine += ", Size " + Resp->S["sizeEstimate"] + ", Labelst " + Resp->S["labelIds"] +
                                       ", Snippet " + Resp->S["snippet"];
                            AllHdrs += HdrLine + IcsCRLF;
                        }
                    }
                    else
                        Errs++;
                    if (Errs > 4)
                        break;
                }
                AddLog(AllHdrs);
            }
            LabelEmailResult->Caption = "Finished Getting Headers for " + MBIds + ", Total Messages: " + IntToStr(Tot);
        }
        else if (EmailType == RestEmailMSRest) {
            _di_ISuperObject MsgJson = ListJson ? ListJson->O["value"] : _di_ISuperObject();
            TSuperArray *Messages = JsonArrayOf(MsgJson);
            const int Tot = Messages ? Messages->Length : 0;
            if (Tot > 0) {
                AddLog("");
                String AllHdrs;
                for (int I = 0; I < Tot; I++) {
                    _di_ISuperObject Msg = Messages->O[I];
                    const String MessId = Msg->S["id"];
                    EmailReadMessId->Items->Add(MessId);
                    const String Snippet = StringReplace(Msg->S["bodyPreview"], IcsCRLF, "\\",
                                                         TReplaceFlags() << rfReplaceAll);   // remove line feeds
                    AllHdrs += "MessId: " + MessId +
                        ", To: " + JsonEmailAddresses(Msg->O["toRecipients"]) +
                        ", From: " + JsonEmailAddresses(Msg->O["from"]) +
                        ", Date: " + Msg->S["sentDateTime"] +
                        ", Subject: " + Msg->S["subject"] +
                        ", Snippet " + Snippet + IcsCRLF;
                }
                AddLog(AllHdrs);
            }
            LabelEmailResult->Caption = "Finished Getting Headers, Total Messages: " + IntToStr(Tot);
        }
    }
    AddLog(LabelEmailResult->Caption);
    AddLog("");
    TimerLogTimer(this);
}

/* Reads one message by ID. Google returns it raw (the whole SMTP message) or full (the
   headers parsed into separate lines and MIME messages into separate parts). */
void __fastcall THttpRestForm::doEmailReadClick(TObject *Sender)
{
    if (EmailReadMessId->ItemIndex < 0)
        return;
    const String MessId = EmailReadMessId->Items->Strings[EmailReadMessId->ItemIndex];
    LabelEmailResult->Caption = "Getting Email Content for ID: " + MessId;
    EmailSetup();
    IcsRestEmail1->GetNewToken(true);   // login to get a token
    ClearRespList();                    // clear token errors
    const TRestEmailFmt EmailFmt = EmailRawHdrs->Checked ? EmailFmtRaw : EmailFmtFull;
    const TRestEmailType EmailType = IcsRestEmail1->RestEmailType;
    if (EmailType == RestEmailMSRest)
        IcsRestEmail1->HdrFieldList = "toRecipients,from,subject,sentDateTime,internetMessageHeaders,body";

    if (!IcsRestEmail1->GetEmail(MessId, EmailFmt)) {
        DisplayJson(IcsRestEmail1->ResponseJson);
        LabelEmailResult->Caption = "Read Email Failed: " + IcsRestEmail1->LastError;
        AddLog(LabelEmailResult->Caption);
        TimerLogTimer(this);
        return;
    }
    // Delphi reads the Google response Json unchecked; it is checked once here (Microsoft raw has none).
    _di_ISuperObject Resp = IcsRestEmail1->ResponseJson;
    if (Resp) {
        DisplayJson(Resp);
        AddLog(Resp->AsJSon(true, false));
    }
    AddLog("");
    if (EmailType == RestEmailGoogle && Resp) {
        if (EmailFmt == EmailFmtRaw) {
            AddLog("MessId: " + MessId + " - Raw Content");
            AddLog(IcsBase64UrlDecode(Resp->S["raw"]));
        }
        else {
            _di_ISuperObject PayloadJson = Resp->O["payload"];
            _di_ISuperObject HdrsJson = PayloadJson ? PayloadJson->O["headers"] : _di_ISuperObject();
            // headers are an array of objects with name and value for each header line
            TSuperArray *Hdrs = JsonArrayOf(HdrsJson);
            if (Hdrs && Hdrs->Length > 0) {
                AddLog("MessId: " + MessId + " - Headers");
                for (int I = 0; I < Hdrs->Length; I++)
                    AddLog(Hdrs->O[I]->S["name"] + ": " + Hdrs->O[I]->S["value"]);
            }
            // there may be a single body, or several MIME parts, of which the text ones are shown
            const String Msg = PayloadJson ? PayloadJson->S["body.data"] : String();
            if (!Msg.IsEmpty()) {
                AddLog("MessId: " + MessId + " - Main Content");
                AddLog(IcsBase64UrlDecode(Msg));   // the email is base64 encoded
            }
            else {
                _di_ISuperObject PartsJson = PayloadJson ? PayloadJson->O["parts"] : _di_ISuperObject();
                TSuperArray *Parts = JsonArrayOf(PartsJson);
                if (Parts && Parts->Length > 0) {
                    AddLog("");
                    AddLog("MessId: " + MessId + " - MIME Parts Tota: " + IntToStr(Parts->Length));
                    for (int I = 0; I < Parts->Length; I++) {
                        if (Parts->O[I]->S["mimeType"].Pos("text/") == 1) {
                            AddLog("");
                            AddLog("Content Part " + IntToStr(I + 1));
                            AddLog(IcsBase64UrlDecode(Parts->O[I]->S["body.data"]));
                        }
                    }
                }
            }
        }
    }
    else if (EmailType == RestEmailMSRest) {
        // raw headers and maybe MIME content
        if (EmailFmt == EmailFmtRaw)
            // may be very long with large attachments; TMimeDecodeEx.DecodeStream(ResponseStream)
            // would decode the headers and attachments
            AddLog(IcsRestEmail1->ResponseRaw);
        else if (Resp) {
            // headers are an array of objects with name and value for each header line
            _di_ISuperObject HdrsJson = Resp->O["internetMessageHeaders"];
            TSuperArray *Hdrs = JsonArrayOf(HdrsJson);
            if (Hdrs && Hdrs->Length > 0) {
                AddLog("MessId: " + MessId + " - Headers");
                for (int I = 0; I < Hdrs->Length; I++)
                    AddLog(Hdrs->O[I]->S["name"] + ": " + Hdrs->O[I]->S["value"]);
            }
            AddLog("");
            const String Content = Resp->S["body.contentType"];
            if (Content == "text" || Content == "html")
                AddLog(Resp->S["body.content"]);
            else
                AddLog("Body Content=" + Content);
        }
    }
    LabelEmailResult->Caption = "Finished Getting Email Content";
    TimerLogTimer(this);
}

/* Sends one email. Google accepts a complete RFC822 SMTP format message and uses its To:,
   CC: and BCC: headers; Microsoft Outlook builds the email from Json, or takes raw. */
void __fastcall THttpRestForm::doEmailSendClick(TObject *Sender)
{
    LabelEmailResult->Caption = "Sending Email";
    EmailSetup();
    IcsRestEmail1->GetNewToken(true);   // login to get a token
    ClearRespList();                    // clear token errors

    const TRestEmailFmt EmailFmt = (EmailRawHdrs->Checked || IcsRestEmail1->RestEmailType == RestEmailGoogle)
        ? EmailFmtRaw : EmailFmtFull;

    // a raw message is built with the SMTP client component; simple messages could be built by hand
    if (EmailFmt == EmailFmtRaw) {
        String FriendlyName;
        SslHtmlSmtpCli1->EmailFiles->Clear();
        SslHtmlSmtpCli1->RcptName->Clear();   // Google does not need this
        SslHtmlSmtpCli1->Allow8bitChars = true;
        SslHtmlSmtpCli1->HdrFrom = EmailFrom->Text;
        SslHtmlSmtpCli1->FromName = ParseEmail(EmailFrom->Text, FriendlyName);
        SslHtmlSmtpCli1->HdrCc = EmailCC->Text;
        SslHtmlSmtpCli1->HdrReplyTo = SslHtmlSmtpCli1->FromName;
        SslHtmlSmtpCli1->HdrSubject = EmailSubject->Text;
        SslHtmlSmtpCli1->ContentType = smtpPlainText;
        SslHtmlSmtpCli1->PlainText->Text = EmailMessage->Text;
        if (!EmailFileAttach->Text.IsEmpty() && FileExists(EmailFileAttach->Text))
            SslHtmlSmtpCli1->EmailFiles->Add(EmailFileAttach->Text);
        String Recip;
        for (int I = 0; I < EmailRecipList->Lines->Count; I++) {
            const String Line = EmailRecipList->Lines->Strings[I];
            if (Line.Pos("*") == 1 || Line.Pos("@") == 0)   // comment or not an address
                continue;
            // Delphi adds the comma for every line after the first, so a skipped first line
            // starts the list with a comma.
            if (!Recip.IsEmpty())
                Recip += ",";
            Recip += Trim(Line);
        }
        SslHtmlSmtpCli1->HdrTo = Recip;
        // the message is written to a file stream, read in OnBeforeOutStreamFree
        FSmtpRawMsg = "";
        SslHtmlSmtpCli1->SendMode = smtpToStream;
        const String TempName = IcsGetTempPath() + IntToStr(static_cast<__int64>(GetTickCount())) + ".tmp";
        AddLog("Preparing Email Raw Message");
        SslHtmlSmtpCli1->SendToFileSync(TempName);
        IcsDeleteFile(TempName, true);   // not needed; Delphi uses DeleteFile
        if (FSmtpRawMsg.IsEmpty()) {
            AddLog("Failed to Read Email Raw Message");
            return;
        }
    }

    // Microsoft Outlook can take a Json message, similar to received messages
    if (IcsRestEmail1->RestEmailType == RestEmailMSRest && EmailFmt == EmailFmtFull) {
        _di_ISuperObject RecipJson = SO(String("[]"));
        for (int I = 0; I < EmailRecipList->Lines->Count; I++) {
            const String Line = EmailRecipList->Lines->Strings[I];
            if (Line.Pos("*") == 1 || Line.Pos("@") == 0)
                continue;
            RecipJson->AsArray()->Add(EmailAddressJson(Line));
        }
        _di_ISuperObject Body = SO();
        Body->S["contentType"] = "text";
        Body->S["content"] = EmailMessage->Text;
        _di_ISuperObject MsgJson = SO();
        MsgJson->S["subject"] = EmailSubject->Text;
        MsgJson->O["body"] = Body;
        MsgJson->O["toRecipients"] = RecipJson;
        MsgJson->O["from"] = EmailAddressJson(EmailFrom->Text);
        if (!EmailCC->Text.IsEmpty()) {
            _di_ISuperObject CcJson = SO(String("[]"));
            CcJson->AsArray()->Add(EmailAddressJson(EmailCC->Text));
            MsgJson->O["ccRecipients"] = CcJson;
        }
        FSmtpRawMsg = MsgJson->AsJSon();
    }

    AddLog("Sending Email");
    if (IcsRestEmail1->SendEmail(FSmtpRawMsg, EmailFmt)) {
        _di_ISuperObject Resp = IcsRestEmail1->ResponseJson;
        if (Resp) {   // Microsoft Outlook has no response
            DisplayJson(Resp);
            LabelEmailResult->Caption = "Sent Email OK, ID: " + IcsRestEmail1->LastEmailId;
            AddLog(LabelEmailResult->Caption);
            AddLog(Resp->S["labelIds"]);
            EmailReadMessId->Items->Clear();
            EmailReadMessId->Items->Add(IcsRestEmail1->LastEmailId);
        }
        else {
            LabelEmailResult->Caption = "Sent Email OK";
            AddLog(LabelEmailResult->Caption);
        }
    }
    else {
        DisplayJson(IcsRestEmail1->ResponseJson);
        LabelEmailResult->Caption = "Sent Email Failed: " + IcsRestEmail1->LastError;
        AddLog(LabelEmailResult->Caption);
    }
    AddLog("");
}

// Reads the raw SMTP message from the file stream into a string for the REST request.
void __fastcall THttpRestForm::SslHtmlSmtpCli1BeforeOutStreamFree(TObject *Sender)
{
    TStream *OutStream = SslHtmlSmtpCli1->OutStream;
    if (!OutStream)
        return;
    const int Len = static_cast<int>(OutStream->Size);
    AddLog("Email Raw Content Size: " + IntToStr(Len));
    AnsiString AStr;
    AStr.SetLength(Len);
    OutStream->Position = 0;
    if (Len > 0)
        OutStream->Read(AStr.c_str(), Len);
    FSmtpRawMsg = String(AStr);
}

void __fastcall THttpRestForm::doEmailDeleteClick(TObject *Sender)
{
    if (EmailReadMessId->ItemIndex < 0)
        return;
    const String MessId = EmailReadMessId->Items->Strings[EmailReadMessId->ItemIndex];
    LabelEmailResult->Caption = "Deleting Email ID: " + MessId;
    EmailSetup();
    IcsRestEmail1->GetNewToken(true);   // login to get a token
    const bool Deleted = IcsRestEmail1->DeleteEmail(MessId);
    DisplayJson(IcsRestEmail1->ResponseJson);
    LabelEmailResult->Caption = Deleted ? String("Deleted Email OK")
                                        : "Delete Email Failed: " + IcsRestEmail1->LastError;
    AddLog(LabelEmailResult->Caption);
    AddLog("");
}

// For Windows 7 to Windows 10 RTM, the old servers are HostIPv4 www.msftncsi.com,
// HostIPv6 ipv6.msftncsi.com, HttpPage /ncsi.txt and HttpText "Microsoft NCSI".
void __fastcall THttpRestForm::doAliveStartClick(TObject *Sender)
{
    doAliveStart->Enabled = false;
    doAliveStop->Enabled = true;
    OpenLogFile();
    LabelInetAlive->Caption = "Starting";
    IcsInetAlive1->AliveMethod = static_cast<TAliveMethod>(AliveMethod->ItemIndex);
    IcsInetAlive1->AliveNets = static_cast<TAliveNetwork>(AliveNets->ItemIndex);
    IcsInetAlive1->DebugLevel = SelectedDebugLevel();
    IcsInetAlive1->Start();
    IcsInetAlive1->CheckNow();
}

void __fastcall THttpRestForm::doAliveStopClick(TObject *Sender)
{
    doAliveStart->Enabled = true;
    doAliveStop->Enabled = false;
    IcsInetAlive1->Stop();
}

void __fastcall THttpRestForm::IcsInetAlive1AliveChange(TObject *Sender)
{
    String S = "Internet Connectivity State: " + AliveStatusLits[IcsInetAlive1->TestBothOnline()] + IcsCRLF;
    S += "IPv4 Connectivity State: " + AliveStatusLits[IcsInetAlive1->AliveIPv4];
    if (IcsInetAlive1->LastDTIPv4.Val > 0)
        S += ", Last Online at " + TimeToStr(IcsInetAlive1->LastDTIPv4);
    S += IcsCRLF;
    S += "IPv6 Connectivity State: " + AliveStatusLits[IcsInetAlive1->AliveIPv6];
    if (IcsInetAlive1->LastDTIPv6.Val > 0)
        S += ", Last Online at " + TimeToStr(IcsInetAlive1->LastDTIPv6);
    LabelInetAlive->Caption = S;
}

void THttpRestForm::SetWsButtons(bool Online)
{
    doWSConnectAsync->Enabled = !Online;
    doWSConnectSync->Enabled = !Online;
    doWSDisconnect->Enabled = Online;
    doWSSendText->Enabled = Online;
    doWSSendLines->Enabled = Online;
    doWSSendBin->Enabled = Online;
    doWSSendLong->Enabled = Online;
    doWSAbort->Enabled = Online;
}

void __fastcall THttpRestForm::SslWebSocketCli1WSConnected(TObject *Sender)
{
    const bool Connected = SslWebSocketCli1->IsWSConnected();
    LabelWSProgress->Caption = (Connected ? "Websocket Connected OK to: " : "Websocket Failed to Connect to: ") +
                               SslWebSocketCli1->URL;
    SetWsButtons(Connected);
    AddLog(LabelWSProgress->Caption);
}

void __fastcall THttpRestForm::SslWebSocketCli1WSDisconnected(TObject *Sender)
{
    SetWsButtons(false);
}

void __fastcall THttpRestForm::SslWebSocketCli1HttpRestProg(TObject *Sender, TLogOption LogOption, const String Msg)
{
    AddLog(Msg);
}

void __fastcall THttpRestForm::SslWebSocketCli1RestRequestDone(TObject *Sender, THttpRequest RqType, Word ErrCode)
{
    LabelWSProgress->Caption = SslWebSocketCli1->ReasonPhrase;   // the REST response, mostly errors
}

void __fastcall THttpRestForm::SslWebSocketCli1WSFrameRcvd(TSslWebSocketCli *Sender, const String APacket,
    TWebSocketReceivedFrame *&AFrame)
{
    if (!AFrame)
        return;
    LabelWsFrames->Caption = "Frames in/out: " + IntToStr(SslWebSocketCli1->WSFrameCounter);
    if (SslWebSocketCli1->DebugLevel >= DebugParams) {
        String S = "Websocket " + GetWSFrameKind(AFrame->Kind) + " received";
        if (AFrame->DataBytes > 0)
            S += ", data length: " + IntToStr(AFrame->DataBytes);
        AddLog(S);
    }
    if (AFrame->DataBytes < 1024 && (AFrame->Kind == wsfkBin || AFrame->Kind == wsfkText))
        AddLog(APacket);

    // look for a Json response
    if (AFrame->DataBytes > 10 && APacket.Pos("{") > 0 && APacket.Pos("}") > 0) {
        try {
            String Packet = APacket;   // ParseStringEx takes a non-const pointer
            String ErrStr;
            _di_ISuperObject RespJson = TSuperObject::ParseStringEx(Packet.c_str(), true, ErrStr);
            if (!ErrStr.IsEmpty())
                AddLog("Failed to parse Json response: " + ErrStr);
            else
                DisplayJson(RespJson);
        }
        catch (Exception &E) {
            AddLog("Failed to parse Json response: " + E.Message);
        }
    }

    // when sending multiple messages, send the next
    if (FWsLineTot > 0)
        PostMessage(Handle, WM_WSMESS, 0, 0);
}

void __fastcall THttpRestForm::SslWebSocketCli1WSFramesDone(TObject *Sender)
{
    if (SslWebSocketCli1->DebugLevel >= DebugParams)
        AddLog("Websocket Frame Queue Empty");
}

void __fastcall THttpRestForm::SslWebSocketCli1WSFrameSent(TSslWebSocketCli *Sender, TWebSocketOutgoingFrame *&AFrame)
{
    if (!AFrame)
        return;
    LabelWsFrames->Caption = "Frames in/out: " + IntToStr(SslWebSocketCli1->WSFrameCounter);
    if (SslWebSocketCli1->DebugLevel >= DebugParams)
        AddLog("Websocket " + GetWSFrameKind(AFrame->Kind) + " sent");
}

// Returns false if a requested SSL client certificate file could not be loaded.
bool THttpRestForm::CommonWsSettings()
{
    bool Result = true;
    // Delphi keeps the previous client certificate when the new selection is None, empty or
    // fails to load, so the next connection would authenticate as the old identity.
    SslWebSocketCli1->SslCliCert->ClearAll();
    SslWebSocketCli1->DebugLevel = SelectedDebugLevel();
    SslWebSocketCli1->WSMaxDump = 256;   // maximum packet data to log
    SslWebSocketCli1->CertVerMethod = static_cast<TCertVerMethod>(CertVerMethod->ItemIndex);
    SslWebSocketCli1->SslCliSecurity = static_cast<TSslCliSecurity>(SslSecurity->ItemIndex);
    SslWebSocketCli1->SslReportChain = ReportCertChain->Checked;
    SslWebSocketCli1->SslRevocation = CertRevoke->Checked;
    if (SslClientCertLoc->ItemIndex == CliCertLocFile)
        Result = LoadClientCert(SslWebSocketCli1->SslCliCert);
    else if (SslClientCertLoc->ItemIndex >= CliCertLocUser && !SslClientCertFile->Text.IsEmpty()) {
        // Delphi ignores a Windows-store selection here and connects without the certificate.
        AddLog("WebSocket does not support an SSL client certificate from the Windows Store");
        Result = false;
    }
    SslWebSocketCli1->ServerAuth = static_cast<THttpAuthType>(AuthType->ItemIndex);
    SslWebSocketCli1->Username = AuthLogin->Text;
    SslWebSocketCli1->Password = AuthPassword->Text;
    SslWebSocketCli1->AuthBearerToken = AuthBearer->Text;
    SslWebSocketCli1->ProxyURL = ProxyURL->Text;
    SslWebSocketCli1->AlpnProtocols->CommaText = AlpnProtos->Text;
    SslWebSocketCli1->ExtraHeaders = ExtraHeaders->Lines;
    SslWebSocketCli1->SocketFamily = static_cast<TSocketFamily>(IpSockFamily->ItemIndex);
    LabelWSProgress->Caption = "";
    return Result;
}

void __fastcall THttpRestForm::doWSAbortClick(TObject *Sender)
{
    SslWebSocketCli1->Abort();
    SetWsButtons(false);
}

// Connects sync or async; the result is reported by OnWSConnected.
void THttpRestForm::MyWSConnect(bool Async)
{
    if (!CommonWsSettings()) {
        AddLog("WebSocket connection not started");
        return;
    }
    if (!SslWebSocketCli1->IsWSProtocolURL(WebSockUrl->Text)) {
        AddLog("URL must start with WS or WSS protocol");
        return;
    }
    SslWebSocketCli1->WSPingSecs = Icsatoi(WebsocketPingSecs->Text);
    if (SslWebSocketCli1->WSPingSecs == 0)
        SslWebSocketCli1->WSPingSecs = 10;
    SslWebSocketCli1->WSFullHdrs = WebsocketAllHeaders->Checked;
    doWSConnectAsync->Enabled = false;
    doWSConnectSync->Enabled = false;
    doWSAbort->Enabled = true;
    // Delphi ignores the result; some failures fire no OnWSConnected (see the Snippets entry in
    // docs\delphi-demo-bugs.md), which would leave both Connect buttons disabled.
    if (!SslWebSocketCli1->WSConnect(WebSockUrl->Text, Async, "") && !SslWebSocketCli1->IsWSConnected())
        SetWsButtons(false);
}

void __fastcall THttpRestForm::doWSConnectAsyncClick(TObject *Sender)
{
    MyWSConnect(true);
}

void __fastcall THttpRestForm::doWSConnectSyncClick(TObject *Sender)
{
    MyWSConnect(false);
}

void __fastcall THttpRestForm::doWSDisconnectClick(TObject *Sender)
{
    LabelWSProgress->Caption = "Disconnecting";
    if (SslWebSocketCli1->IsWSConnected())
        SslWebSocketCli1->WSClose(wscrNormalClosure, "Closed by user");
    else
        AddLog("Websocket Not Connected");
}

// Sends the message ten times as one binary frame.
void __fastcall THttpRestForm::doWSSendBinClick(TObject *Sender)
{
    std::unique_ptr<TMemoryStream> MyStream(new TMemoryStream);
    const RawByteString Buff = StringToUtf8(WebsockMessage->Text);
    for (int I = 1; I <= 10; I++)
        MyStream->Write(Buff.c_str(), Buff.Length());
    MyStream->Position = 0;
    if (SslWebSocketCli1->IsWSConnected())
        SslWebSocketCli1->WSSendBinaryStream(nullptr, MyStream.get());
    else
        AddLog("Websocket Not Connected");
}

// Sends the next line of the multi-line message, after a reply to the previous one.
void __fastcall THttpRestForm::WMWsMess(TMessage &Msg)
{
    if (FWsLineTot <= 0)
        return;
    // Delphi counts down and so sends the lines last first.
    if (FWsLineNext >= FWsLineTot || FWsLineNext >= WebsockMessLines->Lines->Count ||
            !SslWebSocketCli1->IsWSConnected()) {
        FWsLineTot = 0;
        return;
    }
    SslWebSocketCli1->WSSendText(nullptr, WebsockMessLines->Lines->Strings[FWsLineNext++]);
}

// Sends the lines as one message, or as separate messages, each after the reply to the last.
void __fastcall THttpRestForm::doWSSendLinesClick(TObject *Sender)
{
    if (WebsockMessLines->Lines->Count == 0)
        return;
    if (!SslWebSocketCli1->IsWSConnected()) {
        AddLog("Websocket Not Connected");
        return;
    }
    if (WebSocketMLineMReq->Checked) {
        FWsLineNext = 0;
        FWsLineTot = WebsockMessLines->Lines->Count;
        AddLog("Websocket Sending " + IntToStr(FWsLineTot) + " Separate Messages");
        PostMessage(Handle, WM_WSMESS, 0, 0);
    }
    else {
        AddLog("Websocket Sending Single " + IntToStr(WebsockMessLines->Lines->Count) + " Line Message");
        SslWebSocketCli1->WSSendText(nullptr, WebsockMessLines->Text);
    }
}

// Sends the message repeated 1,000 times as one text frame.
void __fastcall THttpRestForm::doWSSendLongClick(TObject *Sender)
{
    String LongText;
    for (int I = 1; I <= 1000; I++)
        LongText += WebsockMessage->Text;
    if (SslWebSocketCli1->IsWSConnected())
        SslWebSocketCli1->WSSendText(nullptr, LongText);
    else
        AddLog("Websocket Not Connected");
}

void __fastcall THttpRestForm::doWSSendTextClick(TObject *Sender)
{
    if (SslWebSocketCli1->IsWSConnected())
        SslWebSocketCli1->WSSendText(nullptr, WebsockMessage->Text);
    else
        AddLog("Websocket Not Connected");
}
