/** Example 025 Xml Handling
 *
 *	Demonstrates loading and saving of configurations via XML
 *	@author Y.M. Bosman	<yoran.bosman@gmail.com>
 */

#include <irrlicht.h>

using namespace irr;
using namespace core;
using namespace scene;
using namespace video;
using namespace io;
using namespace gui;

#ifdef _IRR_WINDOWS_
#pragma comment(lib, "Irrlicht.lib")
#endif


/**
 *	SettingManager class
 *	This class loads and writes the settings
 *	and manages the options.
 *
 *
 *	The class makes  use of irrMap which is a an associative arrays using a red-black tree
 *	it allows easy mapping of a key to a value, along the way there is some information on how to use it
 *
 */

class SettingManager
{
public:

	/**
	 *	Construct setting managers and set default settings
	 */
	SettingManager(const stringw& settings_file): SettingsFile(settings_file), NullDevice(0)
	{
		// Irrlicht null device, we want to load settings before we actually created our device, therefore, nulldevice
		NullDevice	= irr::createDevice(irr::video::EDT_NULL);


		//DriverOptions is an irrlicht map,
		//we can insert values in the map in two ways by calling insert(key,value) or by using the [key] operator
		//the [] operator overrides values if they already exist
		DriverOptions.insert(L"Software",		EDT_SOFTWARE			);
		DriverOptions.insert(L"OpenGL",			EDT_OPENGL				);
		DriverOptions.insert(L"Direct3D9",		EDT_DIRECT3D9			);

		//some resolution options
		ResolutionOptions.insert(L"640x480",	dimension2du(640,480) );
		ResolutionOptions.insert(L"800x600",	dimension2du(800,600) );
		ResolutionOptions.insert(L"1024x768",	dimension2du(1024,768) );

		//our preferred defaults
		SettingMap.insert(L"driver",			L"Direct3D9");		//0 is software
		SettingMap.insert(L"resolution",		L"640x480");		//0 is 640x480
		SettingMap.insert(L"fullscreen",		L"0");				//0 is false

	}

	/**
	 *	Destructor, you could store settings automatically on exit of your application if you wanted to
	 *	in our case we simply drop the nulldevice
	 */
	~SettingManager()
	{
		if(NullDevice)
		{
			NullDevice->closeDevice();
			NullDevice->drop();
		}
	};

	/**
	 *	Load xml from disk, overwrite default settings
	 *	The xml we are trying to load has the following structure
	 *	settings nested in sections nested in the root node, like so
	 *
	 *	<?xml version="1.0"?>
	 *	<mygame>
	 *		<video>
	 *			<setting name="driver" value="Direct3D9" />
	 *			<setting name="fullscreen" value="0" />
	 *			<setting name="resolution" value="1024x768" />
	 *		</video>
	 *	</mygame>
	 */
	bool load()
	{
		//if not able to create device dont attempt to load
		if(!NullDevice)
			return false;

		irr::io::IXMLReader* xml = NullDevice->getFileSystem()->createXMLReader(SettingsFile);	//create xml reader
		if ( !xml )
			return false;

		const stringw settingTag(L"setting");	//we'll be looking for this tag in the xml
		stringw currentSection;					//keep track of our currentsection
		const stringw videoTag(L"video");		//constant for videotag

		//while there is more to read
		while(xml->read())
		{
			//check the node type
			switch(xml->getNodeType())
			{
				//we found a new element
				case irr::io::EXN_ELEMENT:
				{
					//we currently are in the empty or mygame section and find the video tag so we set our current section to video
					if(currentSection.empty() && videoTag.equals_ignore_case(xml->getNodeName()))
					{
						currentSection = videoTag;
					}
					//we are in the video section and we find a setting to parse
					else if (currentSection.equals_ignore_case(videoTag) && settingTag.equals_ignore_case(xml->getNodeName() ))
					{
						//read in the key
						stringw key =  xml->getAttributeValueSafe(L"name");
						//if there actually is a key to set
						if( !key.empty())
						{
							//set the setting in the map to the value,
							//the [] operator overrides values if they already exist or inserts a new key value
							//pair into the settings map if it was not defined yet
							SettingMap[ key ] = xml->getAttributeValueSafe(L"value");
						}
					}

					//..
					// You can add your own sections and tags to read in here
					//..

				}
				break;

				//we found the end of an element
				case irr::io::EXN_ELEMENT_END:
					//we were at the end of the video section so we reset our tag
					currentSection=L"";
				break;
			}
		}

		// don't forget to delete the xml reader
		xml->drop();

		return true;
	}

