/*
  ICS ThrdSrvV3 demo - C++Builder translation of the Delphi demo OverbyteIcsThrdSrvV3_1.pas
  (demos-delphi-vcl), ICS V9.8.

  A threaded TCP server built on TWSocketThrdServer, which manages the worker threads
  itself: several clients share one thread ("Clients Per Thread"). Commands: help, exit,
  who, time. Listens on the telnet port.

  Threads: the form and the server live in the main thread, so ClientCreate, ClientConnect,
  ClientDisconnect and ThreadException run there. TWSocketThrdServer attaches each new client
  to a worker thread, and all other events of that client (data available, line limit,
  background exception) run in that thread. Workers never touch the VCL: Display queues the
  text under a mutex and posts WM_LOG_MESSAGE, and the main thread moves the queue into the
  memo.

  How this C++ version differs from the Delphi original, apart from syntax:
  - The queue is a std::vector under a std::mutex, both form members, and the destructor frees
    the components first so the server and its threads stop before the members they use are
    destroyed. The Delphi FormDestroy freed a global-lock-protected list while threads could
    still be logging, so it is not converted and its .dfm binding is removed.
  - ClientConnect no longer repeats the line mode, line limit and event assignments that
    ClientCreate already made.
  - The "who" command reads a list kept by the main thread under a mutex (filled in
    ClientConnect, trimmed in ClientDisconnect). The Delphi read the server's client list
    from the worker threads while the main thread could be changing it.
  - Thread ids are shown in hex after "$"; the Delphi printed them in decimal after "$".
  - The help text no longer lists "threadexception", a command the Delphi never implemented.
*/

#include <vcl.h>
#pragma hdrstop

#include "OverbyteIcsThrdSrvV3_1.h"
#include "OverbyteIcsUtils.hpp"

#include <algorithm>
#include <memory>

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

TThrdSrvForm *ThrdSrvForm;

namespace {

const String SectionWindow = "WindowTcpSrv";
const String SectionMain = "Main";
const String KeyTop = "Top";
const String KeyLeft = "Left";
const String KeyWidth = "Width";
const String KeyHeight = "Height";
const String KeyCliPerThrd = "ClientsPerThread";

// Holds the memo's updates for the guard's lifetime; on exit it ends the update and scrolls the
// caret into view, as the Delphi finally block does.
class TMemoUpdate
{
public:
    explicit TMemoUpdate(TMemo *Memo) : FMemo(Memo) { FMemo->Lines->BeginUpdate(); }
    ~TMemoUpdate()
    {
        FMemo->Lines->EndUpdate();
        SendMessage(FMemo->Handle, EM_SCROLLCARET, 0, 0);   // Perform(EM_SCROLLCARET, 0, 0) is ambiguous (E4612)
    }
    TMemoUpdate(const TMemoUpdate &) = delete;
    TMemoUpdate &operator=(const TMemoUpdate &) = delete;
private:
    TMemo *FMemo;
};

String ThreadIdHex(NativeUInt Id)
{
    return IntToHex(static_cast<__int64>(Id), 8);
}

} // namespace

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

__fastcall TThrdSrvForm::~TThrdSrvForm()
{
    DestroyComponents();
}

void __fastcall TThrdSrvForm::FormCreate(TObject *Sender)
{
    Caption = Caption + IcsVerLitSample;   // common ICS sample caption: version and date
    FIniFileName = GetIcsIniFileName();
}

void __fastcall TThrdSrvForm::FormShow(TObject *Sender)
{
    if (!FInitialized) {
        FInitialized = true;

        // The second constructor argument (PreserveAnsi) has a Delphi default that the header drops.
        std::unique_ptr<TIcsIniFile> IniFile(new TIcsIniFile(FIniFileName, true));
        Width = IniFile->ReadInteger(SectionWindow, KeyWidth, Width);
        Height = IniFile->ReadInteger(SectionWindow, KeyHeight, Height);
        Top = IniFile->ReadInteger(SectionWindow, KeyTop, (Screen->Height - Height) / 2);
        Left = IniFile->ReadInteger(SectionWindow, KeyLeft, (Screen->Width - Width) / 2);
        ClientsPerThreadEdit->Text = IniFile->ReadString(SectionMain, KeyCliPerThrd, "2");
        DisplayMemo->Clear();
        // Delay the startup code until the form is visible.
        PostMessage(Handle, WM_APPSTARTUP, 0, 0);
    }
}

