/***************************************************************************
    File                 : board.cpp
    Project              : Knights
    Description          : Game board (scene)
    --------------------------------------------------------------------
    SPDX-FileCopyrightText: 2016 Alexander Semke (alexander.semke@web.de)
    SPDX-FileCopyrightText: 2009-2011 Miha Čančula (miha@noughmad.eu)

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

/***************************************************************************
 *                                                                         *
 *  SPDX-License-Identifier: GPL-2.0-or-later
 *                                                                         *
 *  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, write to the Free Software           *
 *   Foundation, Inc., 51 Franklin Street, Fifth Floor,                    *
 *   Boston, MA  02110-1301  USA                                           *
 *                                                                         *
 ***************************************************************************/

#include "board.h"
#include "gamemanager.h"
#include "settings.h"
#include "core/pos.h"
#include "core/move.h"
#include "core/item.h"
#include "rules/chessrules.h"
#include "ui_promotiondialog.h"

#include <KGameGraphicsViewRenderer>

#include <QDialog>
#include <QDialogButtonBox>
#include <QDrag>
#include <QGraphicsSceneMouseEvent>
#include <QMimeData>
#include <QPushButton>
#include <QtMath>

using namespace Knights;

const qreal backgroundZValue = -3.0;
const qreal borderZValue = -2.0;
const qreal notationZValue = -1.0;
const qreal tileZValue = 0.0;
const qreal pieceZValue = 1.0;
const qreal legalMarkerZValue = 3.0;
const qreal dragZValue = 4.0;

const QString backgroundKey = QStringLiteral ( "Background" );
const QString whiteTileKey = QStringLiteral ( "WhiteTile" );
const QString blackTileKey = QStringLiteral ( "BlackTile" );
const QString legalMarkerKey = QStringLiteral ( "Marker" );
const QString motionMarkerKey = QStringLiteral ( "Motion" );
const QString dangerMarkerKey = QStringLiteral ( "Danger" );

const QString tbBorderKey = QStringLiteral ( "TopBottomBorder" );
const QString lrBorderKey = QStringLiteral ( "LeftRightBorder" );
const QString whiteLettersKey = QStringLiteral ( "WhiteLetters" );
const QString blackLettersKey = QStringLiteral ( "BlackLetters" );
const QString whiteNumbersKey = QStringLiteral ( "WhiteNumbers" );
const QString blackNumbersKey = QStringLiteral ( "BlackNumbers" );

Board::Board(KGameThemeProvider* provider, QObject* parent) : QGraphicsScene(parent),
	m_rules(nullptr),
	m_background(nullptr),
	m_displayBorders(true),
	m_displayNotations(true),
	renderer(nullptr),
	m_themeProvider(provider),
	m_dragActive(false),
	draggedPiece(nullptr),
	selectedPiece(nullptr),
	m_paused(false),
	m_animated(true),
	m_currentPlayer(White),
	m_displayedPlayer(NoColor),
	m_drawFrame(true) {

	renderer = new KGameGraphicsViewRenderer(m_themeProvider);
	if (!Manager::self()->rules())
		Manager::self()->setRules(new ChessRules);
	Manager::self()->rules()->setGrid(&m_grid);
	updateTheme();

	connect (provider, &KGameThemeProvider::currentThemeChanged, this, &Board::updateTheme);
}

Board::~Board() {
	qDeleteAll(m_grid);
	qDeleteAll(m_tiles);
	qDeleteAll(markers);
	delete renderer;
	//TODO: this crashes the application, s.a. BUG 405763
	//delete Manager::self()->rules();
}

void Board::addPiece(PieceType type, Color color, const Pos& pos) {
	Piece* t_piece = new Piece ( renderer, type, color, this, pos );
	if ( Settings::animationSpeed() != Settings::EnumAnimationSpeed::Instant )
		t_piece->setPos ( mapToScene ( Pos ( ( pos.first > 4 ) ? 5 : 4, ( pos.second > 4 ) ? 5 : 4 ) ) );
	t_piece->setZValue ( pieceZValue );
	m_grid.insert ( pos, t_piece );
}

