#include "symbol_table.h"
#include "globals.h"
#include "print_enum.h"
#include <stdarg.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>

typedef struct st_entry     entry_t;
typedef struct st_entry_fn  entry_fn_t;
typedef struct st_entry_arr entry_arr_t;
typedef struct symbol_table st_t;

void st_entry_free(entry_t* ent)
{
    if (ent->entry_type == ENTRY_FUN) st_free(((entry_fn_t*)ent)->child_syms);

    free(ent);
}

st_t* st_init(st_t* parent)
{
    st_t* new_st   = malloc(sizeof(st_t));
    new_st->parent = parent;
    new_st->head = new_st->tail = NULL;
    new_st->n_vars              = 0;
    new_st->nest_level          = 0;
    return new_st;
}

void st_free(st_t* st)
{
    if (!st) return;

    entry_t* cur_ent = st->head;
    while (cur_ent)
    {
        entry_t* tmp = cur_ent;
        cur_ent      = cur_ent->next;
        st_entry_free(tmp);
    }

    free(st);
}

entry_t* st_lookup_helper(st_t* st, char* name, enum flags flgs, int frameno, int* saveframeno)
{
    if (st == NULL) return NULL;

    for (entry_t* cur_ent = st->head; cur_ent != NULL; cur_ent = cur_ent->next)
    {
        if (strcmp(cur_ent->name, name) != 0) continue;
        if ((flgs & LOOKUP_FUN) && (cur_ent->entry_type != ENTRY_FUN)) continue;
        if ((flgs & LOOKUP_VAR) && (cur_ent->entry_type != ENTRY_VAR)) continue;

        if (saveframeno) *saveframeno = frameno;

        return cur_ent;
    }

    if (flgs & LOOKUP_GLOBAL)
    {
        return st_lookup_helper(st->parent, name, flgs, frameno + 1, saveframeno);
    }
    return NULL;
}

entry_t* st_lookup(st_t* st, char* name, enum flags flgs, int* saveframeno)
{
    return st_lookup_helper(st, name, flgs, 0, saveframeno);
}

void st_insert_helper(entry_t* new, st_t* st, char* name, type_t type, bool isexp, bool isext)
{
    if (st->tail)
    {
        st->tail->next = new;
    }
    new->prev = st->tail;
    st->tail  = new;

    if (!st->head) st->head = new;

    *new = (entry_t){ .name             = name,
                      .type             = type,
                      .isexported       = isexp,
                      .isextern         = isext,
                      .containing_table = st,
                      .next             = NULL,
                      .prev             = new->prev };
}

entry_arr_t* st_insert_arr(st_t* st, char* name, type_t type, int n_dims, bool isexp, bool isext)
{
    entry_arr_t* new = malloc(sizeof(entry_arr_t) + n_dims * sizeof(entry_t*));
    st_insert_helper((entry_t*)new, st, name, type, isexp, isext);
    new->entry_type = ENTRY_VAR;
    new->n_dims     = n_dims;
    if (!isext) new->frameoffset = st->n_vars++;
    return new;
}

entry_fn_t* st_insert_fn(st_t* st_par, char* name, type_t type, int num_params, bool isexp,
                         bool isext)
{
    entry_fn_t* new = malloc(sizeof(entry_fn_t));
    st_insert_helper((entry_t*)new, st_par, name, type, isexp, isext);

    new->child_syms             = st_init(st_par);
    new->child_syms->nest_level = st_par->nest_level + 1;
    new->num_params             = num_params;
    new->entry_type             = ENTRY_FUN;
    new->fun_num                = global.n_fun++;

    return new;
}

entry_t* st_insert(st_t* st, char* name, type_t type, bool isexp, bool isext)
{
    entry_t* new = malloc(sizeof(entry_t));
    st_insert_helper(new, st, name, type, isexp, isext);
    new->entry_type = ENTRY_VAR;
    if (!isext) new->frameoffset = st->n_vars++;
    return new;
}


void st_append_symtabs(st_t* src, st_t* dst)
{
    if (!dst->head)
    {
        dst->head = src->head;
        dst->tail = src->tail;
    }
    else
    {
        dst->tail->next = src->head;
        src->head->prev = dst->tail;
        dst->tail = src->tail;
    }

    for (entry_t* ent = src->head; ent != NULL; ent = ent->next)
    {
        if (ent->entry_type == ENTRY_VAR)
            ent->frameoffset = dst->n_vars++;
        ent->containing_table = dst;
    }

    src->head = src->tail = NULL;
    st_free(src);
}

bool st_check(st_t* st, char* name, enum flags flgs)
{
    if (!st) return false;

    switch (flgs & ~(_LOOKUP_TYPE))
    {
        case LOOKUP_LOCAL:
        case LOOKUP_GLOBAL:
            return st_lookup(st, name, flgs, NULL);
            break;
        case LOOKUP_PARENT:
            return st_check(st->parent, name, LOOKUP_GLOBAL | (flgs & ~(_LOOKUP_SCOPE)));
            break;
        default:
            return false;
    }
}

int iprintf(int indentation, const char* format, ...)
{
    va_list args;
    va_start(args, format);
    while (indentation--)
        putchar('|');
    int ret = vprintf(format, args);
    va_end(args);
    return ret;
}

void st_print(st_t* st, char* funname, int indent)
{
    if (!funname) funname = "GLOBAL";
    if (!st) return;

    char* dash = "--------------------------------------";
    iprintf(indent, "%s\n", dash);
    iprintf(indent, "%s\n", funname);
    iprintf(indent, "%s\n", dash);

    entry_t* cur_ent = st->head;
    while (cur_ent)
    {
        if (cur_ent->entry_type == ENTRY_FUN)
            st_print(((entry_fn_t*)cur_ent)->child_syms, cur_ent->name, indent + 1);
        else
            iprintf(indent, "    %s %s %d %d %d\n", type_to_str(cur_ent->type), cur_ent->name,
                    cur_ent->frameoffset, cur_ent->isexported, cur_ent->isextern);

        cur_ent = cur_ent->next;
    }
}
