#include <codegen/backend.h>
#include <llvm-c/Core.h>
#include <llvm-c/Target.h>
#include <llvm-c/TargetMachine.h>
#include <llvm-c/Types.h>
#include <llvm/backend.h>
#include <llvm/parser.h>
#include <llvm/llvm-ir/types.h>
#include <llvm/llvm-ir/variables.h>
#include <llvm/llvm-ir/func.h>
#include <set/types.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/log.h>

BackendError export_IR(LLVMBackendCompileUnit* unit, const Target* target,
                       const TargetConfig* config) {
    DEBUG("exporting module to LLVM-IR...");

    BackendError err = SUCCESS;

    // convert module to LLVM-IR
    char* ir = LLVMPrintModuleToString(unit->module);

    char* basename = g_strjoin(".", target->name.str, "ll", NULL);
    // construct file name
    const char* filename = g_build_filename(config->archive_directory, basename, NULL);

    INFO("Writing LLVM-IR to %s", filename);

    DEBUG("opening file...");
    FILE* output = fopen(filename, "w");
    if (output == NULL) {
        ERROR("unable to open file: %s", filename);
        err = new_backend_impl_error(Implementation, NULL,
                                     "unable to open file for writing");
        LLVMDisposeMessage(ir);
    }

    DEBUG("printing LLVM-IR to file...");

    size_t bytes = fprintf(output, "%s", ir);

    // flush and close output file
    fflush(output);
    fclose(output);

    INFO("%ld bytes written to %s", bytes, filename);

    g_free((char*)filename);
    g_free(basename);

    // clean up LLVM-IR string
    LLVMDisposeMessage(ir);

    return err;
}

BackendError emit_module_to_file(LLVMBackendCompileUnit* unit,
                                 LLVMTargetMachineRef target_machine,
                                 LLVMCodeGenFileType file_type, char* error,
                                 const TargetConfig* config) {
    BackendError err = SUCCESS;
    DEBUG("Generating code...");

    const char* basename;
    const char* filename;
    switch (file_type) {
        case LLVMAssemblyFile:
            basename = g_strjoin(".", config->name, "s", NULL);
            filename = g_build_filename(config->archive_directory, basename, NULL);
            break;
        case LLVMObjectFile:
            basename = g_strjoin("", config->name, "o", NULL);
            filename = g_build_filename(config->archive_directory, basename, NULL);
            break;
        default:
            return new_backend_impl_error(Implementation, NULL,
                                          "invalid codegen file");
    }

    if (LLVMTargetMachineEmitToFile(target_machine, unit->module, filename,
                                    file_type, &error) != 0) {
        ERROR("failed to emit code: %s", error);
        err =
            new_backend_impl_error(Implementation, NULL, "failed to emit code");
        LLVMDisposeMessage(error);
    }

    g_free((void*) filename);
    g_free((void*) basename);
    return err;
}

BackendError export_object(LLVMBackendCompileUnit* unit, const Target* target,
                           const TargetConfig* config) {
    BackendError err = SUCCESS;
    DEBUG("exporting object file...");

    INFO("Using target (%s): %s with features: %s", target->name.str,
         target->triple.str, target->features.str);

    LLVMTargetRef llvm_target = NULL;
    char* error = NULL;

    LLVMInitializeAllTargets();
    LLVMInitializeAllTargetInfos();
    LLVMInitializeAllTargetMCs();
    // NOTE: for code generation (assmebly or binary) we need the following:
    LLVMInitializeAllAsmParsers();
    LLVMInitializeAllAsmPrinters();

    DEBUG("creating target...");
    if (LLVMGetTargetFromTriple(target->triple.str, &llvm_target, &error) !=
        0) {
        ERROR("failed to create target machine: %s", error);
        err = new_backend_impl_error(Implementation, NULL,
                                     "unable to create target machine");
        LLVMDisposeMessage(error);
        return err;
    }

    DEBUG("Creating target machine...");
    LLVMTargetMachineRef target_machine = LLVMCreateTargetMachine(
        llvm_target, target->triple.str, target->cpu.str, target->features.str,
        target->opt, target->reloc, target->model);

    if (config->print_asm) {
        err = emit_module_to_file(unit, target_machine, LLVMAssemblyFile, error,
                                  config);
    }

    if (err.kind != Success) {
        return err;
    }

    err = emit_module_to_file(unit, target_machine, LLVMObjectFile, error,
                              config);

    return err;
}

