#include "game.h"
#include "renderer.h"
#include "DGLE2_helpers.h"
#include <gl\gl.h>
#include <gl\glu.h>

#define CAPTION "Tetris"
#ifdef _DEBUG
#define RESOURCE_PATH "..\\..\\release\\resources\\"
#else
#define RESOURCE_PATH "resources\\"
#endif
#define SCREEN_WIDTH 1254
#define SCREEN_HEIGHT 941

using namespace DGLE2;
using namespace std;

template<typename F>
inline CButton<F>::CButton(int x0, int y0, int x1, int y1, const F &callback):
	_x0(x0), _y0(y0), _x1(x1), _y1(y1), _callback(callback), _pressed(false)
	{
	}

template<typename F>
inline bool CButton<F>::Inside(int x, int y) const
{
	return x >= _x0 && x < _x1 && y >= _y0 && y < _y1;
}

template<typename F>
void CButton<F>::Process(const TMouseState &mouse_state)
{
	switch (mouse_state.buttonState)
	{
	case TMouseState::BS_DOWN:
		if (Inside(mouse_state.x, mouse_state.y))
			_pressed = true;
		break;
	case TMouseState::BS_UP:
		_pressed = false;
		if (Inside(mouse_state.x, mouse_state.y))
			_callback();
		break;
	}
}

CGame::CGame(IEngineCore &engineCore):
	_tetris(_countof(_cars)),
	_engineCore(engineCore),
	_rightDuration(0), _leftDuration(0),
	_cwDuration(0), _ccwDuration(0),
	_mousePressed(false), _enableBoost(true),
	_newGameButton(45, 201, 279, 264, CreateCallback(bind2nd(mem_fun_ref(&CGame::SetState), S_NAME_PROMPT), *this)),
	_recordsButton(45, 310, 279, 374, CreateCallback(bind2nd(mem_fun_ref(&CGame::SetState), S_RECORDS), *this)),
	_creditsButton(45, 417, 279, 479, CreateCallback(bind2nd(mem_fun_ref(&CGame::SetState), S_CREDITS), *this)),
	_exitButton(45, 524, 279, 586, CreateConstCallback(mem_fun_ref(&CGame::Exit), *this)),
	_recordsMainMenuButton(974, 814, 1207, 876, CreateCallback(bind2nd(mem_fun_ref(&CGame::SetState), S_MAIN_MENU), *this)),
	_creditsMainMenuButton(964, 830, 1197, 892, CreateCallback(bind2nd(mem_fun_ref(&CGame::SetState), S_MAIN_MENU), *this))
{
	HRESULT hr;
	if (FAILED(hr = _engineCore.InitializeEngine(NULL, CAPTION, TEngWindow(SCREEN_WIDTH, SCREEN_HEIGHT, false))))
		throw CException("Couldn't initialize engine", hr);
	if (FAILED(hr = _engineCore.AddProcedure(EPT_INIT, &Callback<&CGame::Init>, this)))
		throw CException("Couldn't register Init procedure", hr);
	if (FAILED(hr = _engineCore.AddProcedure(EPT_FREE, &Callback<&CGame::Free>, this)))
		throw CException("Couldn't register Free procedure", hr);
	if (FAILED(hr = _engineCore.AddProcedure(EPT_RENDER, &Callback<&CGame::MainMenuRender>, this)))
		throw CException("Couldn't register Render procedure", hr);
	if (FAILED(hr = _engineCore.AddProcedure(EPT_PROCESS, &Callback<&CGame::MainMenuProcess>, this)))
		throw CException("Couldn't register Process procedure", hr);
	get_helpers::Get(&_engineCore, _input);
	get_helpers::Get(&_engineCore, _resMan);
	get_helpers::Get(&_engineCore, _render);
	get_helpers::Get(_resMan, _font);
	PARANOIC_CHECK_HR(_font->SetScale(2));
	PARANOIC_CHECK_HR(_render->GetRender2D(_render2D));
	PARANOIC_CHECK_HR(_render->SetClearColor(TColor4(0, 255)));
}

