/*
	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 "sdlmain.hpp"
#include "observersmanager.hpp"
#include "sizenotifiable.hpp"
#include "sizeratio.hpp"
#include <SDL2/SDL.h>
#include <stdexcept>
#include <sstream>
#include <ciso646>
#include <cstring>
#include <utility>

#if defined(RASPBERRY_PI)
#include <bcm_host.h>
#endif

namespace cloonel {
	namespace {
		///----------------------------------------------------------------------
		///----------------------------------------------------------------------
		std::pair<int, std::string> GetRenderingDriver() {
			typedef std::pair<int, std::string> RetPairType;

			const int count = SDL_GetNumRenderDrivers();
			int opengles = -1;
			int opengles2 = -1;
			int opengl = -1;
			SDL_RendererInfo info;
			for (int z = 0; z < count; ++z) {
				const int ret = SDL_GetRenderDriverInfo(z, &info);
				if (0 == ret) {
					if (std::strcmp("opengles", info.name) == 0)
						opengles = z;
					else if (std::strcmp("opengles2", info.name) == 0)
						opengles2 = z;
					else if (std::strcmp("opengl", info.name) == 0)
						opengl = z;
				}
			}
#if !defined(FORCE_OPENGLES)
			if (opengl > -1)
				return RetPairType(opengl, "opengl");
#endif
			if (opengles2 > -1)
				return RetPairType(opengles2, "opengles2");
			if (opengles > -1)
				return RetPairType(opengles, "opengles");
#if defined(FORCE_OPENGLES)
			if (opengl > -1)
				return RetPairType(opengl, "opengl");
#endif

			return RetPairType(-1, "default");
		}
	} //unnamed namespace

	struct SDLMain::LocalData {
		SDL_Window* window;
		SDL_Renderer* renderer;
		SizeRatio sizeratio;
		ObserversManager<SizeNotifiableBase*> resChangeNotifList;
		bool initialized;
#if defined(RASPBERRY_PI)
		bool bcmInitialized;
#endif
	};

	///------------------------------------------------------------------------
	///------------------------------------------------------------------------
	SDLMain::SDLMain (const char* parGameName, ushort2 parRes, ushort2 parReferenceRes) :
		m_gameName(parGameName),
		m_localData(new LocalData)
	{
		m_localData->sizeratio.SetOriginal(static_cast<float2>(parReferenceRes), static_cast<float2>(parRes));
		m_localData->initialized = false;
#if defined(RASPBERRY_PI)
		m_localData->bcmInitialized = false;
#endif
	}

	///------------------------------------------------------------------------
	///------------------------------------------------------------------------
	SDLMain::~SDLMain() noexcept {
		ClearIFN(*m_localData);
#if defined(RASPBERRY_PI)
		assert(not m_localData->bcmInitialized);
#endif
	}

	///------------------------------------------------------------------------
	///------------------------------------------------------------------------
	void SDLMain::Init() {
		if (not m_localData->initialized)
			InitSDL(*m_localData);
	}

	///------------------------------------------------------------------------
	///------------------------------------------------------------------------
	void SDLMain::InitSDL (LocalData& parInitSDL) {
#if defined(RASPBERRY_PI)
		assert(not parInitSDL.bcmInitialized);
		bcm_host_init();
		parInitSDL.bcmInitialized = true;
#endif

		parInitSDL.window = nullptr;
		parInitSDL.renderer = nullptr;
		parInitSDL.initialized = false;

		if (SDL_Init(SDL_INIT_EVERYTHING) == -1)
			throw std::runtime_error(SDL_GetError());
		parInitSDL.initialized = true;

#if defined(FORCE_OPENGLES)
		SDL_GL_SetAttribute(SDL_GL_CONTEXT_PROFILE_MASK, SDL_GL_CONTEXT_PROFILE_ES);
#endif

		const float2 wh(m_localData->sizeratio.Resolution());
		SDL_Window* const win = SDL_CreateWindow(m_gameName.c_str(), 100, 100, static_cast<int>(wh.x()), static_cast<int>(wh.y()), SDL_WINDOW_SHOWN | SDL_WINDOW_RESIZABLE);
		if (!win)
			throw std::runtime_error(SDL_GetError());
		parInitSDL.window = win;

		const auto rendererDriver = GetRenderingDriver();
		m_rendererName = rendererDriver.second;
		SDL_Renderer* const renderer = SDL_CreateRenderer(win, rendererDriver.first, SDL_RENDERER_ACCELERATED | SDL_RENDERER_PRESENTVSYNC);
		if (!renderer)
			throw std::runtime_error(SDL_GetError());
		parInitSDL.renderer = renderer;
	}

	///------------------------------------------------------------------------
	///------------------------------------------------------------------------
	void SDLMain::ClearIFN (LocalData& parInitSDL) noexcept {
		if (parInitSDL.renderer)
			SDL_DestroyRenderer(parInitSDL.renderer);
		if (parInitSDL.window)
			SDL_DestroyWindow(parInitSDL.window);
		if (parInitSDL.initialized) {
			parInitSDL.initialized = false;
			SDL_Quit();
		}
#if defined(RASPBERRY_PI)
		if (parInitSDL.bcmInitialized) {
			parInitSDL.bcmInitialized = false;
			bcm_host_deinit();
		}
#endif
	}

	///------------------------------------------------------------------------
	///------------------------------------------------------------------------
	SDL_Renderer* SDLMain::GetRenderer() {
		if (m_localData->initialized)
			return m_localData->renderer;
		else
			return nullptr;
	}

	///------------------------------------------------------------------------
	///------------------------------------------------------------------------
	void SDLMain::SetResolution (ushort2 parRes) {
		m_localData->sizeratio.UpdateResolution(static_cast<float2>(parRes));
		{
			SDL_Renderer* const renderer = GetRenderer();
			assert(renderer);
			const int retVal = SDL_RenderSetLogicalSize(renderer, parRes.x(), parRes.y());
			if (retVal) {
				std::ostringstream oss;
				oss << "Error setting logical size to renderer to " << parRes.x() << "x" << parRes.y() << ": " << SDL_GetError();
				throw std::runtime_error(oss.str());
			}

			const SDL_Rect area = { 0, 0, parRes.x(), parRes.y() };
			const int retValViewport = SDL_RenderSetViewport(renderer, &area);
			if (retValViewport) {
				std::ostringstream oss;
				oss << "Error setting viewport to renderer to " << parRes.x() << "x" << parRes.y() << ": " << SDL_GetError();
				throw std::runtime_error(oss.str());
			}
		}

		for (auto currNotifiable : m_localData->resChangeNotifList) {
			currNotifiable->NotifyResChanged(m_localData->sizeratio);
		}
	}

	///------------------------------------------------------------------------
	///------------------------------------------------------------------------
	size_t SDLMain::RegisterForResChange (SizeNotifiableBase* parNotif) {
		parNotif->NotifyResChanged(m_localData->sizeratio);
		return m_localData->resChangeNotifList.Add(parNotif);
	}

	///------------------------------------------------------------------------
	///------------------------------------------------------------------------
	void SDLMain::UnregisterForResChange (size_t parID) noexcept {
		m_localData->resChangeNotifList.Remove(parID);
	}

	///------------------------------------------------------------------------
	///------------------------------------------------------------------------
	void SDLMain::SwapRegisteredForResChange (size_t parID, SizeNotifiableBase* parNotif) {
		m_localData->resChangeNotifList.Update(parID, parNotif);
	}

	///------------------------------------------------------------------------
	///------------------------------------------------------------------------
	ushort2 SDLMain::WidthHeight() const noexcept {
		return static_cast<ushort2>(m_localData->sizeratio.Resolution());
	}

	///------------------------------------------------------------------------
	///------------------------------------------------------------------------
	std::string SDLMain::GetVideoDriverName() const {
		return std::string(SDL_GetCurrentVideoDriver());
	}
} //namespace cloonel
