/****************************************************************************

 Copyright (C) 2002-2014 Gilles Debunne. All rights reserved.

 This file is part of the QGLViewer library version 2.6.1.

 http://www.libqglviewer.com - contact@libqglviewer.com

 This file may be used under the terms of the GNU General Public License 
 versions 2.0 or 3.0 as published by the Free Software Foundation and
 appearing in the LICENSE file included in the packaging of this file.
 In addition, as a special exception, Gilles Debunne gives you certain 
 additional rights, described in the file GPL_EXCEPTION in this package.

 libQGLViewer uses dual licensing. Commercial/proprietary software must
 purchase a libQGLViewer Commercial License.

 This file is provided AS IS with NO WARRANTY OF ANY KIND, INCLUDING THE
 WARRANTY OF DESIGN, MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE.

*****************************************************************************/

#include "agoraViewer.h"
#include "board.h"

#include <fstream>
#include <qmessagebox.h>
#include <qfileinfo.h>
#include <qfiledialog.h>
#include <qstatusbar.h>
#include <qtextedit.h>
#include <qtimer.h>

#include <QGLViewer/manipulatedCameraFrame.h>

#include "ui_agoraWindow.h"
class AgoraWindow : public QMainWindow, public Ui::AgoraWindow {};

using namespace std;
using namespace qglviewer;

static QString colorString[2] = { "White", "Black" };


class BoardConstraint : public Constraint
{
public:
	virtual void constrainRotation(Quaternion& q, Frame * const fr)
	{
		const Vec up = fr->transformOf(Vec(0.0, 0.0, 1.0));
		Vec axis = q.axis();
		float angle = 2.0*acos(q[3]);
		if (fabs(axis*up) > fabs(axis.x))
			axis.projectOnAxis(up);
		else
		{
			angle = (axis.x > 0.0) ? angle : -angle;
			axis.setValue(fabs(axis.x), 0.0, 0.0);
			const float currentAngle = asin(fr->inverseTransformOf(Vec(0.0, 0.0, -1.0)).z);
			if (currentAngle + angle > -0.2)
				angle = -0.2 - currentAngle; // Not too low
			if (currentAngle + angle < -M_PI/2.0)
				angle = -M_PI/2.0 - currentAngle; // Do not pass on the other side
		}
		q = Quaternion(axis, angle);
	}
};



#if QT_VERSION < 0x040000
AgoraViewer::AgoraViewer(QWidget* parent, const char* name)
: QGLViewer(parent, name),
#else
AgoraViewer::AgoraViewer(QWidget* parent)
: QGLViewer(parent),
#endif
boardFileName_("standard.ago"), selectedPiece_(-1),
displayPossibleMoves_(true), animatePlays_(false/*true*/), animationStep_(0)
{
	kfi_ = new KeyFrameInterpolator(new Frame());

	QObject::connect(kfi_, SIGNAL(interpolated()), this, SLOT(updateGL()));
	QObject::connect(kfi_, SIGNAL(endReached()), this, SLOT(simplePlay()));

	undoTimer_ = new QTimer();
#if QT_VERSION >= 0x040000
	undoTimer_->setSingleShot(true);
#endif
	QObject::connect(undoTimer_, SIGNAL(timeout()), this, SLOT(playNextMove()));

	for (int i=0; i<2; ++i)
		connect(&(computerPlayer_[i]), SIGNAL(moveMade(QString, int)), this, SLOT(playComputerMove(QString, int)));

	computerPlayer_[0].setIsActive(false);
	computerPlayer_[1].setIsActive(false);

	QStringList programFileNames;
	programFileNames << "agoraAI" << "agoraAI.exe" << "../AI/agoraAI" << "../../AI/release/agoraAI.exe" << "../../AI/debug/agoraAI.exe" << "../AI/release/agoraAI.exe" << "../AI/debug/agoraAI.exe";
	for (int i=0; i<programFileNames.size(); ++i) {
		if (QFileInfo(programFileNames.at(i)).isExecutable()) {
			for (int c=0; c<2; ++c)
				computerPlayer_[c].setProgramFileName(programFileNames.at(i));
			break;
		}
	}
}


