#pragma once

#include "../atlas/atlas.hpp"
#include "../vulkan/device.hpp"
#include "../vulkan/vulkan.hpp"
#include "../custom-textures/texture_tracker.hpp"

#include <string.h>

namespace PSX
{

struct Vertex
{
	float x, y, w;
	uint32_t color;
	uint16_t u, v;
};

struct TextureWindow
{
	uint8_t mask_x, mask_y, or_x, or_y;
};

struct UVRect
{
	uint16_t min_u, min_v, max_u, max_v;
};

enum class SemiTransparentMode
{
	None,
	Average,
	Add,
	Sub,
	AddQuarter
};

enum class PrimitiveType
{
	Sprite,
	Polygon,
	May_Be_2D_Polygon
};

class Renderer
{
public:
	enum class ScanoutMode
	{
		// Use extra precision bits.
		ABGR1555_555,
		// Use extra precision bits to dither down to a native ABGR1555 image.
		// The dither happens in the wrong place, but should be "good" enough to feel authentic.
		ABGR1555_Dither,
		// MDEC
		BGR24
	};

	enum class ScanoutFilter
	{
		None,
		SSAA,
		MDEC_YUV
	};

	enum class WidthMode
	{
		WIDTH_MODE_256 = 0,
		WIDTH_MODE_320 = 1,
		WIDTH_MODE_512 = 2,
		WIDTH_MODE_640 = 3,
		WIDTH_MODE_368 = 4
	};

	struct DisplayRect
	{
		// Unlike Rect, the x-y coordinates for a DisplayRect can be negative
		int x = 0;
		int y = 0;
		unsigned width = 0;
		unsigned height = 0;

		DisplayRect() = default;
		DisplayRect(int x, int y, unsigned width, unsigned height)
		    : x(x)
		    , y(y)
		    , width(width)
		    , height(height)
		{
		}
	};

	struct RenderState
	{
		//Rect display_mode;
		Rect display_fb_rect;
		TextureWindow texture_window;
		Rect cached_window_rect;
		Rect draw_rect;
		int draw_offset_x = 0;
		int draw_offset_y = 0;
		unsigned palette_offset_x = 0;
		unsigned palette_offset_y = 0;
		unsigned texture_offset_x = 0;
		unsigned texture_offset_y = 0;

		int vert_start = 0x10;
		int vert_end = 0x100;
		int horiz_start = 0x200;
		int horiz_end = 0xC00;

		bool is_pal = false;
		bool is_480i = false;
		WidthMode width_mode = WidthMode::WIDTH_MODE_320;
		int crop_overscan = 0;
		unsigned image_crop = 0;

		// Experimental horizontal offset feature
		int offset_cycles = 0;

		int slstart = 0;
		int slend = 239;

		int slstart_pal = 0;
		int slend_pal = 287;

		unsigned display_fb_xstart = 0;
		unsigned display_fb_ystart = 0;

		TextureMode texture_mode = TextureMode::None;
		SemiTransparentMode semi_transparent = SemiTransparentMode::None;
		PrimitiveType primitive_type = PrimitiveType::Polygon;
		ScanoutMode scanout_mode = ScanoutMode::ABGR1555_555;
		ScanoutFilter scanout_filter = ScanoutFilter::None;
		ScanoutFilter scanout_mdec_filter = ScanoutFilter::None;
		bool dither_native_resolution = false;
		bool force_mask_bit = false;
		bool texture_color_modulate = false;
		bool mask_test = false;
		bool display_on = false;
		bool adaptive_smoothing = true;

		UVRect UVLimits;
	};

	struct SaveState
	{
		std::vector<uint32_t> vram;
		RenderState state;
		TextureTrackerSaveState tracker_state;
	};

	Renderer(Vulkan::Device &device, unsigned scaling, unsigned msaa, const SaveState *save_state);
	~Renderer();

	void set_track_textures(bool enable);
	void set_dump_textures(bool enable);
	void set_replace_textures(bool enable);

	void set_adaptive_smoothing(bool enable)
	{
		render_state.adaptive_smoothing = enable;
	}

	void set_draw_rect(const Rect &rect);
	inline void set_draw_offset(int x, int y)
	{
		render_state.draw_offset_x = x;
		render_state.draw_offset_y = y;
	}

