﻿/*
  ICS Network Tools demo - C++Builder translation of the Delphi demo OverbyteIcsNetTools1.pas
  (demos-delphi-vcl), ICS V9.8.

  Demonstrates the IP Helper functions in OverbyteIcsIpHlpApi (connections, adaptors, interfaces,
  IP addresses, routing, neighbours/ARP, statistics, IP address change monitoring, neighbourhood
  device scanning) plus Whois (TIcsWhoisCli), Ping and Trace Route (TPing, TPingThread), DNS
  queries over UDP/TCP/HTTPS (TDnsQuery, TDnsQueryHttps), forward/reverse/bulk lookups through
  TIcsDomainNameCache, and country/ASN lookups with TIcsGeoTools. Results go to list views and
  memos; log lines are buffered and shown once a second in the log window (OverbyteIcsNetTools2).
  Settings are kept in the ICS ini file, with the same keys as the Delphi build.

  How this C++ version differs from the Delphi original, apart from syntax:
  - The unit-level variables are form members; the domain name cache and the geo tools are created
    with no Owner and held in std::unique_ptr.
  - The USE_IcsGeoTools blocks are always compiled: TIcsGeoTools (IcsGeoUtils) is in the ICS C++
    VCL package, and the country/ASN databases are linked as resources, as in the Delphi.
  - The form owns its ping threads (no FreeOnTerminate) and its destructor waits for them, so no
    OnTerminate can reach a destroyed form. Unfinished pings are not sent WM_QUIT: TPingThread
    blocks in TICMP.Ping and never reads messages. Each ping run has a number (PingThreadNum),
    and results from an earlier run are ignored.
  - The ping and trace loops wait for a message instead of spinning, and stop when the
    application is closing. A ping or trace run cannot start while one is running.
  - Trace route counts finished pings and unanswered name lookups separately (the Delphi uses one
    counter that a cached lookup never decrements).
  - The drop-down lists are filled before the ini file is read, so the saved selections are
    restored (the Delphi fills them afterwards and resets them).
  - OverbyteIcsIpHlpApi.hpp declares the Winsock AF_ address family constants again (the .pas
    does not mark them EXTERNALSYM), which collides with the ws2def.h macros; the header hides
    those macros around that include and restores them after it (same values).
  - Delphi demo bugs are fixed here; each fix has a one-line comment.
*/

#include <vcl.h>
#pragma hdrstop

#include "OverbyteIcsNetTools1.h"
#include "OverbyteIcsNetTools2.h"

#include <algorithm>
#include <functional>

#pragma package(smart_init)
#pragma link "OverbyteIcsDnsHttps"
#pragma link "OverbyteIcsDnsQuery"
#pragma link "OverbyteIcsIpHlpApi"
#pragma link "OverbyteIcsPing"
#pragma link "OverbyteIcsSslBase"
#pragma link "OverbyteIcsWhoisCli"
#pragma resource "*.dfm"
// MAC vendor and port name lists, and the country and ASN databases, linked into the exe
// (found through ..\Source on the library path), so no data files need to be distributed.
#pragma resource "nmap-mac-prefixes.RES"
#pragma resource "ICSPortList.RES"
#pragma resource "dbip-country-lite.RES"
#pragma resource "dbip-asn-lite.RES"

TToolsForm *ToolsForm;

namespace {

const String SectionMainWindow = "MainWindow";
const String KeyTop            = "Top";
const String KeyLeft           = "Left";
const String KeyWidth          = "Width";
const String KeyHeight         = "Height";
const String SectionData       = "Data";

const String MaskResponse3 = "%2s  %5s  %-24s  %-20s %-35s %s";   // trace route, six columns

// Runs a function when the scope ends, the C++ form of a Delphi try/finally.
class TOnScopeExit
{
public:
    explicit TOnScopeExit(std::function<void()> Action) : FAction(std::move(Action)) {}
    ~TOnScopeExit() { FAction(); }
    TOnScopeExit(const TOnScopeExit &) = delete;
    TOnScopeExit &operator=(const TOnScopeExit &) = delete;

private:
    std::function<void()> FAction;
};

// Text for a value from one of the ICS name tables; the number itself when the table has no entry.
template <typename TTable>
String TableText(TTable &Table, __int64 Index)
{
    if (Index < 0 || Index >= static_cast<__int64>(Table.size()))
        return IntToStr(Index);
    return String(Table[static_cast<int>(Index)]);
}

String FirstOrEmpty(const DynamicArray<String> &List)
{
    return List.Length > 0 ? List[0] : String();
}

// Columns of a list view from an ICS header and width table.
template <typename THdrs, typename TWids>
void SetColumns(TListView *View, THdrs &Hdrs, TWids &Wids)
{
    View->Columns->Clear();
    for (size_t i = 0; i < Hdrs.size() && i < Wids.size(); i++) {
        TListColumn *Column = View->Columns->Add();
        Column->Caption = Hdrs[i];
        Column->Width = Wids[i];
    }
}

// Keeps a combo box drop-down as a most-recent-first list.
void AddRecentSList(TStrings *SList, const String &S, int Max)
{
    if (SList->IndexOf(S) < 0) {
        if (SList->Count > Max)
            SList->Delete(SList->Count - 1);
        SList->Insert(0, S);
    }
}

void DelRecentSList(TComboBox *SList)
{
    const int I = SList->Items->IndexOf(SList->Text);
    if (I >= 0) {
        SList->Items->Delete(I);
        if (I == 0)
            SList->ItemIndex = 0;
    }
}

String CleanDomain(const String &Domain)
{
    String Result = Trim(Domain);
    // SubString to the end: the Delphi copies at most 99 characters, which cuts long names
    if (LowerCase(Result).Pos("http://") == 1)
        Result = Result.SubString(8, Result.Length());
    if (LowerCase(Result).Pos("https://") == 1)
        Result = Result.SubString(9, Result.Length());
    // Delphi demo bug fix: the Delphi reads the last character of an empty string
    if (!Result.IsEmpty() && Result[Result.Length()] == L'/')
        Result.SetLength(Result.Length() - 1);
    return Result;
}

String BoolText(bool Value)
{
    return Value ? "True" : "False";
}

int ItemIndexOrZero(int Index)
{
    return Index < 0 ? 0 : Index;
}

// Strings for the bulk lookup memo; Unicode names need a 16-bit wchar_t.
const wchar_t *const DefaultBulkNames[] = {
    L"www.overbyte.eu",
    L"svn.overbyte.be",
    L"wiki.overbyte.eu",
    L"mail.magsys.co.uk",
    L"ipv4.magsys.co.uk",
    L"ipv6.magsys.co.uk",
    L"www.cloudflare.com",
    L"cloudflare-ech.com",
    L"www.microsoft.com",
    L"ipv6.google.com",
    L"www.google.com",
    L"sourceforge.net",
    L"msdn.microsoft.com",
    L"localhost",
    L"127.0.0.1",
    L"::1",
    L"strøm.no",
    L"prøve.ftptest.co.uk",
    L"テスト.ftptest.co.uk",
    L"ölçek.ftptest.co.uk",
    L"www.bad(name).com",
    L"www.-badname.com",
};

static_assert(sizeof(wchar_t) == sizeof(WideChar), "UnicodeString literals need a 16-bit wchar_t");

}  // namespace

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

// Done here, not in OnDestroy: in C++ OnDestroy runs from the VCL base destruction, after the
// members are gone. FClosing makes the thread and lookup handlers return at once; clearing
// FThreads deletes each ping thread, and ~TThread waits for it (running CheckSynchronize, so a
// pending OnTerminate cannot deadlock). Trade-off: closing can stall for up to the ping timeout.
// DestroyComponents frees the ICS components while the members their events use still exist.
__fastcall TToolsForm::~TToolsForm()
{
    FClosing = true;
    FThreads.clear();
    DestroyComponents();
    FNameCache.reset();
    FGeoTools.reset();
}

void TToolsForm::AddLogLine(const String &S)
{
    FBuffLogLines += S + IcsCRLF;
}

void TToolsForm::AddLogLines(TStrings *SL)
{
    FBuffLogLines += SL->Text;
}

void __fastcall TToolsForm::IcsAppLog(TObject *Sender, const System::UnicodeString Msg)
{
    AddLogLine(Msg);
}

// Country and ASN for an IP address, called by the domain name cache when a lookup asks for it.
void __fastcall TToolsForm::IcsGeoEvent(TObject *Sender, const System::UnicodeString IpStr,
                                        System::UnicodeString &Geo)
{
    __int64 AsnNum = 0;
    const String ISO2A = FGeoTools->FindISOA2Code(IpStr);
    const String CountryName = FGeoTools->FindCountry(ISO2A);
    const String AsnName = FGeoTools->FindASNCode(IpStr, AsnNum);
    Geo = ISO2A;
    if (!CountryName.IsEmpty())
        Geo = CountryName;
    if (!AsnName.IsEmpty())
        Geo = Geo + ", " + AsnName;
}

void __fastcall TToolsForm::BulkNamesMemoDblClick(TObject *Sender)
{
    BulkNamesMemo->Lines->Clear();
}

void __fastcall TToolsForm::CommonTimerTimer(TObject *Sender)
{
    // Shows the buffered log lines. Taken out of the buffer first, so lines logged while the log
    // window opens are kept for the next tick. The Delphi hides any exception here; this does not.
    if (!FBuffLogLines.IsEmpty()) {
        String Lines = FBuffLogLines;
        FBuffLogLines = "";
        Lines.SetLength(Lines.Length() - 2);   // remove CRLF
        if (!FormLog->Visible)
            FormLog->Show();
        FormLog->Log->Lines->Add(Lines);
        SendMessage(FormLog->Log->Handle, EM_LINESCROLL, 0, 999999);
    }

    // update IP connections list
    if (IcsTestTrgTick64(FIpConnUpdateTrg)) {
        FIpConnUpdateTrg = Trigger64Disabled;
        doIpConnUpdateClick(this);
        // Delphi demo bug fix: the Delphi re-arms even when Stop was clicked during the update
        if (doIpConnStop->Enabled)
            FIpConnUpdateTrg = IcsGetTrgSecs64(Icsatoi(IpConnsSecs->Text));
    }

    // DNS lookup, timeout
    if (IcsTestTrgTick64(FNameServerTrg)) {
        FNameServerTrg = Trigger64Disabled;
        if (NameDnsOpt->ItemIndex == 3)
            IcsDnsQueryHttps1->AbortQuery();
        else
            IcsDnsQuery1->AbortQuery();
        SetDnsButtons(true);
    }
}

void __fastcall TToolsForm::FormCreate(TObject *Sender)
{
    Caption = Caption + IcsVerLitSample;   // common ICS sample caption: version and date
    FWorkStrings.reset(new TStringList());
    FIpConnUpdateTrg = Trigger64Disabled;
    FNameServerTrg = Trigger64Disabled;

    // TIcsDomainNameCache has no HTTPS lookups (only the derived TIcsDomNameCacheHttps has), so the
    // HTTPS method in the cache settings fails, as in the Delphi. For DNS cache debug logging,
    // point OnAppLogEvent at IcsAppLog (the Delphi assigns an empty handler).
    FNameCache.reset(new TIcsDomainNameCache(nullptr));
    FNameCache->OnGeoEvent = IcsGeoEvent;
    FNameCache->DNMethod = MethodWinsock;

    FGeoTools.reset(new TIcsGeoTools(nullptr));
    FGeoTools->OnAppLog = IcsAppLog;
}