void CGame::operator ()()
{
	srand(GetTickCount());
	HRESULT hr = _engineCore.StartEngine();
	if (FAILED(hr))
		throw CException("Couldn't start engine", hr);
}

TMouseState CGame::MouseState() const
{
	TMouseStates mouse_state;
	PARANOIC_CHECK_HR(_input->GetMouseStates(mouse_state));
	TMouseState::E_BUTTON_STATE button_state = !_mousePressed && mouse_state.bLeftButton ? TMouseState::BS_DOWN : _mousePressed && !mouse_state.bLeftButton ? TMouseState::BS_UP : TMouseState::BS_NONE;
	_mousePressed = mouse_state.bLeftButton;
	return TMouseState(button_state, mouse_state.iX, mouse_state.iY);
}

namespace
{
	template<class IResource>
	void Load(IResourceManager *resMan, const char *fileName, IResource *&resource, uint flags = RES_LOAD_DEFAULT)
	{
		PARANOIC_CHECK_HR(resMan->Load(fileName, reinterpret_cast<IEngBaseObj *&>(resource), flags));
		resource = static_cast<IResource *>(reinterpret_cast<IEngBaseObj *>(resource));
	}
}

inline void CGame::Init()
{
	Load(_resMan, RESOURCE_PATH"меню.jpg", _mainMenuImage);
	Load(_resMan, RESOURCE_PATH"введите имя.jpg", _namePrompt);
	Load(_resMan, RESOURCE_PATH"1.bmp", _cars[0]);
	Load(_resMan, RESOURCE_PATH"2.bmp", _cars[1]);
	Load(_resMan, RESOURCE_PATH"3.bmp", _cars[2]);
	Load(_resMan, RESOURCE_PATH"4.bmp", _cars[3]);
	Load(_resMan, RESOURCE_PATH"5.bmp", _cars[4]);
	Load(_resMan, RESOURCE_PATH"6.bmp", _cars[5]);
	Load(_resMan, RESOURCE_PATH"7.bmp", _cars[6]);
	Load(_resMan, RESOURCE_PATH"Уровень 1.jpg", _backgrounds[0]);
	Load(_resMan, RESOURCE_PATH"Уровень 2.jpg", _backgrounds[1]);
	Load(_resMan, RESOURCE_PATH"уровень 3.jpg", _backgrounds[2]);
	Load(_resMan, RESOURCE_PATH"Уровень_4.jpg", _backgrounds[3]);
	Load(_resMan, RESOURCE_PATH"Рекорды_3.jpg", _recordsImage);
	Load(_resMan, RESOURCE_PATH"Авторы_2.jpg", _credits);
	Load(_resMan, RESOURCE_PATH"Мимика_80_125\\1.jpg", _driverImages[0]);
	Load(_resMan, RESOURCE_PATH"Мимика_80_125\\2.jpg", _driverImages[1]);
	Load(_resMan, RESOURCE_PATH"Мимика_80_125\\3.jpg", _driverImages[2]);
	Load(_resMan, RESOURCE_PATH"Мимика_80_125\\4.jpg", _driverImages[3]);
	Load(_resMan, RESOURCE_PATH"Мимика_80_125\\5.jpg", _driverImages[4]);
	Load(_resMan, RESOURCE_PATH"Мимика_80_125\\6.jpg", _driverImages[5]);
	Load(_resMan, RESOURCE_PATH"Мимика_80_125\\7.jpg", _driverImages[6]);
	Load(_resMan, RESOURCE_PATH"Мимика_80_125\\8.jpg", _driverImages[7]);
	Load(_resMan, RESOURCE_PATH"Мимика_80_125\\9.jpg", _driverImages[8]);
	Load(_resMan, RESOURCE_PATH"Мимика_80_125\\10.jpg", _driverImages[9]);
	Load(_resMan, RESOURCE_PATH"Мимика_80_125\\11.jpg", _driverImages[10]);
	Load(_resMan, RESOURCE_PATH"Мимика_80_125\\12.jpg", _driverImages[11]);
	Load(_resMan, RESOURCE_PATH"Мимика_80_125\\13.jpg", _driverImages[12]);
	Load(_resMan, RESOURCE_PATH"Мимика_80_125\\14.jpg", _driverImages[13]);
	Load(_resMan, RESOURCE_PATH"Мимика_80_125\\15.jpg", _driverImages[14]);
	Load(_resMan, RESOURCE_PATH"Мимика_80_125\\16.jpg", _driverImages[15]);
	Load(_resMan, RESOURCE_PATH"Реплики\\1.jpg", _messages[0]);
	Load(_resMan, RESOURCE_PATH"Реплики\\2.jpg", _messages[1]);
	Load(_resMan, RESOURCE_PATH"Реплики\\3.jpg", _messages[2]);
	Load(_resMan, RESOURCE_PATH"Реплики\\4.jpg", _messages[3]);
	Load(_resMan, RESOURCE_PATH"Реплики\\5.jpg", _messages[4]);
	Load(_resMan, RESOURCE_PATH"Реплики\\6.jpg", _messages[5]);
	Load(_resMan, RESOURCE_PATH"Реплики\\7.jpg", _messages[6]);
	Load(_resMan, RESOURCE_PATH"Реплики\\8.jpg", _messages[7]);
	Load(_resMan, RESOURCE_PATH"music.mp3", _music);
	PARANOIC_CHECK_HR(_music->Play());
}