	inline void set_scissored_invariant(bool invariant)
	{
		queue.scissor_invariant = invariant;
	}

	void set_texture_window(const TextureWindow &rect);
	inline void set_texture_offset(unsigned x, unsigned y)
	{
		atlas.set_texture_offset(x, y);
		render_state.texture_offset_x = x;
		render_state.texture_offset_y = y;
	}

	inline void set_palette_offset(unsigned x, unsigned y)
	{
		atlas.set_palette_offset(x, y);
		render_state.palette_offset_x = x;
		render_state.palette_offset_y = y;
	}

	Vulkan::BufferHandle copy_cpu_to_vram(const Rect &rect);
	void copy_vram_to_cpu_synchronous(const Rect &rect, uint16_t *vram);
	uint16_t *begin_copy(Vulkan::BufferHandle handle);
	void end_copy(Vulkan::BufferHandle handle);

	void notify_texture_upload(Rect rect, uint16_t *vram);

	void blit_vram(const Rect &dst, const Rect &src);

	void set_vram_framebuffer_coords(unsigned xstart, unsigned ystart)
	{
		last_scanout.reset();

		render_state.display_fb_xstart = xstart;
		render_state.display_fb_ystart = ystart;
	}

	void set_horizontal_display_range(int x1, int x2)
	{
		render_state.horiz_start = x1;
		render_state.horiz_end = x2;
	}

	void set_vertical_display_range(int y1, int y2)
	{
		render_state.vert_start = y1;
		render_state.vert_end = y2;
	}

	void set_display_mode(ScanoutMode mode, bool is_pal, bool is_480i, WidthMode width_mode)
	{
		//if (rect != render_state.display_mode || render_state.scanout_mode != mode)
		//	last_scanout.reset();
		last_scanout.reset();

		//render_state.display_mode = rect;
		render_state.scanout_mode = mode;

		render_state.is_pal = is_pal;
		render_state.is_480i = is_480i;
		render_state.width_mode = width_mode;
	}

	void set_horizontal_overscan_cropping(int crop_overscan)
	{
		render_state.crop_overscan = crop_overscan;
	}

	void set_horizontal_offset_cycles(int offset_cycles)
	{
		render_state.offset_cycles = offset_cycles;
	}
	
	void set_horizontal_additional_cropping(unsigned image_crop)
	{
		render_state.image_crop = image_crop;
	}

	void set_visible_scanlines(int slstart, int slend, int slstart_pal, int slend_pal)
	{
		// May need bounds checking to reject bad inputs. Currently assume all inputs are valid.
		render_state.slstart = slstart;
		render_state.slend = slend;
		render_state.slstart_pal = slstart_pal;
		render_state.slend_pal = slend_pal;
	}

	void set_display_filter(ScanoutFilter filter)
	{
		render_state.scanout_filter = filter;
	}

	void set_mdec_filter(ScanoutFilter mdec_filter)
	{
		render_state.scanout_mdec_filter = mdec_filter;
	}

	void toggle_display(bool enable)
	{
		if (enable != render_state.display_on)
			last_scanout.reset();

		render_state.display_on = enable;
	}

	void set_dither_native_resolution(bool enable)
	{
		render_state.dither_native_resolution = enable;
	}

	Vulkan::ImageHandle scanout_vram_to_texture(bool scaled = true);
	Vulkan::ImageHandle scanout_to_texture();

	inline void set_texture_mode(TextureMode mode)
	{
		render_state.texture_mode = mode;
		atlas.set_texture_mode(mode);
	}

	inline void set_semi_transparent(SemiTransparentMode state)
	{
		render_state.semi_transparent = state;
	}

	inline void set_primitive_type(PrimitiveType primitive_type)
	{
		render_state.primitive_type = primitive_type;
	}

	inline void set_force_mask_bit(bool enable)
	{
		render_state.force_mask_bit = enable;
	}

	inline void set_mask_test(bool enable)
	{
		render_state.mask_test = enable;
	}

	inline void set_texture_color_modulate(bool enable)
	{
		render_state.texture_color_modulate = enable;
	}