// I n i t i a l i z a t i o n   f u n c t i o n s

void AgoraViewer::init()
{
	initViewer();
	initSpotLight();
	initBoard();
	fitCameraToBoard();

	setMouseBinding(Qt::NoModifier, Qt::RightButton, CAMERA, ROTATE);
	setMouseBinding(Qt::NoModifier, Qt::LeftButton, SELECT);

#if QT_VERSION >= 0x040000
	// Signals and slots connections
	AgoraWindow* agoWin = (AgoraWindow*)(window());
	connect(agoWin->fileExitAction, SIGNAL(activated()), agoWin, SLOT(close()));
	connect(agoWin->fileOpenAction, SIGNAL(activated()), this, SLOT(load()));
	connect(agoWin->fileSaveAction, SIGNAL(activated()), this, SLOT(save()));
	connect(agoWin->fileSaveAsAction, SIGNAL(activated()), this, SLOT(saveAs()));
	connect(agoWin->gameNewGameAction, SIGNAL(activated()), this, SLOT(newGame()));
	connect(agoWin->gameUndoAction, SIGNAL(activated()), this, SLOT(undo()));
	connect(agoWin->gameRedoAction, SIGNAL(activated()), this, SLOT(redo()));

	connect(agoWin->gameBlackIsHumanAction, SIGNAL(toggled(bool)), this, SLOT(blackPlayerIsHuman(bool)));
	connect(agoWin->gameWhiteIsHumanAction, SIGNAL(toggled(bool)), this, SLOT(whitePlayerIsHuman(bool)));

	connect(agoWin->gameBlackIsHumanAction, SIGNAL(toggled(bool)), agoWin->gameConfigureBlackPlayerAction, SLOT(setDisabled(bool)));
	connect(agoWin->gameWhiteIsHumanAction, SIGNAL(toggled(bool)), agoWin->gameConfigureWhitePlayerAction, SLOT(setDisabled(bool)));

	connect(agoWin->gameConfigureBlackPlayerAction, SIGNAL(activated()), this, SLOT(configureBlackPlayer()));
	connect(agoWin->gameConfigureWhitePlayerAction, SIGNAL(activated()), this, SLOT(configureWhitePlayer()));

	connect(agoWin->togglePossibleMoveAction, SIGNAL(toggled(bool)), this, SLOT(toggleDisplayPossibleMoves(bool)));
	connect(agoWin->toggleAnimationAction, SIGNAL(toggled(bool)), this, SLOT(toggleAnimation(bool)));

	connect(agoWin->helpAboutAction, SIGNAL(activated()), this, SLOT(about()));
	connect(agoWin->helpRulesAction, SIGNAL(activated()), this, SLOT(displayRules()));
#endif
}

void AgoraViewer::initViewer()
{
	glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA);

	// Enable GL textures
	glTexParameterf(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR);
	glTexParameterf(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR);

	// Nice texture coordinate interpolation
	glHint(GL_PERSPECTIVE_CORRECTION_HINT, GL_NICEST);

	const QString texFileName("wood.png");
	QImage img(texFileName);

	if (img.isNull())
	{
		QMessageBox::warning(NULL, QString("Texture file not found"), QString("Unable to load wood texture (%1).").arg(texFileName));
		return;
	}

	// 1E-3 needed. Just try with width=128 and see !
	if ( ( img.width()  != 1<<(int)(1+log(img.width() -1+1E-3) / log(2.0)) ) ||
		( img.height() != 1<<(int)(1+log(img.height()-1+1E-3) / log(2.0))) )
	{
		QMessageBox::warning(NULL, QString("Incorrect texture"), QString("Wood texture dimensions are not powers of 2."));
		return;
	}

	QImage glImg = QGLWidget::convertToGLFormat(img);  // flipped 32bit RGBA

	// Bind the img texture...
	glTexImage2D(GL_TEXTURE_2D, 0, 4, glImg.width(), glImg.height(), 0,	GL_RGBA, GL_UNSIGNED_BYTE, glImg.bits());
	glDisable(GL_TEXTURE_2D);
}