void TToolsForm::LoadGeoDatabases()
{
    if (FGeoTools->IsLoaded())
        return;
    FGeoTools->UseResources = true;
    FGeoTools->LoadDBCountry();
    FGeoTools->LoadDBASN();
    FGeoTools->LoadIcsCountries();
}

// set local IP lists from winsock, hopefully first IPv4 is the one we need
void TToolsForm::SetLocalIPs()
{
    LocalIPv4->Items->Assign(LocalIPList(sfIPv4, IPPROTO_TCP));
    if (LocalIPv4->Text.IsEmpty())
        LocalIPv4->ItemIndex = 0;
    LocalIPv6->Items->Assign(LocalIPList(sfIPv6, IPPROTO_TCP));
    if (LocalIPv6->Text.IsEmpty()) {
        for (int i = 0; i < LocalIPv6->Items->Count; i++) {   // look for first public IPv6 with 2xxx
            if (LocalIPv6->Items->Strings[i].Pos("2") == 1) {
                LocalIPv6->ItemIndex = i;
                break;
            }
        }
    }
}

void __fastcall TToolsForm::FormShow(TObject *Sender)
{
    if (FInitialized)
        return;
    FInitialized = true;

    for (size_t i = 0; i < SocketFamilyNames.size(); i++)
        IpFamily->Items->Add(String(SocketFamilyNames[i]));
    IpFamily->ItemIndex = 0;

    // build drop down list of Whois servers, not needed if only automatic queries are being done
    if (IcsWhoisCli1->WhoisServers->Count > 0) {
        WhoisServer->Items->Clear();
        for (int i = 0; i < IcsWhoisCli1->WhoisServers->Count; i++) {
            const String Entry = IcsWhoisCli1->WhoisServers->Strings[i];
            const int Space = Entry.Pos(IcsSPACE);   // 1-based, 0 when absent
            if (Space == 0)
                WhoisServer->Items->Add(Entry);
            else {
                String Kind = Entry.SubString(1, Space);   // suffix including the trailing space
                if (Kind == "00 ")
                    Kind = "IP Addresses]";
                else if (Kind == "?? ")
                    Kind = "Top Level Domains]";
                else
                    Kind = Kind + "Domains]";
                WhoisServer->Items->Add(Entry.SubString(Space + 1, Entry.Length()) + " [" + Kind);
            }
        }
        WhoisServer->ItemIndex = 0;
    }

    // fill Dns Query drop down
    NameQueryType->Items->Clear();
    for (size_t i = 0; i < DnsReqTable.size(); i++)
        NameQueryType->Items->Add(DnsReqTable[i].Asc + " [" + DnsReqTable[i].Desc + "]");
    NameQueryType->ItemIndex = 0;

    // fill DNS server drop down
    NameServerPublic->Items->Clear();
    for (size_t i = 0; i < DnsPublicServerTable.size(); i++)
        NameServerPublic->Items->Add(DnsPublicServerTable[i]);
    NameServerPublic->ItemIndex = 0;

    // fill DOH server drop down edit
    NameServerHttps->Items->Clear();
    for (size_t i = 0; i < DnsPublicHttpsTable.size(); i++)
        NameServerHttps->Items->Add(DnsPublicHttpsTable[i]);
    if (NameServerHttps->Text.IsEmpty())
        NameServerHttps->Text = NameServerHttps->Items->Strings[0];

    // The second constructor argument (PreserveAnsi) has a Delphi default that the header drops.
    std::unique_ptr<TIcsIniFile> IniFile(new TIcsIniFile(GetIcsIniFileName(), 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);

    // Reads "True"/"False" text, as the Delphi writes it.
    auto ReadChecked = [&IniFile](const String &Key, const String &Default) {
        return IniFile->ReadString(SectionData, Key, Default) == "True";
    };
    const String Section = SectionData;
    DNCacheLAN->ItemIndex = IniFile->ReadInteger(Section, "DNCacheLAN_ItemIndex", DNCacheLAN->ItemIndex);
    DNCacheLog->Checked = ReadChecked("DNCacheLog_Checked", "False");
    DNCacheMax->Text = IniFile->ReadString(Section, "DNCacheMax_Text", DNCacheMax->Text);
    DNCacheMethod->ItemIndex = IniFile->ReadInteger(Section, "DNCacheMethod_ItemIndex", DNCacheMethod->ItemIndex);
    IpConnType->ItemIndex = IniFile->ReadInteger(Section, "IpConnType_ItemIndex", IpConnType->ItemIndex);
    IpConnsSecs->Text = IniFile->ReadString(Section, "IpConnsSecs_Text", IpConnsSecs->Text);
    IpFamily->ItemIndex = IniFile->ReadInteger(Section, "IpFamily_ItemIndex", IpFamily->ItemIndex);
    LocalIPv4->Text = IniFile->ReadString(Section, "LocalIPv4_Text", LocalIPv4->Text);
    LocalIPv6->Text = IniFile->ReadString(Section, "LocalIPv6_Text", LocalIPv6->Text);
    LookupHost->Text = IniFile->ReadString(Section, "LookupHost_Text", LookupHost->Text);
    LookupHost->Items->CommaText = IniFile->ReadString(Section, "LookupHost_Items", LookupHost->Items->CommaText);
    MonIpAddrChanges->Checked = ReadChecked("MonIpAddrChanges_Checked", "True");
    NameDnsOpt->ItemIndex = IniFile->ReadInteger(Section, "NameDnsOpt_ItemIndex", NameDnsOpt->ItemIndex);
    NameHostName->Text = IniFile->ReadString(Section, "NameHostName_Text", NameHostName->Text);
    NameHostName->Items->CommaText = IniFile->ReadString(Section, "NameHostName_Items", NameHostName->Items->CommaText);
    NameQueryType->ItemIndex = IniFile->ReadInteger(Section, "NameQueryType_ItemIndex", NameQueryType->ItemIndex);
    NameProtoTCP->Checked = ReadChecked("NameProtoTCP_Checked", "False");
    NameServerIP->Text = IniFile->ReadString(Section, "NameServerIP_Text", NameServerIP->Text);
    NameServerIP->Items->CommaText = IniFile->ReadString(Section, "NameServerIP_Items", NameServerIP->Items->CommaText);
    NameServerPublic->ItemIndex = IniFile->ReadInteger(Section, "NameServerPublic_ItemIndex", NameServerPublic->ItemIndex);
    NeigbDevCache->Text = IniFile->ReadString(Section, "NeigbDevCache_Text", NeigbDevCache->Text);
    NeigbDevIPv4->Text = IniFile->ReadString(Section, "NeigbDevIPv4_Text", NeigbDevIPv4->Text);
    NeigbDevIPv6->Text = IniFile->ReadString(Section, "NeigbDevIPv6_Text", NeigbDevIPv6->Text);
    NeigbDevScan->Text = IniFile->ReadString(Section, "NeigbDevScan_Text", NeigbDevScan->Text);
    NeigbDevTot->Text = IniFile->ReadString(Section, "NeigbDevTot_Text", NeigbDevTot->Text);
    NeighbPermAddr->Checked = ReadChecked("NeighbPermAddr_Checked", "False");
    PingCount->Text = IniFile->ReadString(Section, "PingCount_Text", PingCount->Text);
    PingLog->Checked = ReadChecked("PingLog_Checked", "False");
    PingQueryName->Text = IniFile->ReadString(Section, "PingQueryName_Text", PingQueryName->Text);
    PingQueryName->Items->CommaText = IniFile->ReadString(Section, "PingQueryName_Items", PingQueryName->Items->CommaText);
    PingTimeoutMs->Text = IniFile->ReadString(Section, "PingTimeoutMs_Text", PingTimeoutMs->Text);
    ResolveIpRem->Text = IniFile->ReadString(Section, "ResolveIpRem_Text", ResolveIpRem->Text);
    RevExtra->Text = IniFile->ReadString(Section, "RevExtra_Text", RevExtra->Text);
    RevLookAddr->Text = IniFile->ReadString(Section, "RevLookAddr_Text", RevLookAddr->Text);
    RevLookAddr->Items->CommaText = IniFile->ReadString(Section, "RevLookAddr_Items", RevLookAddr->Items->CommaText);
    TraceErrors->Text = IniFile->ReadString(Section, "TraceErrors_Text", TraceErrors->Text);
    WhoisQueryName->Text = IniFile->ReadString(Section, "WhoisQueryName_Text", WhoisQueryName->Text);
    WhoisQueryName->Items->CommaText = IniFile->ReadString(Section, "WhoisQueryName_Items", WhoisQueryName->Items->CommaText);
    WhoisServer->Text = IniFile->ReadString(Section, "WhoisServer_Text", WhoisServer->Text);
    WhoisServer->Items->CommaText = IniFile->ReadString(Section, "WhoisServer_Items", WhoisServer->Items->CommaText);
    IniFile.reset();

    // a saved index past the end of a list leaves no selection; the code indexes tables with these
    IpFamily->ItemIndex = ItemIndexOrZero(IpFamily->ItemIndex);
    NameQueryType->ItemIndex = ItemIndexOrZero(NameQueryType->ItemIndex);
    NameServerPublic->ItemIndex = ItemIndexOrZero(NameServerPublic->ItemIndex);

    SetLocalIPs();

    // prepare TListView columns from constants in OverbyteIcsIpHlpApi
    SetColumns(NeigbDevListV, NeighbDevRowHdrs, NeighbDevRowWid);
    SetColumns(IpConnsListV, IpConnsRowHdrs, IpConnsRowWid);
    SetColumns(AdaptorListV, IpAdptRowHdrs, IpAdptRowWid);
    SetColumns(InterfacesListV, IpIfaceRowHdrs, IpIfaceRowWid);
    SetColumns(IpAddressListV, IpAddrRowHdrs, IpAddrRowWid);
    SetColumns(IpPathsListV, IpPathRowHdrs, IpPathRowWid);
    SetColumns(IpRoutingListV, IpRoutingRowHdrs, IpRoutingRowWid);
    SetColumns(NeighbListV, NeighbRowHdrs, NeighbRowWid);

    // get Network Params, DNS servers in particular, ignore error, Auto DNS drop down
    IpHlpNetworkParams(FNetworkParams);
    if (FNetworkParams.DnsServerTot > 0) {
        // update the domain name cache server list and NameServerAuto drop down list
        IpHlpGetDnsServers(FNameCache->DnsServerList);
        NameServerAuto->Items->Assign(FNameCache->DnsServerList);
        NameServerAuto->ItemIndex = 0;
        FNameCache->DnsServerStrat = SrvStratList;
    }

    // the names are not saved, so the defaults are loaded on every start
    if (BulkNamesMemo->Lines->Count == 0) {
        for (const wchar_t *Name : DefaultBulkNames)
            BulkNamesMemo->Lines->Add(Name);
    }

    PageControl1->ActivePage = TabConnections;

    // start update timer
    FIpConnUpdateTrg = Trigger64Disabled;
    FNameServerTrg = Trigger64Disabled;
    CommonTimer->Interval = 1000;
    CommonTimer->Enabled = true;
    MonIpAddrChangesClick(this);   // start IP address change monitor
}

void __fastcall TToolsForm::FormClose(TObject *Sender, TCloseAction &Action)
{
    CommonTimer->Enabled = false;
    if (IcsNeighbDevices1->Active)
        IcsNeighbDevices1->StopMonitor();
    if (FormLog->Visible)
        FormLog->FormClose(this, Action);

    std::unique_ptr<TIcsIniFile> IniFile(new TIcsIniFile(GetIcsIniFileName(), true));
    IniFile->WriteInteger(SectionMainWindow, KeyTop, Top);
    IniFile->WriteInteger(SectionMainWindow, KeyLeft, Left);
    IniFile->WriteInteger(SectionMainWindow, KeyWidth, Width);
    IniFile->WriteInteger(SectionMainWindow, KeyHeight, Height);
    const String Section = SectionData;
    IniFile->WriteInteger(Section, "DNCacheLAN_ItemIndex", DNCacheLAN->ItemIndex);
    IniFile->WriteString(Section, "DNCacheLog_Checked", BoolText(DNCacheLog->Checked));
    IniFile->WriteString(Section, "DNCacheMax_Text", DNCacheMax->Text);
    IniFile->WriteInteger(Section, "DNCacheMethod_ItemIndex", DNCacheMethod->ItemIndex);
    IniFile->WriteInteger(Section, "IpConnType_ItemIndex", IpConnType->ItemIndex);
    IniFile->WriteString(Section, "IpConnsSecs_Text", IpConnsSecs->Text);
    IniFile->WriteInteger(Section, "IpFamily_ItemIndex", IpFamily->ItemIndex);
    IniFile->WriteString(Section, "LocalIPv4_Text", LocalIPv4->Text);
    IniFile->WriteString(Section, "LocalIPv6_Text", LocalIPv6->Text);
    IniFile->WriteString(Section, "LookupHost_Text", LookupHost->Text);
    IniFile->WriteString(Section, "LookupHost_Items", LookupHost->Items->CommaText);
    IniFile->WriteString(Section, "MonIpAddrChanges_Checked", BoolText(MonIpAddrChanges->Checked));
    IniFile->WriteInteger(Section, "NameDnsOpt_ItemIndex", NameDnsOpt->ItemIndex);
    IniFile->WriteString(Section, "NameHostName_Text", NameHostName->Text);
    IniFile->WriteString(Section, "NameHostName_Items", NameHostName->Items->CommaText);
    IniFile->WriteInteger(Section, "NameQueryType_ItemIndex", NameQueryType->ItemIndex);
    IniFile->WriteString(Section, "NameProtoTCP_Checked", BoolText(NameProtoTCP->Checked));
    IniFile->WriteString(Section, "NameServerIP_Text", NameServerIP->Text);
    IniFile->WriteString(Section, "NameServerIP_Items", NameServerIP->Items->CommaText);
    IniFile->WriteInteger(Section, "NameServerPublic_ItemIndex", NameServerPublic->ItemIndex);
    IniFile->WriteString(Section, "NeigbDevCache_Text", NeigbDevCache->Text);
    IniFile->WriteString(Section, "NeigbDevIPv4_Text", NeigbDevIPv4->Text);
    IniFile->WriteString(Section, "NeigbDevIPv6_Text", NeigbDevIPv6->Text);
    IniFile->WriteString(Section, "NeigbDevScan_Text", NeigbDevScan->Text);
    IniFile->WriteString(Section, "NeigbDevTot_Text", NeigbDevTot->Text);
    IniFile->WriteString(Section, "NeighbPermAddr_Checked", BoolText(NeighbPermAddr->Checked));
    IniFile->WriteString(Section, "PingCount_Text", PingCount->Text);
    IniFile->WriteString(Section, "PingLog_Checked", BoolText(PingLog->Checked));
    IniFile->WriteString(Section, "PingQueryName_Text", PingQueryName->Text);
    IniFile->WriteString(Section, "PingQueryName_Items", PingQueryName->Items->CommaText);
    IniFile->WriteString(Section, "PingTimeoutMs_Text", PingTimeoutMs->Text);
    IniFile->WriteString(Section, "ResolveIpRem_Text", ResolveIpRem->Text);
    IniFile->WriteString(Section, "RevExtra_Text", RevExtra->Text);
    IniFile->WriteString(Section, "RevLookAddr_Text", RevLookAddr->Text);
    IniFile->WriteString(Section, "RevLookAddr_Items", RevLookAddr->Items->CommaText);
    IniFile->WriteString(Section, "TraceErrors_Text", TraceErrors->Text);
    IniFile->WriteString(Section, "WhoisQueryName_Text", WhoisQueryName->Text);
    IniFile->WriteString(Section, "WhoisQueryName_Items", WhoisQueryName->Items->CommaText);
    IniFile->WriteString(Section, "WhoisServer_Text", WhoisServer->Text);
    IniFile->WriteString(Section, "WhoisServer_Items", WhoisServer->Items->CommaText);
    IniFile->UpdateFile();
}

void __fastcall TToolsForm::OnNeigbDevLogEvent(TObject *Sender, const System::UnicodeString Msg)
{
    AddLogLine(Msg);
}

void __fastcall TToolsForm::OnNeigbDevDevUpd(TObject *Sender)
{
    if (FClosing)
        return;
    NeigbDevListV->Items->Count = IcsNeighbDevices1->IpTot;
    NeigbDevListV->Refresh();
}

// virtual ListView data event, get and display a row of data
void __fastcall TToolsForm::NeigbDevListVData(TObject *Sender, TListItem *Item)
{
    const int ItemNr = Item->Index;   // line for which data is required
    if (ItemNr >= IcsNeighbDevices1->IpTot)
        return;
    // IpRow has a Pascal var index: it returns the device index and sets AddrNr to the address entry.
    int AddrNr = 0;
    const int Idx = IcsNeighbDevices1->IpRow[ItemNr][AddrNr];
    if (Idx < 0 || Idx >= IcsNeighbDevices1->MacTot)
        return;
    TNeighbDevices Devices = IcsNeighbDevices1->NeighbDevices;   // shares the component's array
    const TNeighbDevice &Device = Devices[Idx];
    if (AddrNr < 0 || AddrNr >= Device.TotAddrNr)
        AddrNr = 0;
    String IpAddr, HostName;
    if (AddrNr < Device.AddrList.Length) {
        IpAddr = Device.AddrList[AddrNr].IpAddr;
        HostName = Device.AddrList[AddrNr].HostName;
    }
    Item->Caption = IpAddr;
    Item->SubItems->Add(HostName);
    Item->SubItems->Add(Device.MacAddress);
    Item->SubItems->Add(Device.MacVendor);
    Item->SubItems->Add(Device.StateDesc);
    Item->SubItems->Add(DateTimeToStr(Device.FirstDT));
    Item->SubItems->Add(DateTimeToStr(Device.LastDT));
    Item->SubItems->Add(IntToStr(Device.Detections));
}

void __fastcall TToolsForm::doNeigbDevStartClick(TObject *Sender)
{
    if (FLocalIpLuid.Value == 0)
        FLocalIpLuid = IpHlpAdpforIP(LocalIPv4->Text);   // assume IPv6 same interface
    if (FLocalIpLuid.Value == 0)
        return;
    doNeigbDevStart->Enabled = false;
    doNeigbDevStop->Enabled = true;
    IcsNeighbDevices1->LocalIps = true;
    IcsNeighbDevices1->InterfaceLuid = FLocalIpLuid;
    IcsNeighbDevices1->LocIpv4 = LocalIPv4->Text;
    IcsNeighbDevices1->LocIpv6 = LocalIPv6->Text;
    IcsNeighbDevices1->ScanStartIPv4 = NeigbDevIPv4->Text;
    IcsNeighbDevices1->ScanStartIPv6 = NeigbDevIPv6->Text;   // Delphi demo bug fix: the Delphi passes the IPv4 start address
    IcsNeighbDevices1->ScanTotIps = Icsatoi(NeigbDevTot->Text);
    IcsNeighbDevices1->UpdCacheSecs = Icsatoi(NeigbDevCache->Text);
    IcsNeighbDevices1->UpdScanSecs = Icsatoi(NeigbDevScan->Text);
    IcsNeighbDevices1->OnDevUpd = OnNeigbDevDevUpd;
    IcsNeighbDevices1->OnLogEvent = OnNeigbDevLogEvent;
    IcsNeighbDevices1->StartMonitor();
}

void __fastcall TToolsForm::doNeigbDevStopClick(TObject *Sender)
{
    doNeigbDevStart->Enabled = true;
    doNeigbDevStop->Enabled = false;
    IcsNeighbDevices1->StopMonitor();
}

void __fastcall TToolsForm::doDeviceEmptyCacheClick(TObject *Sender)
{
    if (!IcsIsProgAdmin()) {
        AddLogLine("This function requires program administrator rights");
        return;
    }
    IcsNeighbDevices1->EmptyCache();
    AddLogLine("Neighbour Cache Emptied");
}

void __fastcall TToolsForm::doDeviceLogIpClick(TObject *Sender)
{
    IcsNeighbDevices1->DispIpDevs(FWorkStrings.get());
    AddLogLines(FWorkStrings.get());
}

void __fastcall TToolsForm::doDeviceLogMacClick(TObject *Sender)
{
    IcsNeighbDevices1->DispMacDevs(FWorkStrings.get());
    AddLogLines(FWorkStrings.get());
}

void __fastcall TToolsForm::doDevicesClearClick(TObject *Sender)
{
    IcsNeighbDevices1->Clear();
    AddLogLine("Neighbour Devices Cleared");
}

void __fastcall TToolsForm::doExitClick(TObject *Sender)
{
    Close();
}

TSocketFamily TToolsForm::SelectedFamily()
{
    int Index = IpFamily->ItemIndex;
    if (Index < 0 || static_cast<size_t>(Index) >= SocketFamilyNames.size())
        Index = 0;
    return static_cast<TSocketFamily>(Index);   // the list holds SocketFamilyNames in enum order
}

int TToolsForm::GetIpFamilyAF()
{
    return WSocketFamilyToAF(SelectedFamily());
}

void __fastcall TToolsForm::doIpConnStartClick(TObject *Sender)
{
    if (Icsatoi(IpConnsSecs->Text) < 2)
        return;   // interval sanity check
    doIpConnStart->Enabled = false;
    doIpConnStop->Enabled = true;
    FIpConnUpdateTrg = Trigger64Immediate;
}

void __fastcall TToolsForm::doIpConnStopClick(TObject *Sender)
{
    FIpConnUpdateTrg = Trigger64Disabled;
    doIpConnStop->Enabled = false;
    doIpConnStart->Enabled = true;
}

// IP connections, report reverse lookup of remote IP address
void __fastcall TToolsForm::IpConnNameUpdateEvent(TObject *Sender, int ItemNr)
{
    if (FClosing)
        return;
    const int Row = FNameCache->GetDNItem(ItemNr).ReqTag;   // GetDNItem returns the record by value
    if (Row < 0 || Row >= FIpConnRows.Length)
        return;
    FIpConnRows[Row].RemoteHost = FNameCache->BuildRespList(ItemNr);
}

void __fastcall TToolsForm::doIpConnUpdateClick(TObject *Sender)
{
    // C++ only: the loop below processes messages, so a click or timer tick could start a second
    // update that replaces FIpConnRows while this one is still reading it.
    if (FIpConnUpdating)
        return;
    FIpConnUpdating = true;
    TOnScopeExit Done([this] { FIpConnUpdating = false; });

    const int ErrorCode = IpHlpConnsTable(FIpConnRows, GetIpFamilyAF(),
                                          static_cast<TTransProt>(ItemIndexOrZero(IpConnType->ItemIndex)));
    if (ErrorCode != NO_ERROR) {
        AddLogLine(SysErrorMessage(ErrorCode));
        return;
    }
    const int Tot = FIpConnRows.Length;
    if (Tot == 0)
        return;
    for (int i = 0; i < Tot && i < FIpConnRows.Length; i++) {
        const String RemoteAddr = FIpConnRows[i].RemoteAddr;
        if (!RemoteAddr.IsEmpty() && RemoteAddr != "0.0.0.0") {
            // if not cached, the event is called to complete RemoteHost
            const String Host = FNameCache->LookupIPOne(RemoteAddr, i, sfAny, IpConnNameUpdateEvent);
            FIpConnRows[i].RemoteHost = Host;
            Application->ProcessMessages();
            if (FClosing || Application->Terminated)
                return;
        }
    }
    IpConnsListV->Items->Count = Tot;
    IpConnsListV->Refresh();
}

void __fastcall TToolsForm::IpConnsListVData(TObject *Sender, TListItem *Item)
{
    const int ItemNr = Item->Index;   // line for which data is required
    if (ItemNr >= FIpConnRows.Length)
        return;
    const TConnInfo &Row = FIpConnRows[ItemNr];
    String DispTime;
    if (Row.CreateDT.Val > 0)
        DispTime = DateTimeToStr(Row.CreateDT);
    Item->Caption = Row.Protocol;
    Item->SubItems->Add(Row.LocalAddr + ":" + LowerCase(Row.LocalPortDesc));
    if (Row.State >= 0) {   // TCP
        if (Row.State != MIB_TCP_STATE_LISTEN) {
            Item->SubItems->Add(Row.RemoteAddr + ":" + LowerCase(Row.RemotePortDesc));
            Item->SubItems->Add(Row.RemoteHost);
        }
        else {
            Item->SubItems->Add("");
            Item->SubItems->Add("");
        }
        Item->SubItems->Add(TableText(TCPConnState, Row.State));
    }
    else {
        Item->SubItems->Add("");
        Item->SubItems->Add("");
        Item->SubItems->Add("");
    }
    Item->SubItems->Add(IntToStr(static_cast<__int64>(Row.ProcessID)) + ":" + ExtractFileName(String(Row.ProcName)));
    Item->SubItems->Add(DispTime);
}

// event called when TIcsIpChanges component detects IP addresses added or deleted
void __fastcall TToolsForm::IcsIpChanges1IpChangesEvent(const TIpAddrInfo &IpAddrInfo,
                                                       TMibNoticationType NotificationType)
{
    if (FClosing)
        return;
    if (NotificationType == MibAddInstance) {
        AddLogLine("New IP address added: " + IpAddrInfo.IpAddress);
        doUpdIpAddrClick(this);
        SetLocalIPs();
    }
    else if (NotificationType == MibDeleteInstance) {
        AddLogLine("Old IP address removed: " + IpAddrInfo.IpAddress);
        doUpdIpAddrClick(this);
        SetLocalIPs();
    }
}

// start TIcsIpChanges component to detect IP addresses added or deleted
void __fastcall TToolsForm::MonIpAddrChangesClick(TObject *Sender)
{
    if (MonIpAddrChanges->Checked) {
        IcsIpChanges1->StartMonitor();
        AddLogLine("Started Monitoring IP Address Changes");
    }
    else {
        IcsIpChanges1->StopMonitor();
        AddLogLine("Stopped Monitoring IP Address Changes");
    }
}

void __fastcall TToolsForm::doLogIpConnsClick(TObject *Sender)
{
    Get_ConnsTable(FWorkStrings.get(), GetIpFamilyAF(),
                   static_cast<TTransProt>(ItemIndexOrZero(IpConnType->ItemIndex)));
    AddLogLines(FWorkStrings.get());
}

void __fastcall TToolsForm::doUpdAdaptorsClick(TObject *Sender)
{
    TAdaptorRows AdpRows;
    int AdpTot = 0;
    const int Error = IpHlpAdaptersAddr(GetIpFamilyAF(), AdpTot, AdpRows);
    if (Error != 0)
        AddLogLine(SysErrorMessage(Error));   // Delphi demo bug fix: the Delphi reports GetLastError instead of Error
    if (AdpTot > 0) {
        AdaptorListV->Items->Clear();
        for (int i = 0; i < AdpTot && i < AdpRows.Length; i++) {
            const TAdaptorInfo &Adp = AdpRows[i];
            String ListIps, ListPref, ListDns, ListGate;
            for (int j = 0; j < Adp.IPAddressTot; j++) {
                ListIps += Adp.IPAddressList[j];
                if (!Adp.IPMaskList[j].IsEmpty())
                    ListIps += "/" + Adp.IPMaskList[j] + " | ";
                else
                    ListIps += " | ";
            }
            for (int j = 0; j < Adp.PrefixTot; j++) {
                ListPref += Adp.PrefixIPAddrList[j];
                if (!Adp.PrefixMaskList[j].IsEmpty())
                    ListPref += "=" + Adp.PrefixMaskList[j] + " | ";
                else
                    ListPref += " | ";
            }
            for (int j = 0; j < Adp.DNSServerTot; j++)
                ListDns += Adp.DNSServerList[j] + " | ";
            for (int j = 0; j < Adp.GatewayTot; j++)
                ListGate += Adp.GatewayList[j] + " | ";

            TListItem *Item = AdaptorListV->Items->Add();
            // Delphi demo bug fix: inside "with Items.Add do", Delphi's Index is the list item's row number
            Item->Caption = IntToStr(static_cast<__int64>(Adp.Index));
            Item->SubItems->Add(Adp.InterfaceName);
            Item->SubItems->Add(Adp.Description);
            Item->SubItems->Add(Adp.FriendlyName);
            Item->SubItems->Add(TableText(AdaptTypes, Adp.aType));
            Item->SubItems->Add(Adp.MacAddress);
            Item->SubItems->Add(Adp.MacVendor);
            Item->SubItems->Add(IcsGetYN(Adp.DHCPEnabled));
            Item->SubItems->Add(IntToStr(Adp.IPAddressTot));
            Item->SubItems->Add(ListIps);
            Item->SubItems->Add(ListPref);
            Item->SubItems->Add(ListDns);
            Item->SubItems->Add(ListGate);
            Item->SubItems->Add(FirstOrEmpty(Adp.DHCPServer));
            Item->SubItems->Add(FirstOrEmpty(Adp.PrimWINSServer));
            Item->SubItems->Add(IntToStr(static_cast<__int64>(Adp.Ipv4Metric)));
            Item->SubItems->Add(TableText(IfOperStatuses, static_cast<int>(Adp.OperStatus)));
            Item->SubItems->Add(Adp.DnsSuffix);
            Item->SubItems->Add(IntToStr(Adp.XmitLinkSpeed));
            Item->SubItems->Add(TableText(NetIfConnectionTypes, static_cast<int>(Adp.ConnectionType)));
            Item->SubItems->Add(TableText(TunnelTypes, static_cast<int>(Adp.TunnelType)));
            Item->SubItems->Add(Adp.AdapterName);
        }
    }

    // network params
    NetParamsListV->Items->Clear();
    auto AddParam = [this](const String &Name, const String &Value) {
        TListItem *Item = NetParamsListV->Items->Add();
        Item->Caption = Name;
        Item->SubItems->Add(Value);
    };
    AddParam("Host Name", FNetworkParams.HostName);
    AddParam("Domain Name", FNetworkParams.DomainName);
    const int DnsTot = std::min(FNetworkParams.DnsServerTot, static_cast<int>(FNetworkParams.DnsServerNames.size()));
    for (int i = 0; i < DnsTot; i++)
        AddParam("DNS Server " + IntToStr(i), FNetworkParams.DnsServerNames[i]);
    AddParam("DHCP Scope", FNetworkParams.ScopeID);
    AddParam("NetBios Node Type", TableText(NETBIOSTypes, FNetworkParams.NodeType));
    AddParam("Routing Enabled", IcsGetYN(FNetworkParams.EnableRouting));
    AddParam("Proxy Enabled", IcsGetYN(FNetworkParams.EnableProxy));
    AddParam("DNS Enabled", IcsGetYN(FNetworkParams.EnableDNS));
}

void __fastcall TToolsForm::doLogAdaptorsClick(TObject *Sender)
{
    Get_AdaptersInfo(FWorkStrings.get());
    AddLogLines(FWorkStrings.get());
}

void __fastcall TToolsForm::doLogNetParamsClick(TObject *Sender)
{
    Get_NetworkParams(FWorkStrings.get());
    AddLogLines(FWorkStrings.get());
}

void __fastcall TToolsForm::doUpdInterfacesClick(TObject *Sender)
{
    TIfRows2 IfRows2;
    int NumEntries = 0;
    const int Error = IpHlpIfTable2(NumEntries, IfRows2);
    if (Error != 0) {
        AddLogLine(SysErrorMessage(Error));   // Delphi demo bug fix: the Delphi reports GetLastError instead of Error
        return;
    }
    if (NumEntries <= 0)
        return;
    InterfacesListV->Items->Clear();
    for (int i = 0; i < NumEntries && i < IfRows2.Length; i++) {
        const TIfRow2 &Row = IfRows2[i];
        TIpAddrInfos IpAddrInfos;
        String ListIps;
        if (IpHlpIpAddrTable(IpAddrInfos, GetIpFamilyAF(), true, false, Row.Mib.InterfaceIndex) == 0) {
            for (int j = 0; j < IpAddrInfos.Length; j++) {
                ListIps += IpAddrInfos[j].IpAddress;
                if (!IpAddrInfos[j].IpMask.IsEmpty())
                    ListIps += "=" + IpAddrInfos[j].IpMask + " | ";
                else
                    ListIps += " | ";
            }
        }
        const TMibIFRow2 &Mib = Row.Mib;
        TListItem *Item = InterfacesListV->Items->Add();
        // Delphi demo bug fix: inside "with Items.Add do", Delphi's Index is the list item's row number
        Item->Caption = IntToStr(Mib.InterfaceIndex);
        Item->SubItems->Add(Row.InterfaceName);
        Item->SubItems->Add(Row.Description);
        Item->SubItems->Add(Row.FriendlyName);
        Item->SubItems->Add(TableText(AdaptTypes, Mib.IfType));
        Item->SubItems->Add(Row.MacAddress);
        Item->SubItems->Add(Row.MacVendor);
        Item->SubItems->Add(IntToStr(IpAddrInfos.Length));
        Item->SubItems->Add(ListIps);
        Item->SubItems->Add(IntToStr(static_cast<__int64>(Mib.Mtu)));
        Item->SubItems->Add(IntToStr(Mib.TransmitLinkSpeed));
        Item->SubItems->Add(TableText(AdminStatuses, static_cast<int>(Mib.AdminStatus)));
        Item->SubItems->Add(TableText(IfOperStatuses, static_cast<int>(Mib.OperStatus)));
        Item->SubItems->Add(TableText(NdisMediums, static_cast<int>(Mib.MediaType)));
        Item->SubItems->Add(TableText(NdisPhysicalMediums, static_cast<int>(Mib.PhysicalMediumType)));
        Item->SubItems->Add(TableText(NetIfAccessTtypes, static_cast<int>(Mib.AccessType)));
        Item->SubItems->Add(TableText(NetIfDirectionTypes, static_cast<int>(Mib.DirectionType)));
        Item->SubItems->Add(GetIfoFlags(Mib.InterfaceAndOperStatusFlags));
        Item->SubItems->Add(TableText(NetIfMediaConnectStates, static_cast<int>(Mib.MediaConnectState)));
        Item->SubItems->Add(TableText(NetIfConnectionTypes, static_cast<int>(Mib.ConnectionType)));
        Item->SubItems->Add(IntToStr(Mib.InOctets));
        Item->SubItems->Add(IntToStr(Mib.OutOctets));
        Item->SubItems->Add(TableText(TunnelTypes, static_cast<int>(Mib.TunnelType)));
    }
}

void __fastcall TToolsForm::doLogInterfacesClick(TObject *Sender)
{
    Get_IfTable2(FWorkStrings.get());
    AddLogLines(FWorkStrings.get());
}

void __fastcall TToolsForm::doUpdIpAddrClick(TObject *Sender)
{
    TIpAddrInfos IpAddrInfos;
    const int ErrorCode = IpHlpIpAddrTable(IpAddrInfos, GetIpFamilyAF());
    if (ErrorCode != NO_ERROR) {
        AddLogLine(SysErrorMessage(ErrorCode));
        return;
    }
    IpAddressListV->Items->Clear();
    for (int i = 0; i < IpAddrInfos.Length; i++) {
        const TIpAddrInfo &Info = IpAddrInfos[i];
        TListItem *Item = IpAddressListV->Items->Add();
        Item->Caption = Info.IpAddress;
        Item->SubItems->Add(Info.IpMask);
        Item->SubItems->Add(Info.TypeStr);
        Item->SubItems->Add(Info.InterfaceName);
        Item->SubItems->Add(Info.MacAddress);
        Item->SubItems->Add(Info.MacVendor);
        Item->SubItems->Add(Info.Description);
        Item->SubItems->Add(Info.FriendlyName);
    }
}

void __fastcall TToolsForm::doLogIpAddrClick(TObject *Sender)
{
    Get_IPAddrTable(FWorkStrings.get());
    AddLogLines(FWorkStrings.get());
}

void __fastcall TToolsForm::dpUpdIpRoutingClick(TObject *Sender)
{
    TIpRouteRows IpRouteRows;
    const unsigned ErrorCode = IpHlpIPForwardTable(IpRouteRows, GetIpFamilyAF());
    if (ErrorCode != NO_ERROR) {
        AddLogLine(SysErrorMessage(ErrorCode));
        return;
    }
    if (IpRouteRows.Length == 0)
        return;
    IpRoutingListV->Items->Clear();
    for (int i = 0; i < IpRouteRows.Length; i++) {
        const TIpRouteRow &Route = IpRouteRows[i];
        TListItem *Item = IpRoutingListV->Items->Add();
        Item->Caption = Route.DestIpAddr;
        Item->SubItems->Add(Route.DestMask);
        Item->SubItems->Add(Route.NextIpAddr);
        Item->SubItems->Add(Route.ProtDesc);
        Item->SubItems->Add(IntToStr(static_cast<__int64>(Route.Metric)));
        Item->SubItems->Add(Route.OrigDesc);
        Item->SubItems->Add(Route.InterfaceName);
        Item->SubItems->Add(Route.IfTypeDesc);
    }
}

void __fastcall TToolsForm::doLogIpRoutingClick(TObject *Sender)
{
    Get_IPForwardTable(FWorkStrings.get());
    AddLogLines(FWorkStrings.get());
}

void __fastcall TToolsForm::doUpdIpPathsClick(TObject *Sender)
{
    TIpPathRows IpPathRows;
    const unsigned ErrorCode = IpHlpIpPathTable(IpPathRows, GetIpFamilyAF());
    if (ErrorCode != NO_ERROR) {
        AddLogLine(SysErrorMessage(ErrorCode));
        return;
    }
    IpPathsListV->Items->Clear();
    for (int i = 0; i < IpPathRows.Length; i++) {
        const TIpPathRow &Path = IpPathRows[i];
        TListItem *Item = IpPathsListV->Items->Add();
        Item->Caption = Path.SourceIpAddr;
        Item->SubItems->Add(Path.DestIpAddr);
        Item->SubItems->Add(Path.CurNextHopIpAddr);
        Item->SubItems->Add(IntToStr(static_cast<__int64>(Path.PathMtu)));
        Item->SubItems->Add(IntToStr(static_cast<__int64>(Path.RttMean)));
        Item->SubItems->Add(IcsGetYN(Path.IsReachable));
        Item->SubItems->Add(IntToStr(Path.LinkTransmitSpeed));
        Item->SubItems->Add(IntToStr(Path.LinkReceiveSpeed));
        Item->SubItems->Add(Path.InterfaceName);
        Item->SubItems->Add(Path.IfTypeDesc);
    }
}

void __fastcall TToolsForm::doLogIpPathClick(TObject *Sender)
{
    Get_IpPathTable(FWorkStrings.get());
    AddLogLines(FWorkStrings.get());
}

void __fastcall TToolsForm::doUpdNeigbIpsClick(TObject *Sender)
{
    TNeighbRows NeighbRows;
    const unsigned ErrorCode = IpHlpIpNeighbTable(NeighbRows, GetIpFamilyAF());
    if (ErrorCode != NO_ERROR) {
        AddLogLine(SysErrorMessage(ErrorCode));
        return;
    }
    NeighbListV->Items->Clear();
    for (int i = 0; i < NeighbRows.Length; i++) {
        const TNeighbRow &Neighb = NeighbRows[i];
        if (!NeighbPermAddr->Checked && Neighb.State == NlnsPermanent)
            continue;
        TListItem *Item = NeighbListV->Items->Add();
        Item->Caption = Neighb.MacAddress;
        Item->SubItems->Add(Neighb.MacVendor);
        Item->SubItems->Add(Neighb.IpAddress);
        Item->SubItems->Add(Neighb.StateDesc);
        Item->SubItems->Add(Neighb.InterfaceName);
        Item->SubItems->Add(Neighb.IfTypeDesc);
        Item->SubItems->Add(IntToStr(Neighb.ReachSecs));
    }
}

void __fastcall TToolsForm::doLogNeighbIPsClick(TObject *Sender)
{
    Get_IPNeighbourTable(FWorkStrings.get(), NeighbPermAddr->Checked);
    AddLogLines(FWorkStrings.get());
}

void __fastcall TToolsForm::doLogARPClick(TObject *Sender)
{
    Get_ARPTable(FWorkStrings.get());
    AddLogLines(FWorkStrings.get());
}

void __fastcall TToolsForm::doUpdStatsClick(TObject *Sender)
{
    Get_TCPStatistics(ListStatsTCP->Items);
    Get_UDPStatistics(ListStatsUDP->Items);
    Get_ICMPStats(ListStatsIcmpIn->Items, ListStatsIcmpOut->Items);
}

void __fastcall TToolsForm::doListStatsClick(TObject *Sender)
{
    Get_TCPStatistics(FWorkStrings.get());
    AddLogLines(FWorkStrings.get());
    Get_UDPStatistics(FWorkStrings.get());
    AddLogLines(FWorkStrings.get());
    std::unique_ptr<TStringList> WorkStrings2(new TStringList());
    Get_ICMPStats(FWorkStrings.get(), WorkStrings2.get());
    AddLogLines(FWorkStrings.get());
    AddLogLines(WorkStrings2.get());
}

void __fastcall TToolsForm::doResolveIPClick(TObject *Sender)
{
    const String RemIP = ResolveIpRem->Text;
    if (FLocalIpLuid.Value == 0)
        FLocalIpLuid = IpHlpAdpforIP(LocalIPv4->Text);
    if (FLocalIpLuid.Value == 0)
        return;
    TNeighbRow NeighbRow;
    const int Error = IpHlpResolveIpNet(RemIP, LocalIPv4->Text, FLocalIpLuid, NeighbRow);
    if (Error != NO_ERROR)
        AddLogLine("Failed to Resolve IP " + RemIP + " - " + SysErrorMessage(Error));
    else
        AddLogLine("Resolved IP " + RemIP + ", MAC " + NeighbRow.MacAddress + " - " + NeighbRow.MacVendor);
}

void __fastcall TToolsForm::doNetBIOSClick(TObject *Sender)
{
    const String RemIP = ResolveIpRem->Text;
    const String Host = GetNetBiosHostByAddr(RemIP);
    if (Host.IsEmpty())
        AddLogLine("Failed to Find " + RemIP);
    else
        AddLogLine("NetBios Name for IP " + RemIP + " - " + Host);
}

void TToolsForm::PingSetButtons(bool Enable)
{
    doPingSync->Enabled = Enable;
    doPingThread->Enabled = Enable;
    doTraceRoute->Enabled = Enable;
    doPingAbort->Enabled = !Enable;
}

void TToolsForm::PumpMessages()
{
    // Waits for a message (at most 50 ms) instead of spinning, then dispatches what arrived.
    MsgWaitForMultipleObjectsEx(0, nullptr, 50, QS_ALLINPUT, MWMO_INPUTAVAILABLE);
    Application->ProcessMessages();
    if (Application->Terminated)
        FStopFlag = true;
}

TPingThread *TToolsForm::NewPingThread(int PingId, const String &Host, TSocketFamily Family, int Timeout, int TTL,
                                       bool LookupReply, TNotifyEvent OnDone)
{
    // Free the threads that have finished (Finished is set after their OnTerminate has run).
    // erase/remove_if: remove_if moves the finished entries to the end, erase deletes them (unique_ptr frees each).
    FThreads.erase(std::remove_if(FThreads.begin(), FThreads.end(),
                                  [](const std::unique_ptr<TPingThread> &T) { return T->Finished; }),
                   FThreads.end());

    // Created suspended; the caller starts it. The form owns it (the Delphi uses FreeOnTerminate).
    FThreads.push_back(std::unique_ptr<TPingThread>(new TPingThread(true)));
    TPingThread *Thread = FThreads.back().get();
    Thread->PingId = PingId;   // keep track of the results
    Thread->PingThreadNum = FPingRun;
    Thread->OnTerminate = OnDone;   // where we get the response
    Thread->PingSrcAddress = LocalIPv4->Text;
    Thread->PingSrcAddress6 = LocalIPv6->Text;
    Thread->PingSocketFamily = Family;
    Thread->PingHostName = Host;   // host name or IP address to ping
    Thread->PingTimeout = Timeout;   // ms
    Thread->PingTTL = TTL;   // hops
    Thread->PingLookupReply = LookupReply;
    return Thread;
}

void __fastcall TToolsForm::PingNameUpdateEvent(TObject *Sender, int ItemNr)
{
    // trace route, report reverse lookup of IP address for each hop
    if (FClosing)
        return;
    if (FPendingLookups > 0)
        FPendingLookups--;
    // As in the Delphi, the tag is the list row, so a late reply from an earlier trace can update
    // the same row of the current one.
    const int Row = FNameCache->GetDNItem(ItemNr).ReqTag;
    if (Row < 0 || Row >= PingListView->Items->Count)
        return;
    TListItem *Item = PingListView->Items->Item[Row];
    if (Item->SubItems->Count > 3)
        Item->SubItems->Strings[3] = FNameCache->BuildRespList(ItemNr);
}

void __fastcall TToolsForm::PingThreadTermPing(TObject *Sender)
{
    // OnTerminate runs on this thread through Synchronize: all public fields of the thread are readable here.
    if (FClosing)
        return;
    TPingThread *Thread = static_cast<TPingThread *>(Sender);   // only wired to TPingThread
    if (Thread->PingThreadNum != FPingRun)
        return;   // C++ only: a result from an earlier run
    if (FPendingPings > 0)
        FPendingPings--;
    if (FStopFlag || Application->Terminated)
        return;

    // update ListView columns
    const int PingId = Thread->PingId;
    if (PingId < 1 || PingId > PingListView->Items->Count)
        return;
    TListItem *Item = PingListView->Items->Item[PingId - 1];
    Item->Caption = IntToStr(PingId);
    if (Thread->ReplyTotal != 0) {
        Item->SubItems->Add(IntToStr(static_cast<__int64>(Thread->ReplyRTT)) + "ms");
        Item->SubItems->Add(Thread->PingHostName);
        Item->SubItems->Add(Thread->ReplyIPAddr);
    }
    else {
        Item->SubItems->Add("");
        Item->SubItems->Add(Thread->PingHostName);
        Item->SubItems->Add(Thread->ErrString);
    }
    if (Thread->ReplyIPAddr.IsEmpty() || Thread->ErrCode == 11001)   // 11001: host not found
        FStopFlag = true;
}

void __fastcall TToolsForm::PingThreadTermTrace(TObject *Sender)
{
    if (FClosing)
        return;
    TPingThread *Thread = static_cast<TPingThread *>(Sender);   // only wired to TPingThread
    if (Thread->PingThreadNum != FPingRun)
        return;   // C++ only: a result from an earlier run
    // The Delphi decrements here only for failed pings and leaves the rest to the name lookup event.
    if (FPendingPings > 0)
        FPendingPings--;
    if (FStopFlag || Application->Terminated)
        return;

    const int PingId = Thread->PingId;
    const int Row = std::max(PingId, 0);
    if (Row >= PingListView->Items->Count)   // sanity test
        return;
    TListItem *Item = PingListView->Items->Item[Row];
    Item->Caption = IntToStr(PingId + 1);
    if (Thread->ErrCode != 0) {
        if (FTraceDoneFlag)
            return;
        Item->SubItems->Add("0ms");
        Item->SubItems->Add("");   // no IP
        Item->SubItems->Add("");   // no country
        Item->SubItems->Add("Request timed out");
        Item->SubItems->Add("");   // no ASN
        FTraceErrs++;
        if (FTraceErrs >= FTraceMaxErrors) {
            if (PingLog->Checked)
                AddLogLine("Stopped Due to Excessive Errors");
            FTraceDoneFlag = true;
        }
        return;
    }

    const String AddrStr = Thread->ReplyIPAddr;   // IP for this hop, subsequent pings build route
    if (!AddrStr.IsEmpty()) {
        if (!FTraceDoneFlag && FTraceIPAddr == AddrStr) {
            FTraceDoneFlag = true;   // finished, reached destination
            AddLogLine("Reached Destination: " + FTraceIPAddr);
        }
        for (int i = 1; i <= TraceMax; i++) {
            if (FTraceAddr[i] == AddrStr)
                return;   // don't repeat ping to same address
        }
    }
    if (PingId >= 1 && PingId <= TraceMax)
        FTraceAddr[PingId] = AddrStr;

    __int64 AsnNum = 0;
    const String ISOA2 = FGeoTools->FindISOA2Code(AddrStr);
    const String Country = FGeoTools->FindCountry(ISOA2);
    const String ASN = FGeoTools->FindASNCode(AddrStr, AsnNum);
    Item->SubItems->Add(IntToStr(static_cast<__int64>(Thread->ReplyRTT)) + "ms");
    Item->SubItems->Add(AddrStr);
    Item->SubItems->Add(Country);

    // start reverse lookup of IP from DNS cache, updates ListView in event if not in cache;
    // LookupIPOne returns the address itself until the name is known.
    const String HostName = FNameCache->LookupIPOne(AddrStr, Row, sfAny, PingNameUpdateEvent, "");
    if (HostName == AddrStr)
        FPendingLookups++;
    Item->SubItems->Add(HostName);
    Item->SubItems->Add(ASN);
}

void __fastcall TToolsForm::IcsPing1DnsLookupDone(TObject *Sender, System::Word Error)
{
    TListItem *Item = PingListView->Items->Add();
    Item->Caption = "";
    Item->SubItems->Add("");
    if (Error != 0) {
        if (PingLog->Checked)
            AddLogLine("DNS lookup error - " + SysErrorMessage(Error));
        Item->SubItems->Add("DNS lookup error");
        Item->SubItems->Add(SysErrorMessage(Error));
        PingSetButtons(true);
        return;
    }
    if (PingLog->Checked)
        AddLogLine("Host '" + IcsPing1->PunycodeHost + "' is " + IcsPing1->DnsResult);
    Item->SubItems->Add("Host '" + IcsPing1->PunycodeHost + "'");   // the Delphi leaves out the closing quote
    Item->SubItems->Add(IcsPing1->DnsResult);
    IcsPing1->Address = IcsPing1->DnsResult;
    if (IcsPing1->Ping() == 0)   // synchronous, fires OnEchoRequest and OnEchoReply
        AddLogLine("Ping failed: " + IcsPing1->LastErrStr);
    PingSetButtons(true);
}

void __fastcall TToolsForm::IcsPing1EchoReply(TObject *Sender, TObject *Icmp, int Status)
{
    TListItem *Item = PingListView->Items->Add();
    Item->Caption = "";
    if (Status != 0) {   // success
        if (PingLog->Checked)
            AddLogLine("Received " + IntToStr(IcsPing1->ReplySize) + " bytes from " + IcsPing1->ReplyIP + " in " +
                       IntToStr(IcsPing1->ReplyRTT) + " msecs");
        Item->SubItems->Add(IntToStr(IcsPing1->ReplyRTT) + "ms");
        Item->SubItems->Add(IcsPing1->ReplyIP);
        Item->SubItems->Add("");
    }
    else {   // Delphi demo bug fix: the Delphi has no begin/end here, so these columns are also added on success
        if (PingLog->Checked)
            AddLogLine("Cannot ping host (" + IcsPing1->HostIP + ") : " + IcsPing1->LastErrStr +
                       ". ReplyStatus = " + IntToStr(IcsPing1->ReplyStatus));
        Item->SubItems->Add("");
        Item->SubItems->Add("Cannot ping host (" + IcsPing1->HostIP + ")");
        Item->SubItems->Add(IcsPing1->LastErrStr + ". ReplyStatus = " + IntToStr(IcsPing1->ReplyStatus));
    }
    PingSetButtons(true);
}

void __fastcall TToolsForm::IcsPing1EchoRequest(TObject *Sender, TObject *Icmp)
{
    if (PingLog->Checked)
        AddLogLine("Sending " + IntToStr(IcsPing1->Size) + " bytes to " + IcsPing1->HostName + " (" +
                   IcsPing1->HostIP + ")");
}

void __fastcall TToolsForm::doPingAbortClick(TObject *Sender)
{
    IcsPing1->CancelDnsLookup();
    FStopFlag = true;   // threaded ping and trace route
    PingSetButtons(true);
}

void __fastcall TToolsForm::doPingClearClick(TObject *Sender)
{
    DelRecentSList(PingQueryName);
}

void __fastcall TToolsForm::doPingSyncClick(TObject *Sender)
{
    if (PingQueryName->Text.IsEmpty())
        return;
    PingSetButtons(false);
    IcsPing1->SrcAddress = LocalIPv4->Text;
    IcsPing1->SrcAddress6 = LocalIPv6->Text;

    // first do async DNS lookup, sync ping from event handler
    if (PingLog->Checked)
        AddLogLine("Resolving host '" + PingQueryName->Text + "'");
    IcsPing1->SocketFamily = SelectedFamily();
    IcsPing1->DnsLookup(PingQueryName->Text);
    AddRecentSList(PingQueryName->Items, PingQueryName->Text, 99);
}

void __fastcall TToolsForm::doPingThreadClick(TObject *Sender)
{
    if (PingQueryName->Text.IsEmpty())
        return;
    if (FPingBusy)
        return;   // C++ only: Abort re-enables the buttons while the run below is still pumping messages
    FPingBusy = true;
    TOnScopeExit Done([this] {
        FPingBusy = false;
        PingSetButtons(true);
    });
    PingQueryName->Text = CleanDomain(PingQueryName->Text);
    PingSetButtons(false);
    AddRecentSList(PingQueryName->Items, PingQueryName->Text, 99);
    RunPingThreads();
}

void TToolsForm::RunPingThreads()
{
    const String Host = PingQueryName->Text;
    const int Count = Icsatoi(PingCount->Text);
    const int Timeout = Icsatoi(PingTimeoutMs->Text);
    const TSocketFamily Family = SelectedFamily();

    FStopFlag = false;   // Delphi demo bug fix: only the trace route resets it, so one failed run stops every later one
    FPingRun++;   // threads of earlier runs carry an older number, so their results are ignored
    if (PingLog->Checked)
        AddLogLine("Pinging: " + Host);
    FPendingPings = 0;
    PingListView->Items->Clear();
    for (int i = 1; i <= Count; i++)   // one row for each ping
        PingListView->Items->Add()->Caption = IntToStr(i);

    // start all pings
    for (int i = 1; i <= Count; i++) {
        NewPingThread(i, Host, Family, Timeout, TraceMax, false, PingThreadTermPing)->Start();
        FPendingPings++;
        // 100ms before next ping, unless ping finishes earlier;
        // also restrict total threads, too many slows things down
        const __int64 EndTimer = IcsGetTrgMSecs64(100);
        while (FPendingPings > 0 && !FStopFlag) {
            PumpMessages();
            if (IcsTestTrgTick64(EndTimer) && FPendingPings < 6)
                break;
        }
        if (FStopFlag)
            break;   // C++ only: the Delphi goes on starting pings whose results it then ignores
    }

    // wait for pings to finish; pings still pending are left to finish on their own
    const __int64 EndTimer = IcsGetTrgSecs64(30);
    while (FPendingPings > 0 && !FStopFlag) {
        PumpMessages();
        if (IcsTestTrgTick64(EndTimer))
            break;
    }

    if (PingLog->Checked) {
        // copy ListView result to log
        for (int i = 0; i < PingListView->Items->Count; i++) {
            TListItem *Item = PingListView->Items->Item[i];
            if (Item->SubItems->Count >= 3)
                AddLogLine(Format(MaskResponse3, ARRAYOFCONST((Item->Caption, Item->SubItems->Strings[0],
                                  Item->SubItems->Strings[1], Item->SubItems->Strings[2], String(), String()))));
        }
        AddLogLine("");
        if (FStopFlag)
            AddLogLine("Stopped by User");
    }
    Beep();
}

void __fastcall TToolsForm::doTraceRouteClick(TObject *Sender)
{
    if (PingQueryName->Text.IsEmpty())
        return;
    if (FPingBusy)
        return;   // C++ only: Abort re-enables the buttons while the run below is still pumping messages
    FPingBusy = true;
    TOnScopeExit Done([this] {
        FPingBusy = false;
        PingSetButtons(true);
    });
    PingSetButtons(false);
    LoadGeoDatabases();
    try {
        RunTraceRoute();
    }
    catch (Exception &) {
        AddLogLine("Error Sending Pings");
        Beep();
    }
}

void TToolsForm::RunTraceRoute()
{
    const String Host = PingQueryName->Text;
    const int Timeout = Icsatoi(PingTimeoutMs->Text);   // ms

    FStopFlag = false;
    FTraceDoneFlag = false;
    FPingRun++;   // threads of earlier runs carry an older number, so their results are ignored
    if (PingLog->Checked)
        AddLogLine("Trace Route to: " + Host);
    FTraceErrs = 0;
    // Delphi demo bug fix: the Delphi ignores the Maximum Traceroute Errors box and always stops after 8
    FTraceMaxErrors = Icsatoi(TraceErrors->Text);
    if (FTraceMaxErrors <= 0)
        FTraceMaxErrors = MaxErrors;
    FPendingPings = 0;
    FPendingLookups = 0;
    FTraceAddr.fill(String());
    FTraceSocketFamily = SelectedFamily();

    // prepare ListView for the maximum pings
    PingListView->Items->Clear();
    for (int i = 1; i <= TraceMax; i++)
        PingListView->Items->Add()->Caption = "";

    // look up host IP address first, from cache
    AddRecentSList(PingQueryName->Items, Host, 99);
    FNameCache->DNMethod = MethodWinsock;
    const int ItemNr = FNameCache->LookupHostSync(Host, 0, FTraceSocketFamily, nullptr, true);   // true: get GEO
    const TDNItem DNItem = FNameCache->GetDNItem(ItemNr);   // returned by value
    TListItem *First = PingListView->Items->Item[0];
    if (DNItem.DNState != StateOK) {
        if (PingLog->Checked)
            AddLogLine("Can Not Find Host " + Host);
        First->SubItems->Add("");
        First->SubItems->Add("Can Not Find Host " + Host);
        return;
    }
    String Source;
    if (DNItem.TotResp4 > 0) {
        FTraceSocketFamily = sfIPv4;
        FTraceIPAddr = DNItem.Responses4[0];
        Source = LocalIPv4->Text;
    }
    else {
        FTraceSocketFamily = sfIPv6;
        FTraceIPAddr = DNItem.Responses6[0];
        Source = LocalIPv6->Text;
        if (Source.IsEmpty()) {   // search first non-local address
            for (int i = 1; i < LocalIPv6->Items->Count; i++) {
                if (LocalIPv6->Items->Strings[i].Pos("fe") == 0) {
                    Source = LocalIPv6->Items->Strings[i];
                    break;
                }
            }
        }
    }
    LabelTraceDest->Caption = "Tracing to IP Address " + FNameCache->BuildRespList(ItemNr);
    if (PingLog->Checked)
        AddLogLine(LabelTraceDest->Caption);

    First->Caption = "1";
    First->SubItems->Add("0ms");
    First->SubItems->Add(Source);
    First->SubItems->Add("");   // country
    First->SubItems->Add(LowerCase(String(LocalHostName())));
    First->SubItems->Add("");   // ASN

    // start all pings, increasing TTL for each hop
    for (int Hop = 1; Hop <= TraceMax; Hop++) {
        Application->ProcessMessages();
        NewPingThread(Hop, FTraceIPAddr, FTraceSocketFamily, Timeout, Hop, true, PingThreadTermTrace)->Start();
        FPendingPings++;
        // 100ms before next hop, unless ping finishes earlier;
        // also restrict total threads, too many slows things down
        const __int64 EndTimer = IcsGetTrgMSecs64(100);
        while (FPendingPings > 0 && !FStopFlag) {
            PumpMessages();
            if (IcsTestTrgTick64(EndTimer) && FPendingPings < 6)
                break;
        }
        if (FTraceDoneFlag || FStopFlag)
            break;   // reached host, too many errors, or stopped
    }

    // wait for pings and name lookups to finish
    AddLogLine("Pending Pings " + IntToStr(FPendingPings + FPendingLookups));
    // Delphi demo bug fix: the Delphi tests the 32-bit IcsGetTrgSecs trigger with IcsTestTrgTick64
    const __int64 EndTimer = IcsGetTrgSecs64(10);
    while ((FPendingPings > 0 || FPendingLookups > 0) && !FStopFlag) {
        PumpMessages();
        if (IcsTestTrgTick64(EndTimer))
            break;
    }

    if (PingLog->Checked) {
        // copy ListView result to log
        for (int i = 0; i < PingListView->Items->Count; i++) {
            TListItem *Item = PingListView->Items->Item[i];
            if (Item->SubItems->Count >= 5)
                AddLogLine(Format(MaskResponse3, ARRAYOFCONST((Item->Caption, Item->SubItems->Strings[0],
                                  Item->SubItems->Strings[1], Item->SubItems->Strings[2],
                                  Item->SubItems->Strings[3], Item->SubItems->Strings[4]))));
        }
        AddLogLine("");
        if (FStopFlag)
            AddLogLine("Stopped by User");
        AddLogLine("Trace Route Completed");
    }
    Beep();
}

void __fastcall TToolsForm::doWhoisAbortClick(TObject *Sender)
{
    doWhoisQuery->Enabled = true;
    doWhoisAbort->Enabled = false;
    IcsWhoisCli1->Abort();
}

void __fastcall TToolsForm::doWhoisClearClick(TObject *Sender)
{
    DelRecentSList(WhoisQueryName);
}

void __fastcall TToolsForm::doWhoisQueryClick(TObject *Sender)
{
    doWhoisQuery->Enabled = false;
    doWhoisAbort->Enabled = true;
    WhoisMemo->Lines->Clear();
    IcsWhoisCli1->Query = WhoisQueryName->Text;
    IcsWhoisCli1->Host = WhoisServer->Text;   // the component strips the " [description]" suffix
    if (WhoisMethod->ItemIndex == 0)
        IcsWhoisCli1->StartAutoQuery();
    else
        IcsWhoisCli1->StartQuery();
    AddRecentSList(WhoisQueryName->Items, WhoisQueryName->Text, 250);
    WhoisMemo->Lines->Add("Query initiated for " + IcsWhoisCli1->Query);
}

void __fastcall TToolsForm::IcsWhoisCli1QueryDone(TObject *Sender, System::Word ErrCode)
{
    doWhoisQuery->Enabled = true;
    doWhoisAbort->Enabled = false;
    WhoisMemo->Lines->Add(IcsWhoisCli1->WhoisResp);
    AddLogLine(IcsWhoisCli1->WhoisResp);
    WhoisMemo->Lines->Add("Query completed");
    WhoisMemo->SelStart = 0;
    SendMessage(WhoisMemo->Handle, EM_SCROLLCARET, 0, 0);
}

void __fastcall TToolsForm::BulkNameUpdateEvent(TObject *Sender, int ItemNr)
{
    if (FClosing)
        return;
    const int LineNr = FNameCache->GetDNItem(ItemNr).ReqTag;
    if (LineNr < 0 || LineNr >= BulkResultMemo->Lines->Count)
        return;
    BulkResultMemo->Lines->Strings[LineNr] = FNameCache->BuildRespList(ItemNr);
}

// The request tag says which lookup finished: 0 and up is a line of the reverse lookup results,
// -9 a forward lookup of a host name, -8 the reverse lookup of that host's first address.
void __fastcall TToolsForm::NameLookUpdateEvent(TObject *Sender, int ItemNr)
{
    if (FClosing)
        return;
    const TDNItem DNItem = FNameCache->GetDNItem(ItemNr);   // returned by value
    const int LineNr = DNItem.ReqTag;
    if (LineNr >= BulkResultMemo->Lines->Count)
        return;
    String S = FNameCache->BuildRespList(ItemNr);
    if (LineNr == 0)
        RevLookHost->Text = S;

    if (LineNr >= 0) {   // reverse lookup
        BulkResultMemo->Lines->Strings[LineNr] = "Reverse Lookup for " + DNItem.Request + " > " + S;
    }
    else if (LineNr == -9) {   // forward lookup, got IP address
        LookupAddr->Text = S;
        AddLogLine("Domain Name Lookup Event for " + DNItem.Request + " > " + S);
        // now reverse lookup first IP address
        if (DNItem.TotResp4 > 0)
            S = DNItem.Responses4[0];
        else if (DNItem.TotResp6 > 0)
            S = DNItem.Responses6[0];
        else
            return;
        FNameCache->LookupIPAsync(S, -8, SelectedFamily(), NameLookUpdateEvent);
    }
    else if (LineNr == -8) {   // forward lookup, got actual host
        LookupOfficial->Text = S;
        AddLogLine("Reverse Lookup Event for " + DNItem.Request + " > " + S);
    }
}

void __fastcall TToolsForm::doAddrLookUpClick(TObject *Sender)
{
    AddLogLine("Starting Look-up of Domain Name " + LookupHost->Text);
    AddRecentSList(LookupHost->Items, LookupHost->Text, 99);
    FNameCache->LookupHostAsync(LookupHost->Text, -9, SelectedFamily(), NameLookUpdateEvent);
}

void __fastcall TToolsForm::doAddrRevLookClick(TObject *Sender)
{
    AddLogLine("Starting Look-up of IP Address " + RevLookAddr->Text);
    BulkResultMemo->Lines->Clear();
    BulkResultMemo->Lines->Add("");   // waiting for result
    AddRecentSList(RevLookAddr->Items, RevLookAddr->Text, 99);
    FNameCache->LookupIPAsync(RevLookAddr->Text, 0, SelectedFamily(), NameLookUpdateEvent);
    const int Extra = Icsatoi(RevExtra->Text);
    if (Extra <= 0)
        return;

    // TSockAddrIn6 is the Windows sockaddr_in6; for an IPv4 address the same bytes are a
    // sockaddr_in (the Delphi uses an "absolute" variable for this view).
    TSockAddrIn6 ScanAddr = WSocketIPAddrToSocAddr(RevLookAddr->Text);
    sockaddr_in *SockAddrIn = reinterpret_cast<sockaddr_in *>(&ScanAddr);
    for (int i = 1; i <= Extra; i++) {
        Application->ProcessMessages();
        // increment IP address by one
        if (ScanAddr.sin6_family == AF_INET) {
            int IpByte = SockAddrIn->sin_addr.S_un.S_un_b.s_b4 + 1;
            if (IpByte > 255) {   // handle wrapping to next byte
                IpByte = 0;
                SockAddrIn->sin_addr.S_un.S_un_b.s_b3++;
            }
            SockAddrIn->sin_addr.S_un.S_un_b.s_b4 = static_cast<UCHAR>(IpByte);
        }
        else if (ScanAddr.sin6_family == AF_INET6) {
            int IpByte = ScanAddr.sin6_addr.u.Byte[15] + 1;
            if (IpByte > 255) {   // handle wrapping to next byte, could use word but then need to swap bytes
                IpByte = 0;
                ScanAddr.sin6_addr.u.Byte[14]++;
            }
            ScanAddr.sin6_addr.u.Byte[15] = static_cast<UCHAR>(IpByte);
        }
        const String NewIP = WSocketSockAddrToStr(ScanAddr);
        AddLogLine("Starting Look-up of IP Address " + NewIP);
        BulkResultMemo->Lines->Add("");   // waiting for result
        FNameCache->LookupIPAsync(NewIP, i, SelectedFamily(), NameLookUpdateEvent);
    }
}

void __fastcall TToolsForm::doBulkDnsLookupClick(TObject *Sender)
{
    if (BulkNamesMemo->Lines->Count == 0)
        return;
    BulkResultMemo->Lines->Clear();
    AddLogLine("Starting Look-up of " + IntToStr(BulkNamesMemo->Lines->Count) + " Domain Names");
    for (int i = 0; i < BulkNamesMemo->Lines->Count; i++) {
        Application->ProcessMessages();
        // Delphi demo bug fix: the Delphi adds no result line for an empty name, so the tag (the
        // name's line) no longer matches the result line after one
        BulkResultMemo->Lines->Add("");   // waiting for result
        if (!BulkNamesMemo->Lines->Strings[i].IsEmpty())
            FNameCache->LookupHostAsync(BulkNamesMemo->Lines->Strings[i], i, SelectedFamily(), BulkNameUpdateEvent);
    }
}

void __fastcall TToolsForm::DelRevLookAddrClick(TObject *Sender)
{
    DelRecentSList(RevLookAddr);
}

void __fastcall TToolsForm::doDelAddrLookClick(TObject *Sender)
{
    DelRecentSList(LookupHost);
}

void __fastcall TToolsForm::doDelNameHostClick(TObject *Sender)
{
    DelRecentSList(NameHostName);
}

void __fastcall TToolsForm::doDNCacheClearClick(TObject *Sender)
{
    FNameCache->MaintClearAll();
    AddLogLine("Cleared Name Server Cache");
}

void __fastcall TToolsForm::doDNCacheListClick(TObject *Sender)
{
    AddLogLine(FNameCache->ListCache());
}

void __fastcall TToolsForm::DNCacheChangeClick(TObject *Sender)
{
    if (!FNameCache)
        return;   // C++ only: the edit's OnChange can fire while the form is still being loaded
    FNameCache->DNMethod = static_cast<TDNMethod>(ItemIndexOrZero(DNCacheMethod->ItemIndex));
    FNameCache->MaxLookups = Icsatoi(DNCacheMax->Text);
    FNameCache->DBLANlookup = static_cast<TDBLANlookup>(ItemIndexOrZero(DNCacheLAN->ItemIndex));
}

void __fastcall TToolsForm::IcsDnsQuery1LogEvent(TObject *Sender, const System::UnicodeString Msg)
{
    AddLogLine(Msg);
}

// Shared by IcsDnsQuery1 and IcsDnsQueryHttps1 (TDnsQueryHttps derives from TDnsQuery).
void __fastcall TToolsForm::IcsDnsQuery1RequestDone(TObject *Sender, System::Word Error)
{
    FNameServerTrg = Trigger64Disabled;
    TDnsQuery *Query = static_cast<TDnsQuery *>(Sender);
    SetDnsButtons(true);
    if (Error != 0) {
        const String Resp = "Error #" + IntToStr(static_cast<int>(Error));
        NameLookupMemo->Lines->Add(Resp);
        AddLogLine(Resp);
        return;
    }

    String Resp;
    const int RCode = Query->ResponseCode;
    if (RCode == DnsRCodeNoError) {   // success
        if (Query->ResponseAuthoritative)
            Resp = String("Answers are Authoritative") + IcsCRLF;
        else
            Resp = String("Answers are not Authoritative") + IcsCRLF;

        if (Query->AnswerTotal <= 0)
            Resp = String("DNS Record Not Found") + IcsCRLF;
        for (int i = 0; i < Query->AnswerTotal; i++) {
            const TRRRecord Rec = Query->AnswerRecord[i];   // the property returns the record by value
            const String STTL = " (TTL " + IntToStr(static_cast<__int64>(Rec.TTL)) + ")";
            String DispName;
            if (Rec.AnswerName == String(Rec.RRName))
                DispName = Rec.AnswerName + ": ";
            else
                DispName = Rec.AnswerName + " (" + String(Rec.RRName) + "): ";
            switch (Rec.RRType) {
            case DnsQueryMX:
                Resp += "Mail Exchange Server for " + DispName + Rec.HostName + " (preference " +
                        IntToStr(Rec.MxPref) + STTL + IcsCRLF;
                break;
            case DnsQueryA:
                Resp += "DNS IPv4 Query for " + DispName + String(Rec.RDData) + STTL + IcsCRLF;
                break;
            case DnsQueryAAAA:
                Resp += "DNS IPv6 Query for " + DispName + String(Rec.RDData) + STTL + IcsCRLF;
                break;
            case DnsQueryNS:
                Resp += "Name Server for " + DispName + Rec.HostName + STTL + IcsCRLF;
                break;
            case DnsQueryCNAME:
                Resp += "Alias (CNAME) for " + DispName + Rec.HostName + IcsCRLF;
                break;
            case DnsQueryPTR:
                Resp += "Domain Name Query for " + DispName + Rec.HostName + IcsCRLF;
                break;
            case DnsQueryTXT:
                Resp += "Text Query for " + DispName + String(Rec.RDData) + IcsCRLF;
                break;
            case DnsQuerySOA:
                Resp += "SOA - Zone of Authority - for " + DispName + IcsCRLF;
                Resp += "Primary Name Server: " + String(Rec.SOA.mname) + IcsCRLF;
                Resp += "Contact: " + String(Rec.SOA.rname) + IcsCRLF;
                Resp += "Serial/version: " + IntToStr(static_cast<__int64>(Rec.SOA.serial)) + IcsCRLF;
                Resp += "Refresh Time: " + IntToStr(static_cast<__int64>(Rec.SOA.refresh)) + IcsCRLF;
                Resp += "Failed Retry Time: " + IntToStr(static_cast<__int64>(Rec.SOA.retry)) + IcsCRLF;
                Resp += "Expire Time: " + IntToStr(static_cast<__int64>(Rec.SOA.expire)) + IcsCRLF;
                Resp += "Minimum TTL Time: " + IntToStr(static_cast<__int64>(Rec.SOA.minimum)) + IcsCRLF;
                break;
            default:
                Resp += FindDnsReqTypeName(Rec.RRType) + " Result for " + DispName + String(Rec.RDData) + IcsCRLF;
                break;
            }
        }
    }
    else {
        // Delphi demo bug fix: the Delphi indexes DnsRCodeTable past its end for codes above 5
        Resp = "Query Failed: " + TableText(DnsRCodeTable, RCode) + IcsCRLF;
    }
    NameLookupMemo->Lines->Add(Resp);
    AddLogLine(Resp);
}

// Applies the server settings; false when there is no host name to query.
bool TToolsForm::SetupNameServer()
{
    if (NameDnsOpt->ItemIndex == 0) {
        if (NameServerAuto->ItemIndex >= 0)
            IcsDnsQuery1->Addr = NameServerAuto->Items->Strings[NameServerAuto->ItemIndex];
    }
    else if (NameDnsOpt->ItemIndex == 1) {
        IcsDnsQuery1->Addr = NameServerIP->Text;
        AddRecentSList(NameServerIP->Items, NameServerIP->Text, 250);
    }
    else if (NameDnsOpt->ItemIndex == 2) {
        if (NameServerPublic->ItemIndex >= 0)
            IcsDnsQuery1->Addr = NameServerPublic->Items->Strings[NameServerPublic->ItemIndex];
    }
    else
        IcsDnsQueryHttps1->DnsSrvUrl = NameServerHttps->Text;
    IcsDnsQuery1->Proto = NameProtoTCP->Checked ? "tcp" : "udp";
    NameHostName->Text = CleanDomain(NameHostName->Text);
    // Delphi demo bug fix: the Delphi returns here but its callers still send a query for no name
    if (NameHostName->Text.IsEmpty())
        return false;
    AddRecentSList(NameHostName->Items, NameHostName->Text, 250);
    FNameServerTrg = IcsGetTrgSecs64(3);   // three second timeout
    SetDnsButtons(false);
    return true;
}

void TToolsForm::SetDnsButtons(bool Value)
{
    doDNSAll->Enabled = Value;
    doDnsRev->Enabled = Value;
    doDNSQuery->Enabled = Value;
}

// UDP may ignore the request in the DNS handlers below; the timer gives up after three seconds.
void __fastcall TToolsForm::doDNSAllClick(TObject *Sender)
{
    if (!SetupNameServer())
        return;
    NameLookupMemo->Lines->Clear();
    if (NameDnsOpt->ItemIndex == 3) {
        IcsDnsQueryHttps1->DOHQueryAll(NameHostName->Text);
        NameLookupMemo->Lines->Add("ALL Query to: " + IcsDnsQueryHttps1->DnsSrvUrl + " for: " + NameHostName->Text);
    }
    else {
        IcsDnsQuery1->QueryAll(NameHostName->Text);
        NameLookupMemo->Lines->Add("ALL Query to: " + IcsDnsQuery1->Addr + " for: " + NameHostName->Text);
    }
}

void __fastcall TToolsForm::doDnsBothAClick(TObject *Sender)
{
    if (!SetupNameServer())
        return;
    NameLookupMemo->Lines->Clear();
    if (NameDnsOpt->ItemIndex == 3) {
        IcsDnsQueryHttps1->DOHQueryBothA(NameHostName->Text);
        NameLookupMemo->Lines->Add("Both A Query to: " + IcsDnsQueryHttps1->DnsSrvUrl + " for: " + NameHostName->Text);
    }
    else {
        IcsDnsQuery1->QueryBothA(NameHostName->Text);
        NameLookupMemo->Lines->Add("Both A Query to: " + IcsDnsQuery1->Addr + " for: " + NameHostName->Text);
    }
}

void __fastcall TToolsForm::doDNSQueryClick(TObject *Sender)
{
    if (!SetupNameServer())
        return;
    NameLookupMemo->Lines->Clear();
    const int QueryNumber = DnsReqTable[ItemIndexOrZero(NameQueryType->ItemIndex)].Num;
    if (NameDnsOpt->ItemIndex == 3) {
        IcsDnsQueryHttps1->DOHQueryAny(NameHostName->Text, QueryNumber);
        NameLookupMemo->Lines->Add("Sent Query to: " + IcsDnsQueryHttps1->DnsSrvUrl + " for: " + NameHostName->Text);
    }
    else {
        IcsDnsQuery1->QueryAny(NameHostName->Text, QueryNumber);
        NameLookupMemo->Lines->Add("Sent Query to: " + IcsDnsQuery1->Addr + " for: " + NameHostName->Text);
    }
}

void __fastcall TToolsForm::doDnsRevClick(TObject *Sender)
{
    if (!SetupNameServer())
        return;
    if (NameDnsOpt->ItemIndex == 3) {
        IcsDnsQueryHttps1->DOHQueryAny(NameHostName->Text, DnsQueryPTR);
        NameLookupMemo->Lines->Add("PTR Query to: " + IcsDnsQueryHttps1->DnsSrvUrl + " for: " + NameHostName->Text);
    }
    else {
        IcsDnsQuery1->QueryAny(NameHostName->Text, DnsQueryPTR);
        NameLookupMemo->Lines->Add("PTR Query to: " + IcsDnsQuery1->Addr + " for: " + NameHostName->Text);
    }
}
