class ImapBuffer {
    U8* rx;
    U8* tx;
};

class ImapClient {
    CTask* mem_task;
    TlsSocket* s;
    JsonObject* o;
    ImapBuffer buf;
    U8 response_text[1024];
    U8 selected[256];
    I64 state;
    I64 tag;
    U0 (*close)();
    U0 (*connect)(U8* host, I64 port);
    U0 (*fetch)(I64 uid);
    JsonArray* (*fetch_array)(U8* sequence_set, U8* data_items);
    U0 (*select)(U8* mailbox);
    U0 (*login)(U8* userid, U8* password);
    U0 (*logout)();
};

#define IMAP_BUFFER_SIZE 65536

#define IMAP_LINE_EQ 1
#define IMAP_LINE_IQ 2

#define IMAP_STATE_NOT_CONNECTED 0
#define IMAP_STATE_NOT_AUTHENTICATED 1
#define IMAP_STATE_AUTHENTICATED 2
#define IMAP_STATE_SELECTED 3
#define IMAP_STATE_LOGOUT 128

#define IMAP_STATE_ERROR 255

#define IMAP_WRAPPER_MAGIC_NUMBER 0xCAFEFACADEDEFACE

JsonArray* @imap_json_array_from_param_list(ImapClient* c, U8* line)
{
    I64 i = 0;
    I64 state = 0;
    JsonArray* a = NULL;
    U8* out = CAlloc(StrLen(line) * 2, c->mem_task);
    while (line[i]) {
        switch (state) {
        case 0:
            if (line[i] < ' ') {
                ++i;
                goto @imap_convert_process_next_ch;
            }
            if (line[i] == ' ') {
                String.Append(out, ",");
                ++i;
                goto @imap_convert_process_next_ch;
            }
            if (line[i] == '(') {
                String.Append(out, "[");
                ++i;
                goto @imap_convert_process_next_ch;
            }
            if (line[i] == ')') {
                String.Append(out, "]");
                ++i;
                goto @imap_convert_process_next_ch;
            }
            if (line[i] == '"') {
                state = IMAP_LINE_EQ;
                String.Append(out, "%c", line[i]);
                ++i;
                goto @imap_convert_process_next_ch;
            }
            state = IMAP_LINE_IQ;
            String.Append(out, "\"");
            String.Append(out, "%c", line[i]);
            ++i;
            break;
        case IMAP_LINE_EQ:
            if (line[i] == '"') {
                state = 0;
            }
            String.Append(out, "%c", line[i]);
            ++i;
            break;
        case IMAP_LINE_IQ:
            if (line[i] == ')' || line[i] == ' ') {
                state = 0;
                String.Append(out, "\"");
                goto @imap_convert_process_next_ch;
            }
            String.Append(out, "%c", line[i]);
            ++i;
            break;
        default:
            // We shouldn't be here! Return empty Array
            StrCpy(out, "[]");
            i = StrLen(line);
            break;
        }
        @imap_convert_process_next_ch:
    }
    a = Json.Parse(out, c->mem_task);
    Free(out);
    return a;
}

U8** @imap_lineate_buffer(ImapClient* c, U8* line_cnt_out)
{
    if (!c || !line_cnt_out)
        return NULL;
    I64 line_cnt = StrOcc(c->buf.rx, '\n');
    *line_cnt_out = line_cnt;
    if (!line_cnt)
        return NULL;
    I64 i = 0;
    U8* p = c->buf.rx;
    U8** lines = CAlloc(sizeof(U8*) * line_cnt, c->mem_task);
    lines[i++] = p++;
    while (*p) {
        switch (*p) {
        case '\r':
            *p = NULL;
            p += 2;
            lines[i++] = p++;
            break;
        default:
            ++p;
            break;
        }
    }
    return lines;
}

U0 @imap_init_buffers(ImapClient* c)
{
    c->buf.rx = CAlloc(IMAP_BUFFER_SIZE, c->mem_task);
    c->buf.tx = CAlloc(IMAP_BUFFER_SIZE, c->mem_task);
}

