#include "url_parser.h"
#include "defines.h"
#include <string.h>
#include <ctype.h>
#include <stdbool.h>
#include <stdlib.h>
#include <math.h>

const char GEMINI_SCHEME[] = "gemini";

char *strdup(const char *c)
{
    if (!c)
        return c;
    char *res = malloc(strlen(c) + 1);
    strcpy(res, c);
    return res;
}

enum UrlParseState
{
    PARSE_FAIL = -1,
    PARSE_SUCCESS = 0,
    SCHEME_START,
    SCHEME,
    NO_SCHEME,
    SPECIAL_REL_OR_AUTH,
    SPECIAL_AUTH_IGN_SLASHES,
    SPECIAL_AUTH_SLASHES,
    PATH_OR_AUTH,
    OPAQUE_PATH,
    AUTHORITY,
    RELATIVE,
    RELATIVE_SLASH,
    PATH,
    QUERY,
    FRAGMENT,
    HOST,
};

#define GC_PARSE_FAIL    \
    {                    \
        st = PARSE_FAIL; \
        goto fail;       \
    }

void GcFreeUrl(struct URL *url)
{
    free(url->fragment);
    free(url->scheme);
    free(url->host);
    free(url->port);
    free(url->path);
    free(url->query);
}

char *SUPPORTED_SCHEMES[] = {
    "http",
    "https",
    "gemini",
    "ftp",
    "mailto",
};

static bool
scheme_is_supported(char *scheme)
{
    for (int i = 0; i < sizeof(SUPPORTED_SCHEMES) / sizeof(*SUPPORTED_SCHEMES); ++i)
    {
        if (strcmp(scheme, SUPPORTED_SCHEMES[i]) == 0)
            return true;
    }
    return false;
}

static bool
scheme_is_special(char *scheme)
{
    return (strcmp(scheme, "http") == 0 ||
            strcmp(scheme, "https") == 0 ||
            strcmp(scheme, "gemini") == 0 ||
            strcmp(scheme, "ftp") == 0);
}

static void shorten_path(char *path)
{
    char *last_slash = strrchr(path, '/');
    if (last_slash)
        last_slash[1] = 0;
    else
        path[0] = 0;
}

