/*
  ICS Web Application Server - C++Builder translation of the Delphi demo
  OverbyteIcsWebAppServerMain.pas (demos-delphi-vcl), ICS V9.8.

  A basic non-SSL demo of the HTTP application server component THttpAppSrv: session based
  login, template pages, a hit counter, a configuration form and an email form (the pages are
  in OverbyteIcsWebAppServerPages). The port comes from the ini file (default 20105); the server
  starts when the form opens and writes a W3C log (webapp-yyyymmdd.log). OverbyteIcsSslWebServ,
  OverbyteIcsSslMultiWebServ and OverbyteIcsBasicWebServer are better SSL samples.

  How this C++ version differs from the Delphi original, apart from syntax:
  - demos-data is found three levels up from the Win64x\<config> output folder (Delphi: four,
    from bin\<platform>\<config>).
  - The form owns the settings the URL handlers share (FPagesData) and points
    WebAppSrvDataModule at it; the unit global SrvCompName is a form member.
  - The ISODateMask and ISOTimeMask constants come from the ICS headers (same values as the
    Delphi demo's own constants, which would clash with the ICS macros of the same names).
  - The DWScript handler (use_DWScript, not defined in the Delphi demo) is not translated.
  - The form destructor frees the components first, because the server can still fire events
    that use the form while it is destroyed.
*/

#include <vcl.h>
#pragma hdrstop

#include "OverbyteIcsWebAppServerMain.h"
#include "OverbyteIcsIniFiles.hpp"
#include "OverbyteIcsUtils.hpp"
#include "OverbyteIcsTicks64.hpp"

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

TWebAppSrvForm *WebAppSrvForm;

namespace {

const wchar_t *const SimpLogName = L"\"webapp-\"yyyymmdd\".log\"";
const wchar_t *const WebLogFields =
    L"#Fields: date time s-sitename s-computername s-ip cs-method cs-uri-stem cs-uri-query "
    L"s-port cs-username c-ip cs-version cs(User-Agent) cs(Referer) cs-host sc-status "
    L"sc-bytes cs-bytes time-taken";

const wchar_t *const SectionWindow = L"Window";   // must be unique for each window
const wchar_t *const KeyTop        = L"Top";
const wchar_t *const KeyLeft       = L"Left";
const wchar_t *const KeyWidth      = L"Width";
const wchar_t *const KeyHeight     = L"Height";

String SpaceToPlus(const String &S)
{
    return StringReplace(S, " ", "+", TReplaceFlags() << rfReplaceAll);
}

} // namespace

__fastcall TWebAppSrvForm::TWebAppSrvForm(TComponent *Owner)
    : TForm(Owner)
{
    WebAppSrvDataModule = &FPagesData;
    RegisterWebAppClasses();
}

__fastcall TWebAppSrvForm::~TWebAppSrvForm()
{
    // Free the server and its clients while this form's C++ members still exist.
    DestroyComponents();
    WebAppSrvDataModule = nullptr;
}

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

void __fastcall TWebAppSrvForm::FormShow(TObject *Sender)
{
    if (FInitialized)
        return;
    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);
    }
    DisplayMemo->Clear();
    PostMessage(Handle, WM_APPSTARTUP, 0, 0);   // start the server once the form is shown
}

void __fastcall TWebAppSrvForm::HousekeepingTimerTimer(TObject *Sender)
{
    CleanupTimeStampedDir(FPagesData.DataDir());
}

