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

  TBinaryWSocket derives from TWSocket and splits the incoming stream into text lines ended by
  CR/LF and binary blocks. A binary block starts with an 8 byte THdrRec (ID, then the size of the
  data that follows). The client switches to binary mode when the server answers "200 OK Binary";
  a block whose first byte is a printable character (32..127) is taken to be text again.

  How this C++ version differs from the Delphi original, apart from syntax:
  - The socket is created with no Owner and held in std::unique_ptr (the form owns nothing here).
  - THdrRec has no byte/word variant: the first ID byte is the low byte of ID.
  - TextLineIDs is a std::bitset<256> returned by reference instead of a TSysCharSet property.
  - The Delphi Timer1Timer handler is not converted: the form has no timer and nothing calls it.
  - FormClose does not append the version text to the caption a second time.
  - Two index resets in ParseReceiveBuffer that Delphi gets wrong are fixed, see the comments there.
*/

#include <vcl.h>
#pragma hdrstop

#include <cstring>
#include <memory>

#include "OverbyteIcsBinCliDemo1.h"
#include "OverbyteIcsIniFiles.hpp"

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

TBinClientForm *BinClientForm;

namespace {

// Offset of the first CR/LF pair in Buf[0..Count-1], or -1
int FindCrLf(const unsigned char *Buf, int Count)
{
    for (int i = 0; i < Count; i++) {
        if (Buf[i] == 13 && i + 1 < Count && Buf[i + 1] == 10)
            return i;
    }
    return -1;
}

// Holds BeginUpdate on a string list until the end of the scope, as Delphi's try/finally does.
class TUpdateGuard
{
public:
    explicit TUpdateGuard(TStrings *Lines) : FLines(Lines) { FLines->BeginUpdate(); }
    ~TUpdateGuard() { FLines->EndUpdate(); }
    TUpdateGuard(const TUpdateGuard &) = delete;
    TUpdateGuard &operator=(const TUpdateGuard &) = delete;
private:
    TStrings *FLines;
};

} // namespace

__fastcall TBinaryWSocket::TBinaryWSocket(TComponent *AOwner)
    : TWSocket(AOwner)
{
    // AssignDefaultValue may not reach the override while the base constructor runs, so set the state here too
    FBinStatus = stBinReady;
    FReceiveMode = rmLine;
    FReadOffs = 0;
    FWriteOffs = 0;
    FRemainingBytes = 0;
    FDataLen = 0;
    for (int ch = 32; ch <= 127; ch++)
        FTextLineIDs.set(ch);
}

void __fastcall TBinaryWSocket::TriggerBinaryBegin(const THdrRec &Header)
{
    FCurrentBlockSize = 0;
    if (FOnBinaryBegin)
        FOnBinaryBegin(this, Header);
}

void __fastcall TBinaryWSocket::TriggerBinaryEnd()
{
    if (FOnBinaryEnd)
        FOnBinaryEnd(this);
    FCurrentBlockSize = 0;
}

void __fastcall TBinaryWSocket::TriggerBinaryReceived(const void *Buf, int Size)
{
    if (FOnBinaryReceived)
        FOnBinaryReceived(this, Buf, Size);
    FCurrentBlockSize += Size;
}

void __fastcall TBinaryWSocket::TriggerLineReceived(const char *Buf)
{
    if (FOnLineReceived)
        FOnLineReceived(this, String(AnsiString(Buf)));
}

