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

  The TcpSrv telnet-style server (help, who, time, exit, binary [size]) listening on several
  sockets at once: the main socket on 0.0.0.0 port telnet, an IPv6 socket on :: port telnet when
  IPv6 is available, and a socket on LocalHost port 24. Start and Stop buttons open and close them.
  The window position is kept in the ICS ini file.

  How this C++ version differs from the Delphi original, apart from syntax:
  - The Delphi dfm does not bind OnDebugDisplay, so the debug log chosen in WMAppStartup was never
    shown; FormCreate assigns it.
  - The unused LeakedSL global is not converted.
*/

#include <vcl.h>
#pragma hdrstop

#include "OverbyteIcsTcpSrv1IPv6.h"

#include <memory>
#include <vector>
#include <cstring>

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

TTcpSrvForm *TcpSrvForm;

namespace {

const String TcpSrvCopyRight = String(L" TcpSrv") + IcsVerLitCopyright;

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

// Header sent in front of the binary data; the client looks at ID and Size.
struct THdrRec
{
    int ID;
    int Size;
};

const int MaxBinarySize = 65535;

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

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

// The multi-listen sockets and the main socket are reported with the same layout.
String ListenerStatus(const String &Which, int Index, const String &Addr, int Port, TSocketState State)
{
    return Format(String("%s index #%d IP/port [%s]:%d State: %s"),
                  ARRAYOFCONST((Which, Index, Addr, Port, String(SocketStateNames[State]))));
}

} // namespace

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

void __fastcall TTcpSrvForm::FormCreate(TObject *Sender)
{
    Caption = Caption + IcsVerLitSample;   // common ICS sample caption: version and date
    FIniFileName = GetIcsIniFileName();
    ReportMemoryLeaksOnShutdown = (DebugHook != 0);
    WSocketServer1->OnDebugDisplay = WSocketServer1DebugDisplay;   // Delphi dfm leaves it unbound
}

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 startup code until the form is visible.
        PostMessage(Handle, WM_APPSTARTUP, 0, 0);
    }
}

void __fastcall TTcpSrvForm::FormClose(TObject *Sender, TCloseAction &Action)
{
    WSocketServer1->MultiClose();
    WSocketServer1->DisconnectAll();
    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();
}

// Shows a line in the memo; past 200 lines the oldest 50 are dropped so the memo cannot overflow.
void TTcpSrvForm::Display(const String &Msg)
{
    TMemoUpdate update(DisplayMemo);
    if (DisplayMemo->Lines->Count > 200) {
        for (int i = 0; i < 50; ++i)
            DisplayMemo->Lines->Delete(0);
    }
    DisplayMemo->Lines->Add(Msg);
}

void __fastcall TTcpSrvForm::doStartClick(TObject *Sender)
{
    Display("Startting Server");
    PostMessage(Handle, WM_APPSTARTUP, 0, 0);
}

void __fastcall TTcpSrvForm::doStopClick(TObject *Sender)
{
    Display("Stopping Server");
    WSocketServer1->MultiClose();
    WSocketServer1->DisconnectAll();
}