void Board::movePiece(const Move& move) {
	qCDebug(LOG_KNIGHTS) << move;
	Move m = move;
	if ( ( m.flag ( Move::Illegal ) && !m.flag ( Move::Forced ) ) ||  m.to() == m.from() || !m_grid.contains ( m.from() ) ) {
		qCWarning(LOG_KNIGHTS) << "Invalid move:" << m;
		return;
	}
	if ( !m.flag ( Move::Forced ) &&
	        ( m_grid[m.from()]->color() != m_currentPlayer || !Manager::self()->rules()->legalMoves(m.from()).contains(m) ) ) {
		qCWarning(LOG_KNIGHTS) << "Move not allowed:" << m;
		return;
	}
	qDeleteAll ( markers );
	markers.clear();
	if ( m.flag(Move::Promote) )
		m_grid[m.from() ]->setPieceType ( m.promotedType() ? m.promotedType() : Queen );

	PieceDataMap map = m.removedPieces();
	PieceDataMap::const_iterator it = map.constBegin();
	PieceDataMap::const_iterator end = map.constEnd();
	for ( ; it != end; ++it ) {
		delete m_grid.value ( it.key(), nullptr );
		m_grid.remove ( it.key() );
	}

	centerOnPos ( m_grid.value ( m.from() ), m.to() );
	m_grid.insert ( m.to(), m_grid.take ( m.from() ) );

	map = m.addedPieces();
	it = map.constBegin();
	end = map.constEnd();
	for ( ; it != end; ++it )
		addPiece ( it.value().second, it.value().first, it.key() );

	if ( m_playerColors & oppositeColor ( m_currentPlayer ) ) {
		// We only display motion and danger markers if the next player is a human
		if ( Settings::showMotion() ) {
			addMarker ( m.from(), Motion );
			addMarker ( m.to(), Motion );
		}
		if ( Settings::showDanger() ) {
			bool check = false;
			for ( Piece* piece : std::as_const(m_grid) ) {
				if ( piece->color() == m_currentPlayer && Manager::self()->rules()->isAttacking ( piece->boardPos() ) ) {
					check = true;
					addMarker ( piece->boardPos(), Danger );
				}
			}
			if ( check ) {
				for ( Piece* piece : std::as_const(m_grid) ) {
					if ( piece->color() != m_currentPlayer && piece->pieceType() == King )
						addMarker ( piece->boardPos(), Danger );
				}
			}
		}
	}

	for ( const Move& additionalMove : m.additionalMoves() )
		movePiece ( additionalMove );

	updateGraphics();
}

void Board::populate() {
	const PieceDataMap pieces = Manager::self()->rules()->startingPieces();
	PieceDataMap::const_iterator it = pieces.constBegin();
	PieceDataMap::const_iterator end = pieces.constEnd();
	for ( ; it != end; ++it )
		addPiece ( it.value().second, it.value().first, it.key() );
	updateGraphics();
}

void Board::addTiles() {
	if ( !m_tiles.isEmpty() ) {
		qCWarning(LOG_KNIGHTS) << "Tiles are already present, delete them first";
		return;
	}
	for ( int i = 1; i < 9; ++i ) {
		for ( int j = 1; j < 9; ++j ) {
			QString key = ( ( i + j ) % 2 == 0 ) ? blackTileKey : whiteTileKey;
			Item* tile = new Item ( renderer, key, this, Pos ( i, j ) );
			tile->setZValue ( tileZValue );
			m_tiles.insert ( Pos ( i, j ), tile );
		}
	}
}

