#include <stdarg.h>
#include <stdbool.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>

#include "Operation.h"
#include "Script.h"

static void sError(Script* sc, const char* format, ...) {
    va_list args;
    va_start(args, format);
    vsnprintf(sc->error, SCRIPT_ERROR_SIZE, format, args);
    va_end(args);
}

static bool sPrinter(Object* o) {
    if(o->type == OT_INT) {
        printf("%d\n", o->data.intValue);
        return false;
    }
    return true;
}

static ObjectPrinter printer = sPrinter;

static bool sRead(Script* sc, void* buffer, int length) {
    if(sc->readIndex + length > sc->byteCodeLength) {
        return true;
    }
    memcpy(buffer, sc->byteCode + sc->readIndex, length);
    sc->readIndex += length;
    return false;
}

static Operation sReadOperation(Script* sc) {
    unsigned char c;
    if(sRead(sc, &c, 1)) {
        return OP_NOTHING;
    } else if(sRead(sc, &sc->line, sizeof(int))) {
        // operation without line
        return OP_NOTHING;
    }
    return c;
}

static void sPush(Script* sc, Object* o) {
    if(sc->stackIndex >= SCRIPT_STACK_SIZE) {
        sError(sc, "stack overflow on line %d", sc->line);
        return;
    }
    sc->stack[sc->stackIndex++] = *o;
}

static bool sPop(Script* sc, Object* o) {
    if(sc->stackIndex <= 0) {
        sError(sc, "stack underflow on line %d", sc->line);
        return true;
    }
    *o = sc->stack[--sc->stackIndex];
    return false;
}

static void sPushInt(Script* sc) {
    int value = 0;
    if(sRead(sc, &value, sizeof(int))) {
        sError(sc, "cannot read an int from the bytecode on line %d", sc->line);
        return;
    }
    Object o = {.type = OT_INT, .data.intValue = value};
    sPush(sc, &o);
}

static void sPushNull(Script* sc) {
    Object o = {.type = OT_NULL};
    sPush(sc, &o);
}

static void sIntBinary(Script* sc, int (*f)(int, int)) {
    Object a;
    if(sPop(sc, &a)) {
        return;
    }
    Object b;
    if(sPop(sc, &b)) {
        return;
    }
    if(a.type != OT_INT || b.type != OT_INT) {
        sError(sc, "object is not an int on line %d", sc->line);
        return;
    }
    Object o = {.type = OT_INT, .data.intValue = f(a.data.intValue, b.data.intValue)};
    sPush(sc, &o);
}

static int sIntAdd(int a, int b) {
    return a + b;
}

static int sIntMul(int a, int b) {
    return a * b;
}

static void sPrint(Script* sc) {
    Object o;
    if(sPop(sc, &o)) {
        return;
    }
    if(printer(&o)) {
        sError(sc, "cannot print given object on line %d", sc->line);
    }
}

static void sConsumeInstruction(Script* sc) {
    switch(sReadOperation(sc)) {
        case OP_NOTHING: break;
        case OP_PUSH_INT: sPushInt(sc); break;
        case OP_PUSH_NULL: sPushNull(sc); break;
        case OP_ADD: sIntBinary(sc, sIntAdd); break;
        case OP_MUL: sIntBinary(sc, sIntMul); break;
        case OP_PRINT: sPrint(sc); break;
    }
}

static bool sHasData(Script* sc) {
    return sc->readIndex < sc->byteCodeLength;
}

Script* sInit(unsigned char* byteCode, int codeLength) {
    Script* sc = malloc(sizeof(Script));
    sc->error[0] = '\0';
    sc->byteCode = byteCode;
    sc->byteCodeLength = codeLength;
    sc->readIndex = 0;
    sc->stackIndex = 0;
    sc->line = 0;
    return sc;
}

void sDelete(Script* sc) {
    free(sc->byteCode);
}

void sRun(Script* sc) {
    while(sHasData(sc)) {
        sConsumeInstruction(sc);
        if(sc->error[0] != '\0') {
            puts(sc->error);
            return;
        }
    }
}

void sSetPrinter(ObjectPrinter p) {
    printer = p;
}