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

#include "utils/ByteCodePrinter.h"

#define LINE_LENGTH 80

static ByteCode* code;
static int readIndex;
static int line;
static char buffer[LINE_LENGTH];
static int bIndex;

static void btPrintHeading() {
    puts("|  Index |  Line |            Operation | Argument 1 | Argument 2 |");
    puts("|--------|-------|----------------------|------------|------------|");
}

static void btAdd(const char* format, ...) {
    va_list args;
    va_start(args, format);
    bIndex += vsnprintf(buffer + bIndex, LINE_LENGTH - bIndex, format, args);
    va_end(args);
}

static void btFillBase() {
    buffer[0] = '\0';
    bIndex = 0;
    btAdd("| %6d | %5d |", readIndex - 1, line);
}

static void btRead(void* buffer, int length) {
    if(readIndex + length > code->length) {
        return;
    }
    memcpy(buffer, code->code + readIndex, length);
    readIndex += length;
}

static void btAddOperation(const char* s) {
    if(s[0] != '\0' && s[1] != '\0' && s[2] != '\0' && s[3] != '\0') {
        s += 3;
    }
    btAdd(" %20s |", s);
}

static void btAddInt() {
    int value = 0;
    btRead(&value, sizeof(int));
    btAdd(" %10d |", value);
}

static void btAddFloat() {
    float value = 0;
    btRead(&value, sizeof(float));
    btAdd(" %10.2f |", value);
}

static void btAddFiller() {
    btAdd("            |");
}

static void sPrintLine() {
    btRead(&line, 2);
    printf("| %6d |-------|----------------------|------------|------------|\n",
           readIndex - 3);
}

static void btPrintOp(const char* op) {
    btFillBase();
    btAddOperation(op);
    btAddFiller();
    btAddFiller();
    puts(buffer);
}

static void btPrintInt(const char* op) {
    btFillBase();
    btAddOperation(op);
    btAddInt();
    btAddFiller();
    puts(buffer);
}

static void btPrintInt2(const char* op) {
    btFillBase();
    btAddOperation(op);
    btAddInt();
    btAddInt();
    puts(buffer);
}

static void btPrintFloat(const char* op) {
    btFillBase();
    btAddOperation(op);
    btAddFloat();
    btAddFiller();
    puts(buffer);
}

#define PRINT_OP_BASE(op, name)                                                \
    case op:                                                                   \
        btPrint##name(#op);                                                    \
        break;
#define PRINT_OP(op) PRINT_OP_BASE(op, Op)
#define PRINT_OP_INT(op) PRINT_OP_BASE(op, Int)
#define PRINT_OP_INT2(op) PRINT_OP_BASE(op, Int2)
#define PRINT_NUMBER_OP(op) PRINT_OP(OP_##op##_INT) PRINT_OP(OP_##op##_FLOAT)
#define PRINT_TYPES(TYPE)                                                      \
    PRINT_OP_INT(OP_LOAD_##TYPE);                                              \
    PRINT_OP_INT(OP_STORE_##TYPE);                                             \
    PRINT_OP_INT(OP_RETURN_##TYPE);                                            \
    PRINT_OP(OP_EQUAL_##TYPE);

static void btConsumeOperation() {
    Operation op = code->code[readIndex++];
    switch(op) {
        PRINT_TYPES(INT);
        PRINT_TYPES(BOOL);
        PRINT_TYPES(FLOAT);
        PRINT_OP(OP_NOTHING);
        PRINT_OP_INT(OP_PUSH_INT);
        PRINT_OP_BASE(OP_PUSH_FLOAT, Float);
        PRINT_OP(OP_PUSH_TRUE);
        PRINT_OP(OP_PUSH_FALSE);
        PRINT_NUMBER_OP(ADD);
        PRINT_NUMBER_OP(SUB);
        PRINT_NUMBER_OP(MUL);
        PRINT_NUMBER_OP(DIV);
        PRINT_OP(OP_MOD_INT);
        PRINT_NUMBER_OP(INVERT_SIGN);
        PRINT_NUMBER_OP(LESS);
        PRINT_NUMBER_OP(GREATER);
        PRINT_OP(OP_NOT);
        PRINT_OP(OP_AND);
        PRINT_OP(OP_OR);
        PRINT_OP(OP_BIT_NOT);
        PRINT_OP(OP_BIT_AND);
        PRINT_OP(OP_BIT_OR);
        PRINT_OP(OP_BIT_XOR);
        PRINT_OP(OP_LEFT_SHIFT);
        PRINT_OP(OP_RIGHT_SHIFT);
        PRINT_NUMBER_OP(PRINT);
        PRINT_OP(OP_PRINT_BOOL);
        PRINT_OP_INT(OP_GOTO);
        PRINT_OP_INT(OP_IF_GOTO);
        PRINT_OP_INT(OP_PEEK_FALSE_GOTO);
        PRINT_OP_INT(OP_PEEK_TRUE_GOTO);
        PRINT_OP_INT2(OP_GOSUB);
        PRINT_OP_INT(OP_RETURN);
        PRINT_OP_INT2(OP_RESERVE);
        case OP_LINE: sPrintLine(); break;
    }
}

void btPrint(ByteCode* bt) {
    code = bt;
    readIndex = 0;
    line = 0;
    btPrintHeading();
    while(readIndex < code->length) {
        btConsumeOperation();
    }
}