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

  The same threaded TCP server as ThrdSrv (telnet port; commands help, exit, who, sleep,
  time), but the worker thread shows its messages with TThread::Synchronize and the client
  is held back until the main thread has finished with the new connection.

  Threads: ClientCreate / ClientConnect / ClientDisconnect run in the main thread.
  ClientCreate detaches the new client socket and starts a TClientThread, which attaches the
  socket to itself and then waits on its start gate. StartConnection (main thread) posts
  the "send the banner" message to the client's window (handled in the worker thread) and
  posts WM_UNLEASH_THREAD to the form; when the form handles it the main thread is out of
  the session-available code, and it opens the gate. The worker then runs the socket's
  MessageLoop, so all further events of that client run in the worker thread. A worker shows
  text with Synchronize: the main thread executes DisplayMsg while the worker waits.

  How this C++ version differs from the Delphi original, apart from syntax:
  - The start gate is an event the worker waits on. The Delphi main thread entered a critical
    section and left it from the posted message; an event also lets ClientDisconnect release a
    worker that was never started.
  - The Delphi posts the client pointer to the form and dereferences it unchecked; here the
    pointer is used only if the server still lists the client.
  - The client's "start connection" message id is allocated from the ICS window handler, as
    TWSocketThrdClient does. The Delphi uses the fixed id WM_USER + 3, which ICS also hands
    out to its own messages on that window.
  - The banner is sent only while the socket is connected (as ICS's own TWSocketThrdClient does).
  - No worker outlives its client socket or the form. TThrdSrvClient::StopThread opens the
    start gate, wakes a "sleep" command, ends the message loop and waits for the thread with no
    time limit (TThread::WaitFor, which runs the workers' Synchronize calls while it waits),
    then deletes it. It runs from ClientDisconnect, from the form destructor for every client
    before the components are freed (the server frees its clients without ClientDisconnect),
    and from the client destructor (the server's other free paths). The Delphi waited at most
    10 s and then let the server free the socket while a worker could still be in its 15 s "sleep".
  - "sleep" waits on the thread's stop event rather than Sleep, so a disconnect ends it at once
    (without the "Wakeup !" line).
  - The "who" command reads a list kept by the main thread under a mutex instead of the server's
    client list, which the main thread can change while a worker reads it.
  - The destructor stops the threads and frees the components before the members they use are
    destroyed.
*/

#include <vcl.h>
#pragma hdrstop

#include "OverbyteIcsThrdSrvV2_1.h"
#include "OverbyteIcsUtils.hpp"

#include <algorithm>
#include <memory>

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

TTcpSrvForm *TcpSrvForm;

namespace {

const String SectionWindow = "WindowTcpSrv";
const String KeyTop = "Top";
const String KeyLeft = "Left";
const String KeyWidth = "Width";
const String KeyHeight = "Height";

// Holds the memo's updates for the guard's lifetime. On every exit path 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);
    }
    TMemoUpdate(const TMemoUpdate &) = delete;
    TMemoUpdate &operator=(const TMemoUpdate &) = delete;
private:
    TMemo *FMemo;
};

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

} // namespace

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

__fastcall TTcpSrvForm::~TTcpSrvForm()
{
    // Not in the Delphi: the server's destructor frees its clients without ClientDisconnect,
    // so every worker is stopped and joined here first, while the memo it displays into exists.
    if (WSocketServer1 != nullptr) {
        for (int i = WSocketServer1->ClientCount - 1; i >= 0; i--)
            static_cast<TThrdSrvClient*>(WSocketServer1->Client[i])->StopThread();
    }
    DestroyComponents();
}

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

void __fastcall TTcpSrvForm::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);
        DisplayMemo->Clear();
        // Delay the startup code until the form is visible.
        PostMessage(Handle, WM_APPSTARTUP, 0, 0);
    }
}

void __fastcall TTcpSrvForm::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->UpdateFile();
}

// Main thread only (worker threads reach it through Synchronize). Keeps the memo to about 200 lines.
void __fastcall TTcpSrvForm::Display(const String &Msg)
{
    TMemoUpdate update(DisplayMemo);
    if (DisplayMemo->Lines->Count > 200) {
        for (int i = 1; i <= 50; i++)
            DisplayMemo->Lines->Delete(0);
    }
    DisplayMemo->Lines->Add(Msg);
}

// Posted from FormShow, so the form is visible when the server starts.
void __fastcall TTcpSrvForm::WMAppStartup(TMessage &Msg)
{
    Display(Trim(String(" ICS ThrdSrv") + IcsVerLitCopyright));
    Display(Trim(Overbyteicswsocket::CopyRight));
    Display(Trim(Overbyteicswsockets::CopyRight));
    Display("");
    Display("MainThreadID : $" + ThreadIdHex(GetCurrentThreadId()));
    WSocketServer1->Proto = "tcp";
    WSocketServer1->Port = "telnet";
    WSocketServer1->Addr = "0.0.0.0";
    WSocketServer1->ClientClass = __classid(TThrdSrvClient);
    WSocketServer1->Listen();
    Display("Waiting for clients on port '" + WSocketServer1->Port + "'...");
}