	inline void set_UV_limits(uint16_t min_u, uint16_t min_v, uint16_t max_u, uint16_t max_v)
	{
		render_state.UVLimits.min_u = min_u;
		render_state.UVLimits.min_v = min_v;
		render_state.UVLimits.max_u = max_u;
		render_state.UVLimits.max_v = max_v;
	}

	// Draw commands
	void clear_rect(const Rect &rect, uint32_t fb_color);
	void draw_line(const Vertex *vertices);
	void draw_triangle(const Vertex *vertices);
	void draw_quad(const Vertex *vertices);

	SaveState save_vram_state();

	void reset_counters()
	{
		counters = {};
	}

	void flush()
	{
		if (cmd)
			device.submit(cmd);
		cmd.reset();
		device.flush_frame();
	}

	Vulkan::Fence flush_and_signal()
	{
		Vulkan::Fence fence;
		if (cmd)
			device.submit(cmd, &fence);
		cmd.reset();
		device.flush_frame();
		return fence;
	}

	struct
	{
		unsigned render_passes = 0;
		unsigned fragment_readback_pixels = 0;
		unsigned fragment_writeout_pixels = 0;
		unsigned draw_calls = 0;
		unsigned vertices = 0;
		unsigned native_draw_calls = 0;
	} counters;

	enum
	{
		SpecConstIndex_TransMode = 0,
		SpecConstIndex_FilterMode = 1,
		SpecConstIndex_BlendMode = 2,
		SpecConstIndex_Scaling = 3,
		SpecConstIndex_Shift = 4,
		SpecConstIndex_OffsetUV = 5,
		SpecConstIndex_Samples = 0,
	};

	enum FilterExclude
	{
		FilterExcludeNone = 0,
		FilterExcludeOpaque = 1,
		FilterExcludeOpaqueAndSemiTrans = 2,
	};

	enum class FilterMode : uint32_t
	{
		NearestNeighbor = 0,
		XBR = 1,
		SABR = 2,
		Bilinear = 3,
		Bilinear3Point = 4,
		JINC2 = 5
	};

	enum class TransMode : uint32_t
	{
		Opaque = 0,
		SemiTrans = 1,
		SemiTransOpaque = 2
	};

	enum class BlendMode : uint32_t
	{
		BlendAdd = 0,
		BlendAvg = 1,
		BlendSub = 2,
		BlendAddQuarter = 3
	};

	void set_filter_mode(FilterMode mode);
	ScanoutMode get_scanout_mode() const
	{
		return render_state.scanout_mode;
	}

	void set_sprite_filter_exclude(FilterExclude exclude)
	{
		sprite_filter_exclude = exclude;
	}

	void set_polygon_2d_filter_exclude(FilterExclude exclude)
	{
		polygon_2d_filter_exclude = exclude;
	}

	void set_scaled_uv_offset(bool offset)
	{
		scaled_uv_offset = offset;
	}

	/* True iff the constructor finished successfully. The Renderer
	 * constructor does not throw; on failure (e.g. RGBA8_UNORM not
	 * supported) it leaves the object in a destroyable but otherwise
	 * unusable state. Callers must check is_valid() before use. */
	bool is_valid() const { return valid; }

private:
	Vulkan::Device &device;
	unsigned scaling;
	unsigned msaa;
	bool scaled_uv_offset = false;
	bool valid = false;
	FilterMode primitive_filter_mode = FilterMode::NearestNeighbor;
	FilterExclude sprite_filter_exclude = FilterExcludeNone;
	FilterExclude polygon_2d_filter_exclude = FilterExcludeNone;
	Vulkan::ImageHandle scaled_framebuffer;
	Vulkan::ImageHandle scaled_framebuffer_msaa;
	Vulkan::ImageHandle bias_framebuffer;
	Vulkan::ImageHandle framebuffer;
	Vulkan::ImageHandle framebuffer_ssaa;
	std::vector<Vulkan::ImageViewHandle> scaled_views;
	FBAtlas atlas;
	bool texture_tracking_enabled = false;
	TextureTracker tracker;

	Vulkan::CommandBufferHandle cmd;

public:
	// Called by FBAtlas (formerly via HazardListener interface).
	void hazard(StatusFlags flags);
	void resolve(Domain target_domain, unsigned x, unsigned y);
	void flush_render_pass(const Rect &rect);
	void discard_render_pass();
	void clear_quad(const Rect &rect, uint32_t fb_color, bool candidate);