// Writes a W3C log line, like Microsoft IIS, for example:
// 2009-07-03 12:34:48 WebAppDemo PC19 0.0.0.0 GET /login/loginform.html - 20105 - 192.168.1.119 HTTP/1.1
//   Mozilla/5.0+(Windows;+U;+Windows+NT+5.1;...) http://pc19:20105/ pc19:20105 200 2101 1138 31
void __fastcall TWebAppSrvForm::HttpAppSrv1AfterAnswer(TObject *Sender, TObject *Client)
{
    TAppHttpConnection *RemoteClient = static_cast<TAppHttpConnection *>(Client);
    String Info = FormatDateTime(ISODateMask L" " ISOTimeMask, Now());
    try {
        const String NewParams = RemoteClient->Params.IsEmpty() ? String("-") : RemoteClient->Params;
        String NewUser = SpaceToPlus(RemoteClient->AuthUserName);
        if (NewUser.IsEmpty())
            NewUser = "-";
        const __int64 CurRead = RemoteClient->ReadCount - RemoteClient->CLastRead;
        const __int64 CurWrite = RemoteClient->WriteCount - RemoteClient->CLastWrite;
        const __int64 Duration = IcsElapsedMsecs64(RemoteClient->CStartTick);
        // #Fields: date time s-sitename s-computername s-ip cs-method cs-uri-stem cs-uri-query
        //   s-port cs-username c-ip cs-version cs(User-Agent) cs(Referer) cs-host sc-status
        //   sc-bytes cs-bytes time-taken
        Info += " WebAppDemo " + FSrvCompName + " " + RemoteClient->Server->Addr + " " + RemoteClient->Method +
                " " + SpaceToPlus(RemoteClient->Path) + " " + SpaceToPlus(NewParams) + " " +
                RemoteClient->Server->Port + " " + NewUser + " " + RemoteClient->GetPeerAddr() + " " +
                RemoteClient->Version + " " + SpaceToPlus(RemoteClient->RequestUserAgent) + " " +
                SpaceToPlus(RemoteClient->RequestReferer) + " " + RemoteClient->RequestHost + " " +
                IntToStr(RemoteClient->AnswerStatus) + " " + IntToStr(CurWrite) + " " + IntToStr(CurRead) +
                " " + IntToStr(Duration);
        IcsSimpleLogging(SimpLogName, Info);
    }
    catch (Exception &) {
        Display("AfterAnswer Error - " + Info);
    }
    RemoteClient->CLastRead = RemoteClient->ReadCount;   // ready for the next request
}

// Keep-alive means a connection may carry several requests, so the data sent is counted per request.
void __fastcall TWebAppSrvForm::HttpAppSrv1BeforeProcessRequest(TObject *Sender, TObject *Client)
{
    TAppHttpConnection *RemoteClient = static_cast<TAppHttpConnection *>(Client);
    RemoteClient->CStartTick = IcsGetTickCount64();
    RemoteClient->CLastWrite = RemoteClient->WriteCount;
}

void __fastcall TWebAppSrvForm::HttpAppSrv1ClientConnect(TObject *Sender, TObject *Client, System::Word Error)
{
    THttpAppSrvConnection *ClientCnx = static_cast<THttpAppSrvConnection *>(Client);
    ClientCnx->WSessionCookie = "OverbyteIcsWebAppServer" + HttpAppSrv1->Port;
    ClientCnx->OnBgException = HttpAppSrvClientBgException;
}

void __fastcall TWebAppSrvForm::HttpAppSrv1DeleteSession(TObject *Sender, TIcsWebSession *Session)
{
    TAppSrvSessionData *MySessionData = dynamic_cast<TAppSrvSessionData *>(Session->SessionData);
    Display("Session for user \"" + (MySessionData ? MySessionData->UserCode : String()) + "\" timed out");
}

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

void __fastcall TWebAppSrvForm::HttpAppSrv1GetDocument(TObject *Sender, TObject *Client, THttpGetFlag &Flags)
{
    THttpAppSrvConnection *ClientCnx = static_cast<THttpAppSrvConnection *>(Client);
    if (ClientCnx->Params.IsEmpty())
        Display(ClientCnx->PeerAddr + " " + ClientCnx->Path);
    else
        Display(ClientCnx->PeerAddr + " " + ClientCnx->Path + "?" + ClientCnx->Params);
}

// Called before OnClientConnect, so a connection can be refused before it is accepted.
void __fastcall TWebAppSrvForm::HttpAppSrv1HttpAcceptFilter(TObject *Sender, TObject *Client,
                                                            TIcsSessIpInfo &SessIpInfo, bool &Allowed)
{
    Display("New connection from " + SessIpInfo.RemoteAddr + ". We can choose to refuse it.");
    Display("Local address " + SessIpInfo.LocalAddr);
    if (SessIpInfo.RemoteAddr == "192.168.1.122") {   // example only, could be an IPv6 or partial address
        Allowed = false;
        Display("Declined connection!!");
    }
}