U0 @imap_free_buffers(ImapClient* c)
{
    if (c->buf.rx)
        Free(c->buf.rx);
    if (c->buf.tx)
        Free(c->buf.tx);
}

U0 @imap_send(ImapClient* c)
{
    c->s->send(c->buf.tx, StrLen(c->buf.tx));
}

U0 @imap_set_response_text(ImapClient* c, U8* s)
{
    // Trim whitespace at both ends
    Bool trim = TRUE;
    while (trim) {
        switch (*s) {
        case ' ':
        case '\t':
        case '\r':
        case '\n':
            ++s;
            break;
        default:
            trim = FALSE;
            break;
        }
    }
    StrCpy(c->response_text, s);
    s = c->response_text + StrLen(c->response_text) - 1;
    trim = TRUE;
    while (trim) {
        switch (*s) {
        case ' ':
        case '\t':
        case '\r':
        case '\n':
            *s = NULL;
            --s;
            break;
        default:
            trim = FALSE;
            break;
        }
    }
}

U0 @imap_error(ImapClient* c, U8* s)
{
    c->state = IMAP_STATE_ERROR;
    @imap_set_response_text(c, s);
}

U0 @imap_receive(ImapClient* c)
{
    U8 buf[16];
    I64 cnt = 0;

    U8* msg;

    while (c->s->state != TCP_SOCKET_STATE_CLOSED) {
        cnt += c->s->receive(c->buf.rx + cnt, IMAP_BUFFER_SIZE);

        if (StrFind("* OK", c->buf.rx)) {
            @imap_set_response_text(c, StrFind("* OK", c->buf.rx) + 4);
            return;
        }
        StrPrint(buf, "A%03d OK", c->tag);
        if (StrFind(buf, c->buf.rx)) {
            @imap_set_response_text(c, StrFind(buf, c->buf.rx) + 7);
            return;
        }

        if (StrFind("* NO", c->buf.rx)) {
            @imap_error(c, StrFind("* NO", c->buf.rx) + 4);
            return;
        }
        StrPrint(buf, "A%03d NO", c->tag);
        if (StrFind(buf, c->buf.rx)) {
            @imap_error(c, StrFind(buf, c->buf.rx) + 7);
            return;
        }

        if (StrFind("* BAD", c->buf.rx)) {
            @imap_error(c, StrFind("* BAD", c->buf.rx) + 5);
            return;
        }
        StrPrint(buf, "A%03d BAD", c->tag);
        if (StrFind(buf, c->buf.rx)) {
            @imap_error(c, StrFind(buf, c->buf.rx) + 8);
            return;
        }
    }
}

U0 @imap_logout(ImapClient* c)
{
    StrPrint(c->buf.tx, "A%03d LOGOUT\r\n", ++c->tag);
    @imap_send(c);

    // Receive data
    @imap_receive(c);

    c->state = IMAP_STATE_LOGOUT;
}

U0 @imap_close(ImapClient* c)
{
    c->s->close();
    @imap_free_buffers(c);
    Free(c->s);

    Free(c->close);
    Free(c->connect);
    Free(c->fetch);
    Free(c->fetch_array);
    Free(c->select);
    Free(c->login);
    Free(c->logout);
    Free(c);
}

U0 @imap_login(ImapClient* c, U8* userid, U8* password)
{
    StrPrint(c->buf.tx, "A%03d LOGIN %s %s\r\n", ++c->tag, userid, password);
    @imap_send(c);

    // Receive data
    @imap_receive(c);
    if (c->state == IMAP_STATE_ERROR)
        return;

    c->state = IMAP_STATE_AUTHENTICATED;
}

