#include "server/snuviscript/Snuvi.h"
#include "Compiler.h"
#include "libraries/Math.h"
#include "libraries/Time.h"
#include "tokenizer/Tokenizer.h"
#include "utils/HashMap.h"
#include "utils/StringBuffer.h"
#include "vm/Script.h"

static const char* unicode(int32 c) {
    static char buffer[5];
    int index = 0;
    if(c > 0xFFFF) {
        buffer[index++] = 0xF0 | ((c >> 18) & 0x07);
        buffer[index++] = 0x80 | ((c >> 12) & 0x3F);
        buffer[index++] = 0x80 | ((c >> 6) & 0x3F);
        buffer[index++] = 0x80 | (c & 0x3F);
    } else if(c > 0x7FF) {
        buffer[index++] = 0xE0 | ((c >> 12) & 0x0F);
        buffer[index++] = 0x80 | ((c >> 6) & 0x3F);
        buffer[index++] = 0x80 | (c & 0x3F);
    } else if(c > 0x7F) {
        buffer[index++] = 0xC0 | ((c >> 6) & 0x1F);
        buffer[index++] = 0x80 | (c & 0x3F);
    } else {
        buffer[index++] = c;
    }
    buffer[index++] = '\0';
    return buffer;
}

static void printString(Script* sc) {
    int length;
    Pointer p;
    if(!sPopPointer(sc, &p) || sGetPointerLength(sc, &p, &length)) {
        return;
    }
    for(int i = 0; i < length; i++) {
        const void* data = sCheckAddress(sc, &p, sizeof(int));
        if(data != nullptr) {
            int c;
            memcpy(&c, data, sizeof(int));
            printf(unicode(c));
        }
        p.offset += sizeof(int);
    }
}

static void printInt32(Script* sc) {
    int32 i;
    if(sPopInt32(sc, &i)) {
        printf("%d", i);
    }
}

static void wait(Script* sc) {
    sError(sc, "w");
}

static Snuvi::Event event = Snuvi::Event::NONE;

static void getEvent(Script* sc) {
    sPushInt32(sc, event);
}

static void initPrinter() {
    Snuvi::initFunction("print", dtVoid(), printString);
    Snuvi::addArgument(dtConst(dtText()));
    Snuvi::addFunction();
    Snuvi::initFunction("print", dtVoid(), printInt32);
    Snuvi::addArgument(dtInt32());
    Snuvi::addFunction();
}

static int scriptId = 0;
static HashMap<int, Script*> scripts;
static Function function;

void Snuvi::init() {
    gfsInit();
    gstsInit();
    lTimeRegister();
    lMathRegister();
    initPrinter();
    Snuvi::initFunction("wait", dtVoid(), wait);
    Snuvi::addFunction();
    Snuvi::initFunction("getEvent", dtInt32(), getEvent);
    Snuvi::addFunction();

    atexit([]() {
        gfsDelete();
        gstsDelete();
        Snuvi::termAll();
    });
}

void Snuvi::initFunction(const char* name, DataType returnType,
                         ScriptFunction sf) {
    gfInit(&function, name, returnType, sf);
}

void Snuvi::addArgument(DataType type) {
    gfAddArgument(&function, type);
}

void Snuvi::addFunction() {
    gfsAdd(&function);
}

static void logError(const char* msg, int line) {
    puts(msg);
    printf("line: %d\n", line);
}

static bool runScript(Script* script) {
    sRun(script);
    if(script->error[0] == 'w' && script->error[1] == '\0') {
        script->error[0] = '\0';
        return false;
    } else if(script->error[0] != '\0') {
        logError(script->error, script->line);
        return true;
    }
    return script->readIndex >= script->code->length;
}

int Snuvi::start(const char* path) {
    StringBuffer<256> s("resources/scripts/");
    s.append(path).append(".snuvi");
    if(tTokenize(s)) {
        logError(tGetError(), tGetLine());
        return -1;
    }
    ByteCode* code = cCompile();
    if(code == nullptr) {
        logError(cGetError(), cGetLine());
        return -1;
    }
    Script* script = sInit(code);
    if(runScript(script)) {
        sDelete(script);
        return -1;
    }
    int id = scriptId++;
    scripts.tryEmplace(id, script);
    return id;
}

void Snuvi::termAll() {
    for(auto& entry : scripts) {
        sDelete(entry.value);
    }
    scripts.clear();
}

void Snuvi::callEvent(Event e) {
    event = e;
    for(auto& entry : scripts) {
        if(runScript(entry.value)) {
            sDelete(entry.value);
            scripts.remove(entry.getKey());
        }
    }
}

static int32 readChar(int& index, const char* s) {
    if(s[index] == '\0') {
        return '\0';
    }
    return s[index++];
}

Pointer Snuvi::toString(Script* sc, const char* s) {
    List<int> data;
    int index = 0;
    while(s[index] != '\0') {
        int32 c = readChar(index, s);
        if((c & 0xE0) == 0xC0) {
            c = ((c & 0x1F) << 6) | (readChar(index, s) & 0x3F);
        } else if((c & 0xF0) == 0xE0) {
            c = ((c & 0xF) << 12) | ((readChar(index, s) & 0x3F) << 6);
            c |= readChar(index, s) & 0x3F;
        } else if((c & 0xF8) == 0xF0) {
            c = ((c & 0x7) << 18) | ((readChar(index, s) & 0x3F) << 12);
            c |= (readChar(index, s) & 0x3F) << 6;
            c |= readChar(index, s) & 0x3F;
        }
        data.add(c);
    }
    Pointer p;
    p.offset = 0;
    p.array = asAllocate(&sc->arrays, sizeof(int32), data.getLength());
    SnuviArray* array = asGet(&sc->arrays, p.array);
    if(array == nullptr) {
        sError(sc, "cannot allocate string memory");
        p.offset = -1;
        p.array = -1;
    } else {
        memcpy(array->data, data.begin(), sizeof(int32) * data.getLength());
    }
    return p;
}