void __fastcall TThrdSrvForm::FormClose(TObject *Sender, TCloseAction &Action)
{
    std::unique_ptr<TIcsIniFile> IniFile(new TIcsIniFile(FIniFileName, true));
    IniFile->WriteInteger(SectionWindow, KeyTop, Top);
    IniFile->WriteInteger(SectionWindow, KeyLeft, Left);
    IniFile->WriteInteger(SectionWindow, KeyWidth, Width);
    IniFile->WriteInteger(SectionWindow, KeyHeight, Height);
    IniFile->WriteString(SectionMain, KeyCliPerThrd, ClientsPerThreadEdit->Text);
    IniFile->UpdateFile();
}

// Main thread. Moves the queued text into the memo, which is trimmed first so it stays near 200 lines.
void __fastcall TThrdSrvForm::WmLogMessage(TMessage &Msg)
{
    TMemoUpdate update(DisplayMemo);
    if (DisplayMemo->Lines->Count > 200) {
        for (int i = 1; i <= 50; i++)
            DisplayMemo->Lines->Delete(0);
    }
    // Take the queue under the lock; the memo is updated after unlocking.
    std::vector<String> pending;
    {
        std::lock_guard<std::mutex> lock(FLogMutex);
        pending.swap(FLogList);
    }
    for (const String &line : pending)
        DisplayMemo->Lines->Add(line);
}

// Any thread. The text is shown later by WmLogMessage in the main thread.
void TThrdSrvForm::Display(const String &Msg)
{
    std::lock_guard<std::mutex> lock(FLogMutex);
    FLogList.push_back(Msg);
    PostMessage(Handle, WM_LOG_MESSAGE, 0, 0);
}

// Posted from FormShow, so the form is visible when the server starts.
void __fastcall TThrdSrvForm::WMAppStartup(TMessage &Msg)
{
    Display(String(" TcpSrv") + IcsVerLitCopyright);
    Display(Overbyteicswsocket::CopyRight);
    Display(Overbyteicswsockets::CopyRight);
    WSocketThrdServer1->Proto = "tcp";
    WSocketThrdServer1->Port = "telnet";   // the Delphi sets 22 under a "Use telnet port" comment; telnet as its .dfm and ThrdSrv/V2
    WSocketThrdServer1->Addr = "0.0.0.0";
    WSocketThrdServer1->ClientClass = __classid(TMyClient);
    WSocketThrdServer1->Listen();
    Display("Waiting for clients on " + WSocketThrdServer1->Addr + ":" + WSocketThrdServer1->Port);
}

// Main thread, before the server attaches the client to a worker thread.
void __fastcall TThrdSrvForm::WSocketThrdServer1ClientCreate(TObject *Sender, TWSocketClient *Client)
{
    auto *cli = static_cast<TMyClient*>(Client);
    cli->LineMode = true;
    cli->LineEdit = true;
    cli->LineLimit = 255;   // do not accept long lines
    cli->OnDataAvailable = ClientDataAvailable;
    cli->OnLineLimitExceeded = ClientLineLimitExceeded;
    cli->OnBgException = ClientBgException;
}

// Main thread.
void __fastcall TThrdSrvForm::WSocketThrdServer1ClientConnect(TObject *Sender, TWSocketClient *Client, System::Word Error)
{
    auto *cli = static_cast<TMyClient*>(Client);
    cli->ConnectTime = Now();
    Display("Client connected. Remote: " + cli->PeerAddr + "/" + cli->PeerPort +
            " Local: " + cli->GetXAddr() + "/" + cli->GetXPort() +
            " ThrdID : $" + ThreadIdHex(cli->ClientThread->ThreadID) +
            " ThrdCnt: #" + IntToStr(WSocketThrdServer1->ThreadCount) +
            "\r\nThere is now " + IntToStr(WSocketThrdServer1->ClientCount) + " clients connected.");

    std::lock_guard<std::mutex> lock(FWhoMutex);
    FWho.push_back(TWhoEntry{cli, cli->PeerAddr, cli->PeerPort, cli->ConnectTime});
}

// Main thread.
void __fastcall TThrdSrvForm::WSocketThrdServer1ClientDisconnect(TObject *Sender, TWSocketClient *Client, System::Word Error)
{
    auto *cli = static_cast<TMyClient*>(Client);
    String threadId = "-1";
    if (cli->ClientThread != nullptr)
        threadId = "$" + ThreadIdHex(cli->ClientThread->ThreadID);
    Display("Client disconnecting: " + cli->PeerAddr + "   " +
            "Duration: " + FormatDateTime(String("hh:nn:ss"), Now() - cli->ConnectTime) +
            " Error: " + IntToStr(static_cast<int>(Error)) +
            " ThrdID: " + threadId +
            " ThrdCnt: #" + IntToStr(WSocketThrdServer1->ThreadCount) +
            "\r\nThere is now " + IntToStr(WSocketThrdServer1->ClientCount - 1) + " clients connected.");

    std::lock_guard<std::mutex> lock(FWhoMutex);
    FWho.erase(std::remove_if(FWho.begin(), FWho.end(),
                              [cli](const TWhoEntry &e) { return e.Client == cli; }),
               FWho.end());
}

