/*
	Copyright 2014 Michele "King_DuckZ" Santullo

	This file is part of CloonelJump.

	CloonelJump is free software: you can redistribute it and/or modify
	it under the terms of the GNU General Public License as published by
	the Free Software Foundation, either version 3 of the License, or
	(at your option) any later version.

	CloonelJump is distributed in the hope that it will be useful,
	but WITHOUT ANY WARRANTY; without even the implied warranty of
	MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
	GNU General Public License for more details.

	You should have received a copy of the GNU General Public License
	along with CloonelJump.  If not, see <http://www.gnu.org/licenses/>.
*/

#include "texture.hpp"
#include "sdlerror.hpp"
#include "sdlmain.hpp"
#include "physicsfswrapper.hpp"
#include <SDL2/SDL.h>
#include <stdexcept>
#include <cassert>
#include <ciso646>
#include <png.h>
#include <endian.h>
#include <vector>

#define lengthof(a) (static_cast<int32_t>(sizeof(a) / sizeof(a[0])))

namespace cloonel {
	namespace {
		enum GraphicFormat {
			GraphicFormat_Unknown,
			GraphicFormat_Png
		};

		enum PixelFormat {
			PixelFormat_RGB_24,
			PixelFormat_RGBA_32
		};

		enum ColorChannelMask {
#if __BYTE_ORDER == __BIG_ENDIAN
			ColorChannelMask_Red = 0xff000000,
			ColorChannelMask_Green = 0xff0000,
			ColorChannelMask_Blue = 0xff00,
			ColorChannelMask_Alpha = 0xff
#elif __BYTE_ORDER == __LITTLE_ENDIAN
			ColorChannelMask_Red = 0xff,
			ColorChannelMask_Green = 0xff00,
			ColorChannelMask_Blue = 0xff0000,
			ColorChannelMask_Alpha = 0xff000000
#else
#	error "Unknonwn endianness"
#endif
		};

		struct GraphicFormatItem {
			const char* extension;
			size_t length;
			GraphicFormat enumvalue;
		};

		struct RectFloat {
			float2 from;
			float2 to;

			RectFloat ( void ) = default;
			RectFloat ( const RectFloat& parOther ) :
				from(parOther.from),
				to(parOther.to)
			{
				assert(IsValid());
			}

			RectFloat ( const SDL_Rect& parRect ) :
				from(static_cast<float>(parRect.x), static_cast<float>(parRect.y)),
				to(static_cast<float>(parRect.x + parRect.w), static_cast<float>(parRect.y + parRect.h))
			{
				assert(IsValid());
			}

			RectFloat ( const float2& parFrom, const float2& parTo ) :
				from(parFrom),
				to(parTo)
			{
				assert(IsValid());
			}

			~RectFloat ( void ) noexcept = default;

			operator SDL_Rect ( void ) const noexcept { return { static_cast<int>(from.x()), static_cast<int>(from.y()), static_cast<int>(Width()), static_cast<int>(Height()) }; }

			float Width ( void ) const noexcept { return to.x() - from.x(); }
			float Height ( void ) const noexcept { return to.y() - from.y(); }
			float2 WidthHeight ( void ) const noexcept { return float2(Width(), Height()); }
			bool IsValid ( void ) const noexcept { return from <= to; }
		};

		const GraphicFormatItem g_graphicFormatItems[] = {
			{".png", 4, GraphicFormat_Png}
		};

		GraphicFormat GuessGraphicFormatFromName (const std::string& parPath) __attribute__((pure));
		bool ClipRect ( RectFloat& parSrc, RectFloat& parDest, const RectFloat& parClip );
#if !defined(NDEBUG)
		bool IsRectCompletelyInsideRect ( const RectFloat& parInner, const RectFloat& parOuter ) __attribute__((pure));
#endif

		///----------------------------------------------------------------------
		///----------------------------------------------------------------------
		SDL_Surface* SurfaceFromPng (PixelFormat parPixelFormat, ushort2 parSize, const png_structp& parPngPtr, const png_infop& parInfoPtr, std::vector<uint8_t>& parBuff) {
			const png_size_t stride = png_get_rowbytes(parPngPtr, parInfoPtr);
			assert(stride > 0);

			parBuff.resize(stride * parSize.y());
			uint8_t* const imagePtr = parBuff.data();

			for (uint16_t y = 0; y < parSize.y(); ++y) {
				png_read_row(parPngPtr, imagePtr + stride * y, nullptr);
			}

			int bpp;
			uint32_t redMask, greenMask, blueMask, alphaMask;
			switch (parPixelFormat) {
			case PixelFormat_RGB_24:
				bpp = 24;
				redMask = ColorChannelMask_Red;
				greenMask = ColorChannelMask_Green;
				blueMask = ColorChannelMask_Blue;
				alphaMask = 0;
				break;
			case PixelFormat_RGBA_32:
				bpp = 32;
				redMask = ColorChannelMask_Red;
				greenMask = ColorChannelMask_Green;
				blueMask = ColorChannelMask_Blue;
				alphaMask = ColorChannelMask_Alpha;
				break;
			default:
				assert(false);
			}

			SDL_Surface* const retSurf = SDL_CreateRGBSurfaceFrom(
				imagePtr,
				parSize.x(),
				parSize.y(),
				bpp,
				static_cast<int>(stride),
				redMask,
				greenMask,
				blueMask,
				alphaMask
			);
			return retSurf;
		}

