// 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

// information taken from https://wiki.vg/NBT
// This is an attempt at creating a uniform model for all NBT tags to allow for a uniform interface based on subclassing a single NBT tag super class.

// NBT tags have a type, optionally a name which consists of the name size and the name string, optionally content type, and optionally a payload which can consist of optionally content type, optionally a content size,
// and the stored content. The format in which they are stored is as follows: <type><name size><name><payload>. All numbers are stored in big endian representation.
// All tag types:
// Generic:    Tag(uint8:tag_type, String:name, uint16:name_size, byte[]:content, int32:size)
// End:        Tag( 0, "",          0,                None,           0) => used to determine the end of a compound tag, only the type gets stored
// Int8:       Tag( 1, String:name, uint16:name_size, int8:content,   1) => a single signed byte, size not stored
// Int16:      Tag( 2, String:name, uint16:name_size, int16:content,  2) => 16 bit signed integer, size not stored
// Int32:      Tag( 3, String:name, uint16:name_size, int32:content,  4) => 32 bit signed integer, size not stored
// Int64:      Tag( 4, String:name, uint16:name_size, int64:content,  8) => 64 bit signed integer, size not stored
// Float:      Tag( 5, String:name, uint16:name_size, float:content,  4) => 32 bit IEEE754 floating point number, size not stored
// Double:     Tag( 6, String:name, uint16:name_size, double:content, 8) => 64 bit IEEE754 floating point number, size not stored
// Int8Array:  Tag( 7, String:name, uint16:name_size, int8[]:content, int32:length) => content stored prefixed with size
// String:     Tag( 8, String:name, uint16:name_size, String:content, uint16:size) => Java style modified UTF-8 string, content stored prefixed with size
// List:       Tag<Tag:type>( 9, String:name, uint16:name_size, Tag[]:content, int32:length) => list of tags of the same type with tag type and name information omitted prefixed by (in order) content type and length
// Compound:   Tag(10, String:name, uint16:name_size, Tag[]:content,  int32:length) => list of tags, last tag is always an end tag, size not stored
// Int32Array: Tag(11, String:name, uint16:name_size, int32[]:content,int32:length) => list of 32 bit signed integers prefixed with its size
// Int64Array: Tag(12, String:name, uint16:name_size, int64[]:content,int32:length) => list of 64 bit signed integers prefixed with its size

#pragma once
#include <cstdint>
#include <vector>
#include <tinyutf8/tinyutf8.h>
#include <mutex>

#include "error.hpp"

namespace NBT {
    namespace Helper {
        ErrorOr<int8_t> readInt8(uint8_t data[], uint64_t dataSize, uint64_t currentPosition);
        ErrorOr<int16_t> readInt16(uint8_t data[], uint64_t dataSize, uint64_t currentPosition);
        ErrorOr<int32_t> readInt32(uint8_t data[], uint64_t dataSize, uint64_t currentPosition);
        ErrorOr<int64_t> readInt64(uint8_t data[], uint64_t dataSize, uint64_t currentPosition);
        ErrorOr<float> readFloat(uint8_t data[], uint64_t dataSize, uint64_t currentPosition);
        ErrorOr<double> readDouble(uint8_t data[], uint64_t dataSize, uint64_t currentPosition);
        ErrorOr<std::vector<int8_t>> readInt8Array(uint8_t data[], uint64_t dataSize, uint64_t currentPosition);
        ErrorOr<tiny_utf8::string> readString(uint8_t data[], uint64_t dataSize, uint64_t currentPosition);
        ErrorOr<std::vector<int32_t>> readInt32Array(uint8_t data[], uint64_t dataSize, uint64_t currentPosition);
        ErrorOr<std::vector<int64_t>> readInt64Array(uint8_t data[], uint64_t dataSize, uint64_t currentPosition);

