// Copyright 2022, FOSS-VG Developers and Contributers
//
// This program is free software: you can redistribute it and/or modify it
// under the terms of the GNU Affero General Public License as published
// by the Free Software Foundation, version 3.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied
// warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
// See the GNU Affero General Public License for more details.
//
// You should have received a copy of the GNU Affero General Public License
// version 3 along with this program.
// If not, see https://www.gnu.org/licenses/agpl-3.0.en.html

#include <bit>
#include <cstdint>
#include <vector>
#include <tinyutf8/tinyutf8.h>

#include "nbt.hpp"
#include "error.hpp"
#include "javacompat.hpp"


#include "../../.endianness"
#ifdef FOSSVG_ENDIAN_BIG_WORD
    #error "Honeywell-316-style endianness is not supported. If you feel like it should, feel free to participate in the project to maintain it."
#endif
#ifdef FOSSVG_ENDIAN_LITTLE_WORD
    #error "PDP-11-style endianness is not supported. If you feel like it should, feel free to participate in the project to maintain it."
#endif
#ifdef FOSSVG_ENDIAN_UNKNOWN
    #error "The endianness of your system could not be determined. Please set it manually. FOSS-VG is currently implemented using some endian-specific functions."
#endif

namespace NBT {
    namespace Helper {
        ErrorOr<int8_t> readInt8(uint8_t data[], uint64_t dataSize, uint64_t currentPosition) {
            if (currentPosition>=dataSize) return ErrorOr<int8_t>(true, ErrorCodes::OUT_OF_RANGE);
            return ErrorOr<int8_t>((int8_t) data[currentPosition]);
        }

        ErrorOr<int16_t> readInt16(uint8_t data[], uint64_t dataSize, uint64_t currentPosition) {
            if (currentPosition>=dataSize) return ErrorOr<int16_t>(true, ErrorCodes::OUT_OF_RANGE);
            if (dataSize<currentPosition+2) return ErrorOr<int16_t>(true, ErrorCodes::OVERRUN);
            return ErrorOr<int16_t>((int16_t) ((static_cast<int16_t>(data[currentPosition]) << 8) | static_cast<int16_t>(data[currentPosition+1])));
        }

        ErrorOr<int32_t> readInt32(uint8_t data[], uint64_t dataSize, uint64_t currentPosition) {
            if (currentPosition>=dataSize) return ErrorOr<int32_t>(true, ErrorCodes::OUT_OF_RANGE);
            if (dataSize<currentPosition+4) return ErrorOr<int32_t>(true, ErrorCodes::OVERRUN);
            return ErrorOr<int32_t>((int32_t) (
                (static_cast<int32_t>(data[currentPosition  ]) << 24) |
                (static_cast<int32_t>(data[currentPosition+1]) << 16) |
                (static_cast<int32_t>(data[currentPosition+2]) <<  8) |
                 static_cast<int32_t>(data[currentPosition+3])
            ));
        }

        ErrorOr<int64_t> readInt64(uint8_t data[], uint64_t dataSize, uint64_t currentPosition) {
            if (currentPosition>=dataSize) return ErrorOr<int64_t>(true, ErrorCodes::OUT_OF_RANGE);
            if (dataSize<currentPosition+8) return ErrorOr<int64_t>(true, ErrorCodes::OVERRUN);
            return ErrorOr<int64_t>((int64_t) (
                (static_cast<int64_t>(data[currentPosition  ]) << 56) |
                (static_cast<int64_t>(data[currentPosition+1]) << 48) |
                (static_cast<int64_t>(data[currentPosition+2]) << 40) |
                (static_cast<int64_t>(data[currentPosition+3]) << 32) |
                (static_cast<int64_t>(data[currentPosition+4]) << 24) |
                (static_cast<int64_t>(data[currentPosition+5]) << 16) |
                (static_cast<int64_t>(data[currentPosition+6]) <<  8) |
                 static_cast<int64_t>(data[currentPosition+7])
            ));
        }

        //FIXME: endian-dependent implementations
        ErrorOr<float> readFloat(uint8_t data[], uint64_t dataSize, uint64_t currentPosition) {
            float* value = new float;
            uint8_t* valueAsBytes = reinterpret_cast<uint8_t*>(value);
            if (dataSize<=currentPosition) return ErrorOr<float>(true, ErrorCodes::OUT_OF_RANGE);
            if (dataSize<currentPosition+4) return ErrorOr<float>(true, ErrorCodes::OVERRUN);
            #ifdef FOSSVG_BIG_ENDIAN
                *valueAsBytes   =   data[currentPosition];
                *(valueAsBytes+1) = data[currentPosition+1];
                *(valueAsBytes+2) = data[currentPosition+2];
                *(valueAsBytes+3) = data[currentPosition+3];
            #else
                #ifdef FOSSVG_LITTLE_ENDIAN
                    *valueAsBytes   =   data[currentPosition+3];
                    *(valueAsBytes+1) = data[currentPosition+2];
                    *(valueAsBytes+2) = data[currentPosition+1];
                    *(valueAsBytes+3) = data[currentPosition];
                #else
                    #error "NBT::Helper::readFloat: An implementation for your endianness is unavailable."
                #endif
            #endif
            float dereferencedValue = *value;
            delete value;
            return ErrorOr<float>(dereferencedValue);
        }

        //FIXME: endian-dependent implementations
        ErrorOr<double> readDouble(uint8_t data[], uint64_t dataSize, uint64_t currentPosition) {
            double* value = new double;
            uint8_t* valueAsBytes = reinterpret_cast<uint8_t*>(value);
            if (dataSize<=currentPosition) return ErrorOr<double>(true, ErrorCodes::OUT_OF_RANGE);
            if (dataSize<currentPosition+8) return ErrorOr<double>(true, ErrorCodes::OVERRUN);
            #ifdef FOSSVG_BIG_ENDIAN
                *valueAsBytes   =   data[currentPosition];
                *(valueAsBytes+1) = data[currentPosition+1];
                *(valueAsBytes+2) = data[currentPosition+2];
                *(valueAsBytes+3) = data[currentPosition+3];
                *(valueAsBytes+4) = data[currentPosition+4];
                *(valueAsBytes+5) = data[currentPosition+5];
                *(valueAsBytes+6) = data[currentPosition+6];
                *(valueAsBytes+7) = data[currentPosition+7];
            #else
                #ifdef FOSSVG_LITTLE_ENDIAN
                    *valueAsBytes   =   data[currentPosition+7];
                    *(valueAsBytes+1) = data[currentPosition+6];
                    *(valueAsBytes+2) = data[currentPosition+5];
                    *(valueAsBytes+3) = data[currentPosition+4];
                    *(valueAsBytes+4) = data[currentPosition+3];
                    *(valueAsBytes+5) = data[currentPosition+2];
                    *(valueAsBytes+6) = data[currentPosition+1];
                    *(valueAsBytes+7) = data[currentPosition];
                #else
                    #error "NBT::Helper::readDouble: An implementation for your endianness is unavailable."
                #endif
            #endif
            double dereferencedValue = *value;
            delete value;
            return ErrorOr<double>(dereferencedValue);
        }

        ErrorOr<std::vector<int8_t>> readInt8Array(uint8_t data[], uint64_t dataSize, uint64_t currentPosition) {

            ErrorOr<int32_t> size = readInt32(data, dataSize, currentPosition);
            if (size.isError) {
                // It is okay to pass up the error code here without further
                // processing because both OVERRUN and OUT_OF_RANGE errors will
                // still apply.
                return ErrorOr<std::vector<int8_t>>(true, size.errorCode);
            }

            // get content
            if (currentPosition+4+size.value > dataSize) return ErrorOr<std::vector<int8_t>>(true, ErrorCodes::OVERRUN);
            std::vector<int8_t> result = std::vector<int8_t>();
            for (int i=0; i<size.value; i++) {
                result.push_back(data[currentPosition+4+i]);
            }
            return ErrorOr<std::vector<int8_t>>(result);
        }

        ErrorOr<tiny_utf8::string> readString(uint8_t data[], uint64_t dataSize, uint64_t currentPosition) {

            ErrorOr<int16_t> stringSize = readInt16(data, dataSize, currentPosition);
            if (stringSize.isError) {
                // It is okay to pass up the error code here without further
                // processing because both OVERRUN and OUT_OF_RANGE errors will
                // still apply.
                return ErrorOr<tiny_utf8::string>(true, stringSize.errorCode);
            }
            if (currentPosition + (uint64_t) stringSize.value + 2 > dataSize) {
                return ErrorOr<tiny_utf8::string>(true, ErrorCodes::OVERRUN);
            }

            ErrorOr<tiny_utf8::string> output = JavaCompat::importJavaString(data+currentPosition, stringSize.value);
            if(output.isError){
                return ErrorOr<tiny_utf8::string>(true, output.errorCode);
            }
            return output;
        }

