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

  Posts form fields to a web server, and uploads a file to it by one of four methods
  (POST name as page, POST name as argument, PUT name as argument, POST multipart/form-data).
  The ICS WebServ demo can act as the server.

  How this C++ version differs from the Delphi original, apart from syntax:
  - Delphi demo defects are corrected where they happen (page-name check, bandwidth option,
    leaked response stream, division by zero in the speed report, multipart delimiters).
  - The form owns each request's send and receive streams, allows one request at a time (the
    Post and Upload buttons are disabled while it runs), and frees the streams on completion,
    on a failed start, and in the destructor. The Delphi frees them only in OnRequestDone.
*/

#include <vcl.h>
#pragma hdrstop

#include "OverbyteIcsHttpPost1.h"

#include <memory>

#include "OverbyteIcsIniFiles.hpp"
#include "OverbyteIcsUrl.hpp"
#include "OverbyteIcsStreams.hpp"
#include "OverbyteIcsUtils.hpp"
#include "OverbyteIcsHtmlUtils.hpp"

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

THttpPostForm *HttpPostForm;

namespace {

const String SectionWindow = "Window";
const String KeyTop = "Top";
const String KeyLeft = "Left";
const String KeyWidth = "Width";
const String KeyHeight = "Height";
const String SectionData = "Data";
const String KeyFirstName = "FirstName";
const String KeyLastName = "LastName";
const String KeyActionURL = "ActionURL";
const String KeyUploadURL = "UploadURL";
const String KeyFilePath = "UploadFilePath";
const String KeyBandwidthLimit = "BandwidthLimit";
const String KeyUploadMethod = "UploadMethod";
const String KeyFileDescr = "FileDescr";

const int MethodPostBinPage = 0;
const int MethodPostBinArgs = 1;
const int MethodPutBinArgs = 2;
const int MethodPostMIME = 3;

// True when the last path segment of the URL looks like a page name (it has a dot).
// The query and fragment are not part of the path, so they are cut off first.
bool UrlEndsWithPageName(const String &Url)
{
    String Path = Url;
    const int Scheme = Path.Pos("://");
    if (Scheme > 0)
        Path = Path.SubString(Scheme + 3, MaxInt);
    for (int i = 1; i <= Path.Length(); i++) {
        if (Path[i] == '?' || Path[i] == '#') {
            Path = Path.SubString(1, i - 1);
            break;
        }
    }
    const int Slash = Path.Pos("/");
    if (Slash == 0)
        return false;   // only a host name
    Path = Path.SubString(Slash, MaxInt);
    return Path.SubString(Path.LastDelimiter("/") + 1, MaxInt).Pos(".") > 0;
}

}  // namespace

__fastcall THttpPostForm::THttpPostForm(TComponent *Owner)
    : TForm(Owner), FInitialized(false), FStartTime(0)
{
}

// THttpCli::Destroy does not free SendStream or RcvdStream, and HttpCli1 is freed after this body (by the
// VCL base destructor), so detach both before the unique_ptr members free them. No OnDestroy: in C++ it
// runs from the VCL base destruction, after the members are gone.
__fastcall THttpPostForm::~THttpPostForm()
{
    if (HttpCli1) {
        HttpCli1->OnRequestDone = nullptr;   // a request may still be pending
        HttpCli1->SendStream = nullptr;
        HttpCli1->RcvdStream = nullptr;
    }
}

// Starts one request. The form keeps ownership of both streams; HttpCli1RequestDone, a failed start, or
// the destructor frees them.
void THttpPostForm::StartRequest(std::unique_ptr<TStream> Send, bool Put)
{
    std::unique_ptr<TStream> Rcvd(new TMemoryStream);
    FSendStream = std::move(Send);
    FRcvdStream = std::move(Rcvd);
    HttpCli1->SendStream = FSendStream.get();
    HttpCli1->RcvdStream = FRcvdStream.get();
    FBusy = true;
    PostButton->Enabled = false;
    UploadButton->Enabled = false;
    try {
        if (Put)
            HttpCli1->PutASync();
        else
            HttpCli1->PostASync();
    }
    catch (...) {
        DetachStreams();   // the request did not start; a RequestDone that still arrives finds FBusy false
        throw;
    }
    // THttpCli.Login can swallow a startup exception and return to httpReady without RequestDone; a finished request has already cleared FBusy.
    if (FBusy && HttpCli1->State == httpReady) {
        Display("Post failed with error: " + IntToStr(HttpCli1->StatusCode) + " " + HttpCli1->ReasonPhrase);
        DetachStreams();
    }
}