        void writeInt8(std::vector<uint8_t>* destination, int8_t data);
        void writeInt16(std::vector<uint8_t>* destination, int16_t data);
        void writeInt32(std::vector<uint8_t>* destination, int32_t data);
        void writeInt64(std::vector<uint8_t>* destination, int64_t data);
        void writeFloat(std::vector<uint8_t>* destination, float data);
        void writeDouble(std::vector<uint8_t>* destination, double data);
        void writeInt8Array(std::vector<uint8_t>* destination, std::vector<int8_t> data);
        void writeInt8Array(std::vector<uint8_t>* destination, int8_t data[], uint32_t dataSize);
        ErrorOrVoid writeString(std::vector<uint8_t>* destination, tiny_utf8::string data);
        void writeInt32Array(std::vector<uint8_t>* destination, std::vector<int32_t> data);
        void writeInt32Array(std::vector<uint8_t>* destination, int32_t data[], uint32_t dataSize);
        void writeInt64Array(std::vector<uint8_t>* destination, std::vector<int64_t> data);
        void writeInt64Array(std::vector<uint8_t>* destination, int64_t data[], uint32_t dataSize);

        ErrorOr<uint64_t> totalTagSize(uint8_t data[], uint64_t dataSize, uint64_t currentPosition);
        ErrorOr<int32_t> containedDataLength(uint8_t data[], uint64_t dataSize, uint64_t currentPosition);
    }

    namespace TagType {
        const uint8_t END        =   0;
        const uint8_t INT8       =   1;
        const uint8_t INT16      =   2;
        const uint8_t INT32      =   3;
        const uint8_t INT64      =   4;
        const uint8_t FLOAT      =   5;
        const uint8_t DOUBLE     =   6;
        const uint8_t INT8_ARRAY =   7;
        const uint8_t STRING     =   8;
        const uint8_t LIST       =   9;
        const uint8_t COMPOUND   =  10;
        const uint8_t INT32_ARRAY=  11;
        const uint8_t INT64_ARRAY=  12;
        // This is a workaround that's not part of the spec.
        const uint8_t INVALID    = 255;
    }

    namespace Tag {

        class Generic {
            protected:
                std::mutex mutex;
                uint8_t type;
            public:
                tiny_utf8::string name;

                Generic();
                virtual ~Generic();

                virtual ErrorOrVoid serialize(std::vector<uint8_t>* rawData);
                virtual ErrorOrVoid serializeWithoutHeader(std::vector<uint8_t>* rawData);
                uint8_t getTagType();
        };

        class End: public Generic {
            public:
                End();

                // This needs a separate serializer because
                // END tags have a special header.
                ErrorOrVoid serialize(std::vector<uint8_t>* rawData) override;
                ErrorOrVoid serializeWithoutHeader(std::vector<uint8_t>* rawData) override;
        };

        class Int8: public Generic {
            private:
                int8_t value;
            public:
                Int8();
                Int8(tiny_utf8::string name, int8_t value);

                ErrorOrVoid serializeWithoutHeader(std::vector<uint8_t>* rawData) override;
                int8_t getValue();
                void setValue(int8_t value);
        };

        class Int16: public Generic {
            private:
                int16_t value;
            public:
                Int16();
                Int16(tiny_utf8::string name, int16_t value);

                ErrorOrVoid serializeWithoutHeader(std::vector<uint8_t>* rawData) override;
                int16_t getValue();
                void setValue(int16_t value);
        };

        class Int32: public Generic {
            private:
                int32_t value;
            public:
                Int32();
                Int32(tiny_utf8::string name, int32_t value);

                ErrorOrVoid serializeWithoutHeader(std::vector<uint8_t>* rawData) override;
                int32_t getValue();
                void setValue(int32_t value);
        };

        class Int64: public Generic {
            private:
                int64_t value;
            public:
                Int64();
                Int64(tiny_utf8::string name, int64_t value);

                ErrorOrVoid serializeWithoutHeader(std::vector<uint8_t>* rawData) override;
                int64_t getValue();
                void setValue(int64_t value);
        };

        class Float: public Generic {
            private:
                float value;
            public:
                Float();
                Float(tiny_utf8::string name, float value);

                ErrorOrVoid serializeWithoutHeader(std::vector<uint8_t>* rawData) override;
                float getValue();
                void setValue(float value);
        };

        class Double: public Generic {
            private:
                double value;
            public:
                Double();
                Double(tiny_utf8::string name, double value);

                ErrorOrVoid serializeWithoutHeader(std::vector<uint8_t>* rawData) override;
                double getValue();
                void setValue(double value);
        };

        class Int8Array: public Generic {
            private:
                std::vector<int8_t> data;
            public:
                Int8Array();
                Int8Array(tiny_utf8::string name, std::vector<int8_t> data);
                Int8Array(tiny_utf8::string name, uint64_t length, int8_t data[]);

