//     ____                                 _
//    / ___|  __ _ _   _  __ _ _ __ ___  __| |
//    \___ \ / _` | | | |/ _` | '__/ _ \/ _` |
//     ___) | (_| | |_| | (_| | | |  __/ (_| |
//    |____/ \__, |\__,_|\__,_|_|  \___|\__,_|
//              |_|
//
// This is a header file with all used by ZLE for the Squared algorithm.
// SFML 3.0 is the main dependency here. Requires C++17.

#pragma once
#include <SFML/Graphics.hpp>
#include <string>
#include <vector>
#include <filesystem>

namespace squ
{
	constexpr std::string_view SQU_VERSION = "SQU002";
	constexpr std::string_view SQV_VERSION = "SQV002";
	class Frame : public sf::Drawable
	{
		friend class FrameAnimation;
		sf::VertexArray arr;
		sf::VertexBuffer buff;
		sf::Vector2u size;
		const sf::Texture* texture;
		enum Settings
		{
			None = 0,
			ConstRotation = 1 << 0,
			ConstSize = 1 << 1,
		};
		static void readObject(sf::InputStream& stream, sf::Vector2i& position, std::uint16_t& rotation, std::uint16_t& scale, std::uint32_t settings, std::uint16_t conRot, std::uint16_t conSize)
		{
			if (settings == None)
			{
				std::uint8_t byte[6];
				for (std::int8_t i = 0; i < 6; i++)
					stream.read(&byte[i], sizeof(byte[i]));
				scale = byte[0] | ((byte[1] & 0b111) << 8);
				rotation = ((byte[1] & 0b11111000) >> 3) | ((byte[2] & 0b1111) << 5);
				position.x = (((byte[2] & 0b11110000) >> 4) | (byte[3] << 4) | ((byte[4] & 0b11) << 12)) - 8192;
				position.y = (((byte[4] & 0b11111100) >> 2) | (byte[5] << 6)) - 8192;
			}
			else if (settings & ConstRotation && settings & ConstSize)
			{
				std::uint8_t byte[4];
				for (std::int8_t i = 0; i < 4; i++)
					stream.read(&byte[i], sizeof(byte[i]));
				scale = conSize;
				rotation = conRot;
				position.x = ((byte[1] << 8) | byte[0]) - 8192;
				position.y = ((byte[3] << 8) | byte[2]) - 8192;
			}
			else if (settings == ConstRotation)
			{
				std::uint8_t byte[5];
				for (std::int8_t i = 0; i < 5; i++)
					stream.read(&byte[i], sizeof(byte[i]));
				scale = byte[0] | ((byte[1] & 0b1111) << 8);
				rotation = conRot;
				position.x = (((byte[1] & 0b11110000) >> 4) | (byte[2] << 4) | ((byte[3] & 0b11) << 12)) - 8192;
				position.y = (((byte[3] & 0b11111100) >> 2) | (byte[4] << 6)) - 8192;
			}
			else if (settings == ConstSize)
			{
				std::uint8_t byte[5];
				for (std::int8_t i = 0; i < 5; i++)
					stream.read(&byte[i], sizeof(byte[i]));
				scale = conSize;
				rotation = byte[0] | ((byte[1] & 0b1111) << 8);
				position.x = (((byte[1] & 0b11110000) >> 4) | (byte[2] << 4) | ((byte[3] & 0b11) << 12)) - 8192;
				position.y = (((byte[3] & 0b11111100) >> 2) | (byte[4] << 6)) - 8192;
			}
		}
	public:

		/// <summary>
		/// Default constructor for a frame.
		/// </summary>
		Frame()
			: size(0, 0), texture(nullptr)
		{

		}

		/// <summary>
		/// Get the size of the whole frame.
		/// </summary>
		/// <returns>Size of the frame in pixels</returns>
		const sf::Vector2u& getSize() const { return size; }

		/// <summary>
		/// Get the texture that is applied to each object in this frame.
		/// Returns nullptr if none is set.
		/// </summary>
		/// <returns>Pointer to a texture used by this frame</returns>
		const sf::Texture* getTexture() const { return texture; }