inline void CGame::Free()
{
	PARANOIC_CHECK_HR(_music->Stop());
	struct 
	{
		void operator ()(IEngBaseObj *object) const
		{
			PARANOIC_CHECK_HR(object->Free());
		}
	} cleaner;
	cleaner(_mainMenuImage);
	cleaner(_namePrompt);
	for_each(_cars, _cars + _countof(_cars), cleaner);
	for_each(_backgrounds, _backgrounds + _countof(_backgrounds), cleaner);
	cleaner(_recordsImage);
	cleaner(_credits);
	for_each(_driverImages, _driverImages + _countof(_driverImages), cleaner);
	for_each(_messages, _messages + _countof(_messages), cleaner);
	cleaner(_music);
}

inline void CGame::MainMenuRender()
{
	PARANOIC_CHECK_HR(_render2D->Begin2D());
	PARANOIC_CHECK_HR(_mainMenuImage->Draw2DSimple(0, 0));
	PARANOIC_CHECK_HR(_render2D->End2D());
}

inline void CGame::MainMenuProcess()
{
	// handle mouse
	TMouseState mouse_state = MouseState();
	_newGameButton.Process(mouse_state);
	_recordsButton.Process(mouse_state);
	_creditsButton.Process(mouse_state);
	_exitButton.Process(mouse_state);
}

inline void CGame::NamePromptRender()
{
	MainMenuRender();
	PARANOIC_CHECK_HR(_render2D->Begin2D());
	PARANOIC_CHECK_HR(_namePrompt->Draw2DSimple(300, 400));
	if (_name[0])
		PARANOIC_CHECK_HR(_font->Draw2D(440, 500, _name, TColor4(0, 255)));
	PARANOIC_CHECK_HR(_render2D->End2D());
}

inline void CGame::NamePromptProcess()
{
	bool enter_pressed;
	PARANOIC_CHECK_HR(_input->GetKey(KEY_RETURN, enter_pressed));
	if (enter_pressed && _name[0])
	{
		PARANOIC_CHECK_HR(_input->EndTextInput());
		SetState(S_TETRIS);
	}
}