                ErrorOrVoid serializeWithoutHeader(std::vector<uint8_t>* rawData) override;

                std::vector<int8_t> getData();
                ErrorOr<int8_t> getValue(uint64_t position);
                void setData(std::vector<int8_t> newData);
                ErrorOrVoid setValue(uint64_t position, int8_t value);
                uint64_t length();
                void addElement(int8_t element);
                ErrorOrVoid removeElement(uint64_t position);
        };

        class String: public Generic {
            private:
                tiny_utf8::string value;
            public:
                String();
                String(tiny_utf8::string name, tiny_utf8::string value);

                ErrorOrVoid serializeWithoutHeader(std::vector<uint8_t>* rawData) override;
                tiny_utf8::string getValue();
                void setValue(tiny_utf8::string value);
        };

        class List: public Generic {
            private:
                std::vector<Generic*> tags;
                uint8_t containedType;
                List(tiny_utf8::string name, std::vector<Generic*> data);
            public:
                List();
                List(tiny_utf8::string name, uint8_t type);

                ~List() override;

                ErrorOrVoid serializeWithoutHeader(std::vector<uint8_t>* rawData) override;

                uint8_t getContainedType();
                ErrorOr<Generic*> getElementPointer(uint64_t position);
                ErrorOrVoid setElementPointerAt(uint64_t position, Generic* pointer);
                ErrorOrVoid appendPointer(Generic* pointer);
                ErrorOrVoid deleteElement(uint64_t position);
                uint64_t length();
                static ErrorOr<List*> constructWithData(tiny_utf8::string name, std::vector<Generic*> data);
        };

        class Compound: public Generic {
            private:
                std::vector<Generic*> tags;
                // built-in end tag
                End* endPointer;
                Compound(tiny_utf8::string name, std::vector<Generic*> data);
            public:
                Compound();
                Compound(tiny_utf8::string name);

                ~Compound() override;

                ErrorOrVoid serializeWithoutHeader(std::vector<uint8_t>* rawData) override;

                ErrorOr<Generic*> getElementPointer(uint64_t position);
                ErrorOrVoid setElementPointerAt(uint64_t position, Generic* pointer);
                ErrorOrVoid appendPointer(Generic* pointer);
                ErrorOrVoid deleteElement(uint64_t position);
                uint64_t length();
                static ErrorOr<Compound*> constructWithData(tiny_utf8::string name, std::vector<Generic*> data);
        };

        class Int32Array: public Generic {
            private:
                std::vector<int32_t> data;
            public:
                Int32Array();
                Int32Array(tiny_utf8::string name, std::vector<int32_t> data);
                Int32Array(tiny_utf8::string name, uint64_t length, int32_t data[]);

                ErrorOrVoid serializeWithoutHeader(std::vector<uint8_t>* rawData) override;

                std::vector<int32_t> getData();
                ErrorOr<int32_t> getValue(uint64_t position);
                void setData(std::vector<int32_t> newData);
                ErrorOrVoid setValue(uint64_t position, int32_t value);
                uint64_t length();
                void addElement(int32_t element);
                ErrorOrVoid removeElement(uint64_t position);
        };

        class Int64Array: public Generic {
            private:
                std::vector<int64_t> data;
            public:
                Int64Array();
                Int64Array(tiny_utf8::string name, std::vector<int64_t> data);
                Int64Array(tiny_utf8::string name, uint64_t length, int64_t data[]);

                ErrorOrVoid serializeWithoutHeader(std::vector<uint8_t>* rawData) override;

                std::vector<int64_t> getData();
                ErrorOr<int64_t> getValue(uint64_t position);
                void setData(std::vector<int64_t> newData);
                ErrorOrVoid setValue(uint64_t position, int64_t value);
                uint64_t length();
                void addElement(int64_t element);
                ErrorOrVoid removeElement(uint64_t position);
        };
    }

    ErrorOr<std::vector<Tag::Generic*>> deserialize(uint8_t data[], uint64_t dataSize, uint64_t initialPosition=0, uint64_t* processedDataSize=nullptr);
    bool validateRawNBTData(uint8_t data[], uint64_t dataSize, uint64_t initialPosition=0, uint64_t* processedDataSize=nullptr);
}