		/// <summary>
		/// Set a texture for each object. This texture is used by each object in this frame.
		/// </summary>
		/// <param name="texture">Pointer to a texture</param>
		void setTexture(const sf::Texture* texture)
		{
			if (texture != this->texture && texture)
			{
				for (std::int32_t i = 0; i < arr.getVertexCount() / 6; i++)
				{
					arr[i * 6 + 0].texCoords = sf::Vector2f(0, 0);
					arr[i * 6 + 1].texCoords = sf::Vector2f(texture->getSize().x, 0);
					arr[i * 6 + 2].texCoords = sf::Vector2f(texture->getSize().x, texture->getSize().y);
					arr[i * 6 + 3].texCoords = sf::Vector2f(0, texture->getSize().y);

					arr[i * 6 + 4].texCoords = arr[i * 6 + 0].texCoords;
					arr[i * 6 + 5].texCoords = arr[i * 6 + 2].texCoords;
				}
			}
			this->texture = texture;
		}

		/// <summary>
		/// This function loads the frame data from disk.
		/// This includes the frame version, size, settings and data for each object: 
		/// position, rotation, size and color. By default, position uses 14 bits per 
		/// coordinate, rotation uses 9 bits, size uses 11 bits and color uses 3 bytes. 
		/// This is in total 9 bytes per object. Depending on optimization flags, such 
		/// as if rotation is constant among objects, or size is constant this changes slightly.
		/// </summary>
		/// <param name="fileName">File to load the frame from</param>
		/// <returns>True on success, false on fail</returns>
		bool loadFromFile(const std::filesystem::path& fileName)
		{
			sf::FileInputStream file;
			if (file.open(fileName.string()))
				return loadFromStream(file);
			return false;
		}

		/// <summary>
		/// Loads the frame from a stream. This function is
		/// used by loadFromFile().
		/// </summary>
		/// <param name="stream">Stream to load the frame from</param>
		/// <returns>True on success, false on fail</returns>
		bool loadFromStream(sf::InputStream& stream)
		{
			std::uint32_t settings = 0;
			std::uint16_t conRot = 0;
			std::uint16_t conSize = 0;
			std::int16_t sizeX;
			std::int16_t sizeY;
			std::uint32_t count;
			std::string ver = "123456";
			stream.read(&ver[0], 6);
			if (ver != SQU_VERSION)
				return false;
			stream.read(&sizeX, sizeof(sizeX));
			stream.read(&sizeY, sizeof(sizeY));
			size.x = sizeX;
			size.y = sizeY;
			stream.read(&count, sizeof(count));
			stream.read(&settings, sizeof(settings));
			if (settings & ConstRotation)
				stream.read(&conRot, sizeof(conRot));
			if (settings & ConstSize)
				stream.read(&conSize, sizeof(conSize));

			arr.setPrimitiveType(sf::PrimitiveType::Triangles);
			arr.resize(count * 6);

			sf::Vector2i position;
			std::uint16_t rotation = 0;
			std::uint16_t scale = 0;
			sf::Color color;
			sf::Transformable tran;

			for (std::int32_t i = 0; i < count; i++)
			{
				readObject(stream, position, rotation, scale, settings, conRot, conSize);

				stream.read(&color.r, sizeof(color.r));
				stream.read(&color.g, sizeof(color.g));
				stream.read(&color.b, sizeof(color.b));

				tran.setPosition(sf::Vector2f(position.x, position.y));
				tran.setRotation(sf::degrees(360 - rotation / 512.f * 360.f));
				tran.setScale(sf::Vector2f(scale, scale));

				arr[i * 6 + 0].position = tran.getTransform().transformPoint(sf::Vector2f(-0.5, -0.5));
				arr[i * 6 + 0].color = color;
				arr[i * 6 + 1].position = tran.getTransform().transformPoint(sf::Vector2f(0.5, -0.5));
				arr[i * 6 + 1].color = color;
				arr[i * 6 + 2].position = tran.getTransform().transformPoint(sf::Vector2f(0.5, 0.5));
				arr[i * 6 + 2].color = color;
				arr[i * 6 + 3].position = tran.getTransform().transformPoint(sf::Vector2f(-0.5, 0.5));
				arr[i * 6 + 3].color = color;

				if (texture)
				{
					arr[i * 6 + 0].texCoords = sf::Vector2f(0, 0);
					arr[i * 6 + 1].texCoords = sf::Vector2f(texture->getSize().x, 0);
					arr[i * 6 + 2].texCoords = sf::Vector2f(texture->getSize().x, texture->getSize().y);
					arr[i * 6 + 3].texCoords = sf::Vector2f(0, texture->getSize().y);
				}

				arr[i * 6 + 4] = arr[i * 6 + 0];
				arr[i * 6 + 5] = arr[i * 6 + 2];
			}
			if (sf::VertexBuffer::isAvailable() && arr.getVertexCount() > 0)
			{
				buff.setUsage(sf::VertexBuffer::Usage::Static);
				buff.setPrimitiveType(arr.getPrimitiveType());
				buff.create(arr.getVertexCount());
				buff.update(&arr[0]);
			}
			return true;
		}

