#include "gen_byte_code.h"

#include "ctinfo.h"
#include "dbug.h"
#include "globals.h"
#include "memory.h"
#include "mymacros.h"
#include "print_enum.h"
#include "queue.h"
#include "symbol_table.h"
#include "traverse.h"
#include "tree_basic.h"
#include "types.h"
#include <string.h>

struct INFO
{
    int                 n_while;
    int                 n_for;
    int                 n_if;
    int                 n_condexpr;
    struct st_entry_fn* current_fn;
    node*               last_stmt;
};

static info* info_init(void)
{
    DBUG_ENTER("info_init");

    info* result = (info*)MEMmalloc(sizeof(struct INFO));
    if (!result) return NULL;

    *result = (info){ 0 };

    DBUG_RETURN(result);
}

static void info_free(info* inf)
{
    DBUG_ENTER("info_free");

    MEMfree(inf);
}

/* Helper functions */

void write_constant_pool(struct queue* queue)
{
    struct cp_link* node = queue->head;
    if (!node) return;

    do
    {
        switch (node->type)
        {
            case TY_int:
                OUT(".const int %d\n", node->ival);
                break;
            case TY_float:
                OUT(".const float %.9g\n", node->fval);
                break;
            default:
                CTIabort("Constant in constant pool with non-number type?");
        }
    } while ((node = node->next));
}

void write_var_ie(struct st_entry* var)
{
    if (var->isextern)
        OUT(".importvar \"%s\" %s\n", var->name, type_to_str(TY_CONSTLESS_TYPE(var->type)));
    else
        OUT(".exportvar \"%s\" %d\n", var->name, var->global_number);
}

void write_fun_ie(struct st_entry_fn* fun)
{
    char* inst = (fun->isextern ? "importfun" : "exportfun");

    OUT(".%s \"%s\" %s", inst, fun->name, type_to_str(fun->type));

    struct st_entry* param    = fun->child_syms->head;
    int              n_params = 0;
    while (n_params++ < fun->num_params)
    {
        OUT_CONTINUE(" %s", type_to_str(TY_CONSTLESS_TYPE(param->type)));
        param = param->next;
    }

    if (fun->isexported)
        OUT_CONTINUE(" _%d_%s\n", fun->fun_num, fun->name);
    else
        OUT_CONTINUE("\n");
}

void write_imports_export(struct queue* queue)
{
    struct ie_item* node = queue->head;

    if (!node) return;

    do
    {
        if (node->entry->entry_type == ENTRY_VAR)
            write_var_ie(node->entry);
        else
            write_fun_ie((struct st_entry_fn*)node->entry);
    } while ((node = node->next));
}

void write_global_table(struct symbol_table* st)
{
    if (!st) return;
    int n_globals = 0;

    for (struct st_entry* ent = st->head; ent != NULL; ent = ent->next)
    {
        if (ent->entry_type == ENTRY_VAR && !ent->isextern)
        {
            ent->global_number = n_globals++;
            if (ent->global_number != ent->frameoffset)
            {
                CTIerror("Global number != frameoffset");
            }
            OUT(".global %s\n", type_to_str(TY_CONSTLESS_TYPE(ent->type)));
        }
    }
}

#define VAR_HELPER(nt, n, inst) var_helper(inst, nt##_RELFRAMENO(n), nt##_STLINK(n))

void var_helper(char* inst, int relframeno, struct st_entry* var)
{
    int    frameoffset = var->frameoffset;
    type_t var_type    = var->type;
    char   ty          = type_to_char(var_type);

    if (var->isextern)
    {
        OUT("%c%se %d\n", ty, inst, var->import_export_num);
    }
    else if (IS_GLOBAL_SYMBOL_TABLE(var->containing_table))
    {
        // global variable
        OUT("%c%sg %d\n", ty, inst, frameoffset);
    }
    else if (relframeno == 0)
    {
        // local variable
        OUT("%c%s %d\n", ty, inst, frameoffset);
    }
    else
    {
        OUT("%c%sn %d %d\n", ty, inst, relframeno, frameoffset);
    }
}

/* Traversal functions */

node* GBCprogram(node* arg_node, info* arg_info)
{
    DBUG_ENTER("GBCprogram");

    TO(PROGRAM_DECLS);

    write_global_table(PROGRAM_SYMBOLTABLE(arg_node));
    write_constant_pool(PROGRAM_CONSTANTPOOL(arg_node));
    write_imports_export(PROGRAM_IMPORTSEXPORTS(arg_node));

    DBUG_RETURN(arg_node);
}