void list_available_targets() {
    DEBUG("initializing all available targets...");
    LLVMInitializeAllTargetInfos();

    printf("Available targets:\n");

    LLVMTargetRef target = LLVMGetFirstTarget();
    while (target) {
        const char* name = LLVMGetTargetName(target);
        const char* desc = LLVMGetTargetDescription(target);
        printf(" - %s: (%s)\n", name, desc);

        target = LLVMGetNextTarget(target);
    }

    char* default_triple = LLVMGetDefaultTargetTriple();

    printf("Default: %s\n", default_triple);

    LLVMDisposeMessage(default_triple);
}

BackendError export_module(LLVMBackendCompileUnit* unit, const Target* target,
                           const TargetConfig* config) {
    DEBUG("exporting module...");

    BackendError err = SUCCESS;

    export_object(unit, target, config);

    if (config->print_ir) {
        export_IR(unit, target, config);
    }

    return err;
}

static BackendError build_module(LLVMBackendCompileUnit* unit,
                                 LLVMGlobalScope* global_scope,
                                 const Module* module) {
    DEBUG("building module...");
    BackendError err = SUCCESS;

    err = impl_types(unit, global_scope, module->types);
    if (err.kind != Success) {
        return err;
    }

    // NOTE: functions of boxes are not stored in the box itself,
    //       thus for a box we only implement the type
    err = impl_types(unit, global_scope, module->boxes);
    if (err.kind != Success) {
        return err;
    }

    err = impl_global_variables(unit, global_scope, module->variables);
    if (err.kind != Success) {
        return err;
    }

    // TODO: implement functions
    err = impl_functions(unit, global_scope, module->functions);

    return err;
}

BackendError parse_module(const Module* module, const TargetConfig* config) {
    DEBUG("generating code for module %p", module);
    if (module == NULL) {
        ERROR("no module for codegen");
        return new_backend_impl_error(Implementation, NULL, "no module");
    }

    LLVMBackendCompileUnit* unit = malloc(sizeof(LLVMBackendCompileUnit));

    // we start with a LLVM module
    DEBUG("creating LLVM context and module");
    unit->context = LLVMContextCreate();
    unit->module =
        LLVMModuleCreateWithNameInContext(config->name, unit->context);

    LLVMGlobalScope* global_scope = new_global_scope(module);

    DEBUG("generating code...");

    BackendError err = build_module(unit, global_scope, module);
    if (err.kind == Success) {
        INFO("Module build successfully...");
        Target target = create_target_from_config(config);

        export_module(unit, &target, config);

        delete_target(target);
    }

    delete_global_scope(global_scope);

    LLVMDisposeModule(unit->module);
    LLVMContextDispose(unit->context);

    free(unit);

    return err;
}

LLVMGlobalScope* new_global_scope(const Module* module) {
    DEBUG("creating global scope...");
    LLVMGlobalScope* scope = malloc(sizeof(LLVMGlobalScope));

    scope->module = (Module*) module;
    scope->functions = g_hash_table_new(g_str_hash, g_str_equal);
    scope->variables = g_hash_table_new(g_str_hash, g_str_equal);
    scope->types = g_hash_table_new(g_str_hash, g_str_equal);

    return scope;
}

void delete_global_scope(LLVMGlobalScope* scope) {
    DEBUG("deleting global scope...");
    g_hash_table_unref(scope->functions);
    g_hash_table_unref(scope->types);
    g_hash_table_unref(scope->variables);
    free(scope);
}