void __fastcall TWebAppSrvForm::HttpAppSrv1ServerStarted(TObject *Sender)
{
    Display("Waiting for client on URL: http://127.0.0.1:" + HttpAppSrv1->Port + "/");   // or any PC IP address
}

void __fastcall TWebAppSrvForm::HttpAppSrv1ServerStopped(TObject *Sender)
{
    HttpAppSrv1->WSessions->SaveToFile(FSessionFile);
    CleanupTimeStampedDir(FPagesData.DataDir());
    Display("Server is now stopped");
}

void __fastcall TWebAppSrvForm::HttpAppSrv1VirtualException(TObject *Sender, Exception *E, THttpMethod Method,
                                                            const String Path)
{
    Display("Exception creating virtual page: " + Path + " - " + E->Message);
}

void __fastcall TWebAppSrvForm::HttpAppSrv1BgException(TObject *Sender, Exception *E, bool &CanClose)
{
    Display("Exception processing page - " + E->ClassName() + ": " + E->Message);
    CanClose = false;   // let the server continue to accept connections
}

void __fastcall TWebAppSrvForm::HttpAppSrvClientBgException(TObject *Sender, Exception *E, bool &CanClose)
{
    Display("Exception processing page - " + E->ClassName() + ": " + E->Message);
    CanClose = true;   // shut the client down
}

void __fastcall TWebAppSrvForm::FormClose(TObject *Sender, TCloseAction &Action)
{
    HttpAppSrv1->Stop();
    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();
}

// Adds a line to the memo; past 200 lines the oldest 50 are removed.
void TWebAppSrvForm::Display(const String &Msg)
{
    if (!DisplayMemo)
        return;
    TStrings *Lines = DisplayMemo->Lines;
    Lines->BeginUpdate();
    __try {
        if (Lines->Count > 200) {
            for (int I = 0; I < 50; I++)
                Lines->Delete(0);
        }
        Lines->Add(Msg);
    }
    __finally {
        Lines->EndUpdate();
        SendMessage(DisplayMemo->Handle, EM_SCROLLCARET, 0, 0);
    }
}

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