	private:
		virtual void draw(sf::RenderTarget& target, sf::RenderStates states) const
		{
			if (texture)
				states.texture = texture;
			if (sf::VertexBuffer::isAvailable())
				target.draw(buff, states);
			else
				target.draw(arr, states);
		}
	};
	class FrameAnimation
	{
		friend class FrameAnimator;
		std::vector<sf::VertexArray> arr;
		std::vector<sf::VertexBuffer> buff;
		sf::Vector2u size;
		std::uint16_t frameCount;
		const sf::Texture* texture;
	public:

		/// <summary>
		/// Default constructor for a frame animation.
		/// </summary>
		FrameAnimation()
			: size(0, 0), texture(nullptr), frameCount(0)
		{

		}

		/// <summary>
		/// Get the size of the whole frame animation.
		/// </summary>
		/// <returns>Size of the frame in pixels</returns>
		const sf::Vector2u& getSize() const { return size; }

		/// <summary>
		/// Get the texture that is applied to each object in
		/// this animation. Returns nullptr if none is set.
		/// </summary>
		/// <returns>Pointer to a texture</returns>
		const sf::Texture* getTexture() const { return texture; }

		/// <summary>
		/// Set a texture for each object. This texture is
		/// used by each object in this frame.
		/// </summary>
		/// <param name="texture">Pointer to a texture</param>
		const std::uint16_t& getFrameCount() const { return frameCount; }

		/// <summary>
		/// Set a texture for each object in the animation.
		/// This texture is used by each object in all frames.
		/// </summary>
		/// <param name="texture">Pointer to a texture</param>
		void setTexture(const sf::Texture* texture)
		{
			if (texture != this->texture && texture)
			{
				for (std::int32_t j = 0; j < arr.size(); j++)
					for (std::int32_t i = 0; i < arr[j].getVertexCount() / 6; i++)
					{
						arr[j][i * 6 + 0].texCoords = sf::Vector2f(0, 0);
						arr[j][i * 6 + 1].texCoords = sf::Vector2f(texture->getSize().x, 0);
						arr[j][i * 6 + 2].texCoords = sf::Vector2f(texture->getSize().x, texture->getSize().y);
						arr[j][i * 6 + 3].texCoords = sf::Vector2f(0, texture->getSize().y);

						arr[j][i * 6 + 4].texCoords = arr[j][i * 6 + 0].texCoords;
						arr[j][i * 6 + 5].texCoords = arr[j][i * 6 + 2].texCoords;
					}
			}
			this->texture = texture;
		}

		/// <summary>
		/// This function loads the animation data from disk.
		/// For all data each frame has see Frame::loadFromFile().
		/// </summary>
		/// <param name="fileName">File to load the animation from</param>
		/// <returns>True on success, false on fail</returns>
		bool loadFromFile(const std::filesystem::path& fileName)
		{
			sf::FileInputStream file;
			if (file.open(fileName.string()))
				return loadFromStream(file);
			return false;
		}