void AgoraViewer::initBoard()
{
	board_ = new Board();

	QStringList boardDir;
	boardFileName_ = "";
	boardDir << "AgoraBoards" << "../AgoraBoards" << "../../AgoraBoards";
	for (int i=0; i<boardDir.size(); ++i) {
		if (QFileInfo(boardDir.at(i)).isDir()) {
			boardFileName_ = boardDir.at(i)+"/standard.ago";
			break;
		}
	}

	if (boardFileName_.isEmpty())
	{
		QMessageBox::warning(NULL ,"Unable to find agora board directory", "Unable to find agora board directory\n(tried "+boardDir.join(", ")+")");
		return;
	}

	newGame();
}

void AgoraViewer::initSpotLight()
{
	glMatrixMode(GL_MODELVIEW);
	glEnable(GL_LIGHT1);
	glLoadIdentity();

	// Light default parameters
	const GLfloat light_ambient[4]  = {2.0, 2.0, 2.0, 1.0};
	const GLfloat light_specular[4] = {1.0, 1.0, 1.0, 1.0};
	const GLfloat light_diffuse[4]  = {2.0, 2.0, 2.0, 1.0};

	glLightf( GL_LIGHT1, GL_SPOT_EXPONENT,  2.0);
	glLightf( GL_LIGHT1, GL_SPOT_CUTOFF,    60.0);
	glLightf( GL_LIGHT1, GL_CONSTANT_ATTENUATION, 0.1f);
	glLightf( GL_LIGHT1, GL_LINEAR_ATTENUATION, 0.3f);
	glLightf( GL_LIGHT1, GL_QUADRATIC_ATTENUATION, 0.3f);
	glLightfv(GL_LIGHT1, GL_AMBIENT,  light_ambient);
	glLightfv(GL_LIGHT1, GL_SPECULAR, light_specular);
	glLightfv(GL_LIGHT1, GL_DIFFUSE,  light_diffuse);
}

void AgoraViewer::newGame()
{
	if (!QFileInfo(boardFileName_).isReadable())
	{
		QMessageBox::warning(NULL ,"Unreadable board file", "Unable to read board file ("+boardFileName_+").\nLoad another board.");
		return;
	}

#if QT_VERSION < 0x040000
	std::ifstream f(boardFileName_.latin1());
#else
	std::ifstream f(boardFileName_.toLatin1().constData());
#endif
	f >> *board_;
	f.close();

	selectedPiece_ = -1;

	playNextMove();
}

void AgoraViewer::fitCameraToBoard()
{
	static BoardConstraint* boardConstraint = new BoardConstraint();

	// Free camera rotation motion
	camera()->frame()->setConstraint(NULL);

	setSceneCenter(Vec(board_->size().width()/2.0, board_->size().height()/2.0, 0.0));
	setSceneRadius(sceneCenter().norm());

	camera()->setUpVector(Vec(0.0, 0.0, 1.0));
	camera()->setPosition(Vec(2.0f*sceneCenter().x, -1.5f*sceneCenter().y, 0.9f*(sceneCenter().x+sceneCenter().y)));
	camera()->lookAt(sceneCenter());
	//showEntireScene();

	// Limit camera rotation motion
	camera()->frame()->setConstraint(boardConstraint);
}


// D r a w i n g   f u n c t i o n s
void AgoraViewer::draw()
{
	glEnable(GL_LIGHTING);

	if (animationStep_ > 0)
	{
		glPushMatrix();
		glMultMatrixd(kfi_->frame()->matrix());
		//Board::drawPiece(board_->blackPlays());
		glPopMatrix();

		//if (animationStep_ > 1)
		//	board_->drawFlippingPieces(currentMove_.end(), animationStep_%2);
	}

	const GLfloat pos[4] = {board_->size().width() / 2.0, board_->size().height() / 2.0, 3.0, 1.0};
	const GLfloat spot_dir[3] = {0.0, 0.0, -1.0};
	glLightfv(GL_LIGHT1, GL_POSITION, pos);
	glLightfv(GL_LIGHT1, GL_SPOT_DIRECTION, spot_dir);

	board_->draw();

	if (selectedPiece_ >= 0)
	{
		board_->drawSelectedPiece(selectedPiece_);
		if (displayPossibleMoves_)
			board_->drawPossibleDestinations(selectedPiece_);
	}
}