node* GBCassign(node* arg_node, info* arg_info)
{
    DBUG_ENTER("GBCassign");

    if (!TY_IS_ARRAY(VARLET_STLINK(ASSIGN_LET(arg_node))->type) ||
        VARLET_IS_ARRAYIDX(ASSIGN_LET(arg_node)))
    {
        TO(ASSIGN_EXPR);
        TO(ASSIGN_LET);
        DBUG_RETURN(arg_node);
    }

    node* let = ASSIGN_LET(arg_node);

    TO(ASSIGN_EXPR);

    OUT("%cnewa\n", type_to_char(TY_ARRAY_ELEMENT_TYPE(VARLET_STLINK(let)->type)));

    var_helper("store", 0, AS_VAR(VARLET_STLINK(let)));

    DBUG_RETURN(arg_node);
}

node* GBCvar(node* arg_node, info* arg_info)
{
    DBUG_ENTER("GBCvar");
    struct st_entry* var = VAR_STLINK(arg_node);

    if (VAR_IS_ARRAYIDX(arg_node)) TRAVopt(EXPRS_EXPR(VAR_INDICES(arg_node)), arg_info);

    VAR_HELPER(VAR, arg_node, "load");

    if (VAR_IS_ARRAYIDX(arg_node)) OUT("%cloada\n", type_to_char(TY_ARRAY_ELEMENT_TYPE(var->type)));

    DBUG_RETURN(arg_node);
}

node* GBCstmts(node* arg_node, info* arg_info)
{
    if (!STMTS_NEXT(arg_node))
    {
        arg_info->last_stmt = arg_node;
    }
    TO(STMTS_STMT);
    TO(STMTS_NEXT);
    return arg_node;
}

node* GBCvarlet(node* arg_node, info* arg_info)
{
    DBUG_ENTER("GBCvarlet");
    struct st_entry* var = VARLET_STLINK(arg_node);

    if (TY_IS_ARRAY(var->type))
    {
        TRAVopt(EXPRS_EXPR(VARLET_INDICES(arg_node)), arg_info);
        VAR_HELPER(VARLET, arg_node, "load");
        OUT("%cstorea\n", type_to_char(TY_ARRAY_ELEMENT_TYPE(var->type)));
    }
    else
        VAR_HELPER(VARLET, arg_node, "store");

    DBUG_RETURN(arg_node);
}

node* GBCcast(node* arg_node, info* arg_info)
{
    DBUG_ENTER("GBCcast");

    TO(CAST_EXPR);

    type_t expr_type = CAST_EXPRTYPE(arg_node);
    type_t cast_type = CAST_TYPE(arg_node);

    if (expr_type == cast_type) DBUG_RETURN(arg_node);

    switch (expr_type)
    {
        case TY_int:
            OUT("i2f\n");
            break;
        case TY_float:
            OUT("f2i\n");
            break;
        default:
            CTIabort("Invalid cast.");
    }

    DBUG_RETURN(arg_node);
}

node* GBCmonop(node* arg_node, info* arg_info)
{
    DBUG_ENTER("GBCmonop");

    TO(MONOP_EXPR);

    type_t type = MONOP_TYPE(arg_node);

    char* inst = "bnot";
    if (type == TY_int)
        inst = "ineg";
    else if (type == TY_float)
        inst = "fneg";

    OUT("%s\n", inst);

    DBUG_RETURN(arg_node);
}

node* GBCbinop(node* arg_node, info* arg_info)
{
    DBUG_ENTER("GBCbinop");

    TO(BINOP_LEFT);
    TO(BINOP_RIGHT);

    binop  op   = BINOP_OP(arg_node);
    type_t type = BINOP_EXPRTYPE(arg_node);

    char ty = type_to_char(type);

    switch (op)
    {
        case BO_add:
            OUT("%cadd\n", ty);
            break;
        case BO_sub:
            OUT("%csub\n", ty);
            break;
        case BO_mul:
            OUT("%cmul\n", ty);
            break;
        case BO_div:
            OUT("%cdiv\n", ty);
            break;
        case BO_mod:
            OUT("%crem\n", ty);
            break;
        case BO_ne:
            OUT("%cne\n", ty);
            break;
        case BO_eq:
            OUT("%ceq\n", ty);
            break;
        case BO_lt:
            OUT("%clt\n", ty);
            break;
        case BO_le:
            OUT("%cle\n", ty);
            break;
        case BO_gt:
            OUT("%cgt\n", ty);
            break;
        case BO_ge:
            OUT("%cge\n", ty);
            break;
        default:
            CTIabort("Either invalid enum value or unconverted and/or.");
    }

    DBUG_RETURN(arg_node);
}

