/*******************************************************************
*
* Copyright 2007  Aron Boström <c02ab@efd.lth.se>
*
* Bovo 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 2, or (at your option)
* any later version.
*
* Bovo 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 Bovo; see the file COPYING.  If not, write to
* the Free Software Foundation, 51 Franklin Street, Fifth Floor,
* Boston, MA 02110-1301, USA.
*
********************************************************************/

/** @file game.cc implements class Game in namespace bovo */

#include "game.h"

#include <QTimer>
#include <QString>
#include <QStringList>

#include "ai.h"
#include "aifactory.h"
#include "board.h"
#include "coord.h"
#include "dimension.h"
#include "move.h"

using namespace ai;

/** namespace for gui stuff */
namespace bovo
{

Game::Game(const Dimension& dimension, Player startingPlayer,
           KGameDifficultyLevel::StandardLevel skill, DemoMode demoMode,
           unsigned int playTime, AiFactory* aiFactory)
  : m_aiFactory(aiFactory), m_curPlayer(startingPlayer),m_computerMark(O),
  m_demoMode(demoMode), m_inUndoState(false), m_playerMark(X),
  m_playTime(playTime), m_replaying(false) {
    m_board = new Board(dimension);
    m_ai = m_aiFactory->createAi(dimension, skill, m_computerMark, demoMode);
    m_winDir = -1;
    m_gameOver = false;
    m_stepCount = 0;
    connect(this, &Game::boardChanged,
            m_ai, &Ai::changeBoard);
    connect(this, &Game::oposerTurn, m_ai, &Ai::slotMove,
            Qt::QueuedConnection);
    connect(m_ai, SIGNAL(move(Move)),
            this,  SLOT(move(Move)));
}

Game::Game(const Dimension& dimension, const QStringList &restoreGame,
           KGameDifficultyLevel::StandardLevel skill, unsigned int playTime,
           AiFactory* aiFactory)
  : m_aiFactory(aiFactory), m_computerMark(O), m_demoMode(NotDemo),
  m_inUndoState(false), m_playerMark(X), m_playTime(playTime),
  m_replaying(false) {
    m_board = new Board(dimension);
    m_ai = m_aiFactory->createAi(dimension, skill, m_computerMark, NotDemo);
    m_winDir = -1;
    m_gameOver = false;
    m_stepCount = 0;
    m_curPlayer = No;
    for (const QString &turn : restoreGame) {
        QStringList tmp = turn.split(QLatin1Char(':'));
        if (tmp.count() != 2) {
            qFatal("Wrong save file format!");
        }
        Player tmpPlayer = (tmp[0] == QLatin1String("1")) ? X : O;
        if (m_curPlayer == No) {
            m_curPlayer = tmpPlayer;
        }
        tmp = tmp[1].split(QLatin1Char(','));
        if (tmp.count() != 2) {
            qFatal("Wrong save file format!");
        }
        bool ok;
        uint x = tmp[0].toUInt(&ok);
        if (!ok) {
            qFatal("Wrong save file format!");
        }
        uint y = tmp[1].toUInt(&ok);
        if (!ok) {
            qFatal("Wrong save file format!");
        }
        Move tmpMove(tmpPlayer, Coord(x, y));
        m_board->setPlayer(tmpMove);
        m_stepCount++;
        m_history << tmpMove;
    }
}

Game::~Game() {
    delete m_board;
    delete m_ai;
}

bool Game::computerTurn() const {
    return m_curPlayer == m_computerMark;
}

DemoMode Game::demoMode() const {
    return m_demoMode;
}

bool Game::isGameOver() const {
    return m_gameOver || m_demoMode;
}

QList<Move> Game::history() const {
    return m_history;
}

Move Game::latestMove() const {
    if (m_history.empty()) {
        return Move();
    } else {
        return m_history.back();
    }
}

bool Game::ok(const Coord& coord) const {
    return m_board->ok(coord);
}

Player Game::player() const {
    return m_playerMark;
}

Player Game::player(const Coord& coord) const {
    return m_board->player(coord);
}

bool Game::save(const QString& filename) const {
    Q_UNUSED( filename );

    QString fileContent;
    fileContent.append(QStringLiteral("<bovo width=\"%1\" height=\"%2\">")
            .arg(QString(), QString()));
    for (const Move &move : std::as_const(m_history)) {
        fileContent.append(QStringLiteral("<move player=\"%1\" x=\"%2\" y=\"%3\" />").
                arg(move.player()).arg(move.x()).arg(move.y()));
    }
    fileContent.append(QLatin1String("</bovo>"));
    return false;
}

QStringList Game::saveLast() const {
    QStringList save;
    for (const Move &move : std::as_const(m_history)) {
        save << QStringLiteral("%1:%2,%3").arg(move.player())
                .arg(move.x()).arg(move.y());
    }
    return save;
}

void Game::setSkill(KGameDifficultyLevel::StandardLevel skill) {
    if (m_ai!=nullptr)
        m_ai->setSkill(skill);
}

void Game::start() {
    if (computerTurn()) {
        Q_EMIT oposerTurn();
    } else {
        Q_EMIT playerTurn();
    }
}

void Game::startRestored() {
    connect(this, &Game::boardChanged,
            m_ai, &Ai::changeBoard);
    for (const Move &move : std::as_const(m_history)) {
        Q_EMIT boardChanged(move);
    }
    connect(this, &Game::oposerTurn, m_ai, &Ai::slotMove,
            Qt::QueuedConnection);
    connect(m_ai, SIGNAL(move(Move)),
            this,  SLOT(move(Move)));
    if (!m_history.isEmpty() && m_history.last().player() == X) {
        m_curPlayer = O;
        Q_EMIT oposerTurn();
    } else {
        m_curPlayer = X;
        Q_EMIT playerTurn();
    }
    if (!m_history.isEmpty()) {
        Q_EMIT undoAble();
    }
}

short Game::winDir() const {
    return m_winDir;
}

bool Game::boardFull() const {
    return m_stepCount >= NUMCOLS * NUMCOLS;
}

void Game::cancelAndWait() {
    m_ai->cancelAndWait();
}

/* public slots */

void Game::move(const Move& move) {
    bool tmp_emptyHistory = m_history.empty();
    if (!m_board->empty(move.coord()) || move.player() != m_curPlayer
         || m_inUndoState) {
        return;
    }
    makeMove(move);
    if (tmp_emptyHistory && !m_history.empty() && !m_demoMode) {
        Q_EMIT undoAble();
    }
}

void Game::replay() {
    if (m_gameOver && !m_replaying) {
        m_replaying = true;
        m_replayIterator = m_history.constBegin();
        m_replayIteratorEnd = m_history.constEnd();
        disconnect(this, &Game::replayBegin, this, &Game::replayNext);
        connect(this, &Game::replayBegin, this, &Game::replayNext);
        Q_EMIT replayBegin();
    }
}

void Game::undoLatest() {
    m_inUndoState = true;
    if (m_history.empty() || m_demoMode) {
        m_inUndoState = false;
        return;
    }
    if (m_gameOver) {
        m_gameOver = false;
        m_winDir = -1;
        connect(this, &Game::boardChanged,
                m_ai, &Ai::changeBoard);
        connect(this, &Game::oposerTurn, m_ai, &Ai::slotMove,
                Qt::QueuedConnection);
        connect(m_ai, SIGNAL(move(Move)),
                this,  SLOT(move(Move)));
    }
    if (m_curPlayer == m_computerMark) {
        m_ai->cancelAndWait();
        Move move(No, m_history.last().coord());
        m_history.removeLast();
        m_board->setPlayer(move);
        m_stepCount--;
        Q_EMIT boardChanged(move);
        m_curPlayer = m_playerMark;
        Q_EMIT playerTurn();
    } else if (m_curPlayer == m_playerMark) {
        Move move(No, m_history.last().coord());
        m_history.removeLast();
        m_board->setPlayer(move);
        m_stepCount--;
        Q_EMIT boardChanged(move);
        if (m_history.count() == 0) {
            m_curPlayer = m_computerMark;
            Q_EMIT oposerTurn();
        } else {
            Move move2(No, m_history.last().coord());
            m_history.removeLast();
            m_board->setPlayer(move2);
            m_stepCount--;
            Q_EMIT boardChanged(move2);
            Q_EMIT playerTurn();
        }
    }
    if (m_history.empty() && !m_demoMode) {
        Q_EMIT undoNotAble();
    }
    m_inUndoState = false;
}

/* private slots */

void Game::replayNext() {
    if (m_replayIterator != m_replayIteratorEnd) {
        QTimer::singleShot(m_playTime, this, &Game::replayNext);
        Q_EMIT boardChanged(*m_replayIterator);
        ++m_replayIterator;
    } else {
        m_replaying = false;
        Q_EMIT replayEnd(winningMoves()); // FIX:!!!!!!!
    }
}

/* private methods */

void Game::makeMove(const Move& move) {
    if (move.player() != m_curPlayer) {
        return;
    }
    m_board->setPlayer(move);
    m_stepCount++;
    m_winDir = win(move.coord());
    if (m_winDir != -1) {
        m_gameOver = true;
    } else {
        if (boardFull()) {
            m_gameOver = true;
        }
    }
    m_history << move;
    m_curPlayer = (m_curPlayer == X ? O : X );
    Q_EMIT boardChanged(move);
    if (m_gameOver) {
        QList<Move> moves = winningMoves();
        Q_EMIT gameOver(moves);
        this->disconnect(m_ai);
    } else {
        if (computerTurn()) {
            if (m_demoMode) {
                QTimer::singleShot(m_playTime, this, &Game::oposerTurn);
            } else {
                Q_EMIT oposerTurn();
            }
        } else {
            if (m_demoMode) {
                QTimer::singleShot(m_playTime, this, &Game::playerTurn);
            } else {
                Q_EMIT playerTurn();
            }
        }
    }
}

Coord Game::next(const Coord& coord, usi dir) const {
    const usi LEFT = 1;
    const usi UP = 2;
    const usi RIGHT = 4;
    const usi DOWN = 8;
    Coord tmp = coord;
    if (dir & LEFT) {
        tmp = tmp.left();
    } else if (dir & RIGHT) {
        tmp = tmp.right();
    }
    if (dir & UP) {
        tmp = tmp.up();
    } else if (dir & DOWN) {
        tmp = tmp.down();
    }
    return tmp;
}

short Game::win(const Coord& c) const {
    const usi LEFT = 1;
    const usi UP = 2;
    const usi RIGHT = 4;
    const usi DOWN = 8;
    usi DIR[8] = {LEFT, RIGHT, UP, DOWN, LEFT | UP, RIGHT | DOWN,
                  LEFT | DOWN, RIGHT | UP};
    Player p = player(c);
    for (int i = 0; i < 4; ++i) {
        usi count = 1;
        Coord tmp = next(c, DIR[2*i]);
        while (m_board->ok(tmp) && player(tmp) == p) {
            ++count;
            tmp = next(tmp, DIR[2*i]);
        }
        tmp = next(c, DIR[2*i+1]);
        while (m_board->ok(tmp) && player(tmp) == p) {
            ++count;
            tmp = next(tmp, DIR[2*i+1]);
        }
        if (count >= 5) {
            return i;
        }
    }
    return -1;
}

QList<Move> Game::winningMoves() const {
    if (m_winDir == -1) {
        return {};
    }
    QList<Move> moves;
    short dy, dx;
    switch (m_winDir) {
        case 0: dx = 1; dy =  0; break;
        case 1: dx = 0; dy =  1; break;
        case 2: dx = 1; dy =  1; break;
        default: dx = 1; dy = -1; break;
    }
    usi x = latestMove().x();
    usi y = latestMove().y();
    Player winner = player(Coord(x, y));
    Player tmp;
    while ((tmp = player(Coord(x, y))) == winner) {
        moves << Move(player(Coord(x, y)), Coord(x, y));
        x += dx;
        y += dy;
    }
    x = latestMove().x() - dx;
    y = latestMove().y() - dy;
    while ((tmp = player(Coord(x, y))) == winner) {
        moves << Move(player(Coord(x, y)), Coord(x, y));
        x -= dx;
        y -= dy;
    }
    return moves;
}

}

#include "moc_game.cpp"