        ErrorOr<std::vector<int32_t>> readInt32Array(uint8_t data[], uint64_t dataSize, uint64_t currentPosition) {

            ErrorOr<int32_t> size = readInt32(data, dataSize, currentPosition);
            if (size.isError) {
                // It is okay to pass up the error code here without further
                // processing because both OVERRUN and OUT_OF_RANGE errors will
                // still apply.
                return ErrorOr<std::vector<int32_t>>(true, size.errorCode);
            }

            // get content
            if (currentPosition+4+(size.value*4) > dataSize) return ErrorOr<std::vector<int32_t>>(true, ErrorCodes::OVERRUN);
            std::vector<int32_t> result = std::vector<int32_t>();
            for (int i=0; i<size.value; i++) {
                ErrorOr<int32_t> nextInt32 = readInt32(data, dataSize, currentPosition+4+(i*4));
                if (nextInt32.isError) return ErrorOr<std::vector<int32_t>>(true, nextInt32.errorCode);
                result.push_back(nextInt32.value);
            }
            return ErrorOr<std::vector<int32_t>>(result);
        }

        ErrorOr<std::vector<int64_t>> readInt64Array(uint8_t data[], uint64_t dataSize, uint64_t currentPosition) {

            ErrorOr<int32_t> size = readInt32(data, dataSize, currentPosition);
            if (size.isError) {
                // It is okay to pass up the error code here without further
                // processing because both OVERRUN and OUT_OF_RANGE errors will
                // still apply.
                return ErrorOr<std::vector<int64_t>>(true, size.errorCode);
            }

            // get content
            if (currentPosition+4+(size.value*8) > dataSize) return ErrorOr<std::vector<int64_t>>(true, ErrorCodes::OVERRUN);
            std::vector<int64_t> result = std::vector<int64_t>();
            for (int i=0; i<size.value; i++) {
                ErrorOr<int64_t> nextInt64 = readInt64(data, dataSize, currentPosition+4+(i*8));
                if (nextInt64.isError) return ErrorOr<std::vector<int64_t>>(true, nextInt64.errorCode);
                result.push_back(nextInt64.value);
            }
            return ErrorOr<std::vector<int64_t>>(result);
        }

        void writeInt8(std::vector<uint8_t>* destination, int8_t data) {
            destination->push_back((uint8_t) data);
        }

        //FIXME: endian dependent implementation
        void writeInt16(std::vector<uint8_t>* destination, int16_t data) {
            int16_t* value = new int16_t;
            uint8_t* valueAsBytes = reinterpret_cast<uint8_t*>(value);
            *value = data;
            #ifdef FOSSVG_BIG_ENDIAN
                destination->push_back(*valueAsBytes);
                destination->push_back(*(valueAsBytes+1));
            #else
                #ifdef FOSSVG_LITTLE_ENDIAN
                    destination->push_back(*(valueAsBytes+1));
                    destination->push_back(*valueAsBytes);
                #else
                    #error "NBT::Helper::writeInt16: An implementation for your endianness is unavailable."
                #endif
            #endif
            delete value;
        }

        //FIXME: endian dependent implementation
        void writeInt32(std::vector<uint8_t>* destination, int32_t data) {
            int32_t* value = new int32_t;
            uint8_t* valueAsBytes = reinterpret_cast<uint8_t*>(value);
            *value = data;
            #ifdef FOSSVG_BIG_ENDIAN
                destination->push_back(*valueAsBytes);
                destination->push_back(*(valueAsBytes+1));
                destination->push_back(*(valueAsBytes+2));
                destination->push_back(*(valueAsBytes+3));
            #else
                #ifdef FOSSVG_LITTLE_ENDIAN
                    destination->push_back(*(valueAsBytes+3));
                    destination->push_back(*(valueAsBytes+2));
                    destination->push_back(*(valueAsBytes+1));
                    destination->push_back(*valueAsBytes);
                #else
                    #error "NBT::Helper::writeInt16: An implementation for your endianness is unavailable."
                #endif
            #endif
            delete value;
        }

        //FIXME: endian dependent implementation
        void writeInt64(std::vector<uint8_t>* destination, int64_t data) {
            int64_t* value = new int64_t;
            uint8_t* valueAsBytes = reinterpret_cast<uint8_t*>(value);
            *value = data;
            #ifdef FOSSVG_BIG_ENDIAN
                destination->push_back(*valueAsBytes);
                destination->push_back(*(valueAsBytes+1));
                destination->push_back(*(valueAsBytes+2));
                destination->push_back(*(valueAsBytes+3));
                destination->push_back(*(valueAsBytes+4));
                destination->push_back(*(valueAsBytes+5));
                destination->push_back(*(valueAsBytes+6));
                destination->push_back(*(valueAsBytes+7));
            #else
                #ifdef FOSSVG_LITTLE_ENDIAN
                    destination->push_back(*(valueAsBytes+7));
                    destination->push_back(*(valueAsBytes+6));
                    destination->push_back(*(valueAsBytes+5));
                    destination->push_back(*(valueAsBytes+4));
                    destination->push_back(*(valueAsBytes+3));
                    destination->push_back(*(valueAsBytes+2));
                    destination->push_back(*(valueAsBytes+1));
                    destination->push_back(*valueAsBytes);
                #else
                    #error "NBT::Helper::writeInt16: An implementation for your endianness is unavailable."
                #endif
            #endif
            delete value;
        }

        //FIXME: endian-specific implementations
        void writeFloat(std::vector<uint8_t>* destination, float data) {
            float* value = new float;
            uint8_t* valueAsBytes = reinterpret_cast<uint8_t*>(value);
            *value = data;
            #ifdef FOSSVG_BIG_ENDIAN
                destination->push_back(*valueAsBytes);
                destination->push_back(*(valueAsBytes+1));
                destination->push_back(*(valueAsBytes+2));
                destination->push_back(*(valueAsBytes+3));
            #else
                #ifdef FOSSVG_LITTLE_ENDIAN
                    destination->push_back(*(valueAsBytes+3));
                    destination->push_back(*(valueAsBytes+2));
                    destination->push_back(*(valueAsBytes+1));
                    destination->push_back(*valueAsBytes);
                #else
                    #error "NBT::Helper::writeInt16: An implementation for your endianness is unavailable."
                #endif
            #endif
            delete value;
        }

        //FIXME: endian-specific implementations
        void writeDouble(std::vector<uint8_t>* destination, double data) {
            double* value = new double;
            uint8_t* valueAsBytes = reinterpret_cast<uint8_t*>(value);
            *value = data;
            #ifdef FOSSVG_BIG_ENDIAN
                destination->push_back(*valueAsBytes);
                destination->push_back(*(valueAsBytes+1));
                destination->push_back(*(valueAsBytes+2));
                destination->push_back(*(valueAsBytes+3));
                destination->push_back(*(valueAsBytes+4));
                destination->push_back(*(valueAsBytes+5));
                destination->push_back(*(valueAsBytes+6));
                destination->push_back(*(valueAsBytes+7));
            #else
                #ifdef FOSSVG_LITTLE_ENDIAN
                    destination->push_back(*(valueAsBytes+7));
                    destination->push_back(*(valueAsBytes+6));
                    destination->push_back(*(valueAsBytes+5));
                    destination->push_back(*(valueAsBytes+4));
                    destination->push_back(*(valueAsBytes+3));
                    destination->push_back(*(valueAsBytes+2));
                    destination->push_back(*(valueAsBytes+1));
                    destination->push_back(*valueAsBytes);
                #else
                    #error "NBT::Helper::writeInt16: An implementation for your endianness is unavailable."
                #endif
            #endif
            delete value;
        }

        void writeInt8Array(std::vector<uint8_t>* destination, std::vector<int8_t> data) {
            writeInt32(destination, data.size());
            for(int8_t datum: data){
                destination->push_back(datum);
            }
        }

        void writeInt8Array(std::vector<uint8_t>* destination, int8_t data[], uint32_t dataSize) {
            writeInt32(destination, dataSize);
            for(uint32_t i=0; i < dataSize; i++){
                destination->push_back(data[i]);
            }
        }

        ErrorOrVoid writeString(std::vector<uint8_t>* destination, tiny_utf8::string data) {
            ErrorOr<std::vector<uint8_t>> exportedString = JavaCompat::exportJavaString(data);
            if(exportedString.isError){
                return ErrorOrVoid(true, ErrorCodes::OVERRUN);
            }
            destination->insert(destination->end(), exportedString.value.begin(), exportedString.value.end());
            return ErrorOrVoid();
        }

        void writeInt32Array(std::vector<uint8_t>* destination, std::vector<int32_t> data) {
            writeInt32(destination, data.size());
            for(int32_t element: data){
                writeInt32(destination, element);
            }
        }

