#pragma once

#if !defined(__STDC_FORMAT_MACROS)
#define __STDC_FORMAT_MACROS
#endif

#include <nan.h>
#include <zlib.h>
#include <cinttypes>
#include <cstdio>

#include "../cpp/sysdep.h"

using namespace v8;

#define THROW(msg) Nan::ThrowError(msg); isInvalid = true; printf("[Error %s:%d] %s\n", __FILE__, __LINE__, msg)

class Decoder {
public:
    Decoder(const Nan::TypedArrayContents<uint8_t>& array)
    : data(*array)
    , size(array.length())
    , isInvalid(false)
    , offset(0)
    {
        const auto version = read8();
        if (version != FORMAT_VERSION) {
            THROW("Bad version number.");
            isInvalid = true;
        }
    }

   Decoder(const uint8_t* data_, size_t length_, bool skipVersion = false)
    : data(data_)
    , size(length_)
    , isInvalid(false)
    , offset(0)
    {
        if (!skipVersion) {
            const auto version = read8();
            if (version != FORMAT_VERSION) {
                THROW("Bad version number.");
                isInvalid = true;
            }
        }
    }

    uint8_t read8() {
        if (offset + sizeof(uint8_t) > size) {
            THROW("Reading a byte passes the end of the buffer.");
            return 0;
        }
        auto val = *reinterpret_cast<const uint8_t*>(data + offset);
        offset += sizeof(uint8_t);
        return val;
    }

    uint16_t read16() {
        if (offset + sizeof(uint16_t) > size) {
            THROW("Reading two bytes passes the end of the buffer.");
            return 0;
        }

        uint16_t val = _erlpack_be16(*reinterpret_cast<const uint16_t*>(data + offset));
        offset += sizeof(uint16_t);
        return val;
    }

    uint32_t read32() {
        if (offset + sizeof(uint32_t) > size) {
            THROW("Reading three bytes passes the end of the buffer.");
            return 0;
        }

        uint32_t val = _erlpack_be32(*reinterpret_cast<const uint32_t*>(data + offset));
        offset += sizeof(uint32_t);
        return val;
    }

    uint64_t read64() {
        if (offset + sizeof(uint64_t) > size) {
            THROW("Reading four bytes passes the end of the buffer.");
            return 0;
        }

        uint64_t val = _erlpack_be64(*reinterpret_cast<const uint64_t*>(data + offset));
        offset += sizeof(val);
        return val;
    }

    Local<Value> decodeSmallInteger() {
        return Nan::New<Integer>(read8());
    }

    Local<Value> decodeInteger() {
        return Nan::New<Integer>((int32_t)read32());
    }

    Local<Value> decodeArray(uint32_t length) {
        Local<Object> array = Nan::New<Array>(length);
        for(uint32_t i = 0; i < length; ++i) {
            auto value = unpack();
            if (isInvalid) {
                return Nan::Undefined();
            }
            Nan::Set(array, i, value);
        }
        return array;
    }

    Local<Value> decodeList() {
        const uint32_t length = read32();
        auto array = decodeArray(length);

        const auto tailMarker = read8();
        if (tailMarker != NIL_EXT) {
            THROW("List doesn't end with a tail marker, but it must!");
            return Nan::Null();
        }

        return array;
    }

    Local<Value> decodeTuple(uint32_t length) {
        return decodeArray(length);
    }

    Local<Value> decodeNil() {
        Local<Object> array = Nan::New<Array>(0);
        return array;
    }

    Local<Value> decodeMap() {
        const uint32_t length = read32();
        auto map = Nan::New<Object>();

        for(uint32_t i = 0; i < length; ++i) {
            const auto key = unpack();
            const auto value = unpack();
            if (isInvalid) {
                return Nan::Undefined();
            }
            Nan::Set(map, key, value);
        }

        return map;
    }

    const char* readString(uint32_t length) {
        if (offset + length > size) {
            THROW("Reading sequence past the end of the buffer.");
            return NULL;
        }

        const uint8_t* str = data + offset;
        offset += length;
        return (const char*)str;
    }

    Local<Value> processAtom(const char* atom, uint16_t length) {
        if (atom == NULL) {
            return Nan::Undefined();
        }

        if (length >= 3 && length <= 5) {
            if (length == 3 && strncmp(atom, "nil", 3) == 0) {
                return Nan::Null();
            }
            else if (length == 4 && strncmp(atom, "null", 4) == 0) {
                return Nan::Null();
            }
            else if(length == 4 && strncmp(atom, "true", 4) == 0) {
                return Nan::True();
            }
            else if (length == 5 && strncmp(atom, "false", 5) == 0) {
                return Nan::False();
            }
        }

        return Nan::New(atom, length).ToLocalChecked();
    }