	/**
	 *	Save the xml to disk
	 *	We use the nulldevice
	 */
	bool save()
	{

		//if not able to create device don't attempt to save
		if(!NullDevice)
			return false;

		//create xml writer
		irr::io::IXMLWriter* xwriter = NullDevice->getFileSystem()->createXMLWriter( SettingsFile );
		if(!xwriter)
			return false;

		//write out the obligatory xml header. Each xml-file needs to have exactly one of those.
		xwriter->writeXMLHeader();

		//start element	mygame, you replace the label "mygame" with anything you want
		xwriter->writeElement(L"mygame");
		xwriter->writeLineBreak();					//new line

		//start section with video settings
		xwriter->writeElement(L"video");
		xwriter->writeLineBreak();					//new line

		// getIterator gets us a pointer to the first node of the settings map
		// every iteration we increase the iterator which gives us the next map node
		// until we reach the end we write settings one by one by using the nodes key and value functions
		map<stringw, stringw>::Iterator i = SettingMap.getIterator();
		for(; !i.atEnd(); i++)
		{
			//write element as <setting name="key" value="x" />
			//the second parameter indicates this is an empty element with no children, just attributes
			xwriter->writeElement(L"setting",true, L"name", i->getKey().c_str(), L"value",i->getValue().c_str() );
			xwriter->writeLineBreak();
		}
		xwriter->writeLineBreak();

		//close video section
		xwriter->writeClosingTag(L"video");
		xwriter->writeLineBreak();

		//..
		// You can add writing sound settings, savegame information etc
		//..

		//close mygame section
		xwriter->writeClosingTag(L"mygame");

		//delete xml writer
		xwriter->drop();

		return true;
	}

	/**
	 *	Set setting in our manager
	 */
	void setSetting(const stringw& name, const stringw& value)
	{
		SettingMap[name]=value;
	}

	/**
	 *	set setting overload to quickly assign integers to our setting map
	 */
	void setSetting(const stringw& name, s32 value)
	{
		SettingMap[name]=stringw(value);
	}

	/**
	 *	Get setting as string
	 *	@param	key	name of setting
	 *	@return	empty string if the settings is not found, else value of the setting
	 */
	stringw getSetting(const stringw& key) const
	{
		//the find function or irrmap returns a pointer to a map Node
		//if the key can be found, otherwise it returns null
		//the map node has the function getValue and getKey, as we already know the key, we return node->getValue()
		map<stringw, stringw>::Node* n = SettingMap.find(key);
		if (n)
			return n->getValue();
		else
			return L"";

	}

	/**
	 *	Get setting as bool
	 *	@param	key	name of setting
	 *  @return	false if the key cannot be found, else true if the setting  == 1
	 */
	bool getSettingAsBoolean(const stringw& key ) const
	{
		stringw s = getSetting(key);
		if(s.empty())
			return false;
		return s.equals_ignore_case(L"1");
	}

	/**
	 *	Get setting as integer NOTE: function is not used in example but provided for completeness
	 *	@param	key	name of setting
	 *  @return	0 if the key cannot be found, else the setting converted to an integer
	 */
	s32 getSettingAsInteger(const stringw& key ) const
	{
		//we implicitly cast to string instead of stringw because strtol10 does not accept wide strings
		stringc s = getSetting(key);
		if(s.empty())
			return 0;

		return strtol10(s.c_str());


	}

public:
	map<stringw,	s32>				DriverOptions;		//available options for driver config
	map<stringw,	dimension2du>		ResolutionOptions;	//available options for resolution config
private:

	SettingManager(const SettingManager& other);			// defined but not implemented
	SettingManager& operator=(const SettingManager& other); // defined but not implemented