        void writeInt32Array(std::vector<uint8_t>* destination, int32_t data[], uint32_t dataSize) {
            writeInt32(destination, dataSize);
            for(uint32_t i = 0; i<dataSize; i++){
                writeInt32(destination, data[i]);
            }
        }

        void writeInt64Array(std::vector<uint8_t>* destination, std::vector<int64_t> data) {
            writeInt32(destination, data.size());
            for(int64_t element: data){
                writeInt64(destination, element);
            }
        }

        void writeInt64Array(std::vector<uint8_t>* destination, int64_t data[], uint32_t dataSize) {
            writeInt32(destination, dataSize);
            for(uint32_t i = 0; i<dataSize; i++){
                writeInt64(destination, data[i]);
            }
        }

        //FIXME: instead of blindly passing the error code upwards, choose
        // one that is applicable to the situation (for example replace
        // OUT_OF_RANGE with OVERRUN where appropriate)
        //
        // The total size in bytes
        //
        // Does not work for compound tags and lists. This is an intended
        // feature as compound tags and lists need to be dealt with
        // separately to avoid unnecessarily long and complex code.
        //
        // Regarding lists specifically: The size of some lists can can
        // be determined easily by looking at the contained data type and
        // size information but cases like string lists or compound lists
        // are significantly more difficult to deal with. Parsing their
        // contents requires special attention anyway due to the tag headers
        // of contained tags being absent so they may as well get treated
        // separately for this as well.
        ErrorOr<uint64_t> totalTagSize(uint8_t data[], uint64_t dataSize, uint64_t currentPosition) {
            uint8_t nextTag;
            if (dataSize <= currentPosition) {
                return ErrorOr<uint64_t>(true, ErrorCodes::OVERRUN);
            } else {
                nextTag = data[currentPosition];
            }
            // deal with compound tags and lists separately
            if (nextTag == TagType::COMPOUND || nextTag == TagType::LIST) return ErrorOr<uint64_t>(true, ErrorCodes::NOT_YET_KNOWN);
            // deal with end tag before trying to access the name
            if (nextTag == TagType::END) return ErrorOr<uint64_t>(1);

            ErrorOr<int16_t> nameSize = Helper::readInt16(data, dataSize, currentPosition+1);
            if (nameSize.isError) {
                return ErrorOr<uint64_t>(true, nameSize.errorCode);
            }
            // add type byte and name size bytes
            uint64_t prefixSize = (uint64_t) nameSize.value + 3;
            switch (nextTag) {
                case TagType::INT8:
                    return ErrorOr<uint64_t>(prefixSize+1);
                case TagType::INT16:
                    return ErrorOr<uint64_t>(prefixSize+2);
                case TagType::INT32:
                    return ErrorOr<uint64_t>(prefixSize+4);
                case TagType::INT64:
                    return ErrorOr<uint64_t>(prefixSize+8);
                case TagType::FLOAT:
                    return ErrorOr<uint64_t>(prefixSize+4);
                case TagType::DOUBLE:
                    return ErrorOr<uint64_t>(prefixSize+8);
                case TagType::INT8_ARRAY: {
                    ErrorOr<int32_t> arrayLength = Helper::readInt32(data, dataSize, currentPosition+prefixSize);
                    if (arrayLength.isError) {
                        return ErrorOr<uint64_t>(true, arrayLength.errorCode);
                    }
                    return ErrorOr<uint64_t>((uint64_t) arrayLength.value + prefixSize + 4);
                }
                case TagType::STRING: {
                    ErrorOr<int16_t> stringSize = Helper::readInt16(data, dataSize, currentPosition+prefixSize);
                    if (stringSize.isError) {
                        return ErrorOr<uint64_t>(true, stringSize.errorCode);
                    }
                    return ErrorOr<uint64_t>((uint64_t) stringSize.value + prefixSize + 2);
                }
                case TagType::INT32_ARRAY: {
                    ErrorOr<int32_t> arrayLength = Helper::readInt32(data, dataSize, currentPosition+prefixSize);
                    if (arrayLength.isError) {
                        return ErrorOr<uint64_t>(true, arrayLength.errorCode);
                    }
                    return ErrorOr<uint64_t>((uint64_t) arrayLength.value*4 + prefixSize + 4);
                }
                case TagType::INT64_ARRAY: {
                    ErrorOr<int32_t> arrayLength = Helper::readInt32(data, dataSize, currentPosition+prefixSize);
                    if (arrayLength.isError) {
                        return ErrorOr<uint64_t>(true, arrayLength.errorCode);
                    }
                    return ErrorOr<uint64_t>((uint64_t) arrayLength.value*8 + prefixSize + 4);
                }
                // unknown tag or parsing error
                default:
                    return ErrorOr<uint64_t>(true, ErrorCodes::UNKNOWN);
            }
        }

        //FIXME: instead of blindly passing the error code upwards, choose
        // one that is applicable to the situation (for example replace
        // OUT_OF_RANGE with OVERRUN where appropriate)
        //
        // Length is the number of stored elements, not to be confused with size
        // which is the size in bytes.
        ErrorOr<int32_t> containedDataLength(uint8_t data[], uint64_t dataSize, uint64_t currentPosition){

            uint8_t nextTag;
            if (dataSize <= currentPosition) {
                return ErrorOr<int32_t>(true, ErrorCodes::OVERRUN);
            } else {
                nextTag = data[currentPosition];
            }

            // deal with compound tags separately
            if (nextTag == TagType::COMPOUND) {
                return ErrorOr<int32_t>(true, ErrorCodes::NOT_YET_KNOWN);
            }

            // deal with end tag before trying to access the name
            if (nextTag == TagType::END) {
                return ErrorOr<int32_t>(0);
            }

            // tags that only ever hold one value
            if (nextTag == TagType::INT8 || nextTag == TagType::INT16 || nextTag == TagType::INT32 || nextTag == TagType::INT64 || nextTag == TagType::FLOAT || nextTag == TagType::DOUBLE) {
                return ErrorOr<int32_t>(1);
            }

            ErrorOr<int16_t> nameSize = Helper::readInt16(data, dataSize, currentPosition+1);
            if (nameSize.isError) {
                return ErrorOr<int32_t>(true, nameSize.errorCode);
            }
            // add type byte and name size bytes
            uint64_t prefixSize = (uint64_t) nameSize.value + 3;
            switch (nextTag) {
                case TagType::INT8_ARRAY: {
                    return Helper::readInt32(data, dataSize, currentPosition+prefixSize);
                }
                case TagType::STRING: {
                    ErrorOr<int16_t> stringSize = Helper::readInt16(data, dataSize, currentPosition+prefixSize);
                    if (stringSize.isError) {
                        return ErrorOr<int32_t>(true, stringSize.errorCode);
                    }
                    return ErrorOr<int32_t>((int32_t) stringSize.value);
                }
                case TagType::LIST: {
                    // add an additional byte for the contained data type
                    return Helper::readInt32(data, dataSize, currentPosition+prefixSize+1);
                }
                case TagType::INT32_ARRAY: {
                    return Helper::readInt32(data, dataSize, currentPosition+prefixSize);
                }
                case TagType::INT64_ARRAY: {
                    return Helper::readInt32(data, dataSize, currentPosition+prefixSize);
                }
                default:
                    // unknown tag or parsing error
                    return ErrorOr<int32_t>(true, ErrorCodes::UNKNOWN);
            }
        }
    }

    namespace Tag {

        Generic::Generic() {
            this->type = TagType::INVALID;
        }

        Generic::~Generic() {}

        ErrorOrVoid Generic::serializeWithoutHeader([[maybe_unused]] std::vector<uint8_t>* rawData) {
            return ErrorOrVoid(true, ErrorCodes::INVALID_TYPE);
        }

        ErrorOrVoid Generic::serialize(std::vector<uint8_t>* rawData) {
            rawData->push_back(this->type);

            if (Helper::writeString(rawData, this->name).isError) {
                return ErrorOrVoid(true, ErrorCodes::OVERRUN);
            }

            return this->serializeWithoutHeader(rawData);
        }

        uint8_t Generic::getTagType(){
            return this->type;
        }


        End::End() {
            this->type = TagType::END;
        }

        ErrorOrVoid End::serializeWithoutHeader([[maybe_unused]] std::vector<uint8_t>* rawData) {
            return ErrorOrVoid();
        }

        ErrorOrVoid End::serialize(std::vector<uint8_t>* rawData) {
            rawData->push_back(this->type);
            return ErrorOrVoid();
        }

        Int8::Int8() {
            this->type = TagType::INT8;
        }

        Int8::Int8(tiny_utf8::string name, int8_t value) {
            this->type = TagType::INT8;
            this->name = name;
            this->value = value;
        }

