/*
  ICS ServiceCmd - C++Builder translation of the Delphi unit OverbyteIcsServiceCmd.pas
  (demos-delphi-vcl), ICS V9.8.

  TTcpDaemon wraps a TWSocketServer and a small command interpreter (exit, time, who, help), so
  the same code can run inside a Windows service (ServiceTcp) or a normal exe. Output goes to the
  OnDisplay callback.

  How this C++ version differs from the Delphi original, apart from syntax:
  - TTcpDaemon is a plain C++ class that owns its TWSocketServer in a std::unique_ptr.
  - The unused TcpCmdVersion and CopyRight constants are not converted.
*/

#include <vcl.h>
#pragma hdrstop

#include "OverbyteIcsServiceCmd.h"

#pragma package(smart_init)

namespace {

bool IsCommand(const String &Word, const String &Command)
{
    return CompareText(Word, Command) == 0;
}

bool IsBlank(wchar_t Ch)
{
    return Ch == L' ' || Ch == L'\t';
}

// Splits a command line into words at spaces or tabs; a word in double quotes may hold spaces
// and a doubled quote inside it stands for one quote. Returns how many words were stored.
int ParseCommandLine(const String &CmdLine, String *Params, int MaxParams)
{
    int index = 0;
    int i = 1;   // String indexes are 1-based
    const int len = CmdLine.Length();
    while (index < MaxParams && i <= len) {
        while (i <= len && IsBlank(CmdLine[i]))
            ++i;
        if (i > len)
            break;
        if (CmdLine[i] == L'"') {
            ++i;
            Params[index] = "";
            while (i <= len) {
                if (CmdLine[i] == L'"') {
                    if (i >= len || CmdLine[i + 1] != L'"') {
                        ++i;
                        break;
                    }
                    Params[index] += "\"";
                    i += 2;
                }
                else {
                    Params[index] += CmdLine[i];
                    ++i;
                }
            }
        }
        else {
            const int start = i;
            while (i <= len && !IsBlank(CmdLine[i]))
                ++i;
            Params[index] = CmdLine.SubString(start, i - start);
        }
        ++index;
    }
    return index;
}

} // namespace

TTcpDaemon::TTcpDaemon()
    : Port("2120"), Addr("0.0.0.0"), FServer(new TWSocketServer(nullptr))
{
    FServer->Banner = "ICS Tcp Service Ready";
}

TTcpDaemon::~TTcpDaemon()
{
}

void TTcpDaemon::Display(const String &Msg)
{
    if (OnDisplay)
        OnDisplay(Msg);
}

void __fastcall TTcpDaemon::WSocketServer1BgException(TObject *Sender, Exception *E, bool &CanClose)
{
    Display("Server exception occured: " + E->ClassName() + ": " + E->Message);
    CanClose = false;   // hoping the server still works
}

void __fastcall TTcpDaemon::WSocketServer1DebugDisplay(TObject *Sender, const String Msg)
{
    Display(Msg);
}

// Called for each new client: line mode and the two per-client events.
void __fastcall TTcpDaemon::WSocketServer1ClientConnect(TObject *Sender, TWSocketClient *Client, System::Word Error)
{
    TTcpSrvClient *cli = static_cast<TTcpSrvClient *>(Client);   // ClientClass is always TTcpSrvClient
    Display("Client connecting: " + cli->PeerAddr);
    cli->LineMode = true;
    cli->LineEdit = true;
    cli->OnDataAvailable = ClientDataAvailable;
    cli->OnBgException = ClientBgException;
    cli->ConnectTime = Now();
}

void __fastcall TTcpDaemon::WSocketServer1ClientDisconnect(TObject *Sender, TWSocketClient *Client, System::Word Error)
{
    TTcpSrvClient *cli = static_cast<TTcpSrvClient *>(Client);
    Display("Client disconnecting: " + cli->PeerAddr + "   Duration: " +
            FormatDateTime("hh:nn:ss", Now() - cli->ConnectTime));
}

void __fastcall TTcpDaemon::ClientDataAvailable(TObject *Sender, System::Word Error)
{
    TTcpSrvClient *cli = static_cast<TTcpSrvClient *>(Sender);
    // Line mode: ReceiveStr returns one complete line.
    cli->RcvdLine = cli->ReceiveStr();
    while (cli->RcvdLine.Length() > 0 &&
           (cli->RcvdLine[cli->RcvdLine.Length()] == L'\r' || cli->RcvdLine[cli->RcvdLine.Length()] == L'\n'))
        cli->RcvdLine.SetLength(cli->RcvdLine.Length() - 1);
    Display("Received from " + cli->GetPeerAddr() + ": '" + cli->RcvdLine + "'");
    ProcessData(cli);
}

// Runs one command line from a client. Slow work should go to a thread so this returns at once.
void TTcpDaemon::ProcessData(TTcpSrvClient *Client)
{
    Client->ParamCount = ParseCommandLine(Client->RcvdLine, Client->Param, 11);
    if (Client->ParamCount <= 0)
        return;

    const String &command = Client->Param[0];
    if (IsCommand(command, "exit")) {
        // Close() here would re-enter DataAvailable with the same line, because a line is
        // removed from the buffer only after it has been processed; CloseDelayed waits.
        Client->CloseDelayed();
    }
    else if (IsCommand(command, "time")) {
        Client->SendStr(DateTimeToStr(Now()) + "\r\n");
    }
    else if (IsCommand(command, "who")) {
        Client->SendStr("There are " + IntToStr(FServer->ClientCount) + " connected users:\r\n");
        for (int i = FServer->ClientCount - 1; i >= 0; --i) {
            TTcpSrvClient *other = static_cast<TTcpSrvClient *>(FServer->Client[i]);
            Client->SendStr(other->PeerAddr + ":" + other->GetPeerPort() + " " +
                            DateTimeToStr(other->ConnectTime) + "\r\n");
        }
    }
    else if (IsCommand(command, "help")) {
        Client->SendStr(String("Commands are:\r\n"
                               "   exit        Close current session\r\n"
                               "   time        Display server time and date\r\n"
                               "   who         Display connected clients\r\n"
                               "   help        Show this help text\r\n"));
    }
    else if (Client->State == wsConnected) {
        Client->SendStr("Unknown command: '" + command + "'\r\nType help if you need help...\r\n");
    }
}

// Background exception of a client socket, likely when the client aborted before data was sent.
void __fastcall TTcpDaemon::ClientBgException(TObject *Sender, Exception *E, bool &CanClose)
{
    Display("Client exception occured: " + E->ClassName() + ": " + E->Message);
    CanClose = true;
}

void TTcpDaemon::Start()
{
    FServer->OnBgException = WSocketServer1BgException;
    FServer->OnClientConnect = WSocketServer1ClientConnect;
    FServer->OnClientDisconnect = WSocketServer1ClientDisconnect;
    FServer->OnDebugDisplay = WSocketServer1DebugDisplay;
    FServer->WSDebugOptions = TWSDebugOptions() << wsdlogConnWS << wsdlogListen << wsdlogErrs << wsdlogAccept;
    FServer->Proto = "tcp";
    FServer->Port = Port;
    FServer->Addr = Addr;
    FServer->ClientClass = __classid(TTcpSrvClient);
    FServer->Listen();
    Display("Server Waiting for clients...");
}

void TTcpDaemon::Stop()
{
    Display("Closing Server");
    FServer->Close();
}

String TTcpDaemon::GetBanner() const
{
    return FServer->Banner;
}

void TTcpDaemon::SetBanner(const String &Value)
{
    FServer->Banner = Value;
}