void Board::mousePressEvent(QGraphicsSceneMouseEvent* e) {
	if ( !Manager::self()->canLocalMove() ) {
		// It is not the human player's turn
		e->ignore();
		return;
	}

	Piece* d_piece = pieceAt ( e->scenePos() );
	if ( !d_piece || d_piece->color() != m_currentPlayer ) {
		// The piece doesn't belong to the player whose turn it is, or there is no piece
		if ( !selectedPiece ) {
			e->ignore();
			return;
		}
		Pos from = selectedPiece->boardPos();
		Pos to = mapFromScene ( e->scenePos() );
		if ( Manager::self()->rules()->legalMoves ( from ).contains ( Move ( from, to ) ) ) {
			Move move ( from, to );
			move.setFlag ( Move::Take, m_grid.contains ( to ) );

			if ( m_grid[from]->pieceType() == Pawn && ( to.second == 1 || to.second == 8 ) ) {
				move.setFlag ( Move::Promote, true );
				move.setPromotedType ( getPromotedType() );
			}
			Q_EMIT pieceMoved(move);
			selectedPiece = nullptr;
		}
	} else {
		// The active player clicked on his/her own piece
		if (d_piece != selectedPiece) {
			qDeleteAll ( markers );
			markers.clear();

			selectedPiece = d_piece;

			Pos t_pos = mapFromScene ( e->scenePos() );
			QList<Move> t_legalMoves = Manager::self()->rules()->legalMoves ( t_pos );
			if ( t_legalMoves.isEmpty() ) {
				e->ignore();
				return;
			}
			d_piece->setZValue ( dragZValue );
			if ( Settings::showMarker() ) {
				for ( const Move& t_move : std::as_const(t_legalMoves) )
					addMarker ( t_move.to(), LegalMove );
			}
			draggedPiece = d_piece;
		}
		m_draggedPos = e->scenePos();
		dragStartPoint = e->screenPos();
	}
}

void Board::mouseReleaseEvent(QGraphicsSceneMouseEvent* e) {
	Q_UNUSED(e);
	draggedPiece = nullptr;
}

void Board::mouseMoveEvent(QGraphicsSceneMouseEvent* e) {
	if (!(e->buttons() & Qt::LeftButton) || m_dragActive)
		return;
	if (draggedPiece && ((e->screenPos() - dragStartPoint).manhattanLength() >= QApplication::startDragDistance()) ) {
		//initiate a new drag event
		drag = new QDrag ( e->widget() );
		drag->setMimeData ( new QMimeData() );
		m_dragActive = true;
		selectedPiece = nullptr;
		drag->exec();
	}
}

void Board::dragLeaveEvent(QGraphicsSceneDragDropEvent* e) {
	Q_UNUSED(e);
	if ( !m_dragActive )
		return;

	qDeleteAll(markers);
	markers.clear();
	centerOnPos(draggedPiece);
	draggedPiece->setZValue(pieceZValue);
	draggedPiece = nullptr;
	m_dragActive = false;
}

void Board::dropEvent(QGraphicsSceneDragDropEvent* e) {
	qDeleteAll(markers);
	markers.clear();

	if (draggedPiece) {
		m_dragActive = false;
		Pos from = draggedPiece->boardPos();
		Pos to = mapFromScene ( e->scenePos() );
		Move move ( from, to );
		if ( !Manager::self()->rules()->legalMoves ( from ).contains ( move ) )
			centerOnPos ( draggedPiece );
		else {
			if ( m_grid[from]->pieceType() == Pawn && ( to.second == 1 || to.second == 8 ) ) {
				move.setFlag ( Move::Promote, true );
				move.setPromotedType ( getPromotedType() );
			}
			Q_EMIT pieceMoved(move);
		}
		draggedPiece->setZValue(pieceZValue);
		draggedPiece = nullptr;
	}
}

void Board::dragEnterEvent(QGraphicsSceneDragDropEvent* e) {
	e->setAccepted(Manager::self()->canLocalMove());
}

void Board::dragMoveEvent(QGraphicsSceneDragDropEvent* e) {
	if ( !draggedPiece ) {
		e->ignore();
		return;
	}
	e->accept();
	qreal x = e->scenePos().x() - m_draggedPos.x();
	qreal y = e->scenePos().y() - m_draggedPos.y();

	draggedPiece->moveBy ( x, y );
	m_draggedPos = e->scenePos();
}

Piece* Board::pieceAt(const QPointF& point) {
	return m_grid.value(mapFromScene(point), nullptr);
}

