/***********************************************************************************************************************************
HTTP URL
***********************************************************************************************************************************/
#include <build.h>

#include "common/debug.h"
#include "common/io/http/url.h"
#include "common/regExp.h"
#include "common/type/stringList.h"

/***********************************************************************************************************************************
Regular expression for URLs. This is not intended to be completely comprehensive, e.g. it is still possible to enter bad hostnames.
The goal is to make sure the syntax is correct enough for the rest of the parsing to succeed.
***********************************************************************************************************************************/
STRING_STATIC(
    HTTP_URL_REGEXP_STR,
    "^(http[s]{0,1}:\\/\\/){0,1}"                                   // Optional protocol (http or https)
    "([^\\[\\:\\/?]+|\\[[a-fA-F0-9:]+\\])"                          // host/ipv4/ipv6
    "(:[1-9][0-9]{0,4}){0,1}"                                       // Optional port
    "(\\/[^?\\/]*)*$");                                             // Optional path

/***********************************************************************************************************************************
Object type
***********************************************************************************************************************************/
struct HttpUrl
{
    HttpUrlPub pub;
};

/***********************************************************************************************************************************
Convert protocol type to a string
***********************************************************************************************************************************/
STRING_STATIC(HTTP_PROTOCOL_HTTP_STR,                               "http");
STRING_STATIC(HTTP_PROTOCOL_HTTPS_STR,                              "https");

static const String *
httpProtocolTypeStr(const HttpProtocolType type)
{
    switch (type)
    {
        case httpProtocolTypeHttp:
            return HTTP_PROTOCOL_HTTP_STR;

        case httpProtocolTypeHttps:
            return HTTP_PROTOCOL_HTTPS_STR;

        default:
            return NULL;
    }
}

/**********************************************************************************************************************************/
FN_EXTERN HttpUrl *
httpUrlNewParse(const String *const url, const HttpUrlNewParseParam param)
{
    FUNCTION_TEST_BEGIN();
        FUNCTION_TEST_PARAM(STRING, url);
        FUNCTION_TEST_PARAM(ENUM, param.type);
        FUNCTION_TEST_PARAM(ENUM, param.defaultType);
    FUNCTION_TEST_END();

    ASSERT(url != NULL);

    OBJ_NEW_BEGIN(HttpUrl, .childQty = MEM_CONTEXT_QTY_MAX)
    {
        *this = (HttpUrl)
        {
            .pub =
            {
                .url = strDup(url),
            },
        };

        MEM_CONTEXT_TEMP_BEGIN()
        {
            // Check that URL format is one we accept
            if (!regExpMatchOne(HTTP_URL_REGEXP_STR, url))
                THROW_FMT(FormatError, "invalid URL '%s'", strZ(url));

            // Determine whether the first part is protocol or host
            StringList *const splitUrl = strLstNewSplitZ(url, "/");

            if (strEqZ(strLstGet(splitUrl, 0), "http:"))
                this->pub.type = httpProtocolTypeHttp;
            else if (strEqZ(strLstGet(splitUrl, 0), "https:"))
                this->pub.type = httpProtocolTypeHttps;

            // If no protocol found then the first part is the host
            if (this->pub.type == httpProtocolTypeAny)
            {
                // Must have either an explicitly set protocol via the type parameter or a default protocol if the type is any
                ASSERT(param.type != httpProtocolTypeAny || param.defaultType != httpProtocolTypeAny);

                this->pub.type = param.type != httpProtocolTypeAny ? param.type : param.defaultType;
            }
            // Else protocol was found
            else
            {
                // Protocol must match expected
                if (param.type != httpProtocolTypeAny && this->pub.type != param.type)
                    THROW_FMT(FormatError, "expected protocol '%s' in URL '%s'", strZ(httpProtocolTypeStr(param.type)), strZ(url));

                // Remove protocol parts from split
                strLstRemoveIdx(splitUrl, 0);
                strLstRemoveIdx(splitUrl, 0);
            }

            // Get host
            const String *host = strLstGet(splitUrl, 0);
            const String *port = NULL;

            // If an IPv6 address
            if (strBeginsWithZ(host, "["))
            {
                // Split closing bracket
                const StringList *const splitHost = strLstNewSplitZ(host, "]");
                ASSERT(strLstSize(splitHost) == 2);

                // Remove opening bracket
                host = strSub(strLstGet(splitHost, 0), 1);

                // Get port if specified
                if (strSize(strLstGet(splitHost, 1)) > 0)
                    port = strSub(strLstGet(splitHost, 1), 1);
            }
            // Else IPv4 or host name
            else
            {
                // Split on colon
                const StringList *const splitHost = strLstNewSplitZ(host, ":");
                ASSERT(strLstSize(splitHost) != 0);

                // First part is the host
                host = strLstGet(splitHost, 0);

                // Second part is the port, if it exists
                if (strLstSize(splitHost) > 1)
                {
                    ASSERT(strLstSize(splitHost) == 2);
                    port = strLstGet(splitHost, 1);
                }
            }

            // Copy host into object context
            MEM_CONTEXT_PRIOR_BEGIN()
            {
                this->pub.host = strDup(host);
            }
            MEM_CONTEXT_PRIOR_END();

            // Get port if specified
            if (port != NULL)
            {
                this->pub.port = cvtZToUInt(strZ(port));
            }
            // Else set default port based on the protocol
            else
            {
                ASSERT(this->pub.type != httpProtocolTypeAny);

                if (this->pub.type == httpProtocolTypeHttp)
                    this->pub.port = 80;
                else
                    this->pub.port = 443;
            }

            // Check for path
            if (strLstSize(splitUrl) > 1)
            {
                // Remove host part so it is easier to construct the path
                strLstRemoveIdx(splitUrl, 0);

                // Construct path and copy into local context
                const String *const path = strLstJoin(splitUrl, "/");

                MEM_CONTEXT_PRIOR_BEGIN()
                {
                    this->pub.path = strNewFmt("/%s", strZ(path));
                }
                MEM_CONTEXT_PRIOR_END();
            }
            // Else default path is /
            else
                this->pub.path = FSLASH_STR;
        }
        MEM_CONTEXT_TEMP_END();
    }
    OBJ_NEW_END();

    FUNCTION_TEST_RETURN(HTTP_URL, this);
}

/**********************************************************************************************************************************/
#ifdef DEBUG

FN_EXTERN void
httpUrlToLog(const HttpUrl *const this, StringStatic *const debugLog)
{
    // Is IPv6 address?
    bool ipv6 = strChr(this->pub.host, ':') != -1;

    strStcFmt(
        debugLog, "{%s://%s%s%s:%u%s}", strZ(httpProtocolTypeStr(this->pub.type)), ipv6 ? "[" : "", strZ(this->pub.host),
        ipv6 ? "]" : "", this->pub.port, strZ(this->pub.path));
}

#endif // DEBUG