    Local<Value> decodeAtom() {
        auto length = read16();
        const char* atom = readString(length);
        return processAtom(atom, length);
    }

    Local<Value> decodeSmallAtom() {
        auto length = read8();
        const char* atom = readString(length);
        return processAtom(atom, length);
    }

    Local<Value> decodeFloat() {
        const uint8_t FLOAT_LENGTH = 31;
        const char* floatStr = readString(FLOAT_LENGTH);
        if (floatStr == NULL) {
            return Nan::Undefined();
        }

        double number;
        char nullTerimated[FLOAT_LENGTH + 1] = {0};
        memcpy(nullTerimated, floatStr, FLOAT_LENGTH);

        auto count = sscanf(nullTerimated, "%lf", &number);
        if (count != 1) {
            THROW("Invalid float encoded.");
            return Nan::Null();
        }

        return Nan::New<Number>(number);
    }

    Local<Value> decodeNewFloat() {
        union {
            uint64_t ui64;
            double df;
        } val;
        val.ui64 = read64();
        return Nan::New<Number>(val.df);
    }

    Local<Value> decodeBig(uint32_t digits) {
        const uint8_t sign = read8();

        if (digits > 8) {
            THROW("Unable to decode big ints larger than 8 bytes");
            return Nan::Null();
        }

        uint64_t value = 0;
        uint64_t b = 1;
        for(uint32_t i = 0; i < digits; ++i) {
            uint64_t digit = read8();
            value += digit * b;
            b <<= 8;
        }

        if (digits <= 4) {
            if (sign == 0) {
                return Nan::New<Integer>(static_cast<uint32_t>(value));
            }

            const bool isSignBitAvailable = (value & (1 << 31)) == 0;
            if (isSignBitAvailable) {
                int32_t negativeValue = -static_cast<int32_t>(value);
                return Nan::New<Integer>(negativeValue);
            }
        }

        char outBuffer[32] = {0}; // 9223372036854775807
        const char* const formatString = sign == 0 ? "%" PRIu64 : "-%" PRIu64;
        const int res = sprintf(outBuffer, formatString, value);

        if (res < 0) {
            THROW("Unable to convert big int to string");
            return Nan::Null();
        }
        const uint8_t length = static_cast<const uint8_t>(res);

        return Nan::New(outBuffer, length).ToLocalChecked();
    }

    Local<Value> decodeSmallBig() {
        const auto bytes = read8();
        return decodeBig(bytes);
    }

    Local<Value> decodeLargeBig() {
        const auto bytes = read32();
        return decodeBig(bytes);
    }

    Local<Value> decodeBinaryAsString() {
        const auto length = read32();
        const char* str = readString(length);
        if (str == NULL) {
            return Nan::Undefined();
        }
        auto binaryString = Nan::New(str, length);
        return binaryString.ToLocalChecked();
    }

    Local<Value> decodeString() {
        const auto length = read16();
        const char* str = readString(length);
        if (str == NULL) {
            return Nan::Undefined();
        }
        auto binaryString = Nan::New(str, length);
        return binaryString.ToLocalChecked();
    }

    Local<Value> decodeStringAsList() {
        const auto length = read16();
        if (offset + length > size) {
            THROW("Reading sequence past the end of the buffer.");
            return Nan::Null();
        }

        Local<Object> array = Nan::New<Array>(length);
        for(uint16_t i = 0; i < length; ++i) {
            Nan::Set(array, i, decodeSmallInteger());
        }

        return array;
    }

    Local<Value> decodeSmallTuple() {
        return decodeTuple(read8());
    }

    Local<Value> decodeLargeTuple() {
        return decodeTuple(read32());
    }

    Local<Value> decodeCompressed() {
        const uint32_t uncompressedSize = read32();

        unsigned long sourceSize = uncompressedSize;
        uint8_t* outBuffer = (uint8_t*)malloc(uncompressedSize);
        const int ret = uncompress(outBuffer, &sourceSize, (const unsigned char*)(data + offset), (uLong)(size - offset));

        offset += sourceSize;
        if (ret != Z_OK) {
            free(outBuffer);
            THROW("Failed to uncompresss compressed item");
            return Nan::Null();
        }

        Decoder children(outBuffer, uncompressedSize, true);
        Nan::MaybeLocal<Value> value = children.unpack();
        free(outBuffer);
        return value.ToLocalChecked();
    }