JsonArray* @imap_fetch_array(ImapClient* c, U8* sequence_set, U8* data_items)
{
    StrPrint(c->buf.tx, "A%03d FETCH %s %s\r\n", ++c->tag, sequence_set, data_items);
    @imap_send(c);

    // Receive data
    @imap_receive(c);
    if (c->state == IMAP_STATE_ERROR)
        return Json.Parse("[]", c->mem_task);

    // Split buffer into lines
    I64 i = 0;
    I64 lines_cnt = 0;
    U8** lines = @imap_lineate_buffer(c, &lines_cnt);
    U8* param_list = NULL;

    if (!lines_cnt) {
        Free(lines);
        return Json.Parse("[]", c->mem_task);
    }

    // Convert each param_list to a JSON array
    JsonArray* a = Json.CreateArray(c->mem_task);
    for (i = 0; i < lines_cnt; i++) {
        param_list = StrFind(" FETCH (", lines[i]);
        if (param_list) {
            a->append(@imap_json_array_from_param_list(c, param_list + 7), JSON_ARRAY);
        }
    }

    // Return array of arrays
    return a;
}

U0 @imap_fetch(ImapClient* c, I64 uid)
{
    StrPrint(c->buf.tx, "A%03d UID FETCH %d BODY.PEEK[]\r\n", ++c->tag, uid);
    @imap_send(c);

    @imap_receive(c);
}

U0 @imap_select(ImapClient* c, U8* mailbox)
{
    StrPrint(c->buf.tx, "A%03d SELECT %s\r\n", ++c->tag, mailbox);
    @imap_send(c);

    @imap_receive(c);
    if (c->state == IMAP_STATE_ERROR)
        return;

    StrCpy(&c->selected, mailbox);
    c->state = IMAP_STATE_SELECTED;
}

U0 @imap_connect(ImapClient* c, U8* host, I64 port)
{
    if (!host || !port || !StrLen(host))
        return;

    U32 addr = @dns_query(host);
    if (addr == U32_MAX)
        return;

    TlsSocket* s = NULL;

    switch (port) {
    case 143:
        s = @tcp_socket_create(host, port);
        break;
    case 993:
        s = @tls_socket_create(host, port);
        break;
    default:
        return;
    }

    if (!s)
        return;

    c->s = s;

    // Receive data
    @imap_receive(c);
    if (c->state == IMAP_STATE_ERROR)
        return;

    c->state = IMAP_STATE_NOT_AUTHENTICATED;
}

U0 @imap_close_wrapper_function()
{
    @imap_close(IMAP_WRAPPER_MAGIC_NUMBER);
}

U0 @imap_connect_wrapper_function(U8* host, I64 port)
{
    @imap_connect(IMAP_WRAPPER_MAGIC_NUMBER, host, port);
}

U0 @imap_select_wrapper_function(U8* mailbox)
{
    @imap_select(IMAP_WRAPPER_MAGIC_NUMBER, mailbox);
}

U0 @imap_login_wrapper_function(U8* userid, U8* password)
{
    @imap_login(IMAP_WRAPPER_MAGIC_NUMBER, userid, password);
}

U0 @imap_logout_wrapper_function()
{
    @imap_logout(IMAP_WRAPPER_MAGIC_NUMBER);
}

U0 @imap_fetch_wrapper_function(I64 uid)
{
    @imap_fetch(IMAP_WRAPPER_MAGIC_NUMBER, uid);
}

U0 @imap_fetch_array_wrapper_function(U8* sequence_set, U8* data_items)
{
    @imap_fetch_array(IMAP_WRAPPER_MAGIC_NUMBER, sequence_set, data_items);
}

