#pragma once
#include <v8.h>
#include <atomic>
#include <cstdint>
#include <cstdlib>
#include <memory>
#include <vector>

#include "transferable.h"
#include "isolate/util.h"

namespace catbox {

using handle_vector_t = std::vector<v8::Local<v8::Value>>;
using transferable_vector_t = std::vector<std::unique_ptr<Transferable>>;
using array_buffer_vector_t = std::vector<std::unique_ptr<class ExternalCopyArrayBuffer>>;
using shared_buffer_vector_t = std::vector<std::unique_ptr<class ExternalCopySharedArrayBuffer>>;

class ExternalCopySerializerDelegate : public v8::ValueSerializer::Delegate {
	private:
		transferable_vector_t& references;
		shared_buffer_vector_t& shared_buffers;

	public:
		v8::ValueSerializer* serializer = nullptr;
		ExternalCopySerializerDelegate(
			transferable_vector_t& references,
			shared_buffer_vector_t& shared_buffers
		);
		void ThrowDataCloneError(v8::Local<v8::String> message) final;
		v8::Maybe<bool> WriteHostObject(v8::Isolate* isolate, v8::Local<v8::Object> object) final;
		v8::Maybe<uint32_t> GetSharedArrayBufferId(v8::Isolate* isolate, v8::Local<v8::SharedArrayBuffer> shared_array_buffer) final;
};

class ExternalCopyDeserializerDelegate : public v8::ValueDeserializer::Delegate {
	private:
		transferable_vector_t& references;

	public:
		v8::ValueDeserializer* deserializer = nullptr;
		explicit ExternalCopyDeserializerDelegate(transferable_vector_t& references);
		v8::MaybeLocal<v8::Object> ReadHostObject(v8::Isolate* isolate) final;
};


class ExternalCopy : public Transferable {
	private:
		size_t size = 0;
		size_t original_size = 0;
		static std::atomic<size_t> total_allocated_size;

	public:
		ExternalCopy();
		explicit ExternalCopy(size_t size);
		ExternalCopy(const ExternalCopy&) = delete;
		ExternalCopy& operator= (const ExternalCopy&) = delete;
		~ExternalCopy() override;

		/**
		 * `Copy` may throw a v8 exception if JSON.stringify(value) throws
		 */
		static std::unique_ptr<ExternalCopy> Copy(
			const v8::Local<v8::Value>& value,
			bool transfer_out = false,
			const handle_vector_t& transfer_list = handle_vector_t()
		);

		/**
		 * If you give this a primitive v8::Value (except Symbol) it will return a ExternalCopy for you.
		 * Otherwise it returns nullptr. This is used to automatically move simple values between
		 * isolates where it is possible to do so perfectly.
		 */
		static std::unique_ptr<ExternalCopy> CopyIfPrimitive(const v8::Local<v8::Value>& value);
		static std::unique_ptr<ExternalCopy> CopyIfPrimitiveOrError(const v8::Local<v8::Value>& value);

		static size_t TotalExternalSize();

		v8::Local<v8::Value> CopyIntoCheckHeap(bool transfer_in = false);
		virtual v8::Local<v8::Value> CopyInto(bool transfer_in = false) = 0;
		void UpdateSize(size_t size);
		size_t OriginalSize() const;
		virtual uint32_t WorstCaseHeapSize() const = 0;
		v8::Local<v8::Value> TransferIn() final;
};

/**
 * This will make a copy of any Number (several C++ types), or Boolean. Strings are handled
 * by the specialization below.
 */
template <typename T, typename V>
class ExternalCopyTemplate : public ExternalCopy {
	private:
		const V value;

	public:
		explicit ExternalCopyTemplate(const v8::Local<v8::Value>& value) : value(v8::Local<T>::Cast(value)->Value()) {}

		v8::Local<v8::Value> CopyInto(bool /*transfer_in*/ = false) final {
			return T::New(v8::Isolate::GetCurrent(), value);
		}

		uint32_t WorstCaseHeapSize() const final {
			return 24;
		}
};

/**
 * String data
 */
class ExternalCopyString : public ExternalCopy {
	private:
		// shared_ptr<> to share external strings between isolates
		using V = std::vector<char>;
		bool one_byte;
		std::shared_ptr<V> value;

		/**
		 * Helper class passed to v8 so we can reuse the same externally allocated memory for strings
		 * between different isolates
		 */
		class ExternalString : public v8::String::ExternalStringResource {
			private:
				std::shared_ptr<V> value;

			public:
				explicit ExternalString(std::shared_ptr<V> value);
				ExternalString(const ExternalString&) = delete;
				ExternalString& operator= (const ExternalString&) = delete;
				~ExternalString() final;
				const uint16_t* data() const final;
				size_t length() const final;
		};

		class ExternalStringOneByte : public v8::String::ExternalOneByteStringResource {
			private:
				std::shared_ptr<V> value;

			public:
				explicit ExternalStringOneByte(std::shared_ptr<V> value);
				ExternalStringOneByte(const ExternalStringOneByte&) = delete;
				ExternalStringOneByte& operator= (const ExternalStringOneByte&) = delete;
				~ExternalStringOneByte() final;
				const char* data() const final;
				size_t length() const final;
		};