Pos Board::mapFromScene(const QPointF& point) {
	Pos pos;
	pos.first = ( point.x() - m_boardRect.left() ) / m_tileSize + 1;
	pos.second = 1 - ( point.y() - m_boardRect.bottom() ) / m_tileSize;
	if ( m_displayedPlayer != White )
		pos = Pos ( 9, 9 ) - pos;
	return pos;
}

QPointF Board::mapToScene(Pos pos) {
	if ( m_displayedPlayer != White )
		pos = Pos ( 9, 9 ) - pos;
	QPointF point;
	point.setX ( m_boardRect.left() + ( pos.first - 1 ) * m_tileSize );
	point.setY ( m_boardRect.bottom() - pos.second * m_tileSize );
	return point;
}

void Board::centerOnPos(Item* item, const Pos& pos, bool animated) {
	item->setBoardPos(pos);
	centerOnPos(item, animated);
}

void Board::centerOnPos(Item* item, bool animated) {
	item->move(mapToScene(item->boardPos()), m_tileSize, animated);
}

void Board::centerAndResize(Item* item, QSize size, bool animated) {
	item->moveAndResize(mapToScene ( item->boardPos() ), m_tileSize, size, animated);
}

bool Board::isInBoard(const Pos& pos) {
	return pos.first > 0 && pos.first < 9 && pos.second > 0 && pos.second < 9;
}

void Board::setPlayerColors(Colors colors) {
	m_playerColors = colors;
	if ( m_playerColors & m_currentPlayer )
		m_displayedPlayer = m_currentPlayer;
	else {
		if ( m_playerColors == Black )
			m_displayedPlayer = Black;
		else
			m_displayedPlayer = White;
	}
	changeDisplayedPlayer();
	populate();
}

void Board::setCurrentColor(Color color) {
	m_currentPlayer = color;
	Color nextPlayer = m_displayedPlayer;
	if ( ( ( m_playerColors & (Black|White) ) == (Black|White) ) && !Settings::flipBoard() )
		nextPlayer = White;
	else if ( m_playerColors & color )
		nextPlayer = color;
	if ( m_displayedPlayer != nextPlayer ) {
		m_displayedPlayer = nextPlayer;
		changeDisplayedPlayer();
	}
	Q_EMIT activePlayerChanged ( m_currentPlayer );
}


void Board::addMarker(const Pos& pos, MarkerType type) {
	QString key;
	switch ( type ) {
	case LegalMove:
		key = legalMarkerKey;
		break;
	case Danger:
		key = dangerMarkerKey;
		break;
	case Motion:
		key = motionMarkerKey;
		break;
	}
	addMarker ( pos, key );
}

void Board::addMarker(const Pos& pos, const QString& spriteKey) {
	if ( markers.contains ( pos ) ) {
		// Prevent two markers (usually Motion and Danger) from being on the same square
		delete markers[pos];
	}
	Item* marker = new Item ( renderer, spriteKey, this, pos );
	centerOnPos ( marker, false );
	marker->setRenderSize ( QSizeF ( m_tileSize, m_tileSize ).toSize() );
	marker->setZValue ( legalMarkerZValue );
	markers.insert ( pos, marker );
}

