/*
  ICS Dll1 - C++Builder translation of the Delphi library OverbyteIcsDll1.dpr
  (demos-delphi-vcl), ICS V9.8.

  Shows a TWSocket used inside a DLL. The only export is IcsDllDemo(HostName, Port,
  Buffer, BufSize): it connects to a TcpSrv demo server, sends "time", waits for the
  reply and returns it in Buffer. On entry *BufSize is the size of Buffer; on exit it
  is the reply length. The return value is 0 or an error code such as 10061 when the
  server is not running. Only basic data types cross the DLL boundary, so any
  language can call it.

  How this C++ version differs from the Delphi original, apart from syntax:
  - The worker thread gets its parameters through the constructor instead of properties.
  - The caller waits with TThread::WaitFor instead of polling a FReady flag and then
    freeing the thread; the thread is held in std::unique_ptr.
  - The exports are extern "C" so the name is exactly IcsDllDemo, as in the Delphi DLL.
  - The call gives up after ReplyTimeoutMs, closes the socket and returns -4; the Delphi
    DLL waited forever for a server that never completed its banner or reply. If the
    timer cannot be created the call returns -5 ("Timer failed") instead of waiting.
  - Winsock stays loaded after the first call until the process exits; the Delphi DLL
    cancelled the force-load at DLL detach, which a C++ DLL cannot do safely because
    unloading Winsock calls FreeLibrary, forbidden under the loader lock.
*/

#include <System.hpp>
#include <System.SysUtils.hpp>
#include <System.Classes.hpp>
#include <windows.h>
#pragma hdrstop

#include <cstring>
#include <memory>

#include "OverbyteIcsWinsock.hpp"
#include "OverbyteIcsWSocket.hpp"
#include "OverbyteIcsTypes.hpp"

#pragma package(smart_init)

namespace {

const UINT ReplyTimeoutMs = 30000;
const int TimeoutErrorCode = -4;
const int TimerErrorCode = -5;

class TClientThread;
thread_local TClientThread *CurrentWorker = nullptr;   // lets the Win32 timer callback find its worker thread object

// Copies Msg to Buffer without overflowing it; *BufSize returns the length copied.
void StrToBuffer(char *Buffer, int *BufSize, const AnsiString &Msg)
{
    if (Msg.Length() < *BufSize)
        *BufSize = Msg.Length();
    if (*BufSize > 0)
        std::memcpy(Buffer, Msg.c_str(), *BufSize);
}

// One thread per call, so several processes or threads can call the DLL at once.
class TClientThread : public TThread
{
public:
    TClientThread(char *HostName, char *Port, char *Buffer, int *BufSize, int *ErrorCode)
        : TThread(true), FClientSocket(nullptr), FBannerReceived(false), FErrorCode(ErrorCode),
          FBuffer(Buffer), FBufSize(BufSize), FHostName(HostName), FPort(Port)
    {
        FreeOnTerminate = false;
    }

protected:
    void __fastcall Execute() override;

private:
    TWSocket *FClientSocket;
    bool FBannerReceived;
    bool FFinished = false;
    int *FErrorCode;
    char *FBuffer;
    int *FBufSize;
    char *FHostName;
    char *FPort;