node* GBCfloat(node* arg_node, info* arg_info)
{
    DBUG_ENTER("GBCfloat");

    float v = FLOAT_VALUE(arg_node);
    if (v == 1.0)
        OUT("floadc_1\n");
    else if (v == 0.0)
        OUT("floadc_0\n");
    else
        OUT("floadc %d\n", FLOAT_CPINDEX(arg_node));

    DBUG_RETURN(arg_node);
}

node* GBCint(node* arg_node, info* arg_info)
{
    DBUG_ENTER("GBCint");

    switch (INT_VALUE(arg_node))
    {
        case 0:
            OUT("iloadc_0\n");
            break;
        case 1:
            OUT("iloadc_1\n");
            break;
        case -1:
            OUT("iloadc_m1\n");
            break;
        default:
            OUT("iloadc %d\n", INT_CPINDEX(arg_node));
    }

    DBUG_RETURN(arg_node);
}

node* GBCbool(node* arg_node, info* arg_info)
{
    DBUG_ENTER("GBCbool");

    BOOL_VALUE(arg_node) ? OUT("bloadc_t\n") : OUT("bloadc_f\n");

    DBUG_RETURN(arg_node);
}

node* GBCcondexpr(node* arg_node, info* arg_info)
{
    DBUG_ENTER("GBCcondexpr");

    int n_condexpr = arg_info->n_condexpr++;

    TO(CONDEXPR_PREDEXPR);
    OUT("branch_f condexpr_falseexpr_%d\n", n_condexpr);

    TO(CONDEXPR_TRUEEXPR);
    OUT("jump condexpr_end_%d\n", n_condexpr);

    OUT("condexpr_falseexpr_%d:\n", n_condexpr);
    TO(CONDEXPR_FALSEEXPR);

    OUT("condexpr_end_%d:\n", n_condexpr);

    DBUG_RETURN(arg_node);
}

node* GBCif(node* arg_node, info* arg_info)
{
    DBUG_ENTER("GBCif");

    int n_if = arg_info->n_if++;

    TO(IF_CONDITION);
    OUT("branch_f if_elseblock_%d\n", n_if);

    TO(IF_BLOCK);
    OUT("jump if_end_%d\n", n_if);

    OUT("if_elseblock_%d:\n", n_if);
    TO(IF_ELSEBLOCK);

    OUT("if_end_%d:\n", n_if);

    DBUG_RETURN(arg_node);
}

node* GBCwhile(node* arg_node, info* arg_info)
{
    DBUG_ENTER("GBCwhile");

    int n_while = arg_info->n_while++;

    if (WHILE_ISDO(arg_node))
    {
        OUT("while_block_%d:\n", n_while);
        TO(WHILE_BLOCK);

        TO(WHILE_CONDITION);
        OUT("branch_t while_block_%d\n", n_while);
    }
    else
    {
        OUT("while_condition_%d:\n", n_while);
        TO(WHILE_CONDITION);
        OUT("branch_f while_end_%d\n", n_while);

        TO(WHILE_BLOCK);
        OUT("jump while_condition_%d\n", n_while);

        OUT("while_end_%d:\n", n_while);
    }

    DBUG_RETURN(arg_node);
}

node* GBCcall(node* arg_node, info* arg_info)
{
    DBUG_ENTER("GBCcall");

    struct st_entry_fn* func                 = CALL_STLINK(arg_node);
    int                 called_fn_nest_level = func->containing_table->nest_level;
    int                 cur_fn_nest_level    = arg_info->current_fn->containing_table->nest_level;

    /* Initiate */
    if (called_fn_nest_level == 0)
        OUT("isrg\n");
    else if (called_fn_nest_level == cur_fn_nest_level)
        OUT("isr\n");
    else if (called_fn_nest_level < cur_fn_nest_level)
        OUT("isrn %d\n", cur_fn_nest_level - called_fn_nest_level);
    else
        OUT("isrl\n");

    /* Stack setup (arguments) */
    TO(CALL_PARAMS);

    /* Jump to subroutine */
    if (func->isextern)
        OUT("jsre %d\n", func->import_export_num);
    else
        OUT("jsr %d _%d_%s\n", func->num_params, func->fun_num, func->name);

    if (CALL_ISSTMT(arg_node) && func->type != TY_void) OUT("%cpop\n", type_to_char(func->type));

    DBUG_RETURN(arg_node);
}