// G a m e   I n t e r f a c e
void AgoraViewer::play(const Move& m)
{
	currentMove_ = m;
	if (animatePlays_)
		animatePlay();
	else
		simplePlay();
}

void AgoraViewer::simplePlay()
{
	board_->play(currentMove_);
	updateGL();
	playNextMove();
}

void AgoraViewer::playNextMove()
{
	static const QString stateFileName = "board.ago";

	// Updates statusBar
#if QT_VERSION < 0x040000
	((QMainWindow*)(((QWidget*)parent())->parent()))->statusBar()->message(board_->statusMessage());
#else
	((QMainWindow*)(((QWidget*)parent())->parent()))->statusBar()->showMessage(board_->statusMessage());
#endif

	if (board_->gameIsOver())
		QMessageBox::information(this, "Game over", board_->statusMessage());
	else
	{
		// Save board state
#if QT_VERSION < 0x040000
		std::ofstream f(stateFileName.latin1());
#else
		std::ofstream f(stateFileName.toLatin1().constData());
#endif
		f << *board_;
		f.close();

		computerPlayer_[board_->blackPlays()].play(board_->blackPlays(), stateFileName, board_->nbMovesLeft());
	}
}

void AgoraViewer::playComputerMove(QString move, int duration)
{
	QString message = colorString[board_->blackPlays()]+" program played %1 in %2 out of %3 seconds.";
	message = message.arg(move).arg(duration/1000.0, 0, 'f', 1).arg(computerPlayer_[board_->blackPlays()].allowedTime()/1000.0, 0, 'f', 1);
#if QT_VERSION < 0x040000
	qWarning(message.latin1());
#else
	qWarning("%s", message.toLatin1().constData());
#endif
	Move m(move);
	if (m.isValid(board_))
		play(m);
	else
		QMessageBox::warning(this, "Unvalid move", "The move "+move+" is not valid for "+colorString[board_->blackPlays()]);
}

void AgoraViewer::animatePlay()
{
	kfi_->deletePath();
	// Cut/pasted from board posOf(). Not shared to prevent QGLViewer dependency in board.h

	//	kfi_->addKeyFrame(Frame(Vec(currentMove_.start().x()+0.5,currentMove_.start().y()+0.5,0.0), Quaternion()));
	//	kfi_->addKeyFrame(Frame(Vec((currentMove_.start().x()+currentMove_.end().x()+1.0)/2.0,
	//		(currentMove_.start().y()+currentMove_.end().y()+1.0)/2.0, 1.0), Quaternion()), 0.6f);
	//  kfi_->addKeyFrame(Frame(Vec(currentMove_.end().x()+0.5,currentMove_.end().y()+0.5,0.0), Quaternion()), 1.2f);
	kfi_->addKeyFrame(Frame(Vec(currentMove_.end().x()+0.5,currentMove_.end().y()+0.5,0.0), Quaternion()));

	animationStep_ = 1;
	kfi_->startInterpolation();
}

// M o v e   S e l e c t i o n
void AgoraViewer::drawWithNames()
{
	// Selection enabled only when the computer program is not active
	if ((!computerPlayer_[board_->blackPlays()].isActive()) && (animationStep_ ==0))
	{
		if (selectedPiece_ >= 0)
			board_->drawPossibleDestinations(selectedPiece_, true);

		// Always drawned, so that a new selection can occur
		board_->drawSelectablePieces();
	}
}

void AgoraViewer::postSelection(const QPoint&)
{
	if (selectedPiece_ >= 0)
	{
		if (selectedName() >= 0)
			if (selectedName() == selectedPiece_)
				selectedPiece_ = -1;
			else
			{
				Move m(board_, selectedPiece_, selectedName(), qApp->keyboardModifiers() != Qt::NoModifier);
				if (m.isValid(board_))
				{
					selectedPiece_ = -1;
					play(m);
				}
				else
					if (board_->canBeSelected(selectedName()))
						selectedPiece_ = selectedName();
					else
						selectedPiece_ = -1;
			}
		else
			selectedPiece_ = -1;
	}
	else
		if (selectedName() >= 0)
			selectedPiece_ = selectedName();
}