// Detaches and frees the current request's streams and allows the next request.
void THttpPostForm::DetachStreams()
{
    HttpCli1->SendStream = nullptr;
    HttpCli1->RcvdStream = nullptr;
    FSendStream.reset();
    FRcvdStream.reset();
    FBusy = false;
    PostButton->Enabled = true;
    UploadButton->Enabled = true;
}

void __fastcall THttpPostForm::FormCreate(TObject *Sender)
{
    Caption = Caption + IcsVerLitSample;   // common ICS sample caption: version and date
    FIniFileName = GetIcsIniFileName(true);
}

void __fastcall THttpPostForm::FormShow(TObject *Sender)
{
    if (!FInitialized) {
        FInitialized = true;
        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);
        FirstNameEdit->Text = IniFile->ReadString(SectionData, KeyFirstName, "John");
        LastNameEdit->Text = IniFile->ReadString(SectionData, KeyLastName, "Doe");
        ActionURLEdit->Text = IniFile->ReadString(SectionData, KeyActionURL,
                                                  "http://localhost/cgi-bin/FormHandler");
        UploadURLEdit->Text = IniFile->ReadString(SectionData, KeyUploadURL,
                                                  "http://localhost/cgi-bin/FileUpload\\unit1.pas");
        FileNameEdit->Text = IniFile->ReadString(SectionData, KeyFilePath, "c:\\temp\\unit1.pas");
        BandwidthLimitEdit->Text = IniFile->ReadString(SectionData, KeyBandwidthLimit, "1000000");
        UploadMethod->ItemIndex = IniFile->ReadInteger(SectionData, KeyUploadMethod, 0);
        FileDescr->Text = IniFile->ReadString(SectionData, KeyFileDescr, "File upload description");
        DisplayMemo->Clear();
    }
    Randomize();
}

void __fastcall THttpPostForm::FormClose(TObject *Sender, TCloseAction &Action)
{
    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->WriteString(SectionData, KeyFirstName, FirstNameEdit->Text);
    IniFile->WriteString(SectionData, KeyLastName, LastNameEdit->Text);
    IniFile->WriteString(SectionData, KeyActionURL, ActionURLEdit->Text);
    IniFile->WriteString(SectionData, KeyUploadURL, UploadURLEdit->Text);
    IniFile->WriteString(SectionData, KeyFilePath, FileNameEdit->Text);
    IniFile->WriteString(SectionData, KeyBandwidthLimit, BandwidthLimitEdit->Text);
    IniFile->WriteInteger(SectionData, KeyUploadMethod, UploadMethod->ItemIndex);
    IniFile->WriteString(SectionData, KeyFileDescr, FileDescr->Text);
    IniFile->UpdateFile();
}

// Adds a line to the log, keeping the last 200 lines.
void THttpPostForm::Display(const String &Msg)
{
    DisplayMemo->Lines->BeginUpdate();
    while (DisplayMemo->Lines->Count > 200)
        DisplayMemo->Lines->Delete(0);
    DisplayMemo->Lines->Add(Msg);
    DisplayMemo->Lines->EndUpdate();
    SendMessage(DisplayMemo->Handle, EM_SCROLLCARET, 0, 0);
}