        ErrorOrVoid Int8::serializeWithoutHeader(std::vector<uint8_t>* rawData) {
            Helper::writeInt8(rawData, this->value);
            return ErrorOrVoid();
        }

        int8_t Int8::getValue() {
            this->mutex.lock();
            int8_t value = this->value;
            this->mutex.unlock();
            return value;
        }

        void Int8::setValue(int8_t value) {
            this->mutex.lock();
            this->value = value;
            this->mutex.unlock();
        }

        Int16::Int16() {
            this->type = TagType::INT16;
        }

        Int16::Int16(tiny_utf8::string name, int16_t value) {
            this->type = TagType::INT16;
            this->name = name;
            this->value = value;
        }

        ErrorOrVoid Int16::serializeWithoutHeader(std::vector<uint8_t>* rawData) {
            Helper::writeInt16(rawData, this->value);
            return ErrorOrVoid();
        }

        int16_t Int16::getValue() {
            this->mutex.lock();
            int16_t value = this->value;
            this->mutex.unlock();
            return value;
        }

        void Int16::setValue(int16_t value) {
            this->mutex.lock();
            this->value = value;
            this->mutex.unlock();
        }

        Int32::Int32() {
            this->type = TagType::INT32;
        }

        Int32::Int32(tiny_utf8::string name, int32_t value) {
            this->type = TagType::INT32;
            this->name = name;
            this->value = value;
        }

        ErrorOrVoid Int32::serializeWithoutHeader(std::vector<uint8_t>* rawData) {
            Helper::writeInt32(rawData, this->value);
            return ErrorOrVoid();
        }

        int32_t Int32::getValue() {
            this->mutex.lock();
            int32_t value = this->value;
            this->mutex.unlock();
            return value;
        }

        void Int32::setValue(int32_t value) {
            this->mutex.lock();
            this->value = value;
            this->mutex.unlock();
        }

        Int64::Int64() {
            this->type = TagType::INT64;
        }

        Int64::Int64(tiny_utf8::string name, int64_t value) {
            this->type = TagType::INT64;
            this->name = name;
            this->value = value;
        }

        ErrorOrVoid Int64::serializeWithoutHeader(std::vector<uint8_t>* rawData) {
            Helper::writeInt64(rawData, this->value);
            return ErrorOrVoid();
        }

        int64_t Int64::getValue() {
            this->mutex.lock();
            int64_t value = this->value;
            this->mutex.unlock();
            return value;
        }

        void Int64::setValue(int64_t value) {
            this->mutex.lock();
            this->value = value;
            this->mutex.unlock();
        }

        Float::Float() {
            this->type = TagType::FLOAT;
        }

        Float::Float(tiny_utf8::string name, float value) {
            this->type = TagType::FLOAT;
            this->name = name;
            this->value = value;
        }

        ErrorOrVoid Float::serializeWithoutHeader(std::vector<uint8_t>* rawData) {
            Helper::writeFloat(rawData, this->value);
            return ErrorOrVoid();
        }

        float Float::getValue() {
            this->mutex.lock();
            float value = this->value;
            this->mutex.unlock();
            return value;
        }

        void Float::setValue(float value) {
            this->mutex.lock();
            this->value = value;
            this->mutex.unlock();
        }

        Double::Double() {
            this->type = TagType::DOUBLE;
        }

        Double::Double(tiny_utf8::string name, double value) {
            this->type = TagType::DOUBLE;
            this->name = name;
            this->value = value;
        }

        ErrorOrVoid Double::serializeWithoutHeader(std::vector<uint8_t>* rawData) {
            Helper::writeDouble(rawData, this->value);
            return ErrorOrVoid();
        }

        double Double::getValue() {
            this->mutex.lock();
            double value = this->value;
            this->mutex.unlock();
            return value;
        }

        void Double::setValue(double value) {
            this->mutex.lock();
            this->value = value;
            this->mutex.unlock();
        }

        Int8Array::Int8Array() {
            this->type = TagType::INT8_ARRAY;
        }

        Int8Array::Int8Array(tiny_utf8::string name, std::vector<int8_t> data) {
            this->type = TagType::INT8_ARRAY;
            this->name = name;
            this->data = data;
        }
        Int8Array::Int8Array(tiny_utf8::string name, uint64_t length, int8_t data[]){
            this->type = TagType::INT8_ARRAY;
            this->name = name;
            this->data = std::vector<int8_t>(data, data+length);
        }

        ErrorOrVoid Int8Array::serializeWithoutHeader(std::vector<uint8_t>* rawData) {
            Helper::writeInt8Array(rawData, this->data);
            return ErrorOrVoid();
        }

        std::vector<int8_t> Int8Array::getData() {
            return this->data;
        }

        ErrorOr<int8_t> Int8Array::getValue(uint64_t position) {
            if (this->data.size() <= position) {
                return ErrorOr<int8_t>(true, ErrorCodes::OUT_OF_RANGE);
            }

            return ErrorOr<int8_t>(this->data.at(position));
        }

        void Int8Array::setData(std::vector<int8_t> newData) {
            this->data = newData;
        }

        ErrorOrVoid Int8Array::setValue(uint64_t position, int8_t value) {
            if (this->data.size() <= position) {
                return ErrorOrVoid(true, ErrorCodes::OUT_OF_RANGE);
            }

            this->data[position] = value;

            return ErrorOrVoid();
        }

        uint64_t Int8Array::length() {
            return this->data.size();
        }

        void Int8Array::addElement(int8_t element) {
            this->data.push_back(element);
        }

        ErrorOrVoid Int8Array::removeElement(uint64_t position) {
            if (position >= this->data.size()) {
                return ErrorOrVoid(true, ErrorCodes::OUT_OF_RANGE);
            }

            this->data.erase(this->data.begin()+position);
            return ErrorOrVoid();
        }

        String::String() {
            this->type = TagType::STRING;
        }

        String::String(tiny_utf8::string name, tiny_utf8::string value) {
            this->type  = TagType::STRING;
            this->name  = name;
            this->value = value;
        }

        ErrorOrVoid String::serializeWithoutHeader(std::vector<uint8_t>* rawData) {
            return Helper::writeString(rawData, this->value);
        }

        tiny_utf8::string String::getValue() {
            return this->value;
        }

        void String::setValue(tiny_utf8::string value) {
            this->value = value;
        }

        List::List() {
            this->type = TagType::LIST;
            this->containedType = TagType::INVALID;
        }

        List::List(tiny_utf8::string name, uint8_t type) {
            this->type = TagType::LIST;
            this->name = name;
            this->containedType = type;
        }

        // WARNING: The pointers inside the vector are automatically cleaned
        // up upon deletion of the List object. Do not retain a copy of them
        // elsewhere and especially do not delete them externally.
        List::List(tiny_utf8::string name, std::vector<Generic*> data) {
            this->type = TagType::LIST;
            this->name = name;
            this->tags = data;

            if (data.size() == 0) {
                this->containedType = TagType::END;
            } else {
                this->containedType = data.at(0)->getTagType();
            }
        }

        ErrorOr<List*> List::constructWithData(tiny_utf8::string name, std::vector<Generic*> data) {
            if (data.size() > 0xFFFFFFFF) {
                return ErrorOr<List*>(true, ErrorCodes::OVERRUN, nullptr);
            }
            if (data.size() > 0) {
                uint8_t dataType = data[0]->getTagType();
                for (uint32_t i=1; i<data.size(); i++) {
                    if (data[i]->getTagType() != dataType) {
                        return ErrorOr<List*>(true, ErrorCodes::MIXED_TYPES, nullptr);
                    }
                }
            }

            return ErrorOr<List*>(new List(name, data));
        }

        List::~List() {
            for (uint64_t i=0; i<this->tags.size(); i++) {
                delete this->tags.at(i);
            }
        }

        uint8_t List::getContainedType() {
            return this->containedType;
        }

        ErrorOrVoid List::serializeWithoutHeader(std::vector<uint8_t>* rawData) {
            if (this->containedType == TagType::INVALID) {
                return ErrorOrVoid(true, ErrorCodes::INVALID_TYPE);
            }
            rawData->push_back(this->containedType);

            // 32 bit signed integer max
            if (this->tags.size() > 0x7FFFFFFF) {
                return ErrorOrVoid(true, ErrorCodes::OVERRUN);
            }
            Helper::writeInt32(rawData, this->tags.size());

            // unsigned integer bc of compiler warning (shouldn't matter)
            for (uint32_t i=0; i<this->tags.size(); i++) {
                ErrorOrVoid result = this->tags.at(i)->serializeWithoutHeader(rawData);
                if (result.isError) {
                    return result;
                }
            }

            return ErrorOrVoid();
        }

        ErrorOr<Generic*> List::getElementPointer(uint64_t position) {
            if (this->tags.size() <= position) {
                return ErrorOr<Generic*>(true, ErrorCodes::OUT_OF_RANGE);
            }
            return ErrorOr<Generic*>(this->tags.at(position));
        }