node* GBCreturn(node* arg_node, info* arg_info)
{
    DBUG_ENTER("GBCreturn");

    type_t return_type = RETURN_TYPE(arg_node);

    TO(RETURN_RETVAL);

    if (return_type == TY_void)
        OUT("return\n");
    else
        OUT("%creturn\n", type_to_char(return_type));

    DBUG_RETURN(arg_node);
}

node* GBCfundef(node* arg_node, info* arg_info)
{
    DBUG_ENTER("GBCfundef");

    struct st_entry_fn* fun_ent     = FUNDEF_STLINK(arg_node);
    int                 n_localvars = fun_ent->child_syms->n_vars - fun_ent->num_params;

    if (fun_ent->isextern) DBUG_RETURN(arg_node);

    OUT("_%d_%s:\n", fun_ent->fun_num, fun_ent->name);

    if (n_localvars) OUT("esr %d\n", n_localvars);

    struct st_entry_fn* cached_fn = arg_info->current_fn;
    arg_info->current_fn          = fun_ent;

    arg_info->last_stmt = NULL;

    node* body = FUNDEF_BODY(arg_node);
    if (body)
    {
        TRAVopt(FUNBODY_VARDECLS(body), arg_info);
        TRAVopt(FUNBODY_STMTS(body), arg_info);

        if (!arg_info->last_stmt || NODE_TYPE(STMTS_STMT(arg_info->last_stmt)) != N_return)
            OUT("return\n");

        OUT_CONTINUE("\n");
        TRAVopt(FUNBODY_FUNDEFS(body), arg_info);
    }

    arg_info->current_fn = cached_fn;

    DBUG_RETURN(arg_node);
}

node* GBCfor(node* arg_node, info* arg_info)
{
    DBUG_ENTER("GBCfor");

    int n_for = arg_info->n_for++;

    struct st_entry* var_ent = FOR_VARSTLINK(arg_node);

    int init_var = var_ent->frameoffset;
    int lim_var  = var_ent->frameoffset - 1;
    int step_var = var_ent->frameoffset - 2;

    // Init for induction variable
    TO(FOR_VARINITVAL);
    OUT("istore %d\n", init_var);

    // Limit for induction variable
    TO(FOR_VARLIMVAL);
    OUT("istore %d\n", lim_var);

    // Step for induction variable
    TO(FOR_VARSTEPVAL);
    OUT("istore %d\n", step_var);

    // For condition
    OUT("for_condition_%d:\n", n_for);

    OUT("iload %d\n", init_var);
    OUT("iload %d\n", lim_var);

    if (NODE_TYPE(FOR_VARSTEPVAL(arg_node)) == N_int)
    {
        int var_step_val = INT_VALUE(FOR_VARSTEPVAL(arg_node));
        if (var_step_val > 0)
            OUT("ilt\n");
        else
            OUT("igt\n");
    }
    else
    {
        OUT("iload %d\n", step_var);
        OUT("iloadc_0\n");
        OUT("igt\n");
        OUT("branch_f for_condition_negstep_%d\n", n_for);

        // Positive step
        OUT("ilt\n");
        OUT("jump for_condition_branch_%d\n", n_for);

        // Negative step
        OUT("for_condition_negstep_%d:\n", n_for);
        OUT("igt\n");
    }

    OUT("for_condition_branch_%d:\n", n_for);
    OUT("branch_f for_end_%d\n", n_for);

    // For code block
    OUT("for_block_%d:\n", n_for);
    TO(FOR_BLOCK);

    // Incr/Decr induction var
    OUT("iload %d\n", init_var);
    OUT("iload %d\n", step_var);
    OUT("iadd\n");
    OUT("istore %d\n", init_var);

    OUT("jump for_condition_%d\n", n_for);

    // END
    OUT("for_end_%d:\n", n_for);

    DBUG_RETURN(arg_node);
}

/*
 * Traversal start function
 */

node* GBCdoGenByteCode(node* syntaxtree)
{
    DBUG_ENTER("GBCdoGenByteCode");
    info* arg_info = info_init();

    TRAVpush(TR_gbc);
    syntaxtree = TRAVdo(syntaxtree, arg_info);
    TRAVpop();

    info_free(arg_info);

    if (global.outfileptr != stdout) fclose(global.outfileptr);

    DBUG_RETURN(syntaxtree);
}