int GcParseUrl(char *input, struct URL *base, struct URL *url)
{
    int input_len = strlen(input);
    int idx = 0;
    int scheme_idx = 0;
    char c;

    int max_scheme_len = base ? MAX(input_len, strlen(base->scheme)) : input_len;
    int max_host_len = base ? MAX(input_len, strlen(base->host)) : input_len;
    int max_port_len = base ? MAX(input_len, strlen(base->port)) : input_len;
    // Path is the odd one out; a relative path can add onto the base path
    int max_path_len = base ? (input_len + strlen(base->path)) : input_len;
    int max_query_len = base ? MAX(input_len, strlen(base->query)) : input_len;
    int max_fragment_len = base ? MAX(input_len, strlen(base->fragment)) : input_len;

    url->scheme = calloc(max_scheme_len + 1, 1);
    url->host = calloc(max_host_len + 1, 1);
    url->port = calloc(max_port_len + 1, 1);
    url->path = calloc(max_path_len + 1, 1);
    url->query = calloc(max_query_len + 1, 1);
    url->fragment = calloc(max_fragment_len + 1, 1);

    char *buffer = calloc(input_len + 1, 1);
    int buf_idx;

    bool atSignSeen = false;
    bool insideBrackets = false;
    bool passwordTokenSeen = false;

    enum UrlParseState st = SCHEME_START;

restart:
    for (idx = 0; input[idx]; ++idx)
    {
        c = input[idx];

        switch (st)
        {
        case SCHEME_START:
            if (isalpha(c))
            {
                url->scheme[scheme_idx++] = tolower(c);
                st = SCHEME;
            }
            else
            {
                st = NO_SCHEME;
                idx--;
            }
            break;
        case SCHEME:
            if (isalpha(c) || c == '+' || c == '-' || c == '.')
            {
                url->scheme[scheme_idx++] = tolower(c);
            }
            else if (c == ':')
            {
                if (!scheme_is_supported(url->scheme))
                    GC_PARSE_FAIL

                if (scheme_is_special(url->scheme) && base && (strcmp(base->scheme, url->scheme) == 0))
                {
                    st = SPECIAL_REL_OR_AUTH;
                }
                else if (scheme_is_special(url->scheme))
                {
                    st = SPECIAL_AUTH_SLASHES;
                }
                else if (input[idx + 1] == '/')
                {
                    st = PATH_OR_AUTH;
                    idx++;
                }
                else
                {
                    st = OPAQUE_PATH;
                }
            }
            else
            {
                scheme_idx = 0;
                url->scheme[0] = 0;
                st = NO_SCHEME;
                goto restart;
            }
            break;
        case NO_SCHEME:
            if (!base)
            {
                // no base URL, also no scheme, must be 'default scheme'
                free(url->scheme);
                url->scheme = strdup("gemini://");
                st = AUTHORITY;
            }
            else if (c == '#')
            {
                // fragment
                GcDie("\pUnimplemented: relative fragment parser");
            }
            else
            {
                st = RELATIVE;
                idx--;
            }
            break;
        case SPECIAL_REL_OR_AUTH:
            if (c == '/' && input[idx + 1] == '/')
            {
                st = SPECIAL_AUTH_IGN_SLASHES;
                idx++;
            }
            else
            {
                st = RELATIVE;
                idx--;
            }
            break;
        case PATH_OR_AUTH:
            if (c == '/')
            {
                st = AUTHORITY;
            }
            else
            {
                st = PATH;
                idx--;
            }
            break;
        case RELATIVE:
            // SAFETY: strcpy is in-bounds by allocation above
            strcpy(url->scheme, base->scheme);

            if (c == '/')
            {
                st = RELATIVE_SLASH;
            }
            else if (scheme_is_special(url->scheme) && c == '\\')
            {
                GC_PARSE_FAIL
            }
            else
            {
                // SAFETY: strcpy is in-bounds by allocation above
                strcpy(url->host, base->host);
                strcpy(url->port, base->port);
                strcpy(url->path, base->path);
                strcpy(url->query, base->query);

                if (c == '?')
                {
                    url->query[0] = 0;
                    st = QUERY;
                }
                else if (c == '#')
                {
                    url->fragment[0] = 0;
                    st = FRAGMENT;
                }
                else
                {
                    url->query[0] = 0;
                    shorten_path(url->path);
                    st = PATH;
                    idx--;
                }
            }
            break;
        case RELATIVE_SLASH:
            if (scheme_is_special(url->scheme) && c == '/')
            {
                st = SPECIAL_AUTH_IGN_SLASHES;
            }
            else if (c == '/')
            {
                st = AUTHORITY;
            }
            else
            {
                strcpy(url->host, base->host);
                strcpy(url->port, base->port);
                strcpy(url->path, base->path);
                strcpy(url->query, base->query);
                st = PATH;
                idx--;
            }
            break;
        case SPECIAL_AUTH_SLASHES:
            if (c == '/' && input[idx + 1] == '/')
            {
                st = SPECIAL_AUTH_IGN_SLASHES;
                idx++;
            }
            else
            {
                GC_PARSE_FAIL;
            }
            break;
        case SPECIAL_AUTH_IGN_SLASHES:
            if (c != '/')
            {
                st = AUTHORITY;
                idx--;
            }
            else
            {
                GC_PARSE_FAIL;
            }
            break;
        case AUTHORITY:
            if (c == '@')
            {
                GC_PARSE_FAIL; // we are not doing credentials
            }
            else if (c == '/' || c == '?' || c == '#')
            {
                idx -= buf_idx + 1;
                st = HOST;
            }
            else
            {
                buf_idx++;
            }
        }
    }

    // for (idx = 0; url[idx]; ++idx)
    // {
    //     c = url[idx];
    //     switch (st)
    //     {
    //     case SCHEME_START:
    //         if (!isalpha(c))
    //         {
    //             st = SCHEME_MISSING;
    //             idx--;
    //         }
    //         break;
    //     case SCHEME:
    //         if (c == ':')
    //         {
    //             // end of scheme
    //             url[idx] = '\0';
    //             outurl->scheme = url;
    //             st = HIER_START;
    //             break;
    //         }
    //         else if (!(isalnum(c) || c == '+' || c == '-' || c == '.'))
    //         {
    //             st = SCHEME_MISSING;
    //             idx--;
    //             break;
    //         }
    //         break;
    //     case SCHEME_MISSING:
    //         outurl->scheme = GEMINI_SCHEME;
    //         if (c == '/')
    //         {
    //             st = PATH_START;
    //         }
    //         else if (c == '[')
    //         {
    //             // start of IPv6 literal; not supported
    //             GC_PARSE_FAIL;
    //         }
    //         else if (isalnum(c))
    //         {
    //             st = HOST_START;
    //         }
    //         else
    //         {
    //             // really weird host?
    //             // fail just in case.
    //             GC_PARSE_FAIL;
    //         }
    //         idx--;
    //         break;
    //     case HIER_START:
    //         if (strncmp(&url[idx], "//", 2) == 0)
    //         {
    //             idx++;
    //             st = HOST_START;
    //         }
    //         else if (c == '/')
    //         {
    //         }
    //         break;
    //     }
    // }
end:
    return 0;

fail:
    GcFreeUrl(url);
    return -1;
}