// Hands out every complete line or binary block in FRcvBuf[FReadOffs..], keeping a partial one for the next call.
// FReadOffs is where unprocessed data starts, FWriteOffs where the next Receive adds data.
void __fastcall TBinaryWSocket::ParseReceiveBuffer()
{
    if (FReceiveMode == rmLine) {
        while (FRemainingBytes > 0 && FReceiveMode == rmLine) {
            const int crlf = FindCrLf(&FRcvBuf[FReadOffs], FRemainingBytes);
            if (crlf < 0) {
                if (FRemainingBytes >= static_cast<int>(sizeof(FRcvBuf))) {
                    FRemainingBytes = 0;
                    FReadOffs = 0;
                    FWriteOffs = 0;
                    Abort();
                    TriggerLineReceived("! Buffer overflow attempt detected");
                }
                else if (FReadOffs != 0) {
                    // move the partial line to the start of the buffer
                    std::memmove(&FRcvBuf[0], &FRcvBuf[FReadOffs], FRemainingBytes);
                    FWriteOffs = FRemainingBytes;
                    FReadOffs = 0;
                }
                return;
            }
            FRcvBuf[FWriteOffs - FRemainingBytes + crlf] = 0;   // end the line where the CR is
            TriggerLineReceived(reinterpret_cast<const char *>(&FRcvBuf[FReadOffs]));
            FRemainingBytes -= crlf + 2;
            if (FRemainingBytes <= 0) {
                FRemainingBytes = 0;
                FWriteOffs = 0;
                FReadOffs = 0;
                return;
            }
            FReadOffs = FWriteOffs - FRemainingBytes;
        }
    }

    if (FReceiveMode != rmBinary)
        return;

    switch (FBinStatus) {
    case stBinReady:
        // a first byte that is a printable character means the server went back to text
        if (FTextLineIDs.test(FRcvBuf[FReadOffs])) {
            FReceiveMode = rmLine;
            ParseReceiveBuffer();
        }
        else if (FRemainingBytes >= static_cast<int>(sizeof(THdrRec))) {
            THdrRec header;
            std::memcpy(&header, &FRcvBuf[FReadOffs], sizeof(header));   // the buffer offset need not be aligned
            FDataLen = header.Size;
            TriggerBinaryBegin(header);
            if (FDataLen <= 0)
                TriggerBinaryEnd();
            else
                FBinStatus = stBinHdrReceived;
            FReadOffs += sizeof(THdrRec);
            FRemainingBytes -= sizeof(THdrRec);
            if (FRemainingBytes > 0) {
                ParseReceiveBuffer();
            }
            else {
                // Delphi reset only FWriteOffs here, so the next data was read 8 bytes too far into the buffer
                FReadOffs = 0;
                FWriteOffs = 0;
            }
        }
        break;
    case stBinHdrReceived:
        if (FRemainingBytes >= FDataLen) {
            TriggerBinaryReceived(&FRcvBuf[FReadOffs], FDataLen);
            FReadOffs += FDataLen;
            FRemainingBytes -= FDataLen;
            FDataLen = 0;
            FBinStatus = stBinReady;
            TriggerBinaryEnd();
            if (FRemainingBytes > 0) {
                ParseReceiveBuffer();
            }
            else {
                // Delphi's indentation hid that FWriteOffs was reset even when more data was left to parse
                FReadOffs = 0;
                FWriteOffs = 0;
            }
        }
        else {
            if (FRemainingBytes > 0) {
                TriggerBinaryReceived(&FRcvBuf[FReadOffs], FRemainingBytes);
                FDataLen -= FRemainingBytes;
            }
            FReadOffs = 0;
            FWriteOffs = 0;
            FRemainingBytes = 0;
        }
        break;
    }
}

bool __fastcall TBinaryWSocket::TriggerDataAvailable(System::Word ErrCode)
{
    const int received = Receive(&FRcvBuf[FWriteOffs], static_cast<int>(sizeof(FRcvBuf)) - FWriteOffs);
    if (received > 0) {
        FRemainingBytes += received;
        FWriteOffs += received;
    }
    if (FRemainingBytes > 0)
        ParseReceiveBuffer();
    return true;
}

// Called by TWSocket when a session closes, so a new connection starts with an empty parser
void __fastcall TBinaryWSocket::AssignDefaultValue()
{
    FReadOffs = 0;
    FWriteOffs = 0;
    FRemainingBytes = 0;
    FDataLen = 0;
    FBinStatus = stBinReady;
    FReceiveMode = rmLine;
    TWSocket::AssignDefaultValue();
}

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

void __fastcall TBinClientForm::ProcessCommand(TObject *Sender, const String &Str)
{
    TBinaryWSocket *cli = static_cast<TBinaryWSocket*>(Sender);
    if (Str.Pos("200 OK Binary") == 1)
        cli->ReceiveMode = rmBinary;
    Display("< " + Str);
}

void __fastcall TBinClientForm::SendData()
{
    try {
        String buf = SendEdit->Text;
        if (AddCRLFCheckBox->Checked)
            buf = buf + "\r\n";
        CliSocket->SendStr(buf);
    }
    catch (Exception &E) {
        Display(E.ClassName() + ": " + E.Message);
    }
    ActiveControl = SendEdit;
    SendEdit->SelectAll();
}

void __fastcall TBinClientForm::CliSocketSessionConnected(TObject *Sender, System::Word ErrCode)
{
    SendButton->Enabled = true;
    if (ErrCode != 0) {
        Display("Can't connect, error #" + IntToStr(ErrCode));
    }
    else {
        DisconnectButton->Enabled = true;
        SendData();
    }
}

void __fastcall TBinClientForm::CliSocketSessionClosed(TObject *Sender, System::Word ErrCode)
{
    DisconnectButton->Enabled = false;
    if (ErrCode != 0)
        Display("Disconnected, error #" + IntToStr(ErrCode));
    else
        Display("Disconnected");
}

