/*
	Copyright 2016, 2017 Michele "King_DuckZ" Santullo

	This file is part of MyCurry.

	MyCurry 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.

	MyCurry 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 MyCurry.  If not, see <http://www.gnu.org/licenses/>.
*/

#include "gamescenebase.hpp"
#include "inputbag.hpp"
#include "sdlmain.hpp"
#include "rect_to_sdl.hpp"
#include "rect.hpp"
#include "texture.hpp"
#include <cassert>
#include <SDL2/SDL.h>
#include <algorithm>
#include <ciso646>
#if !defined(NDEBUG)
#	include "compatibility.h"
#	include <cmath>
#endif

namespace curry {
	namespace {
#if !defined(NDEBUG)
		bool are_equal_rel (float parA, float parB, float parEpsilon) a_pure;

		//see: http://stackoverflow.com/questions/4548004/how-to-correctly-and-standardly-compare-floats
		bool are_equal_rel (float parA, float parB, float parEpsilon) {
			return (std::fabs(parA - parB) <= parEpsilon * std::max(std::fabs(parA), std::fabs(parB)));
		}
#endif

		///---------------------------------------------------------------------
		///---------------------------------------------------------------------
		bool DoEvents (cloonel::InputBag& parInput, cloonel::SDLMain* parSdlMain) {
			using cloonel::InputDevice_Keyboard;

			SDL_Event eve;
			while (SDL_PollEvent(&eve)) {
				switch (eve.type) {
				case SDL_KEYDOWN:
					//eve.key.keysym.sym
					parInput.NotifyKeyAction(InputDevice_Keyboard, eve.key.keysym.scancode, true);
					break;

				case SDL_KEYUP:
					parInput.NotifyKeyAction(InputDevice_Keyboard, eve.key.keysym.scancode, false);
					break;

				case SDL_QUIT:
					return true;

				case SDL_WINDOWEVENT:
					if (SDL_WINDOWEVENT_RESIZED == eve.window.event) {
						parSdlMain->SetResolution(vec2us(static_cast<uint16_t>(eve.window.data1), static_cast<uint16_t>(eve.window.data2)));
					}
					else if (SDL_WINDOWEVENT_CLOSE == eve.window.event) {
						return true;
					}
					break;
				}
			}
			return false;
		}

		///----------------------------------------------------------------------
		///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 clip_rect (Rect<float>& parSrc, Rect<float>& parDest, const Rect<float>& parClip) {
			typedef Rect<float> RectFloat;
			assert(are_equal_rel(parSrc.width(), parDest.width(), 0.00001f));
			assert(parSrc.is_valid());
			assert(parDest.is_valid());

			//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;

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

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

			assert(parDest.from <= dst.from);
			assert(parDest.to >= dst.to);

			RectFloat src;
			{
				const vec2f srcWidthHeight(parSrc.width_height());
				const vec2f scaledOffs((dst.from - parDest.from) / parDest.width_height());
				src.from = parSrc.from + scaledOffs * srcWidthHeight;
				const vec2f scaledCrop((parDest.to - dst.to) / parDest.width_height());
				src.to = parSrc.to - scaledCrop * srcWidthHeight;
				assert(src.is_valid());

				assert(dst.is_valid());
				assert(dst.from >= parClip.from);
				assert(dst.to <= parClip.to);
				assert(are_equal_rel(src.width(), dst.width(), 0.00001f));
			}
			parDest = dst;
			parSrc = src;
			return true;
		}
	} //unnamed namespace

	GameSceneBase::GameSceneBase (cloonel::SDLMain* parSdlMain) :
		m_time0(std::chrono::steady_clock::now()),
		m_input(std::make_unique<cloonel::InputBag>()),
		m_sdlmain(parSdlMain),
		m_screen_width(static_cast<float>(m_sdlmain->WidthHeight().x())),
		m_screen_height(static_cast<float>(m_sdlmain->WidthHeight().y())),
		m_wants_to_quit(false)
	{
		assert(m_sdlmain);
	}

	GameSceneBase::~GameSceneBase() noexcept = default;

	void GameSceneBase::prepare() {
		m_wants_to_quit = false;
		this->on_prepare();

		m_time0 = std::chrono::steady_clock::now();
	}

	void GameSceneBase::destroy() noexcept {
		set_wants_to_quit();
		this->on_destroy();
	}

	bool GameSceneBase::wants_to_quit() const {
		return m_wants_to_quit;
	}

	void GameSceneBase::set_wants_to_quit() {
		m_wants_to_quit = true;
	}

	void GameSceneBase::exec() {
		const auto time1 = std::chrono::steady_clock::now();

		const bool quit = DoEvents(*m_input, m_sdlmain);
		m_input->KeyStateUpdateFinished();
		if (quit)
			set_wants_to_quit();

		const float delta = std::chrono::duration<float>(time1 - m_time0).count();
		m_time0 = time1;
		this->on_update(delta);
		SDL_RenderPresent(this->sdl_main()->GetRenderer());
	}

	cloonel::SDLMain* GameSceneBase::sdl_main() {
		return m_sdlmain;
	}

	cloonel::InputBag& GameSceneBase::input_bag() {
		return *m_input;
	}

	void GameSceneBase::draw_clipped (Texture& parTexture, Rect<float> parSrc, Rect<float> parDest) {
		const vec2f lefttop(0.0f);
		const vec2f rightbottom(m_screen_width, m_screen_height);
		clip_rect(parSrc, parDest, Rect<float>(lefttop, rightbottom));

		auto src = make_sdlrect(parSrc);
		auto dst = make_sdlrect(parDest);
		assert(src.w == dst.w);
		assert(src.h == dst.h);
		SDL_RenderCopy(m_sdlmain->GetRenderer(), parTexture.texture(), &src, &dst);
	}
} //namespace curry
