/////////////////////////////////////////////////////////////////////////////
// File:        mod_sdl.cpp
// Author:      Cesar Mauri Loba (cesar at crea-si dot com)
// Copyright:   (C) 2010 Cesar Mauri Loba - CREA Software Systems
//
//  This program 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.
//
//  This program 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 this program.  If not, see <http://www.gnu.org/licenses/>.
/////////////////////////////////////////////////////////////////////////////
#include "spcore/basictypes.h"
#include "spcore/pin.h"
#include "spcore/component.h"
#include "spcore/libimpexp.h"
#include "mod_sdl/sdlsurfacetype.h"

#ifdef WIN32
#include "windows.h"
#endif

using namespace spcore;
using namespace std;

namespace mod_sdl {

/*
	sdl_config component (singleton)

	Components that provides global SDL Video initialization and
	termination and allows to store the configuration parameters.

	Input pins:
		width (int, rw)
		heiht (int, rw)
		fullscreen (int, rw)

	Output pins:

	Command line:
*/
class SDLConfig : public CComponentAdapter
{
public:
	static const char* getTypeName() { return "sdl_config"; }
	virtual const char* GetTypeName() const { return SDLConfig::getTypeName(); }
	
	SDLConfig(const char * name, int argc, const char * argv[]) 
	: CComponentAdapter(name, argc, argv) 
	, m_fullscreen(false)
	, m_existsDrawer(false)
	, m_width(640)
	, m_height(480)	
	{
		assert (getSpCoreRuntime()->IsMainThread());
		if (!getSpCoreRuntime()->IsMainThread())
			throw std::runtime_error("attempting to initialize sdl_config from other thread than the main one");
		
		RegisterInputPin (*SmartPtr<IInputPin>(new InputPinWidth(*this), false));
		RegisterInputPin (*SmartPtr<IInputPin>(new InputPinHeight(*this), false));
		RegisterInputPin (*SmartPtr<IInputPin>(new InputPinFullscreen(*this), false));

		if (SDL_Init(SDL_INIT_NOPARACHUTE)== -1) 
			throw std::runtime_error(SDL_GetError());

#ifdef WIN32
		/* Sam:
		   We still need to pass in the application handle so that
		   DirectInput will initialize properly when SDL_RegisterApp()
		   is called later in the video initialization.
		 */
		HMODULE handle= GetModuleHandle(NULL);
		SDL_SetModuleHandle(handle);
#endif
	}

	//
	// Convenience methods for drawers
	//

	// Methods to make sure there is only one drawer
	bool ExistsDrawer(bool exists) { 
		bool retval= m_existsDrawer;
		m_existsDrawer= exists;
		return retval;
	}

	int GetWidth() const { return m_width; }
	int GetHeight() const { return m_height; }
	
	// Copy not allowed
	SDLConfig(const SDLConfig &);
	SDLConfig operator= (const SDLConfig &);

private:
	virtual ~SDLConfig() {
		SDL_Quit();
	}

	bool m_fullscreen;
	bool m_existsDrawer;
	int m_width;
	int m_height;
	

	class InputPinWidth : public CInputPinReadWrite<CTypeInt, SDLConfig> {
	public:
		InputPinWidth (SDLConfig & component) : CInputPinReadWrite<CTypeInt, SDLConfig>("width", component) {}
		virtual int DoSend(const CTypeInt & message) {
			if (message.getValue()> 0) {
				m_component->m_width= message.getValue();
				return 0;
			}
			return -1;
		}
		virtual SmartPtr<CTypeInt> DoRead() const {
			SmartPtr<CTypeInt> retval= CTypeInt::CreateInstance();
			retval->setValue(m_component->m_width);
			return retval;
		}
	};

	class InputPinHeight : public CInputPinReadWrite<CTypeInt, SDLConfig> {
	public:
		InputPinHeight (SDLConfig & component) : CInputPinReadWrite<CTypeInt, SDLConfig>("height", component) {}
		virtual int DoSend(const CTypeInt & message) {
			if (message.getValue()> 0) {
				m_component->m_height= message.getValue();
				return 0;
			}
			return -1;
		}
		virtual SmartPtr<CTypeInt> DoRead() const {
			SmartPtr<CTypeInt> retval= CTypeInt::CreateInstance();
			retval->setValue(m_component->m_height);
			return retval;
		}
	};