void __fastcall TBinClientForm::CliSocketBinaryBegin(TObject *Sender, const THdrRec &Header)
{
    Display("! Binary Begin - ID=" + IntToStr(Header.ID) + " size=" + IntToStr(Header.Size));
}

void __fastcall TBinClientForm::CliSocketBinaryReceived(TObject *Sender, const void *Buf, int Size)
{
    // the block is shown as text; AnsiString(ptr, len) copies exactly Size bytes
    Display(String(AnsiString(static_cast<const char *>(Buf), Size)));
}

void __fastcall TBinClientForm::CliSocketBinaryEnd(TObject *Sender)
{
    Display("! Binary End - total size=" + IntToStr(static_cast<TBinaryWSocket*>(Sender)->CurrentBlockSize));
}

void __fastcall TBinClientForm::CliSocketLineReceived(TObject *Sender, const String Line)
{
    ProcessCommand(Sender, Line);
}

void __fastcall TBinClientForm::FormCreate(TObject *Sender)
{
    Caption = Caption + IcsVerLitSample;   // common ICS sample caption: version and date
    FIniFileName = GetIcsIniFileName();
    CliSocket.reset(new TBinaryWSocket(nullptr));
    CliSocket->ComponentOptions = TWSocketOptions() << wsoNoReceiveLoop;
    CliSocket->LineMode = false;   // the socket class does its own line splitting
    CliSocket->ReceiveMode = rmLine;
    CliSocket->OnSessionConnected = CliSocketSessionConnected;
    CliSocket->OnSessionClosed = CliSocketSessionClosed;
    CliSocket->OnBinaryBegin = CliSocketBinaryBegin;
    CliSocket->OnBinaryEnd = CliSocketBinaryEnd;
    CliSocket->OnBinaryReceived = CliSocketBinaryReceived;
    CliSocket->OnLineReceived = CliSocketLineReceived;
}

void __fastcall TBinClientForm::FormClose(TObject *Sender, TCloseAction &Action)
{
    std::unique_ptr<TIcsIniFile> ini(new TIcsIniFile(FIniFileName, true));
    ini->WriteInteger("Window", "Top", Top);
    ini->WriteInteger("Window", "Left", Left);
    ini->WriteInteger("Window", "Width", Width);
    ini->WriteInteger("Window", "Height", Height);
    ini->WriteString("Data", "Server", ServerEdit->Text);
    ini->WriteString("Data", "Port", PortEdit->Text);
    ini->WriteString("Data", "Command", SendEdit->Text);
    ini->UpdateFile();
}

void __fastcall TBinClientForm::FormShow(TObject *Sender)
{
    if (FInitialized)
        return;
    FInitialized = true;

    {
        std::unique_ptr<TIcsIniFile> ini(new TIcsIniFile(FIniFileName, true));
        Top = ini->ReadInteger("Window", "Top", Top);
        Left = ini->ReadInteger("Window", "Left", Left);
        Width = ini->ReadInteger("Window", "Width", Width);
        Height = ini->ReadInteger("Window", "Height", Height);

        PortEdit->Text = ini->ReadString("Data", "Port", "telnet");
        ServerEdit->Text = ini->ReadString("Data", "Server", "localhost");
        SendEdit->Text = ini->ReadString("Data", "Command", "binary 128");
    }

    DisplayMemo->Clear();
    ActiveControl = SendEdit;
    SendEdit->SelectAll();
}

// Adds a line to the memo, dropping the oldest 50 lines once it holds more than 200
void __fastcall TBinClientForm::Display(const String &Msg)
{
    {
        TUpdateGuard guard(DisplayMemo->Lines);
        if (DisplayMemo->Lines->Count > 200) {
            for (int i = 1; i <= 50; i++)
                DisplayMemo->Lines->Delete(0);
        }
        DisplayMemo->Lines->Add(Msg);
    }
    SendMessage(DisplayMemo->Handle, EM_SCROLLCARET, 0, 0);
}

void __fastcall TBinClientForm::DisconnectButtonClick(TObject *Sender)
{
    CliSocket->Close();
}

void __fastcall TBinClientForm::SendButtonClick(TObject *Sender)
{
    if (CliSocket->State == wsConnected) {
        CliSocket->ReceiveMode = rmLine;
        SendData();
        return;
    }

    CliSocket->Proto = "tcp";
    CliSocket->Port = PortEdit->Text;
    CliSocket->Addr = ServerEdit->Text;
    // Connect is asynchronous: OnSessionConnected reports the result and sends the data
    CliSocket->Connect();
    SendButton->Enabled = false;
}