// F i l e   m e n u   f u n c t i o n s
void AgoraViewer::selectBoardFileName()
{
#if QT_VERSION < 0x040000
	boardFileName_ = QFileDialog::getOpenFileName("Boards", "Agora files (*.ago);;All files (*)", this);
#else
	boardFileName_ = QFileDialog::getOpenFileName(this, "Select a saved game", boardFileName_, "Agora files (*.ago);;All files (*)");
#endif
}

void AgoraViewer::load()
{
	selectBoardFileName();

	// In case of Cancel
	if (boardFileName_.isEmpty())
		return;

	newGame();
	fitCameraToBoard();
}

void AgoraViewer::save()
{
#if QT_VERSION < 0x040000
	std::ofstream f(boardFileName_.latin1());
#else
	std::ofstream f(boardFileName_.toLatin1().constData());
#endif
	f << *board_;
	f.close();
}

void AgoraViewer::saveAs()
{
#if QT_VERSION < 0x040000
	boardFileName_ = QFileDialog::getSaveFileName("Boards", "Agora files (*.ago);;All files (*)", this, boardFileName_.latin1());
#else
	boardFileName_ = QFileDialog::getSaveFileName(this, "Save game", boardFileName_, "Agora files (*.ago);;All files (*)");
#endif

	QFileInfo fi(boardFileName_);

#if QT_VERSION < 0x040000
	if (fi.extension().isEmpty())
#else
	if (fi.suffix().isEmpty())
#endif
	{
		boardFileName_ += ".ago";
		fi.setFile(boardFileName_);
	}

#if QT_VERSION < 0x040000
	if (fi.exists())
	{
		if (QMessageBox::warning(this ,"Existing file",
			"File "+fi.fileName()+" already exists.\nSave anyway ?",
			QMessageBox::Yes,QMessageBox::Cancel) == QMessageBox::Cancel)
			return;

		if (!fi.isWritable())
		{
			QMessageBox::warning(this ,"Cannot save", "File "+fi.fileName()+" is not writeable.");
			return;
		}
	}
#endif

	save();
}



// G a m e   m e n u   m e t h o d s

void AgoraViewer::blackPlayerIsHuman(bool on)
{
	setPlayerIsHuman(on, true);
}

void AgoraViewer::whitePlayerIsHuman(bool on)
{
	setPlayerIsHuman(on, false);
}

void AgoraViewer::configureBlackPlayer()
{
	configurePlayer(true);
}

void AgoraViewer::configureWhitePlayer()
{
	configurePlayer(false);
}

void AgoraViewer::setPlayerIsHuman(bool on, bool black)
{
	computerPlayer_[black].setIsActive(!on);
	playNextMove();
}

void AgoraViewer::configurePlayer(bool black)
{
	computerPlayer_[black].configure();
}


// U n d o   a n d   R e do
void AgoraViewer::undo()
{
	if ((animationStep_==0) && (board_->undo()))
		finalizeUndoRedo();
}

void AgoraViewer::redo()
{
	if ((animationStep_==0) && (board_->redo()))
		finalizeUndoRedo();
}

void AgoraViewer::finalizeUndoRedo()
{
	selectedPiece_ = -1;
	updateGL();
#if QT_VERSION < 0x040000
	undoTimer_->start(1000, true);
#else
	undoTimer_->start(1000);
#endif
}

void AgoraViewer::about()
{
	QMessageBox::about(this, "A g o r a", "A g o r a\nCreated by Gilles Debunne\nVersion 2.0 - January 2008");
}

void AgoraViewer::displayRules()
{
	static QTextEdit* te;

	static const QString rules = "<b>A g o r a</b><br><br>" \
		"Agora is a strategy game for two players.<br><br>" \
		"The goal is to have the maximum number of pieces of your color when the number of allowed moves is reached " \
		"(or to convert all the pieces into your color before that).<br><br>" \
		"A piece can be moved on a nearby square... " \
		"details to come.<br><br>" \
		"Enjoy !";

	if (!te)
	{
		te = new QTextEdit(rules);
		te->resize(500,300);
	}

	te->show();
}