// Posts the two name fields as application/x-www-form-urlencoded.
void __fastcall THttpPostForm::PostButtonClick(TObject *Sender)
{
    if (FBusy)
        return;   // one request at a time: it owns HttpCli1's streams
    const AnsiString Data = AnsiString("FirstName=") + UrlEncodeToA(Trim(FirstNameEdit->Text)) +
                            "&LastName=" + UrlEncodeToA(Trim(LastNameEdit->Text)) + "&Submit=Submit";
    std::unique_ptr<TStream> Body(new TMemoryStream);
    Body->Write(Data.c_str(), Data.Length());
    Body->Seek(static_cast<__int64>(0), soBeginning);   // plain 0 is ambiguous between the int and Int64 overloads
    HttpCli1->URL = Trim(ActionURLEdit->Text);
    HttpCli1->ContentTypePost = "application/x-www-form-urlencoded";
    DisplayMemo->Lines->Add("POST data: " + String(Data));
    DisplayMemo->Lines->Add("To URL: " + HttpCli1->URL);
    DisplayMemo->Lines->Add("Content Type: " + HttpCli1->ContentTypePost);
    FStartTime = 0;   // no throughput report for a field post
    StartRequest(std::move(Body), false);
}

void __fastcall THttpPostForm::SelectFileClick(TObject *Sender)
{
    OpenDialog->InitialDir = ExtractFilePath(FileNameEdit->Text);
    OpenDialog->FileName = FileNameEdit->Text;
    if (OpenDialog->Execute())
        FileNameEdit->Text = OpenDialog->FileName;
}

// There are several ways to upload a file to a web server; each needs script or custom code
// at the server that understands the syntax.
void __fastcall THttpPostForm::UploadButtonClick(TObject *Sender)
{
    if (FBusy)
        return;   // one request at a time: it owns HttpCli1's streams
    const int Method = UploadMethod->ItemIndex;
    // Delphi refuses any URL containing a dot, which rejects every host name with a domain; only the page part is tested here.
    if (Method == MethodPostBinPage && UrlEndsWithPageName(UploadURLEdit->Text)) {
        DisplayMemo->Lines->Add("URL can not have a page name");
        return;
    }
    const String FileName = Trim(FileNameEdit->Text);
    const String NameOnly = ExtractFileName(FileName);
    const __int64 FileSize = IcsGetFileSize(FileName);   // sanity check
    if (FileSize <= 0) {
        DisplayMemo->Lines->Add("File not found");
        return;
    }
    HttpCli1->URL = Trim(UploadURLEdit->Text);
    HttpCli1->BandwidthLimit = StrToIntDef(BandwidthLimitEdit->Text, 1000000);
    // Delphi never switches the option off again, so a limit of 0 kept the previous limit.
    if (HttpCli1->BandwidthLimit > 0)
        HttpCli1->Options = HttpCli1->Options << httpoBandwidthControl;
    else
        HttpCli1->Options = HttpCli1->Options >> httpoBandwidthControl;
    FStartTime = GetTickCount();
    DisplayMemo->Lines->Add("File uploading: " + FileName + ", Size: " + IntToKbyte(FileSize));

    if (Method == MethodPostBinPage) {
        std::unique_ptr<TStream> Send(new TIcsBufferedFileStream(FileName, fmOpenRead, MAX_BUFSIZE));
        HttpCli1->ContentTypePost = "application/binary";
        if (!HttpCli1->URL.EndsWith("/"))
            HttpCli1->URL = HttpCli1->URL + "/";
        HttpCli1->URL = HttpCli1->URL + NameOnly;
        DisplayMemo->Lines->Add("To URL: " + HttpCli1->URL);
        DisplayMemo->Lines->Add("Content Type: " + HttpCli1->ContentTypePost);
        StartRequest(std::move(Send), false);
    }
    else if (Method == MethodPostBinArgs || Method == MethodPutBinArgs) {
        std::unique_ptr<TStream> Send(new TIcsBufferedFileStream(FileName, fmOpenRead, MAX_BUFSIZE));
        HttpCli1->ContentTypePost = "application/binary";
        const String Args = String(AnsiString("FileName=") + UrlEncodeToA(NameOnly) + "&FileTitle=" +
                                   UrlEncodeToA(Trim(FileDescr->Text)));
        HttpCli1->URL = HttpCli1->URL + "?" + Args;
        DisplayMemo->Lines->Add("To URL: " + HttpCli1->URL);
        DisplayMemo->Lines->Add("Content Type: " + HttpCli1->ContentTypePost);
        StartRequest(std::move(Send), Method == MethodPutBinArgs);
    }
    else if (Method == MethodPostMIME) {
        const String Boundary = "-----------------------------" + IntToHex(Random(MaxInt), 8) +
                                IntToHex(Random(MaxInt), 8);
        // RFC 2046 delimiters are "--" + boundary; Delphi sent the bare boundary.
        const String Delim = "--" + Boundary;
        HttpCli1->ContentTypePost = "multipart/form-data; boundary=" + Boundary;
        const String Header = Delim + "\r\n" +
            "Content-Disposition: form-data; name=\"FileName\"; FileName=\"" +
            TextToHtmlText(ExtractFileName(NameOnly)) + "\"\r\n" +
            "Content-Type: " + MimeTypesList1->TypeFromFile(NameOnly) + "\r\n\r\n";
        const String Footer = "\r\n" + Delim + "\r\n" +
            "Content-Disposition: form-data; name=\"FileTitle\"\r\n\r\n" +
            TextToHtmlText(FileDescr->Text) + "\r\n" + Delim + "\r\n" +
            "Content-Disposition: form-data; name=\"Submit\"\r\n\r\n" +
            "SubmitFile\r\n" + Delim + "--\r\n";
        std::unique_ptr<TStream> Send(new TMultiPartFileReader(FileName, RawByteString(AnsiString(Header)),
                                                               RawByteString(AnsiString(Footer))));
        Send->Position = 0;
        DisplayMemo->Lines->Add("POST data: " + Header + "\r\n(file)\r\n" + Footer);
        DisplayMemo->Lines->Add("To URL: " + HttpCli1->URL);
        StartRequest(std::move(Send), false);
    }
}

