/*
 * Copyright 2016 - 2022 Gary Talent (gary@drinkingtea.net). All rights reserved.
 */

#pragma once

#include <ox/std/def.hpp>
#include <ox/std/error.hpp>
#include <ox/std/optional.hpp>
#include <ox/std/string.hpp>
#include <ox/claw/read.hpp>
#include <ox/claw/write.hpp>

namespace nostalgia::core {

class Wrap {
	public:
		virtual ~Wrap() = default;
};

template<typename T>
class WrapInline: public Wrap {
	private:
		T m_obj;

	public:
		constexpr WrapInline() = default;

		template<typename... Args>
		constexpr explicit WrapInline(Args &&...args): m_obj(ox::forward<Args>(args)...) {
		}

		[[nodiscard]]
		constexpr auto obj() noexcept {
			return &m_obj;
		}

};

template<typename T, typename... Args>
constexpr auto makeWrap(Args &&...args) noexcept {
	return ox::make_unique<WrapInline<T>>(ox::forward<Args>(args)...);
}

template<typename T>
constexpr auto wrapCast(auto ptr) noexcept {
	return static_cast<WrapInline<T>*>(ptr)->obj();
}

struct BaseConverter {
	virtual ~BaseConverter() noexcept = default;

	[[nodiscard]]
	virtual const char *srcTypeName() noexcept = 0;

	[[nodiscard]]
	virtual int srcTypeVersion() noexcept = 0;

	[[nodiscard]]
	virtual bool srcMatches(const char *srcTypeName, int srcTypeVersion) const noexcept = 0;

	[[nodiscard]]
	virtual bool dstMatches(const char *dstTypeName, int dstTypeVersion) const noexcept = 0;

	virtual ox::Result<ox::UniquePtr<Wrap>> convertPtrToPtr(Wrap *src) noexcept = 0;

	virtual ox::Result<ox::UniquePtr<Wrap>> convertBuffToPtr(const ox::Buffer &srcBuff) noexcept = 0;

	[[nodiscard]]
	inline bool matches(const char *srcTypeName, int srcTypeVersion,
	                    const char *dstTypeName, int dstTypeVersion) const noexcept {
		return srcMatches(srcTypeName, srcTypeVersion)
		    && dstMatches(dstTypeName, dstTypeVersion);
	}

};

template<typename SrcType, typename DstType>
struct Converter: public BaseConverter {

	virtual ox::Error convert(SrcType*, DstType*) noexcept = 0;

	[[nodiscard]]
	const char *srcTypeName() noexcept final {
		return ox::requireModelTypeName<SrcType>();
	}

	[[nodiscard]]
	int srcTypeVersion() noexcept final {
		return ox::requireModelTypeVersion<SrcType>();
	}

	[[nodiscard]]
	bool srcMatches(const char *srcTypeName, int srcTypeVersion) const noexcept final {
		static constexpr auto SrcTypeName = ox::requireModelTypeName<SrcType>();
		static constexpr auto SrcTypeVersion = ox::requireModelTypeVersion<SrcType>();
		return ox_strcmp(srcTypeName, SrcTypeName) == 0
		    && srcTypeVersion == SrcTypeVersion;
	}

	[[nodiscard]]
	bool dstMatches(const char *dstTypeName, int dstTypeVersion) const noexcept final {
		static constexpr auto DstTypeName = ox::requireModelTypeName<DstType>();
		static constexpr auto DstTypeVersion = ox::requireModelTypeVersion<DstType>();
		return ox_strcmp(dstTypeName, DstTypeName) == 0
		    && dstTypeVersion == DstTypeVersion;
	}

	ox::Result<ox::UniquePtr<Wrap>> convertPtrToPtr(Wrap *src) noexcept final {
		auto dst = makeWrap<DstType>();
		oxReturnError(convert(wrapCast<SrcType>(src), wrapCast<DstType>(dst.get())));
		return ox::Result<ox::UniquePtr<Wrap>>(std::move(dst));
	}

	ox::Result<ox::UniquePtr<Wrap>> convertBuffToPtr(const ox::Buffer &srcBuff) noexcept final {
		oxRequireM(src, ox::readClaw<SrcType>(srcBuff));
		auto dst = makeWrap<DstType>();
		oxReturnError(convert(&src, wrapCast<DstType>(dst.get())));
		return ox::Result<ox::UniquePtr<Wrap>>(std::move(dst));
	}

};

ox::Result<ox::UniquePtr<Wrap>> convert(const ox::Buffer &srcBuffer,
                                        const char *dstTypeName, int dstTypeVersion) noexcept;

template<typename DstType>
ox::Result<DstType> convert(const ox::Buffer &srcBuffer) noexcept {
	static constexpr auto DstTypeName = ox::requireModelTypeName<DstType>();
	static constexpr auto DstTypeVersion = ox::requireModelTypeVersion<DstType>();
	oxRequire(out, convert(srcBuffer, DstTypeName, DstTypeVersion));
	return wrapCast<DstType>(out);
}

template<typename DstType>
ox::Error convert(const ox::Buffer &buff, DstType *outObj) noexcept {
	static constexpr auto DstTypeName = ox::requireModelTypeName<DstType>();
	static constexpr auto DstTypeVersion = ox::requireModelTypeVersion<DstType>();
	oxRequire(outPtr, convert(buff, DstTypeName, DstTypeVersion));
	*outObj = std::move(*wrapCast<DstType>(outPtr.get()));
	return OxError(0);
}

template<typename DstType>
ox::Result<ox::Buffer> convertBuffToBuff(const ox::Buffer &srcBuffer, ox::ClawFormat fmt) noexcept {
	static constexpr auto DstTypeName = ox::requireModelTypeName<DstType>();
	static constexpr auto DstTypeVersion = ox::requireModelTypeVersion<DstType>();
	oxRequire(out, convert(srcBuffer, DstTypeName, DstTypeVersion));
	return ox::writeClaw<DstType>(wrapCast<DstType>(out.get()), fmt);
}

}