	public:
		explicit ExternalCopyString(v8::Local<v8::String> string);
		explicit ExternalCopyString(const char* message);
		explicit ExternalCopyString(const std::string& message);
		v8::Local<v8::Value> CopyInto(bool transfer_in = false) final;
		uint32_t WorstCaseHeapSize() const final;
};

/**
 * Serialized value from v8::ValueSerializer,
 */
class ExternalCopySerialized : public ExternalCopy {
	private:
		std::unique_ptr<uint8_t, decltype(std::free)*> buffer;
		size_t size;
		transferable_vector_t references;
		array_buffer_vector_t array_buffers;
		shared_buffer_vector_t shared_buffers;

	public:
		ExternalCopySerialized(
			std::pair<uint8_t*, size_t> val,
			transferable_vector_t references,
			array_buffer_vector_t array_buffers,
			shared_buffer_vector_t shared_buffers
		);
		v8::Local<v8::Value> CopyInto(bool transfer_in = false) final;
		uint32_t WorstCaseHeapSize() const final;
};

/**
 * Make a special case for errors so if someone throws then a similar error will come out the other
 * side.
 */
class ExternalCopyError : public ExternalCopy {
	friend class ExternalCopy;
	public:
		enum class ErrorType { RangeError = 1, ReferenceError, SyntaxError, TypeError, Error };

	private:
		ErrorType error_type;
		std::unique_ptr<ExternalCopyString> message;
		std::unique_ptr<ExternalCopyString> stack;

	public:
		ExternalCopyError(ErrorType error_type, std::unique_ptr<ExternalCopyString> message, std::unique_ptr<ExternalCopyString> stack);
		ExternalCopyError(ErrorType error_type, const char* message, std::string stack = "");
		v8::Local<v8::Value> CopyInto(bool transfer_in = false) final;
		uint32_t WorstCaseHeapSize() const final;
};

/**
 * null and undefined
 */
class ExternalCopyNull : public ExternalCopy {
	public:
		v8::Local<v8::Value> CopyInto(bool transfer_in = false) final;
		uint32_t WorstCaseHeapSize() const final;
};

class ExternalCopyUndefined : public ExternalCopy {
	public:
		v8::Local<v8::Value> CopyInto(bool transfer_in = false) final;
		uint32_t WorstCaseHeapSize() const final;
};

/**
 * Identical to ExternalCopyTemplate except that Date uses `ValueOf` instead of `Value` -.-
 */
class ExternalCopyDate : public ExternalCopy {
	private:
		const double value;

	public:
		explicit ExternalCopyDate(const v8::Local<v8::Value>& value);
		v8::Local<v8::Value> CopyInto(bool transfer_in = false) final;
		uint32_t WorstCaseHeapSize() const final;
};

/**
 * Base class for ArrayBuffer and SharedArrayBuffer
 */
class ExternalCopyBytes : public ExternalCopy {
	protected:
		/**
		 * Holder is responsible for keeping a referenced to the shared_ptr around as long as the  JS
		 * instance is alive.
		 */
		struct Holder {
			static constexpr uint64_t kMagic = 0xa4d3c462f7fd1741;
			uint64_t magic = kMagic;
			v8::Persistent<v8::Object> v8_ptr;
			std::shared_ptr<void> cc_ptr;
			size_t size;

			Holder(const v8::Local<v8::Object>& buffer, std::shared_ptr<void> cc_ptr, size_t size);
			Holder(const Holder&) = delete;
			Holder& operator= (const Holder&) = delete;
			~Holder();
			static void WeakCallbackV8(const v8::WeakCallbackInfo<void>& info);
			static void WeakCallback(void* param);
		};

	public:
		explicit ExternalCopyBytes(size_t size);
};

/**
 * ArrayBuffer instances
 */
class ExternalCopyArrayBuffer : public ExternalCopyBytes {
	private:
		std::shared_ptr<void> value;
		const size_t length;

	public:
		ExternalCopyArrayBuffer(const void* data, size_t length);
		ExternalCopyArrayBuffer(std::shared_ptr<void> ptr, size_t length);
		explicit ExternalCopyArrayBuffer(const v8::Local<v8::ArrayBuffer>& handle);

		static std::unique_ptr<ExternalCopyArrayBuffer> Transfer(const v8::Local<v8::ArrayBuffer>& handle);
		v8::Local<v8::Value> CopyInto(bool transfer_in = false) final;
		uint32_t WorstCaseHeapSize() const final;
		std::shared_ptr<void> GetSharedPointer() const;
		const void* Data() const;
		size_t Length() const;
};

/**
 * SharedArrayBuffer instances
 */
class ExternalCopySharedArrayBuffer : public ExternalCopyBytes {
	private:
		std::shared_ptr<void> value;
		const size_t length;

	public:
		explicit ExternalCopySharedArrayBuffer(const v8::Local<v8::SharedArrayBuffer>& handle);

		v8::Local<v8::Value> CopyInto(bool transfer_in = false) final;
		uint32_t WorstCaseHeapSize() const final;
		std::shared_ptr<void> GetSharedPointer() const;
};

/**
 * All types of TypedArray views w/ underlying buffer handle
 */
class ExternalCopyArrayBufferView : public ExternalCopy {
	public:
		enum class ViewType { Uint8, Uint8Clamped, Int8, Uint16, Int16, Uint32, Int32, Float32, Float64, DataView };

	private:
		std::unique_ptr<ExternalCopyBytes> buffer;
		ViewType type;

	public:
		ExternalCopyArrayBufferView(std::unique_ptr<ExternalCopyBytes> buffer, ViewType type);
		v8::Local<v8::Value> CopyInto(bool transfer_in = false) final;
		uint32_t WorstCaseHeapSize() const final;
};

} // namespace catbox