	class InputPinFullscreen : public CInputPinReadWrite<CTypeBool, SDLConfig> {
	public:
		InputPinFullscreen (SDLConfig & component) : CInputPinReadWrite<CTypeBool, SDLConfig>("fullscreen", component) {}
		virtual int DoSend(const CTypeBool & message) {
			m_component->m_fullscreen= message.getValue();
			return 0;
		}
		virtual SmartPtr<CTypeBool> DoRead() const {
			SmartPtr<CTypeBool> retval =CTypeBool::CreateInstance();
			retval->setValue(m_component->m_fullscreen);
			return retval;
		}
	};
};

typedef SingletonComponentFactory<SDLConfig> SDLConfigFactory;

/**
	sdl_drawer component

	Input pins:
		- draw (any)

		Used to trigger the drawing routine. It draws and but NOT releases all queued 
		surfaces since last "do_draw". If the object passed to this pin is an 
		"sdl_surface" it is also drawn BEFORE the queued ones (as background).
	
		- queue (sdl_surface)

		adds a surface to the queue

	Output pins:
	Command line:
	Comments:
		
*/
class SDLDrawer : public CComponentAdapter {
public:
	static const char* getTypeName() { return "sdl_drawer"; };
	virtual const char* GetTypeName() const { return SDLDrawer::getTypeName(); };

	SDLDrawer(const char * name, int argc, const char * argv[])
	: CComponentAdapter(name, argc, argv) 
	, m_frameBuffer(NULL)	
	{
		assert (getSpCoreRuntime()->IsMainThread());
		if (!getSpCoreRuntime()->IsMainThread())
			throw std::runtime_error("attempting to initialize sdl_drawer from other thread than the main one");

		// Store a pointer to the (single) sdl config component
		SmartPtr<IComponent> configComponent= getSpCoreRuntime()->CreateComponent("sdl_config", "", 0, NULL);
		assert (configComponent.get());
		m_sdlConfig= SmartPtr<SDLConfig>(static_cast<SDLConfig*> (configComponent.get()));
		assert (m_sdlConfig.get());

		// and check that this is the only one drawer present within the system	
		if (m_sdlConfig->ExistsDrawer(true)) 
			throw std::runtime_error(string("Cannot create drawer ") + name + ". Another drawer exists.");
		
		// now comes SDL Video stuff
		if (SDL_WasInit(SDL_INIT_VIDEO)) {
			// Another component is not playing nice and decided to init SDL
			// by its own or indirectly by calling some SDL function.

			//
			// NOTE to SDL based components implementors. Remember that you component
			// should NEVER initialize the SDL library nor use any SDL function BEFORE
			// the method Initialize gets called and AFTER the method Finish gets called.
			// You MUST implement both methods. Moreover, your Finish() method MUST 
			// free all SDL related resources otherwise a crash is almost guaranteed. Finally 
			// remember to be aware of calls before Initialize is called.
			//			
			assert (false);	// Read above

			throw std::runtime_error("SDL library was previously initialized/used");
		}

		// Finaly set video mode and open SDL window
		m_frameBuffer= SDL_SetVideoMode(m_sdlConfig->GetWidth(), m_sdlConfig->GetHeight(), 0, DRAWER_SDL_FLAGS);
		if (!m_frameBuffer) {
			assert (false);
			m_sdlConfig->ExistsDrawer(false);
			throw std::runtime_error("SDL_SetVideoMode failed!");
		}
		
		RegisterInputPin (*SmartPtr<InputPinDoDraw>(new InputPinDoDraw("draw", *this), false));
		RegisterInputPin (*SmartPtr<InputPinQueue>(new InputPinQueue("queue", *this), false));
	}

private:
	enum { DRAWER_SDL_FLAGS= SDL_HWSURFACE | SDL_DOUBLEBUF | SDL_RESIZABLE };

	SDL_Surface* m_frameBuffer;
	vector<SmartPtr<const CTypeSDLSurface> > m_surfaceQueue;
	SmartPtr<SDLConfig> m_sdlConfig;

	~SDLDrawer() {
		SDL_QuitSubSystem(SDL_INIT_VIDEO);
		m_sdlConfig->ExistsDrawer(false);
		assert (!IsInitialized());
		if (IsInitialized())
			getSpCoreRuntime()->LogMessage(ICoreRuntime::LOG_WARNING,
				"destroyed uninitialized", SDLDrawer::getTypeName());
	}