	// Called by TextureTracker (formerly via TextureUploader interface).
	Vulkan::ImageHandle upload_texture(std::vector<LoadedImage> &image);
	Vulkan::ImageHandle create_texture(int width, int height, int levels);
	Vulkan::CommandBufferHandle &command_buffer_hack_fixme();

private:
	void hd_texture_uniforms(HdTextureHandle hd_texture_index);
	HdTextureHandle get_hd_texture_index(const Rect &uvlimits, bool &fastpath_capable_out, bool &cache_hit_out);

	struct
	{
		Vulkan::Program *copy_to_vram;
		Vulkan::Program *copy_to_vram_masked;
		Vulkan::Program *unscaled_quad_blitter;
		Vulkan::Program *scaled_quad_blitter;
		Vulkan::Program *unscaled_dither_quad_blitter;
		Vulkan::Program *scaled_dither_quad_blitter;
		Vulkan::Program *bpp24_quad_blitter;
		Vulkan::Program *bpp24_yuv_quad_blitter;
		Vulkan::Program *resolve_to_scaled;
		Vulkan::Program *resolve_to_unscaled;

		Vulkan::Program *blit_vram_scaled;
		Vulkan::Program *blit_vram_scaled_masked;

		Vulkan::Program *blit_vram_cached_scaled;
		Vulkan::Program *blit_vram_cached_scaled_masked;
		Vulkan::Program *blit_vram_msaa_cached_scaled;
		Vulkan::Program *blit_vram_msaa_cached_scaled_masked;

		Vulkan::Program *blit_vram_unscaled;
		Vulkan::Program *blit_vram_unscaled_masked;
		Vulkan::Program *blit_vram_cached_unscaled;
		Vulkan::Program *blit_vram_cached_unscaled_masked;

		Vulkan::Program *flat;
		Vulkan::Program *textured_scaled;
		Vulkan::Program *textured_unscaled;
		Vulkan::Program *flat_masked;
		Vulkan::Program *textured_masked_scaled;
		Vulkan::Program *textured_masked_unscaled;

		Vulkan::Program *mipmap_resolve;
		Vulkan::Program *mipmap_dither_resolve;
		Vulkan::Program *mipmap_energy_first;
		Vulkan::Program *mipmap_energy;
		Vulkan::Program *mipmap_energy_blur;
	} pipelines;

	Vulkan::ImageHandle dither_lut;

	void init_pipelines();
	void init_primitive_pipelines();
	void init_primitive_feedback_pipelines();
	void ensure_command_buffer();

	RenderState render_state;

	struct BufferVertex
	{
		float x, y, z, w;
		uint32_t color;
		TextureWindow window;
		int16_t pal_x, pal_y, params;
		int16_t u, v, base_uv_x, base_uv_y;
		uint16_t min_u, min_v, max_u, max_v;
	};

	struct BlitInfo
	{
		uint32_t src_offset[2];
		uint32_t dst_offset[2];
		uint32_t extent[2];
		uint32_t mask;
		uint32_t sample;
	};

	struct SemiTransparentState
	{
		int scissor_index;
		HdTextureHandle hd_texture_index;
		SemiTransparentMode semi_transparent;
		bool textured;
		bool masked;
		bool filtering;
		bool scaled_read;
		unsigned shift;
		bool offset_uv;

		bool operator==(const SemiTransparentState &other) const
		{
			return scissor_index == other.scissor_index && hd_texture_index == other.hd_texture_index &&
			       semi_transparent == other.semi_transparent && textured == other.textured && masked == other.masked &&
				   filtering == other.filtering && scaled_read == other.scaled_read && shift == other.shift &&
				   offset_uv == other.offset_uv;
		}

		bool operator!=(const SemiTransparentState &other) const
		{
			return !(*this == other);
		}
	};

	struct ClearCandidate
	{
		Rect rect;
		uint32_t color; /* fb_color */
		float z;
	};

	struct PrimitiveInfo {
		unsigned triangle_index;
		int scissor_index;
		HdTextureHandle hd_texture_index;
		bool filtering;
		bool scaled_read;
		unsigned shift;
		bool offset_uv;