void __fastcall TWebAppSrvForm::WMAppStartup(TMessage &Msg)
{
    HttpAppSrv1->ClientClass = __classid(TAppHttpConnection);   // our derived client class

    // demos-data is three levels up from Win64x\<config> (Delphi: four, from bin\<platform>\<config>).
    const String BaseDir = IcsAbsolutisePath(IncludeTrailingPathDelimiter(ExtractFilePath(Application->ExeName)) +
                                             "..\\..\\..\\demos-data\\");
    FDataDir = BaseDir + "WebAppServerData\\Data";
    FSessionFile = FDataDir + "\\Sessions.dat";
    HttpAppSrv1->DocDir = BaseDir + "WebAppServerData\\wwwRoot";
    HttpAppSrv1->TemplateDir = BaseDir + "WebAppServerData\\Templates";
    HttpAppSrv1->DefaultDoc = "index.html";
    HttpAppSrv1->WSDebugOptions = DefWSDebugServer;   // can be reduced for less logging
    HttpAppSrv1->WSDebugOptions = HttpAppSrv1->WSDebugOptions << wsdlogHdr;           // optional
    HttpAppSrv1->WSDebugOptions = HttpAppSrv1->WSDebugOptions + DefWSDebugDevel;      // all development logging (marked TEMP in Delphi)

    ForceDirectories(FDataDir);
    ForceDirectories(HttpAppSrv1->TemplateDir);
    ForceDirectories(HttpAppSrv1->DocDir);
    ForceDirectories(HttpAppSrv1->DocDir + "\\Js");
    ForceDirectories(HttpAppSrv1->DocDir + "\\Styles");
    ForceDirectories(HttpAppSrv1->DocDir + "\\Images");

    FPagesData.IniFileName = FIniFileName;
    FPagesData.OnDisplay = DisplayHandler;
    FPagesData.SetDataDir(FDataDir);
    FPagesData.ImagesDir = HttpAppSrv1->DocDir + "\\Images";
    FPagesData.LoadConfig();

    // Static documents may only be fetched from these folders.
    HttpAppSrv1->AddGetAllowedPath("/js/", afBeginBy);
    HttpAppSrv1->AddGetAllowedPath("/styles/", afBeginBy);
    HttpAppSrv1->AddGetAllowedPath("/images/", afBeginBy);

    // The dynamic pages.
    HttpAppSrv1->AddGetHandler("/", __classid(TUrlHandlerDefaultDoc));
    HttpAppSrv1->AddGetHandler("/" + HttpAppSrv1->DefaultDoc, __classid(TUrlHandlerDefaultDoc));
    HttpAppSrv1->AddGetHandler(UrlLogin, __classid(TUrlHandlerLoginFormHtml));
    HttpAppSrv1->AddGetHandler(UrlDoLoginSecure, __classid(TUrlHandlerDoLoginSecureHtml));
    HttpAppSrv1->AddGetHandler(UrlCounter, __classid(TUrlHandlerCounterJpg));
    HttpAppSrv1->AddGetHandler(UrlHomePage, __classid(TUrlHandlerHomePageHtml));
    HttpAppSrv1->AddGetHandler(UrlConfigForm, __classid(TUrlHandlerConfigFormHtml));
    HttpAppSrv1->AddGetHandler(UrlConfigLogoPng, __classid(TUrlHandlerConfigLogoPng));
    HttpAppSrv1->AddGetHandler(UrlDoConfigConfirmSaveHtml, __classid(TUrlHandlerDoConfigConfirmSaveHtml));
    HttpAppSrv1->AddPostHandler(UrlDoConfigHtml, __classid(TUrlHandlerDoConfigHtml));
    HttpAppSrv1->AddGetHandler(UrlCounterViewHtml, __classid(TUrlHandlerCounterViewHtml));
    HttpAppSrv1->AddGetHandler(UrlAjaxFetchCounter, __classid(TUrlHandlerAjaxFetchCounter));
    HttpAppSrv1->AddGetHandler(UrlJavascriptErrorHtml, __classid(TUrlHandlerJavascriptErrorHtml));
    HttpAppSrv1->AddGetHandler("/mailer.html", __classid(TUrlHandlerMailer));
    HttpAppSrv1->AddPostHandler("/mailer.html", __classid(TUrlHandlerMailer));
    HttpAppSrv1->AddGetHandler(UrlHeadForm, __classid(TUrlHandlerHead));

    // The simplest possible handler.
    HttpAppSrv1->AddGetHandler("/HelloWorld.html", __classid(TUrlHandlerHelloWorld));

    if (FileExists(FSessionFile)) {
        try {
            HttpAppSrv1->WSessions->LoadFromFile(FSessionFile);
            Display(IntToStr(HttpAppSrv1->SessionsCount) + " sessions loaded");
        }
        catch (Exception &) {
            // Clear anything partly loaded and delete the corrupted file.
            HttpAppSrv1->WSessions->Clear();
            DeleteFile(FSessionFile);
            Display("Unable to load existing sessions");
        }
    }

    // Clean up temporary files left by a previous run, then every five minutes.
    CleanupTimeStampedDir(FPagesData.DataDir());
    HousekeepingTimer->Interval = 5 * 60 * 1000;
    HousekeepingTimer->Enabled = true;

    // Start the log file.
    FSrvCompName = IcsGetCompName();
    IcsSimpleLogging(SimpLogName, FormatDateTime("\"#Date: \"" ISODateMask L" " ISOTimeMask L"\"", Now()));
    IcsSimpleLogging(SimpLogName, WebLogFields);

    // Start the HTTP server.
    HttpAppSrv1->KeepAliveTimeSec = 30;
    HttpAppSrv1->SessionTimeout = 5 * 60;   // seconds; must be set after loading the sessions
    HttpAppSrv1->Port = FPagesData.Port;
    HttpAppSrv1->Start();
}