	bool SanityCheck() {
		if (!getSpCoreRuntime()->IsMainThread()) {
			// Messages should come only from the main thread
			assert (false);
			getSpCoreRuntime()->LogMessage(ICoreRuntime::LOG_ERROR, 
				"received message from other thread other than the main one", SDLDrawer::getTypeName());
			return false;
		}
		if (!IsInitialized()) {
			// Message received before initialitzation completed
			// This could happen if the component which calls us has been initialized
			// before than us. This message could have been generated as part of the initialization
			// proces but not as a result of runing the entire graph, so we issue a warning and
			// ignore the message
			getSpCoreRuntime()->LogMessage(ICoreRuntime::LOG_WARNING,
				"ignored message because component is not initialized", SDLDrawer::getTypeName());
			return false;
		}
		return true;
	}

	int Queue (const CTypeSDLSurface& message) {
		if (!SanityCheck()) return -1;
		m_surfaceQueue.push_back (SmartPtr<const CTypeSDLSurface>(&message));
		return 0;
	}

	inline
	void DrawSurface (const CTypeSDLSurface& s) {
		SDL_Rect position;
		
		position.x= s.getX();
		position.y= s.getY();
		// Cast away constantness (needed to call C API, hope surface isn't modified)
		// TODO: currently return value is not checked
		SDL_BlitSurface( const_cast<SDL_Surface*>(s.getSurface()), NULL, m_frameBuffer, &position);
	}

	int DrawBuffers (const CTypeAny & message) {
		if (!SanityCheck()) return -1;

		// Time to draw everything

		// Clear background
		if (SDL_MUSTLOCK(m_frameBuffer))
			SDL_LockSurface(m_frameBuffer);
		SDL_FillRect(m_frameBuffer, NULL, 0);

		// IF draw message contains a surface use it as background
		if (message.GetTypeID()== CTypeSDLSurface::getTypeID()) 		
			DrawSurface (*sptype_static_cast<CTypeSDLSurface>(&message));
		
		// Finaly blit each queued surface
		vector<SmartPtr<const CTypeSDLSurface> >::iterator it= m_surfaceQueue.begin();
		for (;it!= m_surfaceQueue.end(); ++it) DrawSurface (**it);		

		// Flip buffers
		SDL_Flip(m_frameBuffer);
		if (SDL_MUSTLOCK(m_frameBuffer))
			SDL_UnlockSurface(m_frameBuffer);

		// Clear queue
		m_surfaceQueue.clear();

		// Process SDL events
		SDL_Event event; 
		while(SDL_PollEvent(&event)) {  
			// Loop until there are no events left on the queue 
			switch(event.type)
			{ 
			case SDL_VIDEORESIZE:  
				m_frameBuffer= SDL_SetVideoMode(event.resize.w, event.resize.h, 0, DRAWER_SDL_FLAGS);
				assert (m_frameBuffer);
				break;
			// Ignore SDL window close
			//case SDL_QUIT:
			//	return false;
			}
		}
		return 0;
	}

	class InputPinDoDraw : public CInputPinWriteOnly<CTypeAny, SDLDrawer> {
	public:
		InputPinDoDraw (const char * name, SDLDrawer & component) : CInputPinWriteOnly<CTypeAny, SDLDrawer>(name, component) {}
		virtual int DoSend(const CTypeAny & message) {
			return m_component->DrawBuffers(message);
		}
	};

	class InputPinQueue : public CInputPinWriteOnly<CTypeSDLSurface, SDLDrawer> {
	public:
		InputPinQueue (const char * name, SDLDrawer & component) : CInputPinWriteOnly<CTypeSDLSurface, SDLDrawer>(name, component) {}
		virtual int DoSend(const CTypeSDLSurface & message) {			
			return m_component->Queue (message);
		}
	};
};

typedef ComponentFactory<SDLDrawer> SDLDrawerFactory;

/*
	Type CTypeSDLSurface factory class
*/
typedef SimpleTypeFactory<CTypeSDLSurface> CTypeSDLSurfaceFactory;


/**
	mod_sdl module
*/
class SDLBaseModule : public spcore::CModuleAdapter {
public:
	SDLBaseModule() {
		// Types
		RegisterTypeFactory(SmartPtr<spcore::ITypeFactory>(new CTypeSDLSurfaceFactory(), false));
		// Components
		RegisterComponentFactory(SmartPtr<spcore::IComponentFactory>(new SDLDrawerFactory(), false));
		RegisterComponentFactory(SmartPtr<spcore::IComponentFactory>(new SDLConfigFactory(), false));
	}
	virtual const char * GetName() const { return "mod_sdl"; }
};

static spcore::IModule* g_module= NULL;
SPEXPORT_FUNCTION spcore::IModule* module_create_instance()
{
	if (g_module== NULL) g_module= new SDLBaseModule();
	return g_module;
}

}