template<bool rotate>
void CRenderer<rotate>::operator ()(unsigned int row, unsigned int column, unsigned char style) const
{
	//PARANOIC_CHECK_HR(_render2D.DrawRect(DGLE2::TRectF(column * 30, row * 30, 30, 30)));
	PARANOIC_CHECK_HR(_cars[style]->Draw2D(column * 30, row * 30, 30, 30, rotate ? 90 : 0));
}

template class CRenderer<false>;
template class CRenderer<true>;

namespace
{
	static void SetViewport(int x, int y, unsigned int width, unsigned int height)
	{
		glViewport(x, y, width, height);
		glLoadIdentity();
		gluOrtho2D(0, width, height, 0);
	}
}

inline void CGame::TetrisRender()
{
	PARANOIC_CHECK_HR(_render2D->Begin2D());
	PARANOIC_CHECK_HR(_backgrounds[_curLevel]->Draw2DSimple(0, 0));
	glMatrixMode(GL_PROJECTION);

#pragma region("tetris")
	glPushMatrix();
	SetViewport(297, 940 - 887, 10 * 30, 20 * 30);

	_tetris.Traverse(CRenderer<true>(_cars));
	_tetris.GetCurFigure().Traverse<true>(CRenderer<false>(_cars));

	glPopMatrix();
#pragma endregion

#pragma region("next")
	glPushMatrix();
	SetViewport(694, 940 - 410, 4 * 30, 4 * 30);

	_tetris.GetNextFigure().Traverse<false>(CRenderer<false>(_cars));

	glPopMatrix();
#pragma endregion

#pragma region("time")
	glPushMatrix();
	SetViewport(716, 940 - 609, 78, 39);

	DWORD time_elapsed = (GetTickCount() - _startTickCount) / 1000;
	PARANOIC_CHECK_HR(_font->Draw2D(0, 0, (IntToStr(time_elapsed / 60) + ':' + IntToStr(time_elapsed % 60)).c_str()));

	glPopMatrix();
#pragma endregion

#pragma region("score")
	glPushMatrix();
	SetViewport(713, 940 - 811, 78, 38);

	// there is not unsigned int IntToStr() imlementation
	PARANOIC_CHECK_HR(_font->Draw2D(0, 0, IntToStr<unsigned long>(_tetris.Score()).c_str()));

	glPopMatrix();
#pragma endregion

	glMatrixMode(GL_MODELVIEW);
	glViewport(0, 0, SCREEN_WIDTH, SCREEN_HEIGHT);

#pragma region("driver")
	PARANOIC_CHECK_HR(_driverImages[_curDriverImage]->Draw2DSimple(967, 372));
#pragma endregion

#pragma region("message")
	PARANOIC_CHECK_HR(_messages[_curMessage]->Draw2DSimple(862, 724));
#pragma endregion

	PARANOIC_CHECK_HR(_render2D->End2D());
}