// Posted by TThrdSrvClient::StartConnection. Opens the start gate of that client's worker.
void __fastcall TTcpSrvForm::WMUnleashThread(TMessage &Msg)
{
    auto *cli = reinterpret_cast<TThrdSrvClient*>(Msg.LParam);
    // The client may be gone by now; only a pointer the server still lists is dereferenced.
    if (WSocketServer1->IsClient(cli) && cli->ClientThread != nullptr)
        cli->ClientThread->Unleash();
}

// Main thread. Detaches the new client socket from the main thread and hands it to a worker.
void __fastcall TTcpSrvForm::WSocketServer1ClientCreate(TObject *Sender, TWSocketClient *Client)
{
    auto *cli = static_cast<TThrdSrvClient*>(Client);
    cli->ThreadDetach();
    cli->MultiThreaded = true;
    cli->ClientThread = new TClientThread(cli, Display);
    cli->ClientThread->Start();
    // Wait until the thread has attached the socket to itself; give up if it died first.
    while (!cli->ClientThread->ThreadAttached && !cli->ClientThread->Finished)
        Sleep(0);
    // Not in the Delphi: a worker that died before attaching rejects the client; the server then frees it (its destructor joins the dead worker).
    if (!cli->ClientThread->ThreadAttached)
        throw Exception("Client worker thread failed to attach the socket");
}

// Main thread.
void __fastcall TTcpSrvForm::WSocketServer1ClientConnect(TObject *Sender, TWSocketClient *Client, System::Word Error)
{
    auto *cli = static_cast<TThrdSrvClient*>(Client);
    Display("Client connected. Remote: " + cli->PeerAddr + "/" + cli->PeerPort +
            " Local: " + cli->GetXAddr() + "/" + cli->GetXPort() +
            " ThreadID : $" + ThreadIdHex(static_cast<DWORD>(cli->ClientThread->ThreadID)));
    cli->LineMode = true;
    cli->LineEdit = true;
    cli->LineLimit = 80;   // do not accept long lines
    cli->OnDataAvailable = ClientDataAvailable;
    cli->OnLineLimitExceeded = ClientLineLimitExceeded;
    cli->OnBgException = ClientBgException;
    cli->ConnectTime = Now();

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

// Main thread.
void __fastcall TTcpSrvForm::WSocketServer1ClientDisconnect(TObject *Sender, TWSocketClient *Client, System::Word Error)
{
    auto *cli = static_cast<TThrdSrvClient*>(Client);
    Display("Client disconnecting: " + cli->PeerAddr + "   " +
            "Duration: " + FormatDateTime(String("hh:nn:ss"), Now() - cli->ConnectTime) +
            " ThreadID : $" + ThreadIdHex(GetCurrentThreadId()));

    {
        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());
    }

    // The server frees the client when this handler returns, so the worker must be gone first.
    cli->StopThread();
}