        ErrorOrVoid List::setElementPointerAt(uint64_t position, Generic* pointer) {
            if (this->tags.size() <= position) {
                delete pointer;
                return ErrorOrVoid(true, ErrorCodes::OUT_OF_RANGE);
            }

            if (pointer->getTagType() != this->containedType) {
                delete pointer;
                return ErrorOrVoid(true, ErrorCodes::INVALID_TYPE);
            }

            delete this->tags[position];
            this->tags[position] = pointer;
            return ErrorOrVoid();
        }

        ErrorOrVoid List::appendPointer(Generic* pointer) {
            if (pointer->getTagType() != this->containedType) {
                return ErrorOrVoid(true, ErrorCodes::INVALID_TYPE);
            }

            this->tags.push_back(pointer);

            return ErrorOrVoid();
        }

        ErrorOrVoid List::deleteElement(uint64_t position) {
            if (position >= this->tags.size()) {
                return ErrorOrVoid(true, ErrorCodes::OUT_OF_RANGE);
            }

            delete this->tags[position];
            this->tags.erase(this->tags.begin()+position);

            return ErrorOrVoid();
        }

        uint64_t List::length() {
            return this->tags.size();
        }

        Compound::Compound() {
            this->type = TagType::COMPOUND;
            this->endPointer = new End();
        }

        Compound::Compound(tiny_utf8::string name) {
            this->type = TagType::COMPOUND;
            this->name = name;
            this->endPointer = new End();
        }

        Compound::Compound(tiny_utf8::string name, std::vector<Generic*> data) {
            this->type = TagType::COMPOUND;
            this->name = name;
            this->tags = data;
            this->endPointer = new End();
        }

        ErrorOr<Compound*> Compound::constructWithData(tiny_utf8::string name, std::vector<Generic*> data) {
            if (data.size() > 0) {
                for (uint64_t i=0; i<data.size(); i++) {
                    if (data[i]->getTagType() == TagType::END && i != data.size()-1) {
                        return ErrorOr<Compound*>(true, ErrorCodes::NOT_ALLOWED, nullptr);
                    }
                }
                if (data[data.size()-1]->getTagType() == TagType::END) {
                    return ErrorOr<Compound*>(new Compound(name, std::vector(data.begin(), data.end()-1)));
                }
            }
            return ErrorOr<Compound*>(new Compound(name, data));
        }

        Compound::~Compound() {
            for (uint64_t i=0; i<this->tags.size(); i++) {
                delete this->tags.at(i);
            }
            delete this->endPointer;
        }

        ErrorOrVoid Compound::serializeWithoutHeader(std::vector<uint8_t>* rawData) {
            for (uint64_t i=0; i<this->tags.size(); i++) {
                ErrorOrVoid result = this->tags.at(i)->serialize(rawData);
                if (result.isError) {
                    return result;
                }
            }
            this->endPointer->serialize(rawData);

            return ErrorOrVoid();
        }

        ErrorOr<Generic*> Compound::getElementPointer(uint64_t position) {
            if (position > this->tags.size()) {
                return ErrorOr<Generic*>(true, ErrorCodes::OUT_OF_RANGE);
            }

            if (position == this->tags.size()) {
                return this->endPointer;
            }

            return ErrorOr<Generic*>(this->tags.at(position));
        }

        ErrorOrVoid Compound::setElementPointerAt(uint64_t position, Generic* pointer) {
            if (position == this->tags.size() || pointer->getTagType() == TagType::END) {
                if (position == this->tags.size() && pointer->getTagType() == TagType::END) {
                    delete pointer;
                    // do nothing, already have one of those
                    return ErrorOrVoid();
                } else {
                    delete pointer;
                    // End tags may only go at the end and
                    // the end may only hold an end tag.
                    return ErrorOrVoid(true, ErrorCodes::NOT_ALLOWED);
                }
            }
            if (position > this->tags.size()) {
                return ErrorOrVoid(true, ErrorCodes::OUT_OF_RANGE);
            }

            delete this->tags[position];
            this->tags[position] = pointer;

            return ErrorOrVoid();
        }

        ErrorOrVoid Compound::appendPointer(Generic* pointer) {
            if (pointer->getTagType() == TagType::END) {
                delete pointer;
                return ErrorOrVoid(true, ErrorCodes::NOT_ALLOWED);
            }
            this->tags.push_back(pointer);
            return ErrorOrVoid();
        }

        ErrorOrVoid Compound::deleteElement(uint64_t position) {
            // built-in end tag
            if (position == this->tags.size()) {
                return ErrorOrVoid(true, ErrorCodes::NOT_ALLOWED);
            }
            if (position > this->tags.size()) {
                return ErrorOrVoid(true, ErrorCodes::OUT_OF_RANGE);
            }

            delete this->tags[position];
            this->tags.erase(this->tags.begin()+position);

            return ErrorOrVoid();
        }

        uint64_t Compound::length() {
            // account for built-in end tag
            return this->tags.size()+1;
        }

        Int32Array::Int32Array() {
            this->type = TagType::INT32_ARRAY;
        }

        Int32Array::Int32Array(tiny_utf8::string name, std::vector<int32_t> data) {
            this->type = TagType::INT32_ARRAY;
            this->name = name;
            this->data = data;
        }

        Int32Array::Int32Array(tiny_utf8::string name, uint64_t length, int32_t data[]) {
            this->type = TagType::INT32_ARRAY;
            this->name = name;
            this->data = std::vector(data, data+length);
        }

        ErrorOrVoid Int32Array::serializeWithoutHeader(std::vector<uint8_t>* rawData) {
            Helper::writeInt32Array(rawData, this->data);
            return ErrorOrVoid();
        }

        std::vector<int32_t> Int32Array::getData() {
            return this->data;
        }

        ErrorOr<int32_t> Int32Array::getValue(uint64_t position) {
            if (position >= this->data.size()) {
                return ErrorOr<int32_t>(true, ErrorCodes::OUT_OF_RANGE);
            }

            return ErrorOr<int32_t>(this->data.at(position));
        }

        void Int32Array::setData(std::vector<int32_t> newData) {
            this->data = newData;
        }

        ErrorOrVoid Int32Array::setValue(uint64_t position, int32_t value) {
            if (position >= this->data.size()) {
                return ErrorOrVoid(true, ErrorCodes::OUT_OF_RANGE);
            }

            this->data[position] = value;

            return ErrorOrVoid();
        }

        uint64_t Int32Array::length() {
            return this->data.size();
        }

        void Int32Array::addElement(int32_t element) {
            this->data.push_back(element);
        }

        ErrorOrVoid Int32Array::removeElement(uint64_t position) {
            if (position >= this->data.size()) {
                return ErrorOrVoid(true, ErrorCodes::OUT_OF_RANGE);
            }

            this->data.erase(this->data.begin()+position);
            return ErrorOrVoid();
        }

        Int64Array::Int64Array() {
            this->type = TagType::INT64_ARRAY;
        }

        Int64Array::Int64Array(tiny_utf8::string name, std::vector<int64_t> data) {
            this->type = TagType::INT64_ARRAY;
            this->name = name;
            this->data = data;
        }

        Int64Array::Int64Array(tiny_utf8::string name, uint64_t length, int64_t data[]) {
            this->type = TagType::INT64_ARRAY;
            this->name = name;
            this->data = std::vector(data, data+length);
        }

        ErrorOrVoid Int64Array::serializeWithoutHeader(std::vector<uint8_t>* rawData) {
            Helper::writeInt64Array(rawData, this->data);
            return ErrorOrVoid();
        }

        std::vector<int64_t> Int64Array::getData() {
            return this->data;
        }

        ErrorOr<int64_t> Int64Array::getValue(uint64_t position) {
            if (position >= this->data.size()) {
                return ErrorOr<int64_t>(true, ErrorCodes::OUT_OF_RANGE);
            }

            return ErrorOr<int64_t>(this->data[position]);
        }

        void Int64Array::setData(std::vector<int64_t> newData) {
            this->data = newData;
        }

        ErrorOrVoid Int64Array::setValue(uint64_t position, int64_t value) {
            if (position >= this->data.size()) {
                return ErrorOrVoid(true, ErrorCodes::OUT_OF_RANGE);
            }

            this->data[position] = value;
            return ErrorOrVoid();
        }

        uint64_t Int64Array::length() {
            return this->data.size();
        }

        void Int64Array::addElement(int64_t element) {
            this->data.push_back(element);
        }

        ErrorOrVoid Int64Array::removeElement(uint64_t position) {
            if (position >= this->data.size()) {
                return ErrorOrVoid(true, ErrorCodes::OUT_OF_RANGE);
            }

            this->data.erase(this->data.begin()+position);
            return ErrorOrVoid();
        }

    }