inline void CGame::TetrisProcess()
{
	// change level if necessary
	DWORD cur_tick_count = GetTickCount();
	if (cur_tick_count - _startTickCount >= _levelDuration && _curLevel < _countof(_backgrounds) - 1)
	{
		_curLevel++;
		unsigned int cur_score = _tetris.Score();
		_tetris.~CTetris();
		new (&_tetris) CTetris(_countof(_cars), cur_score);
		_startTickCount = cur_tick_count;
	}
	if (cur_tick_count - _driverTickCount >= _driverDuration)
	{
		++_curDriverImage %= _countof(_driverImages);
		++_curMessage %= _countof(_messages);
		_driverTickCount = cur_tick_count;
	}

	// handle keyboard
	bool right_pressed, left_pressed;

	PARANOIC_CHECK_HR(_input->GetKey(KEY_RIGHT, right_pressed));
	PARANOIC_CHECK_HR(_input->GetKey(KEY_LEFT, left_pressed));
	if (right_pressed && !left_pressed)
	{
		if ((_rightDuration %= _shiftDelay) == 0)
			_tetris.MoveRight();
		_rightDuration++;
	}
	else
		_rightDuration = 0;
	if (left_pressed && !right_pressed)
	{
		if ((_leftDuration %= _shiftDelay) == 0)
			_tetris.MoveLeft();
		_leftDuration++;
	}
	else
		_leftDuration = 0;

	PARANOIC_CHECK_HR(_input->GetKey(KEY_X, right_pressed));
	PARANOIC_CHECK_HR(_input->GetKey(KEY_Z, left_pressed));
	if (right_pressed && !left_pressed)
	{
		if ((_cwDuration %= _rotateDelay) == 0)
			_tetris.Rotate(true);
		_cwDuration++;
	}
	else
		_cwDuration = 0;
	if (left_pressed && !right_pressed)
	{
		if ((_ccwDuration %= _rotateDelay) == 0)
			_tetris.Rotate(false);
		_ccwDuration++;
	}
	else
		_ccwDuration = 0;

	bool boost_pressed;
	PARANOIC_CHECK_HR(_input->GetKey(KEY_DOWN, boost_pressed));
	if (!boost_pressed)
		_enableBoost = true;

	switch (_tetris.Process(boost_pressed && _enableBoost))
	{
	case CTetris::E_OVERFLOW:
		{
			E_STATE new_state = S_MAIN_MENU;
			TRecords::iterator cur_user_record = _records.find(_name);
			if (cur_user_record != _records.end()) // old user
			{
				if (_tetris.Score() > cur_user_record->second)
				{
					cur_user_record->second = _tetris.Score();
					new_state = S_RECORDS;
				}
			}
			else // new user
			{
				// new user should have greater score in order to replace old user
				bool insert = false;
				if (_records.size() < _maxRecords)
					insert = true;
				else
				{
					TRecords::const_iterator min_record = min_element(_records.begin(), _records.end());
					if (_tetris.Score() > min_record->second)
					{
						_records.erase(min_record);
						insert = true;
					}
				}
				if (insert)
				{
					_records.insert(TRecords::value_type(_name, _tetris.Score()));
					new_state = S_RECORDS;
				}
			}
			_tetris.~CTetris();
			new (&_tetris) CTetris(_countof(_cars));
			SetState(new_state);
		}
		break;
	case CTetris::E_NEW_FIGURE:
		_enableBoost = !boost_pressed;
		break;
	}
}

inline void CGame::RecordsRender()
{
	PARANOIC_CHECK_HR(_render2D->Begin2D());
	PARANOIC_CHECK_HR(_recordsImage->Draw2DSimple(0, 0));
	typedef vector<TRecords::const_pointer> TSortedRecords;
	TSortedRecords sorted_records(_records.size());

	struct
	{
		TRecords::const_pointer operator ()(TRecords::const_reference record) const
		{
			return &record;
		}
	} addr;
	transform(_records.begin(), _records.end(), sorted_records.begin(), addr);

	struct
	{
		bool operator ()(TSortedRecords::const_reference left, TSortedRecords::const_reference right) const
		{
			return left->second > right->second;
		}
	} records_orderer;
	sort(sorted_records.begin(), sorted_records.end(), records_orderer);

	class CRecordsRenderer
	{
	public:
		CRecordsRenderer(IBitmapFont &font, int xName, int xScore, int y): _font(font), _xName(xName), _xScore(xScore), _y(y) {}
		void operator ()(TSortedRecords::const_reference record) const
		{
			// there is not unsigned int IntToStr() imlementation
			string score_str = IntToStr<unsigned long>(record->second);
			_font.Draw2D(_xName, _y, record->first.c_str(), TColor4(0, 255));
			_font.Draw2D(_xScore, _y, score_str.c_str(), TColor4(0, 255));
			uint width, name_height, score_height;
			PARANOIC_CHECK_HR(_font.GetTextDimensions(record->first.c_str(), width, name_height));
			PARANOIC_CHECK_HR(_font.GetTextDimensions(score_str.c_str(), width, score_height));
			_y += max(name_height, score_height);
		}
	private:
		IBitmapFont &_font;
		const int _xName, _xScore;
		mutable int _y;
	};
	for_each(sorted_records.begin(), sorted_records.end(), CRecordsRenderer(*_font, 250, 700, 350));

	PARANOIC_CHECK_HR(_render2D->End2D());
}