// Runs in the client's worker thread.
void __fastcall TTcpSrvForm::ClientLineLimitExceeded(TObject *Sender, int Cnt, bool &ClearData)
{
    auto *cli = static_cast<TThrdSrvClient*>(Sender);
    cli->ClientThread->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 TTcpSrvForm::ClientDataAvailable(TObject *Sender, System::Word Error)
{
    auto *cli = static_cast<TThrdSrvClient*>(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);
    cli->ClientThread->Display("Received from " + cli->GetPeerAddr() + ": '" + cli->RcvdLine + "' " +
                               "ThreadID: $" + ThreadIdHex(GetCurrentThreadId()));
    ProcessData(cli);
}

// Runs in the client's worker thread, so it must not touch the VCL.
void TTcpSrvForm::ProcessData(TThrdSrvClient *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  sleep\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 (isCommand(L"sleep")) {
        Client->SendStr(String("Now sleeping for 15\"...\r\n"));
        // Not Sleep(15000) as in the Delphi: a disconnect or form close ends the wait at once.
        if (!Client->ClientThread->WaitForStop(15000))
            Client->SendStr(String("Wakeup !\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 TTcpSrvForm::WSocketServer1BgException(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 TTcpSrvForm::ClientBgException(TObject *Sender, Exception *E, bool &CanClose)
{
    auto *cli = static_cast<TThrdSrvClient*>(Sender);
    cli->ClientThread->Display("Client exception occured: " + E->ClassName() + ": " + E->Message);
    CanClose = true;   // goodbye client
}

TClientThread::TClientThread(TWSocket *WSocket, TDisplayProc OnDisplay)
    : TThread(true), ThreadAttached(false), FWSocket(WSocket), FOnDisplay(OnDisplay),
      FStartGate(CreateEvent(nullptr, TRUE, FALSE, nullptr)),   // manual reset, closed
      FStopEvent(CreateEvent(nullptr, TRUE, FALSE, nullptr))    // manual reset, not signalled
{
}

__fastcall TClientThread::~TClientThread()
{
    CloseHandle(FStopEvent);
    CloseHandle(FStartGate);
}

void TClientThread::Unleash()
{
    SetEvent(FStartGate);
}

// Any thread. Once set, every later WaitForStop returns at once.
void TClientThread::RequestStop()
{
    SetEvent(FStopEvent);
}

// Worker thread. Waits up to TimeoutMs; true when RequestStop ended the wait early.
bool TClientThread::WaitForStop(DWORD TimeoutMs)
{
    return WaitForSingleObject(FStopEvent, TimeoutMs) == WAIT_OBJECT_0;
}

// Attaches the client socket to this thread, waits at the start gate, then runs the socket's
// own message loop until the main thread posts WM_QUIT to it.
void __fastcall TClientThread::Execute()
{
    FWSocket->ThreadAttach();
    ThreadAttached = true;
    Sleep(0);   // let the main thread carry on starting the connection
    WaitForSingleObject(FStartGate, INFINITE);
    FWSocket->MessageLoop();
    Sleep(0);
    FWSocket->ThreadDetach();
}

// Called in the worker thread: stores the text, then has the main thread show it.
void TClientThread::Display(const String &Msg)
{
    FMsg = Msg;   // Synchronize takes a method without parameters, so the text goes in a field
    Synchronize(&DisplayMsg);
}

// Runs in the main thread, only through Synchronize.
void __fastcall TClientThread::DisplayMsg()
{
    if (FOnDisplay)
        FOnDisplay(FMsg);
}

// Not in the Delphi: also joins a worker the server would otherwise free under it (paths in
// TCustomWSocketServer that free a client without ClientDisconnect).
__fastcall TThrdSrvClient::~TThrdSrvClient()
{
    StopThread();
}

// Main thread. Ends the worker and waits for it with no time limit, so it can no longer use this
// socket or the form; the Delphi gave up after 10 s. Does nothing once the thread is gone.
// TThread::WaitFor runs CheckSynchronize while it waits in the main thread, so a worker blocked in
// Synchronize (Display) still finishes. ClientThread stays set until the join: queued events
// still run in the worker and use it.
void TThrdSrvClient::StopThread()
{
    if (ClientThread == nullptr)
        return;
    ClientThread->Unleash();   // a worker still waiting at its gate could not see WM_QUIT
    ClientThread->RequestStop();
    PostThreadMessage(static_cast<DWORD>(ClientThread->ThreadID), WM_QUIT, 0, 0);
    ClientThread->WaitFor();
    delete ClientThread;
    ClientThread = nullptr;
}

// Main thread, called by the server once the connection is accepted. The worker is waiting at
// its gate, so the banner message is queued for it now and the gate is opened from the form.
void __fastcall TThrdSrvClient::StartConnection()
{
    PostMessage(Handle, FMsgStartCnx, 0, 0);
    PostMessage(TcpSrvForm->Handle, WM_UNLEASH_THREAD, 0, reinterpret_cast<LPARAM>(this));
}

// ICS allocates the window message ids of a socket from a per-thread pool; this adds one.
int __fastcall TThrdSrvClient::MsgHandlersCount()
{
    return 1 + TWSocketClient::MsgHandlersCount();
}

void __fastcall TThrdSrvClient::AllocateMsgHandlers()
{
    TWSocketClient::AllocateMsgHandlers();
    FMsgStartCnx = FWndHandler->AllocateMsgHandler(this);
}

void __fastcall TThrdSrvClient::FreeMsgHandlers()
{
    if (FWndHandler != nullptr)
        FWndHandler->UnregisterMessage(FMsgStartCnx);
    TWSocketClient::FreeMsgHandlers();
}

void __fastcall TThrdSrvClient::WndProc(TMessage &MsgRec)
{
    if (MsgRec.Msg == FMsgStartCnx)
        WMStartCnx(MsgRec);
    else
        TWSocketClient::WndProc(MsgRec);
}

// Runs in the worker thread, once it is past the gate and in its message loop.
void __fastcall TThrdSrvClient::WMStartCnx(TMessage &Msg)
{
    if (FBanner.Length() > 0 && State == wsConnected)
        SendStr(RawByteString(AnsiString(FBanner) + FLineEnd));
}
