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

  Shows a TSslHttpRest used inside a DLL. The only export is IcsDllDemo(URL, Buffer,
  BufSize): it gets the URL (http or https) and returns the document in Buffer. On
  entry *BufSize is the size of Buffer; on exit it is the document length, or minus the
  required length when Buffer was too short (Buffer then holds the start of the
  document). The return value is the HTTP status such as 200, or 404 when the document
  is not found, or an error code. 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 request is aborted after RequestTimeoutMs and the call returns -4; the Delphi DLL
    waited forever for a peer that stopped responding. If the timer cannot be created the
    call returns -5 ("Timer failed") instead of waiting.
  - The request ends on OnRestRequestDone, which the component also calls when an async
    request fails to start; the Delphi OnRequestDone is not called then, so the Delphi
    DLL waited in its message loop forever. A request refused before it starts (for
    example an invalid URL) is also reported, again because the Delphi DLL waited forever.
*/

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

#include <cstring>
#include <memory>

#include "OverbyteIcsTypes.hpp"
#include "OverbyteIcsSslHttpRest.hpp"

#pragma package(smart_init)

namespace {

const UINT RequestTimeoutMs = 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 *Url, char *Buffer, int *BufSize, int *ErrorCode)
        : TThread(true), FHttpCli(nullptr), FDone(false), FUrl(Url), FErrorCode(ErrorCode),
          FBuffer(Buffer), FBufSize(BufSize)
    {
        FreeOnTerminate = false;
    }

protected:
    void __fastcall Execute() override;

private:
    TSslHttpRest *FHttpCli;
    bool FDone;
    char *FUrl;
    int *FErrorCode;
    char *FBuffer;
    int *FBufSize;

    void __fastcall HttpCliRequestDone(TObject *Sender, THttpRequest RqType, System::Word ErrCode);

public:
    void OnTimeout();
};

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

// The component is event driven, so Execute only creates it, starts the request and runs
// the message loop; the work happens in HttpCliRequestDone. It 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<TSslHttpRest> httpCli(new TSslHttpRest(nullptr));
        FHttpCli = httpCli.get();
        FHttpCli->OnRestRequestDone = HttpCliRequestDone;

        // Async request: returns 0 once started and the result arrives in the event.
        const int started = FHttpCli->RestRequest(httpGET, String(AnsiString(FUrl)), true);
        if (started != 0 && !FDone) {
            *FErrorCode = started;
            StrToBuffer(FBuffer, FBufSize, AnsiString(FHttpCli->ReasonPhrase));
            return;
        }

        // Thread timer (no window): its callback is dispatched by MessageLoop below.
        CurrentWorker = this;
        const UINT_PTR timer = SetTimer(nullptr, 0, RequestTimeoutMs, RequestTimerProc);
        if (timer == 0) {
            // Without the timer the wait would be unbounded; abort the request and fail the call.
            CurrentWorker = nullptr;
            FDone = true;
            FHttpCli->Abort();
            *FErrorCode = TimerErrorCode;
            StrToBuffer(FBuffer, FBufSize, AnsiString("Timer failed"));
            return;
        }

        // Runs until the handler or the timeout posts WM_QUIT.
        FHttpCli->CtrlSocket->MessageLoop();

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

// THttpCli::Abort makes the component call OnRestRequestDone; FDone keeps that call from
// overwriting the timeout result.
void TClientThread::OnTimeout()
{
    if (!FDone) {
        FDone = true;
        *FErrorCode = TimeoutErrorCode;
        StrToBuffer(FBuffer, FBufSize, AnsiString("Timeout"));
        FHttpCli->Abort();
    }
    ::PostQuitMessage(0);
}

void __fastcall TClientThread::HttpCliRequestDone(TObject *Sender, THttpRequest RqType, System::Word ErrCode)
{
    if (!FDone) {
        FDone = true;
        if (ErrCode != 0) {
            *FErrorCode = ErrCode;
            StrToBuffer(FBuffer, FBufSize, AnsiString(FHttpCli->ReasonPhrase));
        }
        else if (FHttpCli->StatusCode != 200) {
            *FErrorCode = FHttpCli->StatusCode;
            StrToBuffer(FBuffer, FBufSize, AnsiString(FHttpCli->ReasonPhrase));
        }
        else {
            *FErrorCode = FHttpCli->StatusCode;
            // The property rebuilds the whole body on every read, so read it once.
            const AnsiString body = FHttpCli->ResponseOctet;
            int n = body.Length();
            if (n > *FBufSize) {
                // Caller's buffer is too small: copy what fits and return minus the needed size.
                n = *FBufSize;
                *FBufSize = -body.Length();
            }
            else
                *FBufSize = n;
            if (n > 0)
                std::memcpy(FBuffer, body.c_str(), n);
        }
    }
    PostMessage(FHttpCli->CtrlSocket->Handle, WM_QUIT, 0, 0);
}

} // namespace

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