// Posted from FormShow and from the Start button; configures the listeners and starts them.
void __fastcall TTcpSrvForm::WMAppStartup(TMessage &Msg)
{
    Display(TcpSrvCopyRight);
    Display(Overbyteicswsocket::CopyRight);
    Display(Overbyteicswsockets::CopyRight);
    String MyHostName;
    WSocket_gethostname(MyHostName);
    Display(" I am \"" + MyHostName + "\"");
    Display(" IP: " + LocalIPList(sfAny)->Text);
    WSocketServer1->Addr = "0.0.0.0";
    WSocketServer1->Port = "telnet";
    WSocketServer1->ClientClass = __classid(TTcpSrvClient);
    WSocketServer1->WSDebugOptions = TWSDebugOptions() << wsdlogConnWS << wsdlogErrs << wsdlogListen
                                                       << wsdlogAccept << wsdlogInfo;

    WSocketServer1->MultiListenSockets->Clear();
    // Additional listening sockets besides the main one.
    if (IsIPv6Available()) {
        TWSocketMultiListenItem *ipv6Socket = WSocketServer1->MultiListenSockets->Add();
        ipv6Socket->Addr = "::";   // IPv6 unspecified address, same as IPv4 "0.0.0.0"
        ipv6Socket->Port = "telnet";
    }
    TWSocketMultiListenItem *localSocket = WSocketServer1->MultiListenSockets->Add();
    localSocket->Addr = "LocalHost";
    localSocket->Port = "24";
    localSocket->SocketFamily = sfIPv4;   // sfAny would take whichever family the OS prefers

    try {
        // MultiListen starts every listener that is not listening yet, the main socket included.
        // It raises if one fails, most likely because IP:Port is in use; the demo only shows it.
        WSocketServer1->MultiListen();
    }
    catch (Exception &E) {
        Display(E.ClassName() + " " + E.Message);
    }

    Display(ListenerStatus("Main        ", -1, WSocketServer1->Addr, WSocketServer1->PortNum,
                           WSocketServer1->State));
    for (int i = 0; i < WSocketServer1->MultiListenSockets->Count; ++i) {
        TWSocketMultiListenItem *item = WSocketServer1->MultiListenSockets->Items[i];
        Display(ListenerStatus("Multi-listen", i + 2, item->AddrResolved, item->PortNum, item->State));
    }
}

void __fastcall TTcpSrvForm::WSocketServer1ClientConnect(TObject *Sender, TWSocketClient *Client, System::Word Error)
{
    TTcpSrvClient *cli = static_cast<TTcpSrvClient *>(Client);   // ClientClass is always TTcpSrvClient
    Display("Client connected. Remote: [" + cli->PeerAddr + "]:" + cli->PeerPort +
            " Local: [" + cli->GetXAddr() + "]:" + cli->GetXPort());
    Display("There is now " + IntToStr(static_cast<TWSocketServer *>(Sender)->ClientCount) +
            " clients connected.");
    cli->LineMode = true;
    cli->LineEdit = true;
    cli->LineLimit = 80;   // do not accept long lines
    // Tag the client with the port of the listening socket that accepted it.
    if (WSocketServer1->MultiListenIndex == -1)
        cli->ListeningSocketsPortNum = WSocketServer1->PortNum;   // the main socket
    else
        cli->ListeningSocketsPortNum = WSocketServer1->MultiListenSockets->Items[WSocketServer1->MultiListenIndex]->PortNum;
    cli->OnDataAvailable = ClientDataAvailable;
    cli->OnLineLimitExceeded = ClientLineLimitExceeded;
    cli->OnBgException = ClientBgException;
    cli->ConnectTime = Now();
}

void __fastcall TTcpSrvForm::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));
    // The client is still counted while its disconnect event runs.
    Display("There is now " + IntToStr(static_cast<TWSocketServer *>(Sender)->ClientCount - 1) +
            " clients connected.");
}

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

// Triggered instead of raising an exception.
void __fastcall TTcpSrvForm::WSocketServer1Exception(TObject *Sender, ESocketException *SocExcept)
{
    Display("Server socket exception): " + SocExcept->Message);
    Display("Server socket exception (friendly): " + SocExcept->FriendlyMsg);
    Display("Exception details - Error: " + SocExcept->ErrorMessage +
            ", Errnr: " + IntToStr(SocExcept->ErrorCode) + ", Function: " +
            SocExcept->Func + ", IP: " + SocExcept->IPStr + ", Port: " +
            SocExcept->PortStr + ", Proto: " + SocExcept->ProtoStr);
}