	map<stringw, stringw>	SettingMap;						//current config

	stringw					SettingsFile;					// location of the xml, usually the
	irr::IrrlichtDevice*	NullDevice;

};

/**
 *	Application context for global variables
 */
struct SAppContext
{
	SAppContext()
		: Device(0),Gui(0), Driver(0), Settings(0), ShouldQuit(false),
		ButtonSave(0), ButtonExit(0),  ListboxDriver(0),
		ListboxResolution(0), CheckboxFullscreen(0)
	{
	}

	~SAppContext()
	{
		if(Settings)
			delete Settings;

		if(Device)
		{
			Device->closeDevice();
			Device->drop();
		}
	}

	IrrlichtDevice *	Device;
	IGUIEnvironment*	Gui;
	IVideoDriver*		Driver;
	SettingManager*		Settings;
	bool				ShouldQuit;

	//settings dialog
	IGUIButton*			ButtonSave;
	IGUIButton*			ButtonExit;
	IGUIListBox*		ListboxDriver;
	IGUIListBox*		ListboxResolution;
	IGUICheckBox*		CheckboxFullscreen;
};

/*
	A typical event receiver.
*/
class MyEventReceiver : public IEventReceiver
{
public:
	MyEventReceiver(SAppContext & a) : App(a) { }

	virtual bool OnEvent(const SEvent& event)
	{
		if (event.EventType == EET_GUI_EVENT )
		{
			switch ( event.GUIEvent.EventType )
			{
				//handle button click events
				case EGET_BUTTON_CLICKED:
				{
					//Our save button was called so we obtain the settings from our dialog and save them
					if ( event.GUIEvent.Caller == App.ButtonSave )
					{
						//if there is a selection write it
						if( App.ListboxDriver->getSelected() != -1)
							App.Settings->setSetting(L"driver",	App.ListboxDriver->getListItem(App.ListboxDriver->getSelected()));

						//if there is a selection write it
						if( App.ListboxResolution->getSelected() != -1)
							App.Settings->setSetting(L"resolution", App.ListboxResolution->getListItem(App.ListboxResolution->getSelected()));

						App.Settings->setSetting(L"fullscreen",	App.CheckboxFullscreen->isChecked());


						if(App.Settings->save())
						{
							App.Gui->addMessageBox(L"settings save",L"settings saved, please restart for settings to change effect","",true);
						}
					}
					// cancel/exit button clicked, tell the application to exit
					else if( event.GUIEvent.Caller == App.ButtonExit)
					{
						App.ShouldQuit = true;
					}
				}
				break;
			}
		}

		return false;
	}

private:
	SAppContext & App;
};


/**
 *	Function to create a video settings dialog
 *	This dialog shows the current settings from the configuration xml and allows them to be changed
 */