// Runs in the client's worker thread.
void __fastcall TThrdSrvForm::ClientLineLimitExceeded(TObject *Sender, int Cnt, bool &ClearData)
{
    auto *cli = static_cast<TMyClient*>(Sender);
    Display("Line limit exceeded from " + cli->GetPeerAddr() + ". Closing.");
    ClearData = true;
    cli->Close();
}

// Runs in the client's worker thread. Line mode, so each event is one complete line.
void __fastcall TThrdSrvForm::ClientDataAvailable(TObject *Sender, System::Word Error)
{
    auto *cli = static_cast<TMyClient*>(Sender);
    cli->RcvdLine = cli->ReceiveStr();
    int len = cli->RcvdLine.Length();
    while (len > 0 && (cli->RcvdLine[len] == L'\r' || cli->RcvdLine[len] == L'\n'))
        len--;
    cli->RcvdLine.SetLength(len);
    Display("Received from " + cli->GetPeerAddr() + ": '" + cli->RcvdLine + "'");
    ProcessData(cli);
}

// Runs in the client's worker thread, so it must not touch the VCL.
void TThrdSrvForm::ProcessData(TMyClient *Client)
{
    // Lambda: true when the received line equals Command, ignoring case (Delphi CompareText = 0).
    auto isCommand = [Client](const wchar_t *Command) { return SameText(Client->RcvdLine, String(Command)); };

    if (isCommand(L"help"))
        Client->SendStr(String("Commands are:\r\n  exit\r\n  who\r\n  time\r\n"));
    else if (isCommand(L"exit"))
        // Close here would re-enter DataAvailable with the same line (it is removed from the
        // buffer after the handler returns); CloseDelayed waits until the handler is done.
        Client->CloseDelayed();
    else if (isCommand(L"time"))
        Client->SendStr(DateTimeToStr(Now()) + "\r\n");
    else if (isCommand(L"who")) {
        std::vector<TWhoEntry> who;
        {
            std::lock_guard<std::mutex> lock(FWhoMutex);
            who = FWho;
        }
        Client->SendStr(String("There are ") + IntToStr(static_cast<int>(who.size())) + " connected users:\r\n");
        for (auto it = who.rbegin(); it != who.rend(); ++it)
            Client->SendStr(it->Addr + ":" + it->Port + " " + DateTimeToStr(it->ConnectTime) + "\r\n");
    }
    else if (Client->State == wsConnected)
        Client->SendStr("Unknown command: '" + Client->RcvdLine + "'\r\n");
}

// Main thread: the listening socket had a background exception.
void __fastcall TThrdSrvForm::WSocketThrdServer1BgException(TObject *Sender, Exception *E, bool &CanClose)
{
    Display("Server exception occured: " + E->ClassName() + ": " + E->Message);
    CanClose = false;   // hoping that the server will still work
}

// Runs in the client's worker thread.
void __fastcall TThrdSrvForm::ClientBgException(TObject *Sender, Exception *E, bool &CanClose)
{
    Display("Client exception occured: " + E->ClassName() + ": " + E->Message);
    CanClose = true;   // goodbye client
}

void __fastcall TThrdSrvForm::ClientsPerThreadEditChange(TObject *Sender)
{
    // An empty or non-numeric entry means 1, as in the Delphi (its except branch).
    WSocketThrdServer1->ClientsPerThread = StrToIntDef(static_cast<TEdit*>(Sender)->Text, 1);
}

void __fastcall TThrdSrvForm::DisconnectAllButtonClick(TObject *Sender)
{
    WSocketThrdServer1->DisconnectAll();
}

void __fastcall TThrdSrvForm::ClearMemoButtonClick(TObject *Sender)
{
    DisplayMemo->Clear();
}

// Main thread: a worker thread of the server reported an error.
void __fastcall TThrdSrvForm::WSocketThrdServer1ThreadException(TObject *Sender, TWsClientThread *AThread, const String AErrMsg)
{
    Display(AErrMsg);
}