// Triggered before a connection is accepted and before a client object exists.
void __fastcall TTcpSrvForm::WSocketServer1SessionAvailable(TObject *Sender, System::Word ErrCode)
{
    TWSocketServer *server = static_cast<TWSocketServer *>(Sender);
    if (server->MultiListenIndex == -1) {
        Display("Main socket received a connection request on [" + server->Addr + "]:" +
                IntToStr(server->PortNum) + ", error #" + IntToStr(ErrCode));
    }
    else {
        TWSocketMultiListenItem *item = server->MultiListenSockets->Items[server->MultiListenIndex];
        Display("Multi-listen socket index #" + IntToStr(server->MultiListenIndex) +
                " received a connection request on [" + item->AddrResolved + "]:" +
                IntToStr(item->PortNum) + ", error #" + IntToStr(ErrCode));
    }
}

void __fastcall TTcpSrvForm::WSocketServer1SessionClosed(TObject *Sender, System::Word ErrCode)
{
    TWSocketServer *server = static_cast<TWSocketServer *>(Sender);
    if (server->MultiListenIndex == -1) {
        Display("Main session closed [" + server->Addr + "]:" + IntToStr(server->PortNum) +
                ", error #" + IntToStr(ErrCode));
    }
    else {
        TWSocketMultiListenItem *item = server->MultiListenSockets->Items[server->MultiListenIndex];
        Display("Multi-listen session closed [" + item->AddrResolved + "]:" +
                IntToStr(item->PortNum) + ", error #" + IntToStr(ErrCode));
    }
}

void __fastcall TTcpSrvForm::ClientLineLimitExceeded(TObject *Sender, int Cnt, bool &ClearData)
{
    TTcpSrvClient *cli = static_cast<TTcpSrvClient *>(Sender);
    Display("Line limit exceeded from [" + cli->GetPeerAddr() + "]. Closing.");
    ClearData = true;
    cli->Close();
}

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

void TTcpSrvForm::ProcessData(TTcpSrvClient *Client)
{
    const String &line = Client->RcvdLine;
    if (IsCommand(line, "help")) {
        Client->SendStr(String("Commands are:\r\n  exit\r\n  who\r\n  time\r\n  binary [size]\r\n"));
    }
    else if (IsCommand(line.SubString(1, 6), "binary")) {
        const int size = StrToIntDef(line.SubString(7, line.Length()), 0);
        if (size <= 0) {
            Client->SendStr(String("500 Error binary size not spezified\r\n"));
        }
        else if (size > MaxBinarySize) {
            Client->SendStr("500 Error binary size limited to " + IntToStr(MaxBinarySize) + " bytes\r\n");
        }
        else {
            Client->SendStr("200 OK Binary " + IntToStr(size) + " bytes requested\r\n");
            // Header, then size bytes of '1' with the last one replaced by 'E'.
            std::vector<char> buffer(sizeof(THdrRec) + size, '1');
            const THdrRec header = {0, size};   // an ID below 32 marks valid binary data
            std::memcpy(buffer.data(), &header, sizeof(header));
            buffer.back() = 'E';
            Client->Send(buffer.data(), static_cast<int>(buffer.size()));
        }
    }
    else if (IsCommand(line, "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(line, "time")) {
        Client->SendStr(DateTimeToStr(Now()) + "\r\n");
    }
    else if (IsCommand(line, "who")) {
        // The client's Server property is a TCustomWSocketServer, which hides ClientCount.
        TWSocketServer *server = static_cast<TWSocketServer *>(Client->Server);
        Client->SendStr("There are " + IntToStr(server->ClientCount) + " connected users:\r\n");
        for (int i = server->ClientCount - 1; i >= 0; --i) {
            TTcpSrvClient *other = static_cast<TTcpSrvClient *>(server->Client[i]);
            Client->SendStr(other->PeerAddr + ":" + other->GetPeerPort() + " " +
                            DateTimeToStr(other->ConnectTime) + "\r\n");
        }
    }
    else if (Client->State == wsConnected) {
        Client->SendStr("Unknown command: '" + line + "'\r\n");
    }
}

// Background exception of the listening socket; this server keeps running.
void __fastcall TTcpSrvForm::WSocketServer1BgException(TObject *Sender, Exception *E, bool &CanClose)
{
    Display("Server exception occured: " + E->ClassName() + ": " + E->Message);
    CanClose = false;
}

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