#include "ccc.h"
#include "codegen.h"
#include "scope.h"
#include <stdlib.h>
#include <stdio.h>

#define CGEN_PANIC(format, ...) {\
    fprintf(\
        stderr,\
        "ccc: code gen error: " format "\n" __VA_OPT__(,)\
        __VA_ARGS__);\
    exit(1);\
}

static const struct storage_location RV_LOC = {
    .type = REGISTER,
    .label = "rax",
};

static struct scope* scope;

static void emit_storage_loc(
    FILE* outfile,
    const struct storage_location* loc
) {
    switch (loc->type) {
        case REGISTER:
        case JMP_LABEL:
            fprintf(outfile, "%s", loc->label);
            break;
        case BP_OFFSET:
            if (loc->offset < 0)
                fprintf(outfile, "[rbp + %lld]", -loc->offset);
            else if (loc->offset > 0)
                fprintf(outfile, "[rbp - %lld]", loc->offset);
            else
                fprintf(outfile, "[rbp]");
    }
}

static void emit_int_lit(
    FILE* outfile,
    const struct int_lit_node* node,
    const struct storage_location* storage
) {
    if (storage != NULL) {
        fprintf(outfile, "\tmov ");
        emit_storage_loc(outfile, storage);
        fprintf(outfile, ", %lld\n", node->val);
    }
}

static void emit_expr(
    FILE* outfile,
    const struct expr_node* node,
    const struct storage_location* storage
) {
    switch (node->type) {
        case EXPR_EMPTY:
            break;
        case EXPR_INT_LIT:
            emit_int_lit(outfile, &node->as._int_lit, storage);
            break;
    }
}

static void emit_stmt(FILE* outfile, const struct stmt_node* node);

static struct type_def get_type_def(const char* type_name) {
    struct type_def type_def;
    if (!scope_get_type(scope, &type_def, type_name))
        CGEN_PANIC("size of type %s is not known", type_name);
    return type_def;
}

static void emit_var_decl(FILE* outfile, const struct var_decl_node* node) {
    struct type_def type_def = get_type_def(node->type.type);
    fprintf(outfile, "\tsub rsp, %llu\n", type_def.size);
    scope->bp_offset += type_def.size;
    scope_define_var(scope, (struct var_def) {
        .name = node->ident,
        .loc = {
            .type = BP_OFFSET,
            .offset = scope->bp_offset,
        },
    });
}

static void emit_return(FILE* outfile, const struct return_node* node) {
    if (node->ret_val != NULL) emit_expr(outfile, node->ret_val, &RV_LOC);
    fprintf(outfile, "\tmov rsp, rbp\n");
    fprintf(outfile, "\tpop rbp\n");
    fprintf(outfile, "\tret\n");
}

static void emit_group(FILE* outfile, const struct group_node* node) {
    const struct stmt_node* body_node = node->body_head;
    while (body_node != NULL) {
        emit_stmt(outfile, body_node);
        body_node = body_node->next;
    }
}

static void emit_stmt_group(FILE* outfile, const struct group_node* node) {
    scope_push(&scope);
    scope->bp_offset = scope->next_out->bp_offset; /* don't reset bp */

    emit_group(outfile, node);

    scope_pop(&scope);
    if (scope->bp_offset == 0)
        fprintf(outfile, "\tmov rsp, rbp\n");
    else {
        fprintf(outfile, "\tlea rsp, ");
        emit_storage_loc(outfile, &(struct storage_location) {
            .type = BP_OFFSET,
            .offset = scope->bp_offset,
        });
        fprintf(outfile, "\n");
    }
}

static void emit_stmt(FILE* outfile, const struct stmt_node* node) {
    switch (node->type) {
        case STMT_VAR_DECL:
            emit_var_decl(outfile, &node->as._var_decl);
            break;
        case STMT_RETURN:
            emit_return(outfile, &node->as._return);
            break;
        case STMT_EXPR:
            emit_expr(outfile, &node->as._expr, NULL);
            break;
        case STMT_GROUP:
            emit_stmt_group(outfile, &node->as._group);
            break;
    }
}

static void emit_fn_decl(FILE* outfile, const struct fn_decl_node* node) {
    fprintf(outfile, "%s:\n", node->name);
    fprintf(outfile, "\tpush rbp\n");
    fprintf(outfile, "\tmov rbp, rsp\n");

    scope_push(&scope);
    scope->bp_offset = 0;

    struct var_decl_node* args_node = node->args_head;
    long long arg_bp_offset = -8;
    while (args_node != NULL) {
        struct type_def type_def = get_type_def(args_node->type.type);
        scope_define_var(scope, (struct var_def) {
            .name = args_node->ident,
            .loc = {
                .type = BP_OFFSET,
                .offset = arg_bp_offset,
            },
        });
        arg_bp_offset -= type_def.size;
        args_node = args_node->next;
    }

    emit_group(outfile, &node->body);

    scope_pop(&scope);

    fprintf(outfile, "\tmov rsp, rbp\n");
    fprintf(outfile, "\tpop rbp\n");
    fprintf(outfile, "\tret\n");
}

static void emit_root_node(FILE* outfile, const struct root_node* node) {
    switch (node->type) {
        case ROOT_FN_DECL:
            emit_fn_decl(outfile, &node->as._fn_decl);
            break;
    }
}

void emit_code(const struct root_node* ast, const char* path) {
    FILE* outfile = fopen(path, "w");
    if (outfile == NULL) CCC_PANIC;

    fprintf(outfile, "section .text\n");
    scope_push(&scope);

    /* output all non-static function declarations as globals */
    const struct root_node* node = ast;
    while (node != NULL) {
        if (node->type == ROOT_FN_DECL) {
            const char* fn_name = node->as._fn_decl.name;
            scope_define_var(scope, (struct var_def) {
                .name = fn_name,
                .loc = {
                    .type = JMP_LABEL,
                    .label = fn_name, 
                },
            });
            fprintf(outfile, "global %s\n", fn_name);
        }

        node = node->next;
    }

    fprintf(outfile, "\n");

    /* actual code body */
    node = ast;
    while (node != NULL) {
        emit_root_node(outfile, node);
        fprintf(outfile, "\n");
        node = node->next;
    }

    scope_pop(&scope);
    fclose(outfile);
}