ImapClient* @imap_new(CTask* mem_task)
{
    if (!mem_task) {
        return NULL;
    }

    ImapClient* c = CAlloc(sizeof(ImapClient), mem_task);
    c->mem_task = mem_task;
    c->o = Json.CreateObject(mem_task);
    c->state = 0;
    StrCpy(&c->selected, "");
    @imap_init_buffers(c);
    c->tag = 1;

    U64 a;
    I64 buffer_size = (MSize(&@imap_close_wrapper_function) + MSize(&@imap_connect_wrapper_function) + MSize(&@imap_fetch_wrapper_function) + MSize(&@imap_fetch_array_wrapper_function) + MSize(&@imap_select_wrapper_function) + MSize(&@imap_login_wrapper_function) + MSize(&@imap_logout_wrapper_function));
    buffer_size += buffer_size % 16;

    U64 code_ptr = CAlloc(buffer_size, c->mem_task->code_heap);
    I64 code_size = 0;

    // Create a copy of function and patch close
    code_size = MSize(&@imap_close_wrapper_function);
    c->close = code_ptr;
    MemCpy(c->close, &@imap_close_wrapper_function, code_size);
    code_ptr += code_size;

    a = c->close;
    while (a(U64*)[0] != IMAP_WRAPPER_MAGIC_NUMBER)
        ++a;
    MemSetI64(a, c, 1);
    a += 9;
    @patch_call_rel32(a, &@imap_close);

    // Create a copy of function and patch connect
    code_size = MSize(&@imap_connect_wrapper_function);
    c->connect = code_ptr;
    MemCpy(c->connect, &@imap_connect_wrapper_function, code_size);
    code_ptr += code_size;

    a = c->connect;
    while (a(U64*)[0] != IMAP_WRAPPER_MAGIC_NUMBER)
        ++a;
    MemSetI64(a, c, 1);
    a += 9;
    @patch_call_rel32(a, &@imap_connect);

    // Create a copy of function and patch select
    code_size = MSize(&@imap_select_wrapper_function);
    c->select = code_ptr;
    MemCpy(c->select, &@imap_select_wrapper_function, code_size);
    code_ptr += code_size;

    a = c->select;
    while (a(U64*)[0] != IMAP_WRAPPER_MAGIC_NUMBER)
        ++a;
    MemSetI64(a, c, 1);
    a += 9;
    @patch_call_rel32(a, &@imap_select);

    // Create a copy of function and patch login
    code_size = MSize(&@imap_login_wrapper_function);
    c->login = code_ptr;
    MemCpy(c->login, &@imap_login_wrapper_function, code_size);
    code_ptr += code_size;

    a = c->login;
    while (a(U64*)[0] != IMAP_WRAPPER_MAGIC_NUMBER)
        ++a;
    MemSetI64(a, c, 1);
    a += 9;
    @patch_call_rel32(a, &@imap_login);

    // Create a copy of function and patch logout
    code_size = MSize(&@imap_logout_wrapper_function);
    c->logout = code_ptr;
    MemCpy(c->logout, &@imap_logout_wrapper_function, code_size);
    code_ptr += code_size;

    a = c->logout;
    while (a(U64*)[0] != IMAP_WRAPPER_MAGIC_NUMBER)
        ++a;
    MemSetI64(a, c, 1);
    a += 9;
    @patch_call_rel32(a, &@imap_logout);

    // Create a copy of function and patch fetch
    code_size = MSize(&@imap_fetch_wrapper_function);
    c->fetch = code_ptr;
    MemCpy(c->fetch, &@imap_fetch_wrapper_function, code_size);
    code_ptr += code_size;

    a = c->fetch;
    while (a(U64*)[0] != IMAP_WRAPPER_MAGIC_NUMBER)
        ++a;
    MemSetI64(a, c, 1);
    a += 9;
    @patch_call_rel32(a, &@imap_fetch);

    // Create a copy of function and patch fetch_array
    code_size = MSize(&@imap_fetch_array_wrapper_function);
    c->fetch_array = code_ptr;
    MemCpy(c->fetch_array, &@imap_fetch_array_wrapper_function, code_size);
    code_ptr += code_size;

    a = c->fetch_array;
    while (a(U64*)[0] != IMAP_WRAPPER_MAGIC_NUMBER)
        ++a;
    MemSetI64(a, c, 1);
    a += 9;
    @patch_call_rel32(a, &@imap_fetch_array);

    return c;
}

"imap ";