void __fastcall THttpPostForm::HttpCli1RequestDone(TObject *Sender, THttpRequest RqType, System::Word ErrCode)
{
    if (!FBusy)
        return;   // the request failed to start and its streams are already freed
    // Take the streams into locals, so they are freed when this handler returns, whatever path it takes.
    std::unique_ptr<TStream> Sent = std::move(FSendStream);
    std::unique_ptr<TStream> Rcvd = std::move(FRcvdStream);
    DetachStreams();
    const __int64 ByteCount = Sent ? Sent->Size : 0;   // Delphi uses Integer, which overflows past 2 GB

    if (ErrCode != 0) {
        Display("Post failed with error #" + IntToStr(ErrCode));
        return;
    }
    if (HttpCli1->StatusCode != 200) {
        Display("Post failed with error: " + IntToStr(HttpCli1->StatusCode) + " " + HttpCli1->ReasonPhrase);
        return;
    }
    Display("Post was OK. Response was:");
    AnsiString Data;   // the WebServ demo sends AnsiString replies
    if (Rcvd) {
        Rcvd->Seek(static_cast<__int64>(0), soBeginning);
        Data.SetLength(static_cast<int>(Rcvd->Size));
        if (Data.Length() > 0)
            Rcvd->Read(&Data[1], Data.Length());
    }
    Display(String(Data));
    if (FStartTime != 0) {
        // GetTickCount can return the same tick for a fast upload; Delphi then divided by zero.
        const int Duration = std::max(1, static_cast<int>(GetTickCount() - FStartTime));
        // Delphi says "Received" but ByteCount is the size of what was sent.
        String Temp = "Sent " + IntToStr(ByteCount) + " bytes, ";
        if (Duration < 5000)
            Temp += IntToStr(Duration) + " milliseconds";
        else
            Temp += IntToStr(Duration / 1000) + " seconds";
        const __int64 BytesSec = (1000 * ByteCount) / Duration;
        Temp += " (" + IntToKbyte(BytesSec) + "bytes/sec)";
        Display(Temp);
    }
}