		///----------------------------------------------------------------------
		///----------------------------------------------------------------------
		GraphicFormat GuessGraphicFormatFromName (const std::string& parPath) {
			const size_t dotPos = parPath.find_last_of('.');
			if (parPath.npos == dotPos) {
				return GraphicFormat_Unknown;
			}

			const size_t extLen = parPath.size() - dotPos;
			for (size_t z = 0; z < sizeof(g_graphicFormatItems) / sizeof(g_graphicFormatItems[0]); ++z) {
				const GraphicFormatItem& currItem = g_graphicFormatItems[z];
				if (currItem.length == extLen and parPath.compare(dotPos, currItem.length, currItem.extension) == 0) {
					return currItem.enumvalue;
				}
			}
			return GraphicFormat_Unknown;
		}

		///----------------------------------------------------------------------
		///----------------------------------------------------------------------
		void ReadDataFromInputStream (png_structp parPngPtr, png_bytep parOutBytes, png_size_t parByteCountToRead) {
			if (not png_get_io_ptr(parPngPtr))
				return;
			PhysicsFSFile& rawfile = *static_cast<PhysicsFSFile*>(png_get_io_ptr(parPngPtr));

			const int64_t read = rawfile.Read(static_cast<void*>(parOutBytes), static_cast<uint32_t>(parByteCountToRead), 1);
			if (read != static_cast<int64_t>(parByteCountToRead))
				return;

			return;
		}

		///----------------------------------------------------------------------
		///http://blog.hammerian.net/2009/reading-png-images-from-memory/
		///----------------------------------------------------------------------
		SDL_Surface* LoadNewPngSurface (const std::string& parPath, std::vector<uint8_t>& parBuff) {
			PhysicsFSFile rawfile(parPath.c_str(), PhysicsFSFile::OpenMode_Read, "graphics");
			unsigned char header[8];
			assert(rawfile.IsOpen());

			//Read the signature from the input stream
			rawfile.Read(header, lengthof(header), 1);
			if (png_sig_cmp(header, 0, lengthof(header)))
				return nullptr;

			//Get the file info struct
			png_structp pngptr = png_create_read_struct(PNG_LIBPNG_VER_STRING, nullptr, nullptr, nullptr);
			if (not pngptr)
				return nullptr;

			//Get the data info struct
			png_infop infoptr = png_create_info_struct(pngptr);
			if (not infoptr) {
				png_destroy_read_struct(&pngptr, nullptr, nullptr);
				return nullptr;
			}

			//Set the function that will be called to read data from the stream
			png_set_read_fn(pngptr, &rawfile, ReadDataFromInputStream);
			//Tell the lib we already verified the signature
			png_set_sig_bytes(pngptr, lengthof(header));

			//Read the header
			png_uint_32 width, height;
			int bitDepth, colorType;
			png_read_info(pngptr, infoptr);
			const png_uint_32 headerGetInfoRetVal = png_get_IHDR(pngptr, infoptr, &width, &height, &bitDepth, &colorType, nullptr, nullptr, nullptr);
			if (static_cast<png_uint_32>(-1) == headerGetInfoRetVal) {
				png_destroy_read_struct(&pngptr, &infoptr, nullptr);
				return nullptr;
			}

			SDL_Surface* retSurf;
			switch (colorType) {
			case PNG_COLOR_TYPE_RGB:
				assert(3 * bitDepth == 24);
				retSurf = SurfaceFromPng(PixelFormat_RGB_24, ushort2(static_cast<uint8_t>(width), static_cast<uint8_t>(height)), pngptr, infoptr, parBuff);
				break;

			case PNG_COLOR_TYPE_RGB_ALPHA:
				assert(bitDepth * 4 == 32);
				retSurf = SurfaceFromPng(PixelFormat_RGBA_32, ushort2(static_cast<uint8_t>(width), static_cast<uint8_t>(height)), pngptr, infoptr, parBuff);
				break;

			default:
				png_destroy_read_struct(&pngptr, &infoptr, nullptr);
				retSurf = nullptr;
			}

			return retSurf;
		}