    // the same comment about blindly passing up error codes applies to this function
    // FIXME: memory leak when returning errors
    ErrorOr<std::vector<Tag::Generic*>> deserializeRawListContents(uint8_t data[], uint64_t dataSize, uint64_t initialPosition, uint64_t* processedDataSize) {
        std::vector<Tag::Generic*> contents;
        ErrorOr<std::vector<Tag::Generic*>> returnValue;
        // get contained data length by reading it manually because
        // the function that does it normally can't deal with
        // headerless tags
        //
        // add one byte to position to skip the type byte
        ErrorOr<int32_t> elementCount = Helper::readInt32(data, dataSize, initialPosition+1);
        if (elementCount.isError) {
            // this is before the creation of any pointers so we can just return
            // without using the returnError label at the end of this function
            return ErrorOr<std::vector<Tag::Generic*>>(true, elementCount.errorCode);
        }

        uint8_t contentType = data[initialPosition];
        // contained type byte + 4 length bytes = 5
        *processedDataSize = 5;
        switch (contentType) {
            case TagType::END: {
                // everything except content has been touched at this point
                // and a list of end tags has no content that could be read
                for (int32_t i=0; i<elementCount.value; i++) {
                    contents.push_back(new Tag::End());
                }
                break;
            }
            case TagType::INT8: {
                for (int32_t i=0; i<elementCount.value; i++) {
                    ErrorOr<int8_t> nextInt = Helper::readInt8(data, dataSize, initialPosition+*processedDataSize);
                    if (nextInt.isError) {
                        returnValue = ErrorOr<std::vector<Tag::Generic*>>(true, nextInt.errorCode);
                        goto returnError;
                    }
                    contents.push_back(new Tag::Int8("", nextInt.value));
                    *processedDataSize += 1;
                }
                break;
            }
            case TagType::INT16: {
                for (int32_t i=0; i<elementCount.value; i++) {
                    ErrorOr<int16_t> nextInt = Helper::readInt16(data, dataSize, initialPosition+*processedDataSize);
                    if (nextInt.isError) {
                        returnValue = ErrorOr<std::vector<Tag::Generic*>>(true, nextInt.errorCode);
                        goto returnError;
                    }
                    contents.push_back(new Tag::Int16("", nextInt.value));
                    *processedDataSize += 2;
                }
                break;
            }
            case TagType::INT32: {
                for (int32_t i=0; i<elementCount.value; i++) {
                    ErrorOr<int32_t> nextInt = Helper::readInt32(data, dataSize, initialPosition+*processedDataSize);
                    if (nextInt.isError) {
                        returnValue = ErrorOr<std::vector<Tag::Generic*>>(true, nextInt.errorCode);
                        goto returnError;
                    }
                    contents.push_back(new Tag::Int32("", nextInt.value));
                    *processedDataSize += 4;
                }
                break;
            }
            case TagType::FLOAT: {
                for (int32_t i=0; i<elementCount.value; i++) {
                    ErrorOr<float> nextFloat = Helper::readFloat(data, dataSize, initialPosition+*processedDataSize);
                    if (nextFloat.isError) {
                        returnValue = ErrorOr<std::vector<Tag::Generic*>>(true, nextFloat.errorCode);
                        goto returnError;
                    }
                    contents.push_back(new Tag::Float("", nextFloat.value));
                    *processedDataSize += 4;
                }
                break;
            }
            case TagType::INT64: {
                for (int32_t i=0; i<elementCount.value; i++) {
                    ErrorOr<int64_t> nextInt = Helper::readInt64(data, dataSize, initialPosition+*processedDataSize);
                    if (nextInt.isError) {
                        returnValue = ErrorOr<std::vector<Tag::Generic*>>(true, nextInt.errorCode);
                        goto returnError;
                    }
                    contents.push_back(new Tag::Int64("", nextInt.value));
                    *processedDataSize += 8;
                }
                break;
            }
            case TagType::DOUBLE: {
                for (int32_t i=0; i<elementCount.value; i++) {
                    ErrorOr<double> nextDouble = Helper::readDouble(data, dataSize, initialPosition+*processedDataSize);
                    if (nextDouble.isError) {
                        returnValue = ErrorOr<std::vector<Tag::Generic*>>(true, nextDouble.errorCode);
                        goto returnError;
                    }
                    contents.push_back(new Tag::Double("", nextDouble.value));
                    *processedDataSize += 8;
                }
                break;
            }
            case TagType::INT8_ARRAY: {
                for (int32_t i=0; i<elementCount.value; i++) {
                    ErrorOr<std::vector<int8_t>> nextArray = Helper::readInt8Array(data, dataSize, initialPosition+*processedDataSize);
                    if (nextArray.isError) {
                        returnValue = ErrorOr<std::vector<Tag::Generic*>>(true, nextArray.errorCode);
                        goto returnError;
                    }
                    contents.push_back(new Tag::Int8Array("", nextArray.value));
                    // +4 for the header of the array
                    *processedDataSize += (uint64_t) nextArray.value.size() + 4;
                }
                break;
            }
            case TagType::STRING: {
                for (int32_t i=0; i<elementCount.value; i++) {
                    ErrorOr<tiny_utf8::string> nextString = Helper::readString(data, dataSize, initialPosition+*processedDataSize);
                    if (nextString.isError) {
                        returnValue = ErrorOr<std::vector<Tag::Generic*>>(true, nextString.errorCode);
                        goto returnError;
                    }
                    contents.push_back(new Tag::String("", nextString.value));
                    // this cannot be an error because it just got read
                    int16_t nextStringSize = Helper::readInt16(data, dataSize, initialPosition+*processedDataSize).value;
                    *processedDataSize += (uint64_t) nextStringSize + 2;
                }
                break;
            }
            case TagType::LIST: {
                uint64_t* containedDataSize = new uint64_t;
                for (int32_t i=0; i<elementCount.value; i++) {
                    *containedDataSize = 0;

                    ErrorOr<std::vector<Tag::Generic*>> nextListContents = deserializeRawListContents(data, dataSize, initialPosition+*processedDataSize, containedDataSize);
                    if (nextListContents.isError) {
                        delete containedDataSize;
                        returnValue = ErrorOr<std::vector<Tag::Generic*>>(true, nextListContents.errorCode);
                        goto returnError;
                    }
                    contents.push_back(Tag::List::constructWithData("", nextListContents.value).value);
                    *processedDataSize += *containedDataSize;
                }
                delete containedDataSize;
                break;
            }
            case TagType::COMPOUND: {
                uint64_t* containedDataSize = new uint64_t;
                for (int32_t i=0; i<elementCount.value; i++) {
                    *containedDataSize = 0;
                    ErrorOr<std::vector<Tag::Generic*>> nextCompoundData = deserialize(data, dataSize, initialPosition+*processedDataSize, containedDataSize);
                    if (nextCompoundData.isError) {
                        delete containedDataSize;
                        returnValue = ErrorOr<std::vector<Tag::Generic*>>(true, nextCompoundData.errorCode);
                        goto returnError;
                    }
                    contents.push_back(reinterpret_cast<Tag::Generic*>(Tag::Compound::constructWithData("", nextCompoundData.value).value));
                    *processedDataSize += *containedDataSize;
                }
                delete containedDataSize;
                break;
            }
            case TagType::INT32_ARRAY: {
                for (int32_t i=0; i<elementCount.value; i++) {
                    ErrorOr<std::vector<int32_t>> nextArray = Helper::readInt32Array(data, dataSize, initialPosition+*processedDataSize);
                    if (nextArray.isError) {
                        returnValue = ErrorOr<std::vector<Tag::Generic*>>(true, nextArray.errorCode);
                        goto returnError;
                    }
                    contents.push_back(new Tag::Int32Array("", nextArray.value));
                    // +4 for the header of the array
                    *processedDataSize += (uint64_t) nextArray.value.size() * 4 + 4;
                }
                break;
            }
            case TagType::INT64_ARRAY: {
                for (int32_t i=0; i<elementCount.value; i++) {
                    ErrorOr<std::vector<int64_t>> nextArray = Helper::readInt64Array(data, dataSize, initialPosition+*processedDataSize);
                    if (nextArray.isError) {
                        returnValue = ErrorOr<std::vector<Tag::Generic*>>(true, nextArray.errorCode);
                        goto returnError;
                    }
                    contents.push_back(new Tag::Int64Array("", nextArray.value));
                    // +4 for the header of the array
                    *processedDataSize += (uint64_t) nextArray.value.size() * 8 + 4;
                }
                break;
            }
            default:
                returnValue = ErrorOr<std::vector<Tag::Generic*>>(true, ErrorCodes::INVALID_TYPE);
                goto returnError;
        }
        return ErrorOr<std::vector<Tag::Generic*>>(contents);

        returnError:
            for (uint64_t i=0; i<contents.size(); i++) {
                delete contents.at(i);
            }
            return returnValue;
    }

