#pragma once
#include <Metal/Metal.hpp>
#include <array>
#include <string>

#include "boost/icl/interval.hpp"
#include "helpers.hpp"
#include "math_util.hpp"
#include "objc_helper.hpp"
#include "opengl.hpp"
#include "pica_to_mtl.hpp"

template <typename T>
using Interval = boost::icl::right_open_interval<T>;

namespace Metal {
	template <typename Format_t>
	struct RenderTarget {
		MTL::Device* device;

		u32 location;
		Format_t format;
		OpenGL::uvec2 size;
		bool valid;

		// Range of VRAM taken up by buffer
		Interval<u32> range;

		MTL::Texture* texture = nullptr;

		RenderTarget() : valid(false) {}

		RenderTarget(MTL::Device* dev, u32 loc, Format_t format, u32 x, u32 y, bool valid = true)
			: device(dev), location(loc), format(format), size({x, y}), valid(valid) {
			u64 endLoc = (u64)loc + sizeInBytes();
			// Check if start and end are valid here
			range = Interval<u32>(loc, (u32)endLoc);
		}

		Math::Rect<u32> getSubRect(u32 inputAddress, u32 width, u32 height) {
			const u32 startOffset = (inputAddress - location) / sizePerPixel(format);
			const u32 x0 = (startOffset % (size.x() * 8)) / 8;
			const u32 y0 = (startOffset / (size.x() * 8)) * 8;
			return Math::Rect<u32>{x0, size.y() - y0, x0 + width, size.y() - height - y0};
		}

		// For 2 textures to "match" we only care about their locations, formats, and dimensions to match
		// For other things, such as filtering mode, etc, we can just switch the attributes of the cached texture
		bool matches(RenderTarget& other) {
			return location == other.location && format == other.format && size.x() == other.size.x() && size.y() == other.size.y();
		}

		void allocate() {
			MTL::PixelFormat pixelFormat = MTL::PixelFormatInvalid;
			if (std::is_same<Format_t, PICA::ColorFmt>::value) {
				pixelFormat = PICA::toMTLPixelFormatColor((PICA::ColorFmt)format);
			} else if (std::is_same<Format_t, PICA::DepthFmt>::value) {
				pixelFormat = PICA::toMTLPixelFormatDepth((PICA::DepthFmt)format);
			} else {
				panic("Invalid format type");
			}

			MTL::TextureDescriptor* descriptor = MTL::TextureDescriptor::alloc()->init();
			descriptor->setTextureType(MTL::TextureType2D);
			descriptor->setPixelFormat(pixelFormat);
			descriptor->setWidth(size.u());
			descriptor->setHeight(size.v());
			descriptor->setUsage(MTL::TextureUsageRenderTarget | MTL::TextureUsageShaderRead);
			descriptor->setStorageMode(MTL::StorageModePrivate);
			texture = device->newTexture(descriptor);
			texture->setLabel(toNSString(
				std::string(std::is_same<Format_t, PICA::ColorFmt>::value ? "Color" : "Depth") + " render target " + std::to_string(size.u()) + "x" +
				std::to_string(size.v())
			));
			descriptor->release();
		}

		void free() {
			valid = false;

			if (texture) {
				texture->release();
			}
		}

		u64 sizeInBytes() { return (size_t)size.x() * (size_t)size.y() * PICA::sizePerPixel(format); }
	};

	using ColorRenderTarget = RenderTarget<PICA::ColorFmt>;
	using DepthStencilRenderTarget = RenderTarget<PICA::DepthFmt>;
}  // namespace Metal