inline void CGame::RecordsProcess()
{
	_recordsMainMenuButton.Process(MouseState());
}

inline void CGame::CreditsRender()
{
	PARANOIC_CHECK_HR(_render2D->Begin2D());
	PARANOIC_CHECK_HR(_credits->Draw2DSimple(0, 0));
	PARANOIC_CHECK_HR(_render2D->End2D());
}

inline void CGame::CreditsProcess()
{
	_creditsMainMenuButton.Process(MouseState());
}

void CGame::SetState(E_STATE state)
{
	// remove current callbacks
	PARANOIC_CHECK_HR(_engineCore.RemoveProcedure(EPT_RENDER, Callback<&CGame::MainMenuRender>, this));
	PARANOIC_CHECK_HR(_engineCore.RemoveProcedure(EPT_PROCESS, Callback<&CGame::MainMenuProcess>, this));
	PARANOIC_CHECK_HR(_engineCore.RemoveProcedure(EPT_RENDER, Callback<&CGame::TetrisRender>, this));
	PARANOIC_CHECK_HR(_engineCore.RemoveProcedure(EPT_PROCESS, Callback<&CGame::TetrisProcess>, this));
	PARANOIC_CHECK_HR(_engineCore.RemoveProcedure(EPT_RENDER, Callback<&CGame::RecordsRender>, this));
	PARANOIC_CHECK_HR(_engineCore.RemoveProcedure(EPT_PROCESS, Callback<&CGame::RecordsProcess>, this));
	PARANOIC_CHECK_HR(_engineCore.RemoveProcedure(EPT_RENDER, Callback<&CGame::CreditsRender>, this));
	PARANOIC_CHECK_HR(_engineCore.RemoveProcedure(EPT_PROCESS, Callback<&CGame::CreditsProcess>, this));

	// set new callbacks
	void (CALLBACK *render_callback)(void *), (CALLBACK *process_callback)(void *);
	switch (state)
	{
	case S_MAIN_MENU:
		render_callback = Callback<&CGame::MainMenuRender>;
		process_callback = Callback<&CGame::MainMenuProcess>;
		break;
	case S_NAME_PROMPT:
		render_callback = Callback<&CGame::NamePromptRender>;
		process_callback = Callback<&CGame::NamePromptProcess>;
		PARANOIC_CHECK_HR(_input->BeginTextInput(_name, sizeof _name));
		break;
	case S_TETRIS:
		render_callback = Callback<&CGame::TetrisRender>;
		process_callback = Callback<&CGame::TetrisProcess>;
		_curLevel = _curDriverImage = _curMessage = 0;
		_driverTickCount = _startTickCount = GetTickCount();
		break;
	case S_RECORDS:
		render_callback = Callback<&CGame::RecordsRender>;
		process_callback = Callback<&CGame::RecordsProcess>;
		break;
	case S_CREDITS:
		render_callback = Callback<&CGame::CreditsRender>;
		process_callback = Callback<&CGame::CreditsProcess>;
		break;
	}
	PARANOIC_CHECK_HR(_engineCore.AddProcedure(EPT_RENDER, render_callback, this));
	PARANOIC_CHECK_HR(_engineCore.AddProcedure(EPT_PROCESS, process_callback, this));
}

void CGame::Exit() const
{
	PARANOIC_CHECK_HR(_engineCore.QuitEngine());
}

template<void (CGame::*callback)()>
void CALLBACK CGame::Callback(void *game)
{
	(reinterpret_cast<CGame *>(game)->*callback)();
}