		/// <summary>
		/// Loads multiple frames from a stream. This function is
		/// used by loadFromFile().
		/// </summary>
		/// <param name="stream">Stream to load frames from</param>
		/// <returns>True on success, false on fail</returns>
		bool loadFromStream(sf::InputStream& stream)
		{
			std::uint32_t settings = 0;
			std::uint16_t conRot = 0;
			std::uint16_t conSize = 0;
			std::uint16_t sizeX;
			std::uint16_t sizeY;
			std::uint16_t count;
			std::string ver = "123456";
			stream.read(&ver[0], 6);
			if (ver != SQV_VERSION)
				return false;
			stream.read(&sizeX, sizeof(sizeX));
			stream.read(&sizeY, sizeof(sizeY));
			stream.read(&count, sizeof(count));
			size.x = sizeX;
			size.y = sizeY;

			arr.resize(count);
			if (sf::VertexBuffer::isAvailable())
				buff.resize(count);
			frameCount = count;

			sf::Vector2i position;
			std::uint16_t rotation = 0;
			std::uint16_t scale = 0;
			sf::Color color;
			sf::Transformable tran;

			for (std::int32_t i = 0; i < count; i++)
			{
				std::uint32_t objectCount = 0;
				stream.read(&objectCount, sizeof(objectCount));
				arr[i].setPrimitiveType(sf::PrimitiveType::Triangles);
				arr[i].resize(objectCount * 6);
				stream.read(&settings, sizeof(settings));
				if (settings & Frame::ConstRotation)
					stream.read(&conRot, sizeof(conRot));
				if (settings & Frame::ConstSize)
					stream.read(&conSize, sizeof(conSize));

				for (std::int32_t j = 0; j < objectCount; j++)
				{
					Frame::readObject(stream, position, rotation, scale, settings, conRot, conSize);

					stream.read(&color.r, sizeof(color.r));
					stream.read(&color.g, sizeof(color.g));
					stream.read(&color.b, sizeof(color.b));

					tran.setPosition(static_cast<sf::Vector2f>(position));
					tran.setRotation(sf::degrees(360 - rotation / 512.f * 360.f));
					tran.setScale(sf::Vector2f(scale, scale));

					arr[i][j * 6 + 0].position = tran.getTransform().transformPoint(sf::Vector2f(-0.5, -0.5));
					arr[i][j * 6 + 0].color = color;
					arr[i][j * 6 + 1].position = tran.getTransform().transformPoint(sf::Vector2f(0.5, -0.5));
					arr[i][j * 6 + 1].color = color;
					arr[i][j * 6 + 2].position = tran.getTransform().transformPoint(sf::Vector2f(0.5, 0.5));
					arr[i][j * 6 + 2].color = color;
					arr[i][j * 6 + 3].position = tran.getTransform().transformPoint(sf::Vector2f(-0.5, 0.5));
					arr[i][j * 6 + 3].color = color;

					if (texture)
					{
						arr[i][j * 6 + 0].texCoords = sf::Vector2f(0, 0);
						arr[i][j * 6 + 1].texCoords = sf::Vector2f(texture->getSize().x, 0);
						arr[i][j * 6 + 2].texCoords = sf::Vector2f(texture->getSize().x, texture->getSize().y);
						arr[i][j * 6 + 3].texCoords = sf::Vector2f(0, texture->getSize().y);
					}

					arr[i][j * 6 + 4] = arr[i][j * 6 + 0];
					arr[i][j * 6 + 5] = arr[i][j * 6 + 2];
				}
				if (sf::VertexBuffer::isAvailable() && arr[i].getVertexCount() > 0)
				{
					buff[i].setUsage(sf::VertexBuffer::Usage::Static);
					buff[i].setPrimitiveType(arr[i].getPrimitiveType());
					buff[i].create(arr[i].getVertexCount());
					buff[i].update(&arr[i][0]);
				}
			}
			return true;
		}
	};
	class FrameAnimator : public sf::Drawable, public sf::Transformable
	{
	public:
		enum Status
		{
			Stopped,
			Paused,
			Playing,
		};
	private:
		Status status;
		const FrameAnimation* anim;
		std::uint16_t atFrame;
		std::uint8_t fps;
		sf::Time offset;
		bool loop;
	public:

		/// <summary>
		/// Default constructor for a frame animator.
		/// </summary>
		FrameAnimator()
			: anim(nullptr), atFrame(0), fps(10), offset(sf::Time::Zero), loop(false), status(Stopped)
		{

		}

		/// <summary>
		/// Get the animation that is being played.
		/// Returns nullptr if none is set.
		/// </summary>
		/// <returns>Pointer to a frame animation</returns>
		const FrameAnimation* getAnimation() const { return anim; }

		/// <summary>
		/// Get the animation state.
		/// </summary>
		/// <returns>Status of the animation</returns>
		const Status& getStatus() const { return status; }

		/// <summary>
		/// Get the animation frame index of a frame that is
		/// currently being rendered to the screen.
		/// </summary>
		/// <returns>Index of the frame</returns>
		const std::uint16_t& getPlayingFrame() const { return atFrame; }

		/// <summary>
		/// Get elapsed time since the animation started playing.
		/// </summary>
		/// <returns>Time since start</returns>
		const sf::Time& getPlayingOffset() const { return offset; }

		/// <summary>
		/// Get the number of frames being shown to the screen every second.
		/// </summary>
		/// <returns>Number of frames in a second</returns>
		const std::uint8_t& getFramesPerSecond() const { return fps; }

		/// <summary>
		/// Get the state of looping.
		/// </summary>
		/// <returns>True if loop is on, false otherwise</returns>
		const bool& getLoop() const { return loop; }

		/// <summary>
		/// Set looping state. If looping is on, the animation
		/// will reset when it passes the last frame.
		/// </summary>
		/// <param name="state">True to play in a loop, false to play once</param>
		void setLoop(bool state)
		{
			loop = state;
		}

		/// <summary>
		/// Start playing the animation. If the animation is playing,
		/// update should be called every frame to change which frame
		/// is being displayed.
		/// </summary>
		void play()
		{
			if (status == Stopped)
			{
				atFrame = 0;
				offset = sf::Time::Zero;
			}
			status = Playing;
		}

		/// <summary>
		/// Pause the animation without resetting the playing frame.
		/// The animation can be resumed with play().
		/// </summary>
		void pause()
		{
			status = Paused;
		}

		/// <summary>
		/// Stops the animation and resets the playing frame to 0.
		/// </summary>
		void stop()
		{
			atFrame = 0;
			offset = sf::Time::Zero;
			status = Stopped;
		}

		/// <summary>
		/// Set the number of frames to be played every second.
		/// </summary>
		/// <param name="fps">Number of frames to play per second</param>
		void setFramesPerSecond(std::uint8_t fps)
		{
			this->fps = fps;
		}

		/// <summary>
		/// Set the animation that should be played. This should
		/// be called before trying to play an animation.
		/// </summary>
		/// <param name="animation">A read only pointer to the animation</param>
		void setAnimation(const FrameAnimation* animation)
		{
			this->anim = animation;
		}

		/// <summary>
		/// Updates the animation. Changes the playing frame or
		/// stops the animation if necessary.
		/// </summary>
		/// <param name="deltaTime">Time since last update</param>
		void update(const sf::Time& deltaTime)
		{
			if (status != Playing)
				return;
			if (!anim)
				return;
			offset += deltaTime;
			atFrame = offset.asSeconds() * fps;
			if (atFrame >= anim->getFrameCount())
			{
				if (!loop)
					status = Stopped;
				else
				{
					atFrame = 0;
					offset = sf::Time::Zero;
				}
			}
		}
	private:
		virtual void draw(sf::RenderTarget& target, sf::RenderStates states) const
		{
			if (!anim)
				return;
			if (atFrame >= anim->getFrameCount())
				return;
			states.transform *= getTransform();
			if (anim->texture)
				states.texture = anim->texture;
			if (sf::VertexBuffer::isAvailable())
				target.draw(anim->buff[atFrame], states);
			else
				target.draw(anim->arr[atFrame], states);
		}
	};
}