void Board::updateTheme() {
	delete m_background;
	if ( renderer->spriteExists ( backgroundKey ) ) {
		m_background = new Item ( renderer, backgroundKey, this, Pos() );
		m_background->setZValue ( backgroundZValue );
	} else
		m_background = nullptr;

	qDeleteAll ( m_borders );
	m_borders.clear();
	qDeleteAll ( m_notations );
	m_notations.clear();
	m_displayBorders = Settings::borderDisplayType() != Settings::EnumBorderDisplayType::None
	                   && renderer->spriteExists ( lrBorderKey )
	                   && renderer->spriteExists ( tbBorderKey );
	m_displayNotations = Settings::borderDisplayType() == Settings::EnumBorderDisplayType::Notation
	                     && renderer->spriteExists ( whiteLettersKey )
	                     && renderer->spriteExists ( blackLettersKey )
	                     && renderer->spriteExists ( whiteNumbersKey )
	                     && renderer->spriteExists ( blackNumbersKey );
	if ( m_displayBorders ) {
		m_borders << new Item ( renderer, tbBorderKey, this, Pos() );

		Item *tItem = new Item ( renderer, lrBorderKey, this, Pos() );
		tItem->setRotation ( 180 );
		m_borders << tItem;

		tItem = new Item ( renderer, tbBorderKey, this, Pos() );
		tItem->setRotation ( 180 );
		m_borders << tItem;

		m_borders << new Item ( renderer, lrBorderKey, this, Pos() );
		for ( Item* item : std::as_const(m_borders) )
			item->setZValue ( borderZValue );
	}
	if ( m_displayNotations ) {
		QString lettersKey;
		QString numbersKey;
		if ( m_displayedPlayer == White ) {
			lettersKey = whiteLettersKey;
			numbersKey = whiteNumbersKey;
		} else {
			lettersKey = blackLettersKey;
			numbersKey = blackNumbersKey;
		}
		m_notations << new Item ( renderer, lettersKey, this, Pos() );
		m_notations << new Item ( renderer, numbersKey, this, Pos() );
		m_notations << new Item ( renderer, lettersKey, this, Pos() );
		m_notations << new Item ( renderer, numbersKey, this, Pos() );
		for ( Item* item : std::as_const(m_notations) )
			item->setZValue ( notationZValue );
	}
	addTiles();
	updateGraphics();
}

void Board::updateGraphics() {
	if (sceneRect().isEmpty()) {
	    return;
	}
	if ( m_background )
		m_background->setRenderSize ( sceneRect().size().toSize() );
	QSizeF tileSize = renderer->boundsOnSprite ( whiteTileKey ).size();
	QSizeF boardSize = 8 * tileSize;
	qreal sideMargin;
	qreal topMargin;
	if ( m_displayBorders ) {
		sideMargin = renderer->boundsOnSprite ( lrBorderKey ).width();
		topMargin = renderer->boundsOnSprite ( tbBorderKey ).height();
	} else {
		sideMargin = 0.0;
		topMargin = 0.0;
	}
	boardSize = boardSize + 2 * QSize ( sideMargin, topMargin );
	qreal ratio = qMin ( sceneRect().width() / boardSize.width(), sceneRect().height() / boardSize.height() );
	sideMargin *= ratio;
	topMargin *= ratio;

	QSizeF tpSize = tileSize * ratio;
	m_tileSize = qFloor ( qMin ( tpSize.width(), tpSize.height() ) );
	/*
	if ( m_displayBorders )
	{
	    if (  m_tileSize % 2 )
	    {
	        m_tileSize -= 1;
	    }
	    sideMargin = m_tileSize / 2;
	    topMargin = m_tileSize / 2;
	}
	*/

	QSize hBorderSize = QSize ( 8 * m_tileSize + 2 * qRound ( sideMargin ), qRound ( topMargin ) );
	QSize vBorderSize = QSize ( qRound ( sideMargin ), 8 * m_tileSize );
	int hBorderMargin = qRound ( topMargin );
	int vBorderMargin = qRound ( sideMargin );


	sideMargin = qMax ( sideMargin, ( sceneRect().width() - 8 * m_tileSize ) / 2 );
	topMargin = qMax ( topMargin, ( sceneRect().height() - 8 * m_tileSize ) / 2 );
	m_boardRect = QRect( sceneRect().topLeft().toPoint() + QPoint( sideMargin, topMargin ),
	                     QSize( m_tileSize, m_tileSize ) * 8);

	QSize tSize = QSizeF ( m_tileSize, m_tileSize ).toSize();
	/*
	 * For historical reasons, QRect's
	 */
	QPointF topBorderPoint = m_boardRect.topRight() + QPoint ( vBorderMargin, 0 );
	QPointF rightBorderPoint = m_boardRect.bottomRight() + QPoint ( vBorderMargin, 0 );
	QPointF bottomBorderPoint = m_boardRect.bottomLeft() - QPoint ( vBorderMargin, 0 );
	QPointF leftBorderPoint = m_boardRect.topLeft() - QPoint ( vBorderMargin, 0 );

	for ( Piece* p : std::as_const(m_grid) )
		centerAndResize ( p, tSize );
	for ( Item* t : std::as_const(m_tiles) )
		centerAndResize ( t, tSize, Settings::animateBoard() );
	for ( Item* t : std::as_const(markers) )
		centerAndResize ( t, tSize );
	if ( m_displayBorders ) {
		m_borders[0]->moveAndResize ( bottomBorderPoint, m_tileSize, hBorderSize, Settings::animateBoard() );
		m_borders[1]->moveAndResize ( rightBorderPoint, m_tileSize, vBorderSize, Settings::animateBoard() );
		m_borders[2]->moveAndResize ( topBorderPoint, m_tileSize, hBorderSize, Settings::animateBoard() );
		m_borders[3]->moveAndResize ( leftBorderPoint, m_tileSize, vBorderSize, Settings::animateBoard() );
	}
	if ( m_displayNotations ) {
		m_notations[0]->moveAndResize ( bottomBorderPoint, m_tileSize, hBorderSize, Settings::animateBoard() );
		m_notations[1]->moveAndResize ( m_boardRect.topRight(), m_tileSize, vBorderSize, Settings::animateBoard() );
		m_notations[2]->moveAndResize ( m_boardRect.topLeft() - QPointF ( vBorderMargin, hBorderMargin ), m_tileSize, hBorderSize, Settings::animateBoard() );
		m_notations[3]->moveAndResize ( leftBorderPoint, m_tileSize, vBorderSize, Settings::animateBoard() );
	}
}