    void __fastcall ClientWSocketDataAvailable(TObject *Sender, System::Word Error);
    void __fastcall ClientWSocketSessionConnected(TObject *Sender, System::Word Error);
    void __fastcall ClientWSocketSessionClosed(TObject *Sender, System::Word Error);

public:
    void OnTimeout();
};

// Runs on the worker thread, dispatched by MessageLoop.
void CALLBACK ReplyTimerProc(HWND, UINT, UINT_PTR, DWORD)
{
    if (CurrentWorker)
        CurrentWorker->OnTimeout();
}

// TWSocket is event driven, so Execute only creates it and runs its message loop; the
// work happens in the event handlers. The socket must be created inside Execute so its
// window and winsock messages belong to this thread, not the caller's.
void __fastcall TClientThread::Execute()
{
    try {
        std::unique_ptr<TWSocket> socket(new TWSocket(nullptr));
        FClientSocket = socket.get();
        FClientSocket->OnDataAvailable = ClientWSocketDataAvailable;
        FClientSocket->OnSessionConnected = ClientWSocketSessionConnected;
        FClientSocket->OnSessionClosed = ClientWSocketSessionClosed;
        FClientSocket->LineMode = true;
        FClientSocket->Addr = String(AnsiString(FHostName));
        FClientSocket->Port = String(AnsiString(FPort));
        FClientSocket->Proto = "tcp";
        FClientSocket->Connect();

        // Thread timer (no window): its callback is dispatched by MessageLoop below.
        CurrentWorker = this;
        const UINT_PTR timer = SetTimer(nullptr, 0, ReplyTimeoutMs, ReplyTimerProc);
        if (timer == 0) {
            // Without the timer the wait would be unbounded; the unique_ptr closes the socket on return.
            CurrentWorker = nullptr;
            *FErrorCode = TimerErrorCode;
            StrToBuffer(FBuffer, FBufSize, AnsiString("Timer failed"));
            return;
        }

        // Runs until the handlers or the timeout post WM_QUIT.
        FClientSocket->MessageLoop();

        KillTimer(nullptr, timer);
        CurrentWorker = nullptr;
    }
    catch (Exception &E) {
        CurrentWorker = nullptr;
        *FErrorCode = -3;
        StrToBuffer(FBuffer, FBufSize, AnsiString(E.ClassName() + ":" + E.Message));
    }
    ReturnValue = 0;
}

// The socket is closed by the unique_ptr in Execute when the loop ends.
void TClientThread::OnTimeout()
{
    if (!FFinished) {
        FFinished = true;
        *FErrorCode = TimeoutErrorCode;
        StrToBuffer(FBuffer, FBufSize, AnsiString("Timeout"));
    }
    ::PostQuitMessage(0);
}

void __fastcall TClientThread::ClientWSocketSessionConnected(TObject *Sender, System::Word Error)
{
    if (Error != 0) {
        FFinished = true;
        *FErrorCode = Error;
        StrToBuffer(FBuffer, FBufSize, AnsiString("Connect failed"));
        PostMessage(FClientSocket->Handle, WM_QUIT, 0, 0);
    }
}

void __fastcall TClientThread::ClientWSocketSessionClosed(TObject *Sender, System::Word Error)
{
    PostMessage(FClientSocket->Handle, WM_QUIT, 0, 0);
}

// Line mode, so this fires once per complete line: first the banner the server sends on
// connect, then the reply to our command.
void __fastcall TClientThread::ClientWSocketDataAvailable(TObject *Sender, System::Word Error)
{
    AnsiString line = FClientSocket->ReceiveStrA();
    if (!FBannerReceived) {
        FBannerReceived = true;
        FClientSocket->SendStr(RawByteString("time\r\n"));
        return;
    }
    // Copy the reply without its trailing CR/LF, never overflowing the caller's buffer.
    if (line.Length() < *FBufSize)
        *FBufSize = line.Length() - 2;
    if (*FBufSize > 0)
        std::memcpy(FBuffer, line.c_str(), *FBufSize);
    FFinished = true;
    FClientSocket->CloseDelayed();
    *FErrorCode = 0;
}

} // namespace

extern "C" __declspec(dllexport) int WINAPI IcsDllDemo(char *HostName, char *Port, char *Buffer, int *BufSize)
{
    int result = -1;
    try {
        WSocketForceLoadWinsock();
        std::unique_ptr<TClientThread> worker(new TClientThread(HostName, Port, Buffer, BufSize, &result));
        worker->Start();
        worker->WaitFor();
    }
    catch (Exception &E) {
        result = -2;
        StrToBuffer(Buffer, BufSize, AnsiString(E.ClassName() + ": " + E.Message));
    }
    return result;
}