    // comment about blindly passing up error codes applies here
    //
    // The return value of this function is a vector of tags
    // instead of a compound tag due to a spec extension that allows
    // for any bare tag to be valid NBT data without a containing
    // compound tag. This also just makes the implementation easier.
    ErrorOr<std::vector<Tag::Generic*>> deserialize(uint8_t data[], uint64_t dataSize, uint64_t initialPosition, uint64_t* processedDataSize){
        if (initialPosition >= dataSize) {
            if (processedDataSize!=nullptr) *processedDataSize=0;
            return ErrorOr<std::vector<Tag::Generic*>>(true, ErrorCodes::OUT_OF_RANGE);

            // An interesting question at this point is whether we should
            // consider empty input valid or invalid NBT data.
            //
            // The original spec says that the top-most tag is always a
            // compound (or in more recent times, the Microsoft-commercialized
            // in-game-purchase-enabling version also allows list tags)
            // which automatically means that no data is invalid data...
            // I don't see a reason why having a different tag as the top-most
            // tag shouldn't be valid NBT in which case we have to face the
            // question whether no data is invalid or just empty NBT data.
            //
            // This seems like a reasonable extension to the spec to me and
            // it should be backwards compatible AFAIK.
            //
            // - BodgeMaster
        }

        std::vector<Tag::Generic*> tags = std::vector<Tag::Generic*>();
        ErrorOr<std::vector<Tag::Generic*>> returnValue;
        uint64_t currentPosition = initialPosition;
        while (currentPosition<dataSize) {
            ErrorOr<uint64_t> nextTagSize = Helper::totalTagSize(data, dataSize, currentPosition);
            if (nextTagSize.isError) {
                if (nextTagSize.errorCode == ErrorCodes::NOT_YET_KNOWN) {
                    ErrorOr<tiny_utf8::string> tagName = Helper::readString(data, dataSize, currentPosition+1);
                    if (tagName.isError) {
                        returnValue = ErrorOr<std::vector<Tag::Generic*>>(true, tagName.errorCode);
                        goto returnNow;
                    }

                    // used seek to the start of the list's/compound’s contents
                    //
                    // there is no way this is an error bc it gets
                    // checked while trying to parse the string above
                    int16_t nameSize = Helper::readInt16(data, dataSize, currentPosition+1).value;

                    uint64_t* processedTagSize = new uint64_t;
                    *processedTagSize = 0;

                    if (data[currentPosition]==TagType::LIST) {
                        // type byte + two name size bytes = 3
                        ErrorOr<std::vector<Tag::Generic*>> listData = deserializeRawListContents(data, dataSize, currentPosition + (uint64_t) nameSize + 3, processedTagSize);
                        if (listData.isError) {
                            delete processedTagSize;
                            returnValue = ErrorOr<std::vector<Tag::Generic*>>(true, listData.errorCode);
                            goto returnNow;
                        }
                        tags.push_back(Tag::List::constructWithData(tagName.value, listData.value).value);
                        *processedTagSize += (uint64_t) nameSize + 3;
                    }
                    if (data[currentPosition]==TagType::COMPOUND) {
                        // type byte + two name size bytes = 3
                        ErrorOr<std::vector<Tag::Generic*>> compoundData = deserialize(data, dataSize, currentPosition + (uint64_t) nameSize + 3, processedTagSize);
                        if (compoundData.isError) {
                            delete processedTagSize;
                            returnValue = ErrorOr<std::vector<Tag::Generic*>>(true, compoundData.errorCode);
                            goto returnNow;
                        }
                        tags.push_back(reinterpret_cast<Tag::Generic*>(Tag::Compound::constructWithData(tagName.value, compoundData.value).value));
                        *processedTagSize += (uint64_t) nameSize + 3;
                    }
                    currentPosition += *processedTagSize;

                    delete processedTagSize;
                    continue;
                }
                returnValue = ErrorOr<std::vector<Tag::Generic*>>(true, nextTagSize.errorCode);
                goto returnNow;
            }

            if (currentPosition + nextTagSize.value > dataSize) {
                returnValue = ErrorOr<std::vector<Tag::Generic*>>(true, ErrorCodes::OVERRUN);
                goto returnNow;
            }

            // recursion abort condition
            if (data[currentPosition]==TagType::END) {
                // not appending an end tag as it is built into
                // the compound anyway
                currentPosition++;
                returnValue = ErrorOr<std::vector<Tag::Generic*>>(tags);
                goto returnNow;
            }

            // nameSize cannot be an error here bc it got checked in
            // nextTagSize() already
            int16_t nameSize = Helper::readInt16(data, dataSize, currentPosition+1).value;

            ErrorOr<tiny_utf8::string> name = Helper::readString(data, dataSize, currentPosition+1);
            if (name.isError) {
                returnValue = ErrorOr<std::vector<Tag::Generic*>>(true, name.errorCode);
                goto returnNow;
            }

            // Overrun / out of range errors have already been ruled out by
            // checking the tag size against the total amount of data.
            switch (data[currentPosition]) {
                case TagType::INT8: {
                    int8_t content = Helper::readInt8(data, dataSize, currentPosition+nameSize+3).value;
                    tags.push_back(new Tag::Int8(name.value, content));
                    break;
                }
                case TagType::INT16: {
                    int16_t content = Helper::readInt16(data, dataSize, currentPosition+nameSize+3).value;
                    tags.push_back(new Tag::Int16(name.value, content));
                    break;
                }
                case TagType::INT32: {
                    int32_t content = Helper::readInt32(data, dataSize, currentPosition+nameSize+3).value;
                    tags.push_back(new Tag::Int32(name.value, content));
                    break;
                }
                case TagType::INT64: {
                    int64_t content = Helper::readInt64(data, dataSize, currentPosition+nameSize+3).value;
                    tags.push_back(new Tag::Int64(name.value, content));
                    break;
                }
                case TagType::FLOAT: {
                    float content = Helper::readFloat(data, dataSize, currentPosition+nameSize+3).value;
                    tags.push_back(new Tag::Float(name.value, content));
                    break;
                }
                case TagType::DOUBLE: {
                    double content = Helper::readDouble(data, dataSize, currentPosition+nameSize+3).value;
                    tags.push_back(new Tag::Double(name.value, content));
                    break;
                }
                case TagType::INT8_ARRAY: {
                    std::vector<int8_t> content = Helper::readInt8Array(data, dataSize, currentPosition+nameSize+3).value;
                    tags.push_back(new Tag::Int8Array(name.value, content));
                    break;
                }
                case TagType::STRING: {
                    ErrorOr<tiny_utf8::string> content = Helper::readString(data, dataSize, currentPosition+nameSize+3);
                    if (content.isError) {
                        returnValue = ErrorOr<std::vector<Tag::Generic*>>(true, content.errorCode);
                        goto returnNow;
                    }
                    tags.push_back(new Tag::String(name.value, content.value));
                    break;
                }
                case TagType::INT32_ARRAY: {
                    std::vector<int32_t> content = Helper::readInt32Array(data, dataSize, currentPosition+nameSize+3).value;
                    tags.push_back(new Tag::Int32Array(name.value, content));
                    break;
                }
                case TagType::INT64_ARRAY: {
                    std::vector<int64_t> content = Helper::readInt64Array(data, dataSize, currentPosition+nameSize+3).value;
                    tags.push_back(new Tag::Int64Array(name.value, content));
                    break;
                }
                default: {
                    returnValue = ErrorOr<std::vector<Tag::Generic*>>(true, ErrorCodes::UNKNOWN);
                    goto returnNow;
                }
            }

            currentPosition += nextTagSize.value;
        }
        returnValue = ErrorOr<std::vector<Tag::Generic*>>(tags);
        goto returnNow;

        returnNow:
            if (processedDataSize!=nullptr) {
                *processedDataSize = currentPosition-initialPosition;
            }
            if (returnValue.isError) {
                for (uint64_t i=0; i<tags.size(); i++) {
                    delete tags[i];
                }
            }
            return returnValue;
    }

