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

  A basic TCP server (telnet port) that runs each client's socket events in its own
  worker thread, so a lengthy blocking command (try "sleep") does not stop the other
  clients. Commands: help, exit, who, sleep, time.

  Threads: the server component and the form live in the main thread, so the
  ClientCreate / ClientConnect / ClientDisconnect events run there. ClientCreate detaches
  the new client socket from the main thread and starts a TClientThread; the thread attaches
  the socket (its hidden window) to itself and runs the socket's MessageLoop, so every event
  of that client (data available, line limit, background exception) runs in the worker
  thread. Workers never touch the VCL: TClientThread::Display queues the text under a mutex
  and posts WM_LOGMSG, and the main thread moves the queue into the memo.

  How this C++ version differs from the Delphi original, apart from syntax:
  - No worker outlives its client socket or the form. TThrdSrvClient::StopThread wakes a
    "sleep" command, ends the message loop, waits for the thread with no time limit and 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 (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.
  - The Delphi OnDisplay property of the thread is not converted: it was set but never used.
  - The queue and the mutexes are form members; the destructor stops the threads and frees the
    components before those members are destroyed.
*/

#include <vcl.h>
#pragma hdrstop

#include "OverbyteIcsThrdSrv1.h"
#include "OverbyteIcsUtils.hpp"

#include <algorithm>
#include <memory>

#pragma package(smart_init)
#pragma link "OverbyteIcsWndControl"
#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 queue and mutexes still exist.
    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. Keeps the memo to about 200 lines.
void 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);
}

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

void __fastcall TTcpSrvForm::WmLogMessage(TMessage &Msg)
{
    // Take the queue under the lock, then update the memo without holding it.
    std::vector<String> pending;
    {
        std::lock_guard<std::mutex> lock(FLogMutex);
        pending.swap(FLogList);
    }
    for (const String &line : pending)
        Display(line);
}

// 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 + "'...");
}

// 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(this, cli);
    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)
{
    // Called with a string literal; SameText compares without regard to case.
    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(TTcpSrvForm *Form, TWSocket *WSocket)
    : TThread(true), ThreadAttached(false), FForm(Form), FWSocket(WSocket),
      FStopEvent(CreateEvent(nullptr, TRUE, FALSE, nullptr))   // manual reset, not signalled
{
}

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

// 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;
}

// 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.
// ClientThread stays set until the join: queued events still run in the worker and use it.
void TThrdSrvClient::StopThread()
{
    if (ClientThread == nullptr)
        return;
    ClientThread->RequestStop();
    PostThreadMessage(static_cast<DWORD>(ClientThread->ThreadID), WM_QUIT, 0, 0);
    ClientThread->WaitFor();
    delete ClientThread;
    ClientThread = nullptr;
}

// TWSocket is event driven, so the thread attaches the socket to itself and 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
    FWSocket->MessageLoop();
    Sleep(0);
    FWSocket->ThreadDetach();
}

void TClientThread::Display(const String &Msg)
{
    FForm->QueueLog(Msg);
}
