#define _GNU_SOURCE
#include <stdio.h>
#include <string.h>
#include <stdlib.h>
#include <ctype.h>
#include <stdbool.h>
#include "parser.h"

/* Consume all (non-newline) whitespace.
 * Returns the number of whitespace characters consumed. */
int ConsumeWhitespace(char *s)
{
    int i = 0;
    while (s[i])
    {
        switch (s[i])
        {
        case ' ':
        case '\t':
        case '\r':
        case '\f':
        case '\v':
            break;
        default:
            goto end;
        }
        i++;
    }
end:
    return i;
}

/* Return a pointer to the first line-ending character
 * in this line (i.e. CR in case of CRLF, LF in case of just LF,
 * or NUL if this line does not end with CR or LF). */
char *LineEnd(char *line)
{
    while (*line)
    {
        switch (*line)
        {
        case '\r':
            if (line[1] == '\n')
                return line;
            break;
        case '\n':
            return line;
            break;
        default:
            break;
        }
        line++;
    }
    return line;
}

/* Parse a line in-place, without allocating any new memory */
enum gem_parse_result
GemParseLine(struct gem_parse_state *s, struct gem_line *l)
{
    int i = 0;
    char *line;
    char *line_end;
    char *line_next;
    enum gem_parse_result res = PARSE_LINE;

    line = s->buf;
    line_end = LineEnd(s->buf);
    if (*line_end == 0)
        line_next = line_end;
    else if (*line_end == '\n')
        line_next = line_end + 1;
    else if (*line_end == '\r')
        line_next = line_end + 2;

    *line_end = 0;

    if (strncmp(line, "```", 3) == 0)
    {
        s->preformatted = !s->preformatted;
        res = PARSE_NOLINE;
    }
    else if (s->preformatted)
    {
        l->type = LT_PRETEXT;
        l->text = line;
    }
    else if (*line == '>')
    {
        i++;
        i += ConsumeWhitespace(&line[i]);
        l->type = LT_QUOTE;
        l->text = &line[i];
    }
    else if (*line == '#')
    {
        i = strchrnul(line, ' ') - line;
        if (i > 3)
            i = 3;
        l->level = i;
        i += ConsumeWhitespace(&line[i]);
        l->text = &line[i];
        l->type = LT_HEADING;
    }
    else if (*line == '*')
    {
        i++;
        i += ConsumeWhitespace(&line[i]);
        l->type = LT_LISTITEM;
        l->text = &line[i];
    }
    else if (strncmp(line, "=>", 2) == 0)
    {
        l->type = LT_LINK;
        i = 2;
        i += ConsumeWhitespace(&line[i]);
        l->url = &line[i];
        for (; line[i] && !isspace(line[i]); i++)
            ;
        if (line[i] == 0)
        {
            l->text = l->url;
        }
        else
        {
            line[i++] = 0;
            i += ConsumeWhitespace(&line[i]);
            l->text = &line[i];
        }
    }
    else
    {
        l->type = LT_TEXT;
        l->text = line;
    }

    s->buf = line_next;
    return res;
}

const char *gem_typename(enum gem_linetype lt)
{
    switch (lt)
    {
    case LT_TEXT:
        return "text";
    case LT_HEADING:
        return "heading";
    case LT_LINK:
        return "link";
    case LT_QUOTE:
        return "quote";
    case LT_LISTITEM:
        return "list item";
    case LT_PRETEXT:
        return "preformatted text";
    default:
        return "unknown type";
    }
}

// clang-format off
static int VALID_CODES[] = {
    10, 11,
    20,
    30, 31,
    40, 41, 42, 43, 44,
    50, 51, 52, 53, 54,
    60, 61, 62
};
// clang-format on

int is_valid_response_code(int code)
{
    for (int i = 0; i < sizeof(VALID_CODES) / sizeof(int); ++i)
    {
        if (code == VALID_CODES[i])
            return true;
    }
    return false;
}

struct gem_response GemParseResponse(char *line)
{
    char c;
    struct gem_response resp;
    int state = 0;
    int resp_code;

    for (int idx = 0; c = line[idx]; ++idx)
    {
        switch (state)
        {
        case 0:
            // expecting response code: two digits
            if (c == ' ')
            {
                line[idx] = '\0';
                resp_code = atoi(line);
                line[idx] = ' ';
                if (resp_code < 10 || resp_code > 69)
                {
                    resp.type = RT_INVALID;
                    goto end;
                }
                else if (!is_valid_response_code(resp_code))
                {
                    resp_code /= 10;
                    resp_code *= 10;
                }
                resp.type = (enum gem_resptype)resp_code;
                state = 1;
            }
            else if (!isdigit(c))
            {
                resp.type = RT_INVALID;
                goto end;
            }
            break;
        case 1:
            if (c == '\r' || c == '\n' || c == ' ')
            {
                line[idx] = '\0';
                resp.data = line + 3;
                goto end;
            }
            break;
        }
    }
    if (!resp.data)
    {
        resp.data = line + 3;
    }
end:
    return resp;
}