    bool validateRawListContents(uint8_t data[], uint64_t dataSize, uint64_t initialPosition, uint64_t* processedDataSize) {
        // get contained data length by reading it manually because
        // the function that does it normally can't deal with
        // headerless tags
        //
        // add one byte to position to skip the type byte
        ErrorOr<int32_t> elementCount = Helper::readInt32(data, dataSize, initialPosition+1);
        if (elementCount.isError) {
            return false;
        }

        uint8_t contentType = data[initialPosition];
        // contained type byte + 4 length bytes = 5
        *processedDataSize = 5;
        switch (contentType) {
            case TagType::END:
                // everything except content has been touched at this point
                // and a list of end tags has no content
                return true;
            case TagType::INT8: {
                *processedDataSize += (uint64_t) elementCount.value;
                return initialPosition + *processedDataSize < dataSize;
            }
            case TagType::INT16: {
                *processedDataSize += (uint64_t) elementCount.value * 2;
                return initialPosition + *processedDataSize < dataSize;
            }
            case TagType::INT32:
            case TagType::FLOAT: {
                *processedDataSize += (uint64_t) elementCount.value * 4;
                return initialPosition + *processedDataSize < dataSize;
            }
            case TagType::INT64:
            case TagType::DOUBLE: {
                *processedDataSize += (uint64_t) elementCount.value * 8;
                return initialPosition + *processedDataSize < dataSize;
            }
            case TagType::INT8_ARRAY: {
                for (int32_t i=0; i<elementCount.value; i++) {
                    ErrorOr<std::vector<int8_t>> nextArray = Helper::readInt8Array(data, dataSize, initialPosition+*processedDataSize);
                    if (nextArray.isError) {
                        return false;
                    }
                    // +4 for the header of the array
                    *processedDataSize += (uint64_t) nextArray.value.size() + 4;
                }
                return true;
            }
            case TagType::STRING: {
                for (int32_t i=0; i<elementCount.value; i++) {
                    ErrorOr<tiny_utf8::string> nextString = Helper::readString(data, dataSize, initialPosition+*processedDataSize);
                    if (nextString.isError) {
                        return false;
                    }
                    // this cannot be an error because it just got checked
                    int16_t nextStringSize = Helper::readInt16(data, dataSize, initialPosition+*processedDataSize).value;
                    *processedDataSize += (uint64_t) nextStringSize + 2;
                }
                return true;
            }
            case TagType::LIST: {
                uint64_t* containedDataSize = new uint64_t;
                for (int32_t i=0; i<elementCount.value; i++) {
                    *containedDataSize = 0;

                    if (validateRawListContents(data, dataSize, initialPosition+*processedDataSize, containedDataSize)) {
                        *processedDataSize += *containedDataSize;
                    } else {
                        delete containedDataSize;
                        return false;
                    }
                }
                delete containedDataSize;
                return true;
            }
            case TagType::COMPOUND: {
                uint64_t* containedDataSize = new uint64_t;
                for (int32_t i=0; i<elementCount.value; i++) {
                    *containedDataSize = 0;
                    if (validateRawNBTData(data, dataSize, initialPosition+*processedDataSize, containedDataSize)) {
                        *processedDataSize += *containedDataSize;
                    } else {
                        delete containedDataSize;
                        return false;
                    }
                }
                delete containedDataSize;
                return true;
            }
            case TagType::INT32_ARRAY: {
                for (int32_t i=0; i<elementCount.value; i++) {
                    ErrorOr<std::vector<int32_t>> nextArray = Helper::readInt32Array(data, dataSize, initialPosition+*processedDataSize);
                    if (nextArray.isError) {
                        return false;
                    }
                    // +4 for the header of the array
                    *processedDataSize += (uint64_t) nextArray.value.size() * 4 + 4;
                }
                return true;
            }
            case TagType::INT64_ARRAY: {
                for (int32_t i=0; i<elementCount.value; i++) {
                    ErrorOr<std::vector<int64_t>> nextArray = Helper::readInt64Array(data, dataSize, initialPosition+*processedDataSize);
                    if (nextArray.isError) {
                        return false;
                    }
                    // +4 for the header of the array
                    *processedDataSize += (uint64_t) nextArray.value.size() * 8 + 4;
                }
                return true;
            }
            default:
                return false;
        }
    }

    bool validateRawNBTData(uint8_t data[], uint64_t dataSize, uint64_t initialPosition, uint64_t* processedDataSize){
        if (initialPosition >= dataSize) {
            // Yes, this *could* return an instance of ErrorOr with
            // ErrorCodes::OVERRUN but we only care to know if what is
            // at that position is valid NBT which it clearly isn't according
            // to the original spec.
            if (processedDataSize!=nullptr) *processedDataSize=0;
            return false;

            // An interesting question at this point is whether we should
            // consider empty input valid or invalid NBT data.
            //
            // The original spec says that the top-most tag is always a
            // compound (or in more recent times, the Microsoft-commercialized
            // in-game-purchase-enabling version also allows list tags)
            // which automatically means that no data is invalid data...
            // I don't see a reason why having a different tag as the top-most
            // tag shouldn't be valid NBT in which case we have to face the
            // question whether no data is invalid or just empty NBT data.
            //
            // This seems like a reasonable extension to the spec to me and
            // it should be backwards compatible AFAIK.
            //
            // - BodgeMaster
        }

        bool returnValue;
        uint64_t currentPosition = initialPosition;
        while (currentPosition<dataSize) {
            ErrorOr<uint64_t> nextTagSize = Helper::totalTagSize(data, dataSize, currentPosition);
            if (nextTagSize.isError) {
                if (nextTagSize.errorCode == ErrorCodes::NOT_YET_KNOWN) {
                    // attempt parsing the name
                    ErrorOr<tiny_utf8::string> tagName = Helper::readString(data, dataSize, currentPosition+1);
                    if (tagName.isError) {
                        returnValue = false;
                        goto returnNow;
                    }

                    // used seek to the start of the list's/compound’s contents
                    //
                    // there is no way this is an error bc it gets
                    // checked while trying to parse the string above
                    int16_t nameSize = Helper::readInt16(data, dataSize, currentPosition+1).value;

                    uint64_t* processedTagSize = new uint64_t;
                    *processedTagSize = 0;

                    if (data[currentPosition]==TagType::LIST) {
                        // type byte + two name size bytes = 3
                        if (!validateRawListContents(data, dataSize, currentPosition + (uint64_t) nameSize + 3, processedTagSize)) {
                            delete processedTagSize;
                            returnValue = false;
                            goto returnNow;
                        }
                        *processedTagSize += (uint64_t) nameSize + 3;
                    }
                    if (data[currentPosition]==TagType::COMPOUND) {
                        // type byte + two name size bytes = 3
                        if (!validateRawNBTData(data, dataSize, currentPosition + (uint64_t) nameSize + 3, processedTagSize)) {
                            delete processedTagSize;
                            returnValue = false;
                            goto returnNow;
                        }
                        *processedTagSize += (uint64_t) nameSize + 3;
                    }
                    currentPosition += *processedTagSize;

                    delete processedTagSize;
                    continue;
                }
                returnValue = false;
                goto returnNow;
            }

            if (currentPosition + nextTagSize.value > dataSize) {
                returnValue = false;
                goto returnNow;
            }

            // recursion abort condition
            if (data[currentPosition]==TagType::END) {
                currentPosition++;
                returnValue = true;
                goto returnNow;
            }

            // nameSize cannot be an error here bc it got checked in
            // nextTagSize() already
            int16_t nameSize = Helper::readInt16(data, dataSize, currentPosition+1).value;

            // attempt parsing the name
            //
            // This shouldn't matter too much here as the only error condition
            // the parser function deals with rn is an overrun which is already
            // being guarded against with
            // if (currentPosition + nextTagSize.value > dataSize) {
            //     returnValue = false;
            //     goto returnNow;
            // }
            // It might, however, turn out to be a useful check in the future.
            ErrorOr<tiny_utf8::string> name = Helper::readString(data, dataSize, currentPosition+1);
            if (name.isError) {
                returnValue = false;
                goto returnNow;
            }

            switch (data[currentPosition]) {
                case TagType::INT8:
                case TagType::INT16:
                case TagType::INT32:
                case TagType::INT64:
                case TagType::FLOAT:
                case TagType::DOUBLE:
                case TagType::INT8_ARRAY:
                    break;
                case TagType::STRING: {
                    // attempt parsing the content
                    //
                    // This shouldn't matter too much here as the only
                    // error condition the parser function deals with rn is
                    // an overrun which is already being guarded against with
                    // if (currentPosition + nextTagSize.value > dataSize) {
                    //     returnValue = false;
                    //     goto returnNow;
                    // }
                    // It might, however, turn out to be a useful check
                    // in the future.
                    //
                    // type byte + two name size bytes = 3
                    ErrorOr<tiny_utf8::string> content = Helper::readString(data, dataSize, currentPosition+nameSize+3);
                    if (content.isError) {
                        returnValue = false;
                        goto returnNow;
                    }
                    break;
                }
                case TagType::INT32_ARRAY:
                case TagType::INT64_ARRAY:
                    break;
                default:
                    returnValue = false;
                    goto returnNow;
            }

            currentPosition += nextTagSize.value;
        }
        returnValue = true;
        goto returnNow;

        returnNow:
            if (processedDataSize!=nullptr) {
                *processedDataSize = currentPosition-initialPosition;
            }
            return returnValue;
    }
}