		// needed for emplace_back
		PrimitiveInfo(
			unsigned triangle_index,
			int scissor_index = -1,
			HdTextureHandle hd_texture_index = HdTextureHandle::make_none(),
			bool filtering = false,
			bool scaled_read = false,
			unsigned shift = 0,
			bool offset_uv = false
		)
			: triangle_index(triangle_index), scissor_index(scissor_index), hd_texture_index(hd_texture_index),
			filtering(filtering), scaled_read(scaled_read), shift(shift), offset_uv(offset_uv)
		{}
	};

	struct OpaqueQueue
	{
		// Non-textured primitives.
		std::vector<BufferVertex> opaque;
		std::vector<PrimitiveInfo> opaque_scissor;

		// Textured primitives, no semi-transparency.
		std::vector<BufferVertex> opaque_textured;
		std::vector<PrimitiveInfo> opaque_textured_scissor;

		// Textured primitives, semi-transparency enabled.
		std::vector<BufferVertex> semi_transparent_opaque;
		std::vector<PrimitiveInfo> semi_transparent_opaque_scissor;

		std::vector<BufferVertex> semi_transparent;
		std::vector<SemiTransparentState> semi_transparent_state;

		std::vector<Vulkan::ImageHandle> textures;

		std::vector<VkRect2D> scaled_resolves;
		std::vector<VkRect2D> unscaled_resolves;
		std::vector<BlitInfo> scaled_blits;
		std::vector<BlitInfo> scaled_masked_blits;
		std::vector<BlitInfo> unscaled_blits;
		std::vector<BlitInfo> unscaled_masked_blits;

		std::vector<VkRect2D> scissors;
		std::vector<ClearCandidate> clear_candidates;
		VkRect2D default_scissor;
		bool scissor_invariant = false;
	} queue;
	unsigned primitive_index = 0;
	bool render_pass_is_feedback = false;
	float last_uv_scale_x, last_uv_scale_y;

	void dispatch(const std::vector<BufferVertex> &vertices, std::vector<PrimitiveInfo> &scissors, bool textured = false);
	static bool primitive_info_sort_gt(const PrimitiveInfo &a, const PrimitiveInfo &b);
	void render_opaque_primitives();
	void render_opaque_texture_primitives();
	void render_semi_transparent_opaque_texture_primitives();
	void render_semi_transparent_primitives();
	void semi_transparent_set_state(const SemiTransparentState &state);
	void dispatch_set_scaled_read_texture(bool scaled_read, bool textured);
	void reset_queue();

	float allocate_depth(Domain domain, const Rect &rect);

	void build_attribs(BufferVertex *verts, const Vertex *vertices, unsigned count, HdTextureHandle &hd_texture_index,
		bool &filtering, bool &scaled_read, unsigned &shift, bool &offset_uv);
	void build_line_quad(Vertex *quad, const Vertex *line);
	std::vector<BufferVertex> *select_pipeline(unsigned prims, int scissor, HdTextureHandle hd_texture,
		bool filtering, bool scaled_read, unsigned shift, bool offset_uv);
	bool get_filer_exclude(FilterExclude exclude)
	{
		if (
			render_state.primitive_type == PrimitiveType::Sprite &&
			sprite_filter_exclude >= exclude
		)
			return true;
		if (
			render_state.primitive_type == PrimitiveType::May_Be_2D_Polygon &&
			polygon_2d_filter_exclude >= exclude
		)
			return true;
		return false;
	}

	void flush_resolves();
	void flush_blits();
	void flush_blit(const std::vector<BlitInfo> &infos, Vulkan::Program &program, bool scaled);
	void reset_scissor_queue();
	const ClearCandidate *find_clear_candidate(const Rect &rect) const;

	Rect compute_window_rect(const TextureWindow &window);

	Vulkan::ImageHandle last_scanout;
	Vulkan::ImageHandle reuseable_scanout;
	DisplayRect compute_display_rect();

	Rect compute_vram_framebuffer_rect();

	void mipmap_framebuffer();
	void ssaa_framebuffer();
	Vulkan::BufferHandle quad;
};
}