		///----------------------------------------------------------------------
		///Adjustes parSrc and parDest so that parDest will fit into parClip and
		///parSrc to be the relevant part of the source texture to be drawn.
		///Returns true if such operation was possible and parSrc and parDest
		///got set to a valid value, otherwise false.
		///----------------------------------------------------------------------
		bool ClipRect (RectFloat& parSrc, RectFloat& parDest, const RectFloat& parClip) {
			assert(parSrc.IsValid());
			assert(parDest.IsValid());

			//If the dest rect is completely out of the clipping region, there
			//is nothing to do at all.
			if (parDest.to <= parClip.from or parDest.from >= parClip.to)
				return false;

			{
				const RectFloat clip(float2(0.0f), float2(1.0f));
				const float2 srcWidthHeight(parSrc.WidthHeight());
				const float2 scaledOffs((parDest.from - parClip.from) / parClip.WidthHeight());
				parSrc.from -= float2(std::min(0.0f, scaledOffs.x() * srcWidthHeight.x()), std::min(0.0f, scaledOffs.y() * srcWidthHeight.y()));
				const float2 scaledCrop((parClip.to - parDest.to) / parClip.WidthHeight());
				parSrc.to += float2(std::min(0.0f, scaledCrop.x() * srcWidthHeight.x()), std::min(0.0f, scaledCrop.y() * srcWidthHeight.y()));
				assert(parSrc.IsValid());
			}

			RectFloat dst(parDest.from - parClip.from, parDest.to - parClip.from);
			dst.from.x() = std::max(dst.from.x(), parClip.from.x());
			dst.from.y() = std::max(dst.from.y(), parClip.from.y());
			dst.to.x() = std::min(dst.to.x(), parClip.to.x());
			dst.to.y() = std::min(dst.to.y(), parClip.to.y());

			if (not dst.IsValid())
				return false;

			parDest.from += parClip.from;
			parDest.to += parClip.from;

			assert(parDest.IsValid());
			return true;
		}

#if !defined(NDEBUG)
		///----------------------------------------------------------------------
		///----------------------------------------------------------------------
		bool IsRectCompletelyInsideRect (const RectFloat& parInner, const RectFloat& parOuter) {
			return parInner.from >= parOuter.from and parInner.to <= parOuter.to;
		}
#endif
	} //unnamed namespace

	///--------------------------------------------------------------------------
	///--------------------------------------------------------------------------
	Texture::Texture (const std::string& parPath, SDLMain* parMain, bool parLoadNow) :
		m_path(parPath),
		m_texture(nullptr),
		m_sdlmain(parMain)
	{
		if (parLoadNow)
			Reload();
	}

	///--------------------------------------------------------------------------
	///--------------------------------------------------------------------------
	Texture::~Texture() noexcept {
		Destroy();
	}

	///--------------------------------------------------------------------------
	///--------------------------------------------------------------------------
	void Texture::Destroy() noexcept {
		if (m_texture) {
			SDL_DestroyTexture(m_texture);
			m_texture = nullptr;
		}
	}

	///--------------------------------------------------------------------------
	///--------------------------------------------------------------------------
	void Texture::Reload() {
		const GraphicFormat fmt = GuessGraphicFormatFromName(m_path);
		Destroy();

		SDL_Surface* surf = nullptr;
		std::vector<uint8_t> memBuff;
		switch (fmt) {
		case GraphicFormat_Png:
			surf = LoadNewPngSurface(m_path.c_str(), memBuff);
			break;

		default:
			throw std::runtime_error(std::string("Unsupported file format for \"") + m_path + "\"");
		}

		if (nullptr == surf)
			throw std::runtime_error(GetFullErrorMessage(__PRETTY_FUNCTION__, m_path));

		m_texture = SDL_CreateTextureFromSurface(m_sdlmain->GetRenderer(), surf);
		SDL_FreeSurface(surf);
		if (m_texture) {
			int2 wh;
			SDL_QueryTexture(m_texture, nullptr, nullptr, &wh.x(), &wh.y());
			m_size = static_cast<float2>(wh);
		}
	}

	///--------------------------------------------------------------------------
	///--------------------------------------------------------------------------
	void Texture::Render (const float2& parPos, const float2& parSize, const float2& parScaling, bool parClip) const {
		assert(IsLoaded());

		const float2 pos(parPos * parScaling);
		const float2 siz(parSize * parScaling);
		RectFloat dest(pos, pos + siz);
		RectFloat src(float2(0.0f), m_size);

		if (parClip) {
			const RectFloat clip(float2(0.0f), static_cast<float2>(m_sdlmain->WidthHeight()));
			const bool visible = ClipRect(src, dest, clip);
			if (not visible)
				return;
		}
#if !defined(NDEBUG)
		else {
			const RectFloat clip(float2(0.0f), static_cast<float2>(m_sdlmain->WidthHeight()));
			assert(IsRectCompletelyInsideRect(dest, clip));
		}
#endif

		const SDL_Rect sdlsrc(src);
		SDL_Rect sdldst(dest);
		sdldst.y = m_sdlmain->WidthHeight().y() - sdldst.y - sdldst.h;

		//By here everything is nice and clean
		assert(sdlsrc.x >= 0 and sdlsrc.y >= 0);
		assert(sdlsrc.w >= 0 and sdlsrc.w <= static_cast<int>(m_size.x()));
		assert(sdlsrc.h >= 0 and sdlsrc.h <= static_cast<int>(m_size.y()));
		assert(sdlsrc.x + sdlsrc.w <= m_sdlmain->WidthHeight().x());
		assert(sdlsrc.y + sdlsrc.h <= m_sdlmain->WidthHeight().y());
		SDL_RenderCopy(m_sdlmain->GetRenderer(), m_texture, &sdlsrc, &sdldst);
	}
} //namespace cloonel