void Board::changeDisplayedPlayer() {
	for ( Piece* p : std::as_const(m_grid) )
		centerOnPos ( p );
	for ( Item* i : std::as_const(markers) )
		centerOnPos ( i );
	if ( Settings::animateBoard() ) {
		for ( Item* i : std::as_const(m_tiles) )
			centerOnPos ( i );
	}
	if ( m_displayNotations ) {
		if ( m_displayedPlayer == White ) {
			m_notations[0]->setSpriteKey ( whiteLettersKey );
			m_notations[1]->setSpriteKey ( whiteNumbersKey );
			m_notations[2]->setSpriteKey ( whiteLettersKey );
			m_notations[3]->setSpriteKey ( whiteNumbersKey );
		} else {
			m_notations[0]->setSpriteKey ( blackLettersKey );
			m_notations[1]->setSpriteKey ( blackNumbersKey );
			m_notations[2]->setSpriteKey ( blackLettersKey );
			m_notations[3]->setSpriteKey ( blackNumbersKey );
		}
	}
	Q_EMIT displayedPlayerChanged(m_displayedPlayer);
}

PieceType Board::getPromotedType() {
	PieceType piece = Queen;
	QPointer<QDialog> dialog = new QDialog();
	dialog->setWindowTitle(i18nc("@title:window", "Promotion"));
	QWidget *widget = new QWidget();
	QVBoxLayout *layout = new QVBoxLayout();
	QDialogButtonBox *bBox = new QDialogButtonBox(QDialogButtonBox::Ok | QDialogButtonBox::Cancel);
	bBox->button(QDialogButtonBox::Ok)->setText(i18nc("@action:button", "Promote"));
	bBox->button(QDialogButtonBox::Ok)->setDefault(true);

	Ui::PromotionWidget ui;
	ui.setupUi ( widget );

	layout->addWidget ( widget );
	layout->addWidget ( bBox );
	dialog->setLayout ( layout );

	connect (bBox, &QDialogButtonBox::accepted, dialog, &QDialog::accept);
	connect (bBox, &QDialogButtonBox::rejected, dialog, &QDialog::reject);

	if ( dialog->exec() == QDialog::Accepted ) {
		if ( ui.radioButtonKnight->isChecked() )
			piece = Knight;
		else if ( ui.radioButtonBishop->isChecked() )
			piece = Bishop;
		else if ( ui.radioButtonRook->isChecked() )
			piece = Rook;
	}
	delete dialog;
	return piece;
}

#include "moc_board.cpp"