void createSettingsDialog(SAppContext& app)
{
	// first get rid of alpha in gui
	for (irr::s32 i=0; i<irr::gui::EGDC_COUNT ; ++i)
	{
		irr::video::SColor col = app.Gui->getSkin()->getColor((irr::gui::EGUI_DEFAULT_COLOR)i);
		col.setAlpha(255);
		app.Gui->getSkin()->setColor((irr::gui::EGUI_DEFAULT_COLOR)i, col);
	}

	//create video settings windows
	gui::IGUIWindow* windowSettings = app.Gui->addWindow(rect<s32>(10,10,400,400),true,L"Videosettings");
	app.Gui->addStaticText (L"Select your desired video settings", rect< s32 >(10,20, 200, 40), false, true, windowSettings);

	// add listbox for driver choice
	app.Gui->addStaticText (L"Driver", rect< s32 >(10,50, 200, 60), false, true, windowSettings);
	app.ListboxDriver = app.Gui->addListBox(rect<s32>(10,60,220,120), windowSettings, 1,true);

	//add all available options to the driver choice listbox
	map<stringw, s32>::Iterator i = app.Settings->DriverOptions.getIterator();
	for(; !i.atEnd(); i++)
		app.ListboxDriver->addItem(i->getKey().c_str());

	//set currently selected driver
	app.ListboxDriver->setSelected(app.Settings->getSetting("driver").c_str());

	// add listbox for resolution choice
	app.Gui->addStaticText (L"Resolution", rect< s32 >(10,130, 200, 140), false, true, windowSettings);
	app.ListboxResolution  = app.Gui->addListBox(rect<s32>(10,140,220,200), windowSettings, 1,true);

	//add all available options tothe resolution listbox
	map<stringw, dimension2du>::Iterator ri = app.Settings->ResolutionOptions.getIterator();
	for(; !ri.atEnd(); ri++)
		app.ListboxResolution->addItem(ri->getKey().c_str());

	//set currently selected resolution
	app.ListboxResolution->setSelected(app.Settings->getSetting("resolution").c_str());

	//add checkbox to toggle fullscreen, initially set to loaded setting
	app.CheckboxFullscreen = app.Gui->addCheckBox(app.Settings->getSettingAsBoolean("fullscreen"),
													rect<s32>(10,220,220,240),
													windowSettings, -1,
													L"Fullscreen");

	//last but not least add save button
	app.ButtonSave = app.Gui->addButton(rect<s32>(80,250,150,270), windowSettings, 2, L"Save video settings");

	//exit/cancel button
	app.ButtonExit = app.Gui->addButton(rect<s32>(160,250,240,270), windowSettings, 2, L"Cancel and exit");
}

int main()
{
	//create new application context
	SAppContext app;

	//create device creation parameters that can get overwritten by our settings file
	SIrrlichtCreationParameters param;
	param.DriverType = EDT_SOFTWARE;
	param.WindowSize.set(640,480);

	/**
	 *  Try to load config.
	 *	I leave it as an exercise of the reader to store the configuration in the local application data folder,
	 *	the only logical place to store config data for games. For all other operating systems I redirect to your manuals
	 */
	app.Settings = new SettingManager("../../media/settings.xml");
	if( !app.Settings->load() )
	{
		// ...
		// Here add your own exception handling, for now we continue because there are defaults set in SettingManager constructor
		// ...
	}
	else
	{
		//settings xml loaded from disk,

		//map driversetting to driver type and test if the setting is valid
		//the DriverOptions map contains string representations mapped to to irrlicht E_DRIVER_TYPE enum
		//e.g "direct3d9" will become 4
		//see DriverOptions in the settingmanager class for details
		map<stringw, s32>::Node* driver = app.Settings->DriverOptions.find( app.Settings->getSetting("driver") );

		if(driver)
		{
			if( irr::IrrlichtDevice::isDriverSupported( static_cast<E_DRIVER_TYPE>( driver->getValue() )))
			{
				// selected driver is supported, so we use it.
				param.DriverType = static_cast<E_DRIVER_TYPE>( driver->getValue());
			}
		}

		//map resolution setting to dimension in a similar way as demonstrated above
		map<stringw, dimension2du>::Node* res = app.Settings->ResolutionOptions.find( app.Settings->getSetting("resolution") );
		if(res)
		{
			param.WindowSize = res->getValue();
		}

		//get fullscreen setting from config
		param.Fullscreen = app.Settings->getSettingAsBoolean("fullscreen");
	}

	//create the irrlicht device using the settings
	app.Device = createDeviceEx(param);
	if (app.Device == 0)
	{
		// You can add your own exception handling on driver failure
		exit(0);
	}

	app.Device->setWindowCaption(L"Xmlhandling - Irrlicht engine tutorial");
	app.Driver	= app.Device->getVideoDriver();
	app.Gui		= app.Device->getGUIEnvironment();

	createSettingsDialog(app);

	//set event receiver so we can respond to gui events
	MyEventReceiver receiver(app);
	app.Device->setEventReceiver(&receiver);

	//enter main loop
	while(!app.ShouldQuit && app.Device->run())
	{
		if(app.Device->isWindowActive())
		{
			app.Driver->beginScene(true, true, SColor(0,200,200,200));
			app.Gui->drawAll();
			app.Driver->endScene();
		}
		app.Device->sleep(10);
	}

	//app destroys device in destructor

	return 0;
}