    Local<Value> decodeReference() {
        auto reference = Nan::New<Object>();
        Nan::Set(reference, Nan::New("node").ToLocalChecked(), unpack()).FromJust();

        Local<Object> ids = Nan::New<Array>(1);
        Nan::Set(ids, 0, Nan::New<Integer>(read32())).FromJust();
        Nan::Set(reference, Nan::New("id").ToLocalChecked(), ids).FromJust();

        Nan::Set(reference, Nan::New("creation").ToLocalChecked(), Nan::New<Integer>(read8())).FromJust();

        return reference;
    }

    Local<Value> decodeNewReference() {
        auto reference = Nan::New<Object>();

        uint16_t len = read16();
        Nan::Set(reference, Nan::New("node").ToLocalChecked(), unpack()).FromJust();
        Nan::Set(reference, Nan::New("creation").ToLocalChecked(), Nan::New<Integer>(read8())).FromJust();

        Local<Object> ids = Nan::New<Array>(len);
        for(uint16_t i = 0; i < len; ++i) {
            Nan::Set(ids, i, Nan::New<Integer>(read32())).FromJust();
        }
        Nan::Set(reference, Nan::New("id").ToLocalChecked(), ids).FromJust();

        return reference;
    }

    Local<Value> decodePort() {
        auto port = Nan::New<Object>();
        Nan::Set(port, Nan::New("node").ToLocalChecked(), unpack()).FromJust();
        Nan::Set(port, Nan::New("id").ToLocalChecked(), Nan::New<Integer>(read32())).FromJust();
        Nan::Set(port, Nan::New("creation").ToLocalChecked(), Nan::New<Integer>(read8())).FromJust();
        return port;
    }

    Local<Value> decodePID() {
        auto pid = Nan::New<Object>();
        Nan::Set(pid, Nan::New("node").ToLocalChecked(), unpack()).FromJust();
        Nan::Set(pid, Nan::New("id").ToLocalChecked(), Nan::New<Integer>(read32())).FromJust();
        Nan::Set(pid, Nan::New("serial").ToLocalChecked(), Nan::New<Integer>(read32())).FromJust();
        Nan::Set(pid, Nan::New("creation").ToLocalChecked(), Nan::New<Integer>(read8())).FromJust();
        return pid;
    }

    Local<Value> decodeExport() {
        auto exp = Nan::New<Object>();
        Nan::Set(exp, Nan::New("mod").ToLocalChecked(), unpack()).FromJust();
        Nan::Set(exp, Nan::New("fun").ToLocalChecked(), unpack()).FromJust();
        Nan::Set(exp, Nan::New("arity").ToLocalChecked(), unpack()).FromJust();
        return exp;
    }

    Local<Value> unpack() {
        if (isInvalid) {
            return Nan::Undefined();
        }

        if(offset >= size) {
            THROW("Unpacking beyond the end of the buffer");
            return Nan::Undefined();
        }

        const auto type = read8();
        switch(type) {
            case SMALL_INTEGER_EXT:
                return decodeSmallInteger();
            case INTEGER_EXT:
                return decodeInteger();
            case FLOAT_EXT:
                return decodeFloat();
            case NEW_FLOAT_EXT:
                return decodeNewFloat();
            case ATOM_EXT:
                return decodeAtom();
            case SMALL_ATOM_EXT:
                return decodeSmallAtom();
            case SMALL_TUPLE_EXT:
                return decodeSmallTuple();
            case LARGE_TUPLE_EXT:
                return decodeLargeTuple();
            case NIL_EXT:
                return decodeNil();
            case STRING_EXT:
                return decodeStringAsList();
            case LIST_EXT:
                return decodeList();
            case MAP_EXT:
                return decodeMap();
            case BINARY_EXT:
                return decodeBinaryAsString();
            case SMALL_BIG_EXT:
                return decodeSmallBig();
            case LARGE_BIG_EXT:
                return decodeLargeBig();
            case REFERENCE_EXT:
                return decodeReference();
            case NEW_REFERENCE_EXT:
                return decodeNewReference();
            case PORT_EXT:
                return decodePort();
            case PID_EXT:
                return decodePID();
            case EXPORT_EXT:
                return decodeExport();
            case COMPRESSED:
                return decodeCompressed();
            default:
                THROW("Unsupported erlang term type identifier found");
                return Nan::Undefined();
        }

        return Nan::Undefined();
    }
private:
    const uint8_t* const data;
    const size_t size;
    bool isInvalid;
    size_t offset;
};
