/*
 * gamemodel.cpp - Battleship game plugin
 * Copyright (C) 2014  Aleksey Andreev
 *
 * This program 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
 * of the License, or (at your option) any later version.
 *
 * 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, see <https://www.gnu.org/licenses/>.
 *
 */

#include <QCryptographicHash>
#include <QRandomGenerator>

#include "gamemodel.h"

GameModel::GameModel(QObject *parent) :
    QObject(parent), status_(StatusNone), lastShot_(-1), draw_(false), oppDraw_(false), myAccept_(false),
    oppResign_(false), myResign_(false), destroyed_(false)
{
    connect(&myBoard_, SIGNAL(shipDestroyed(int)), this, SLOT(myShipDestroyed()), Qt::DirectConnection);
}

void GameModel::init()
{
    setStatus(StatusBoardInit);
    opBoard_.init(GameBoard::CellUnknown, false);
    myBoard_.init(GameBoard::CellFree, true);
    myBoard_.makeShipRandomPosition();
}

void GameModel::setError() { setStatus(StatusError); }

bool GameModel::initOpponentBoard(const QStringList &data)
{
    bool err = false;
    int  cnt = data.count();
    for (int i = 0; i < cnt; ++i) {
        const QString str = data.at(i);
        const QString t   = str.section(';', 0, 0);
        int           n   = str.section(';', 1, 1).toInt();
        const QString s   = str.section(';', 2);
        if (t == "cell") {
            if (!opBoard_.updateCellDigest(n, s)) {
                err = true;
                break;
            }
        } else if (t == "ship") {
            if (!opBoard_.updateShipDigest(n, s)) {
                err = true;
                break;
            }
        }
    }
    return !err;
}

bool GameModel::uncoverOpponentBoard(const QStringList &data)
{
    bool err = false;
    int  cnt = data.count();
    for (int i = 0; i < cnt; ++i) {
        const QString         str  = data.at(i);
        int                   pos  = str.section(';', 0, 0).toInt();
        GameBoard::CellStatus cs   = (str.section(';', 1, 1) == "1") ? GameBoard::CellOccupied : GameBoard::CellFree;
        const QString         seed = str.section(';', 2);
        if (!opBoard_.updateCell(pos, cs, seed)) {
            err = true;
            break;
        }
    }
    emit oppBoardUpdated(0, 0, 10, 10);
    return !err;
}

void GameModel::setOpponentDraw(bool draw) { oppDraw_ = draw; }

void GameModel::setOpponentAcceptedDraw(bool accept)
{
    if (draw_) {
        if (accept)
            setStatus(StatusDraw);
        else
            draw_ = false;
    }
}

void GameModel::opponentResign()
{
    oppResign_ = true;
    setStatus(StatusWin);
}

void GameModel::sendCoveredBoard()
{
    myBoard_.calculateCellsHash();
    emit gameEvent("covered-board\n" + myBoard_.toStringList(true).join("\n"));
}

void GameModel::localTurn(int pos)
{
    if (status_ == StatusMyTurn) {
        lastShot_    = pos;
        QString data = QString("turn\npos;%1").arg(pos);
        if (draw_)
            data.append("\ndraw");
        setStatus(StatusWaitingTurnAccept);
        emit gameEvent(data);
    }
}

void GameModel::opponentTurn(int pos)
{
    if (status_ == StatusWaitingOpponent) {
        lastShot_  = pos;
        destroyed_ = false;
        draw_      = false;
        if (pos != -1) {
            myBoard_.shot(pos);
            int  row = pos / 10;
            int  col = pos % 10;
            emit myBoardUpdated(col, row, 1, 1);
            if (lastShotResult() == "miss")
                setStatus(StatusMyTurn);
            else if (myBoard_.isAllDestroyed())
                setStatus(StatusLose);
            else if (oppDraw_)
                setStatus(StatusMyTurn);
            else
                setStatus(StatusWaitingOpponent);
        }
    }
}

bool GameModel::handleResult()
{
    if (myAccept_) {
        setStatus(StatusDraw);
        return true;
    }
    if (myResign_) {
        setStatus(StatusLose);
        return true;
    }
    return false;
}

bool GameModel::handleTurnResult(const QString &res, const QString &seed)
{
    GameBoard::CellStatus cs = GameBoard::CellUnknown;
    if (res == "miss")
        cs = GameBoard::CellMiss;
    else if (res == "hit" || res == "destroy")
        cs = GameBoard::CellHit;
    if (cs != GameBoard::CellUnknown && opBoard_.updateCell(lastShot_, cs, seed)) {
        int snum = -1;
        if (res != "destroy" || (snum = opBoard_.findAndInitShip(lastShot_)) != -1) {
            QPoint p(lastShot_ / 10, lastShot_ % 10);
            QRect  r;
            r.setTopLeft(p);
            r.setSize(QSize(1, 1));
            if (snum != -1) {
                opBoard_.setShipDestroy(snum, true);
                r = opBoard_.shipRect(snum, true);
            }
            if (cs == GameBoard::CellMiss)
                setStatus(StatusWaitingOpponent);
            else if (snum != -1 && opBoard_.isAllDestroyed())
                setStatus(StatusWin);
            else if (draw_)
                setStatus(StatusWaitingOpponent);
            else
                setStatus(StatusMyTurn);
            emit oppBoardUpdated(r.left(), r.top(), r.width(), r.height());
            return true;
        }
    }
    setStatus(StatusError);
    return false;
}

QString GameModel::lastShotResult() const
{
    QString res;
    if (lastShot_ != -1) {
        const GameBoard::GameCell  &cell = myBoard_.cell(lastShot_);
        const GameBoard::CellStatus cs   = cell.status;
        if (cs == GameBoard::CellHit) {
            if (destroyed_)
                res = "destroy";
            else
                res = "hit";
        } else
            res = "miss";
    }
    return res;
}

QString GameModel::lastShotSeed() const
{
    QString res;
    if (lastShot_ != -1)
        res = myBoard_.cell(lastShot_).seed;
    return res;
}

QStringList GameModel::getUncoveredBoard() const { return myBoard_.toStringList(false); }

void GameModel::setStatus(GameStatus s)
{
    status_ = s;
    emit statusChanged();
}

void GameModel::myShipDestroyed() { destroyed_ = true; }

void GameModel::setLocalDraw(bool draw) { draw_ = draw; }

void GameModel::localAccept()
{
    if (status_ == StatusMyTurn && oppDraw_) {
        myAccept_ = true;
        setStatus(StatusDraw);
        emit gameEvent("turn\naccept");
    }
}

void GameModel::localResign()
{
    if (status_ == StatusMyTurn) {
        myResign_ = true;
        setStatus(StatusLose);
        emit gameEvent("turn\nresign");
    }
}

//------------- GameBoard ----------------

GameBoard::GameBoard(QObject *parent) : QObject(parent) { }

void GameBoard::init(CellStatus s, bool genseed)
{
    cells_.clear();
    qDeleteAll(ships_);
    ships_.clear();

    for (int i = 0; i < 100; ++i) {
        cells_.append(GameCell(s));
        if (genseed)
            cells_[i].seed = genSeed(32);
    }
    ships_.append(new GameShip(5, QString(), this));
    ships_.append(new GameShip(4, QString(), this));
    ships_.append(new GameShip(3, QString(), this));
    ships_.append(new GameShip(2, QString(), this));
    ships_.append(new GameShip(2, QString(), this));
    ships_.append(new GameShip(1, QString(), this));
    ships_.append(new GameShip(1, QString(), this));
}

void GameBoard::makeShipRandomPosition()
{
    int scnt = ships_.count();
    for (int snum = 0; snum < scnt; ++snum) {
        GameShip          *ship = ships_.at(snum);
        int                slen = ship->length();
        GameShip::ShipType dir;
        for (;;) {
            dir = GameShip::ShipHorizontal;
            int div;
            if (slen > 1 && (QRandomGenerator::global()->generate() & 1)) {
                dir = GameShip::ShipVertical;
                div = 100 - 10 * (slen - 1);
            } else
                div = 100 - (slen - 1);
            ship->setDirection(dir);
            ship->setPosition(QRandomGenerator::global()->generate() % div);
            if (isShipPositionLegal(snum))
                break;
        }
        int offset = 1;
        if (dir == GameShip::ShipVertical)
            offset = 10;
        int                pos = ship->position();
        QCryptographicHash sha1(QCryptographicHash::Sha1);
        for (; slen != 0; --slen) {
            cells_[pos].ship   = snum;
            cells_[pos].status = CellOccupied;
            sha1.addData(cells_.at(pos).seed.toUtf8());
            pos += offset;
        }
        ship->setDigest(sha1.result().toHex());
    }
}

void GameBoard::calculateCellsHash()
{
    int                cnt = cells_.count();
    QCryptographicHash sha1(QCryptographicHash::Sha1);
    for (int i = 0; i < cnt; ++i) {
        sha1.reset();
        sha1.addData(cells_.at(i).seed.toUtf8());
        sha1.addData((cells_.at(i).ship == -1) ? "0" : "1");
        cells_[i].digest = QString(sha1.result().toHex());
    }
}

bool GameBoard::updateCellDigest(int pos, const QString &digest)
{
    if (pos >= 0 && pos < cells_.count() && digest.length() == 40) {
        cells_[pos].digest = digest;
        return true;
    }
    return false;
}

bool GameBoard::updateCell(int pos, CellStatus cs, const QString &seed)
{
    if (pos >= 0 && pos < cells_.count()) {
        if (!cells_.at(pos).seed.isEmpty()) // again shot at this position
            return true;

        QString seed_   = seed + ((cs == CellHit || cs == CellOccupied) ? "1" : "0");
        QString digest_ = QCryptographicHash::hash(seed_.toUtf8(), QCryptographicHash::Sha1).toHex();
        if (digest_ == cells_.at(pos).digest) {
            cells_[pos].seed = seed;
            if (cells_.at(pos).status == CellUnknown)
                cells_[pos].status = cs;
            return true;
        }
    }
    return false;
}

bool GameBoard::updateShipDigest(int length, const QString &digest)
{
    GameShip *ship = findShip(length, QString());
    if (ship) {
        ship->setDigest(digest);
        return true;
    }
    return false;
}

void GameBoard::shot(int pos)
{
    CellStatus cs = cells_.at(pos).status;
    if (cs == CellFree)
        cells_[pos].status = CellMiss;
    else if (cs == CellOccupied) {
        cells_[pos].status = CellHit;
        int       snum     = cells_.at(pos).ship;
        GameShip *ship     = ships_.at(snum);
        bool      destr    = true;
        int       p        = -1;
        while ((p = ship->nextPosition(p)) != -1)
            if (cells_.at(p).status != CellHit) {
                destr = false;
                break;
            }
        if (destr) {
            ship->setDestroyed(true);
            emit shipDestroyed(snum);
        }
    }
}

GameShip::ShipType GameBoard::shipDirection(int pos)
{
    GameShip::ShipType dir = GameShip::ShipDirUnknown;
    if ((pos >= 10 && (cells_.at(pos - 10).status == CellHit || cells_.at(pos - 10).status == CellOccupied))
        || (pos <= 89 && (cells_.at(pos + 10).status == CellHit || cells_.at(pos + 10).status == CellOccupied)))
        dir = GameShip::ShipVertical;
    else {
        int col = pos % 10;
        if ((col > 0 && (cells_.at(pos - 1).status == CellHit || cells_.at(pos - 1).status == CellOccupied))
            || (col < 9 && (cells_.at(pos + 1).status == CellHit || cells_.at(pos + 1).status == CellOccupied)))
            dir = GameShip::ShipHorizontal;
    }
    return dir;
}

int GameBoard::findAndInitShip(int pos)
{
    GameShip::ShipType dir = shipDirection(pos);
    if (dir == GameShip::ShipDirUnknown)
        dir = GameShip::ShipHorizontal; // For one-cell ship
    // Find starting coordinate
    int spos = pos;
    if (dir == GameShip::ShipHorizontal)
        while (spos % 10 != 0 && (cells_.at(spos - 1).status == CellHit || cells_.at(spos - 1).status == CellOccupied))
            --spos;
    else
        while (spos >= 10 && (cells_.at(spos - 10).status == CellHit || cells_.at(spos - 10).status == CellOccupied))
            spos -= 10;
    // There do calculating the ship's hash and size
    int                ssz  = 0;
    int                epos = spos;
    QCryptographicHash sha1(QCryptographicHash::Sha1);
    for (;;) {
        ++ssz;
        sha1.addData(cells_.at(epos).seed.toUtf8());
        if (dir == GameShip::ShipHorizontal) {
            if (epos % 10 == 9 || (cells_.at(epos + 1).status != CellHit && cells_.at(epos + 1).status != CellOccupied))
                break;
            ++epos;
        } else {
            if (epos >= 90 || (cells_.at(epos + 10).status != CellHit && cells_.at(epos + 10).status != CellOccupied))
                break;
            epos += 10;
        }
    }
    // Find ship by hash and size
    QString digest = QString(sha1.result().toHex());
    int     snum   = -1;
    for (int i = 0; i < ships_.count(); ++i) {
        GameShip *ship = ships_.at(i);
        if (ship->length() == ssz && ship->digest() == digest) {
            ship->setDirection(dir);
            ship->setPosition(spos);
            snum = i;
            break;
        }
    }
    if (snum != -1) {
        // Update cells
        for (int i = 0; i < ssz; ++i) {
            cells_[spos].ship = snum;
            ++spos;
            if (dir == GameShip::ShipVertical)
                spos += 9;
        }
    }
    return snum;
}

QRect GameBoard::shipRect(int snum, bool margin) const
{
    QRect     r;
    GameShip *ship = ships_.at(snum);
    int       pos  = ship->position();
    r.setTopLeft(QPoint(pos % 10, pos / 10));
    if (ship->direction() == GameShip::ShipHorizontal) {
        r.setWidth(ship->length());
        r.setHeight(1);
    } else {
        r.setWidth(1);
        r.setHeight(ship->length());
    }
    if (margin) {
        r.adjust(-1, -1, 1, 1);
        r = r.intersected(QRect(0, 0, 10, 10));
    }
    return r;
}

void GameBoard::setShipDestroy(int n, bool margin)
{
    GameShip *ship = ships_.at(n);
    if (!ship->isDestroyed()) {
        ship->setDestroyed(true);
        if (margin)
            fillShipMargin(n);
        emit shipDestroyed(n);
    }
}

const GameBoard::GameCell &GameBoard::cell(int pos) const { return cells_.at(pos); }

QStringList GameBoard::toStringList(bool covered) const
{
    QStringList res;
    int         cnt = cells_.count();
    for (int i = 0; i < cnt; ++i) {
        const GameCell &c = cells_.at(i);
        QString         s;
        if (covered)
            s = QString("cell;%1;%2").arg(i).arg(c.digest);
        else
            s = QString("%1;%2;%3").arg(i).arg((c.ship == -1) ? "0" : "1").arg(c.seed);
        res.append(s);
    }
    if (covered) {
        cnt = ships_.count();
        for (int i = 0; i < cnt; ++i) {
            const GameShip *ship = ships_.at(i);
            res.append(QString("ship;%1;%2").arg(ship->length()).arg(ship->digest()));
        }
    }
    return res;
}

bool GameBoard::isAllDestroyed() const
{
    foreach (const GameShip *ship, ships_)
        if (!ship->isDestroyed())
            return false;
    return true;
}

QString GameBoard::genSeed(int len)
{
    static QString chars("1234567890qwertyuiopasdfghjklzxcvbnmQWERTYUIOPASDFGHJKLZXCVBNM-=[]/!@#$%^&*()");
    int            ccnt = chars.length();
    int            rnd  = 0;
    QString        res;
    for (int i = 0; i < len; ++i) {
        if (rnd < ccnt)
            rnd = QRandomGenerator::global()->generate();
        res.append(chars.at(rnd % ccnt));
        rnd /= ccnt;
    }
    return res;
}

GameShip *GameBoard::findShip(int length, const QString &digest)
{
    foreach (GameShip *ship, ships_)
        if (ship->length() == length && ship->digest() == digest)
            return ship;
    return nullptr;
}

bool GameBoard::isShipPositionLegal(int shipNum)
{
    GameShip          *ship = ships_.at(shipNum);
    GameShip::ShipType dir  = ship->direction();
    int                pos  = ship->position();
    int                len  = ship->length();
    int                off  = (dir == GameShip::ShipHorizontal) ? 1 : 10;
    int                row  = pos / 10;
    int                col  = pos % 10;
    int                epos = pos + (len - 1) * off;
    if ((dir == GameShip::ShipHorizontal && (int)(epos) / 10 != row) || (dir == GameShip::ShipVertical && epos >= 100))
        return false;

    int m = 1;
    if (dir == GameShip::ShipHorizontal) {
        if (row > 0) {
            ++m;
            pos -= 10;
        }
        if (col > 0) {
            --pos;
            ++len;
        }
        if (row < 9)
            ++m;
        if (epos % 10 < 9)
            ++len;
    } else {
        if (col > 0) {
            ++m;
            pos -= 1;
        }
        if (row > 0) {
            pos -= 10;
            ++len;
        }
        if (col < 9)
            ++m;
        if (epos < 90)
            ++len;
    }
    for (; m != 0; --m) {
        int p = pos;
        for (int l = len; l != 0; --l) {
            CellStatus cs = cells_.at(p).status;
            if ((cs == CellOccupied || cs == CellHit) && cells_.at(p).ship != shipNum)
                return false;
            p += off;
        }
        pos += (dir == GameShip::ShipHorizontal) ? 10 : 1;
    }
    return true;
}

void GameBoard::fillShipMargin(int n)
{
    GameShip          *ship = ships_.at(n);
    int                pos  = ship->position();
    int                len  = ship->length();
    GameShip::ShipType dir  = ship->direction();
    struct {
        int offset;
        int weight;
    } m[8];
    m[7].offset = -11;
    m[0].offset = -10;
    m[1].offset = -9;
    m[6].offset = -1;
    m[2].offset = 1;
    m[5].offset = 9;
    m[4].offset = 10;
    m[3].offset = 11;
    for (int i = 1; i <= len; ++i) {
        int row = pos / 10;
        int col = pos % 10;
        for (int k = 0; k < 8; ++k)
            m[k].weight = 0;
        if (row > 0) {
            ++m[7].weight;
            ++m[0].weight;
            ++m[1].weight;
        }
        if (row < 9) {
            ++m[5].weight;
            ++m[4].weight;
            ++m[3].weight;
        }
        if (col > 0) {
            ++m[7].weight;
            ++m[6].weight;
            ++m[5].weight;
        }
        if (col < 9) {
            ++m[1].weight;
            ++m[2].weight;
            ++m[3].weight;
        }
        int nextOffset;
        if (dir == GameShip::ShipHorizontal) {
            nextOffset = 1;
            ++m[0].weight;
            ++m[4].weight;
            if (i == 1) {
                ++m[7].weight;
                ++m[6].weight;
                ++m[5].weight;
            }
            if (i == len) {
                ++m[1].weight;
                ++m[2].weight;
                ++m[3].weight;
            }
        } else {
            nextOffset = 10;
            ++m[6].weight;
            ++m[2].weight;
            if (i == 1) {
                ++m[7].weight;
                ++m[0].weight;
                ++m[1].weight;
            }
            if (i == len) {
                ++m[5].weight;
                ++m[4].weight;
                ++m[3].weight;
            }
        }
        for (int k = 0; k < 8; ++k)
            if (m[k].weight == 3 || ((k & 1) == 0 && m[k].weight == 2)) {
                if (cells_.at(pos + m[k].offset).status == CellUnknown)
                    cells_[pos + m[k].offset].status = CellMargin;
            }
        pos += nextOffset;
    }
}

//------------- GameShip ----------------

GameShip::GameShip(int len, const QString &digest, QObject *parent) :
    QObject(parent), length_(len), direction_(ShipDirUnknown), firstPos_(-1), destroyed_(false), digest_(digest)
{
}

void GameShip::setDirection(ShipType dir) { direction_ = dir; }

void GameShip::setPosition(int pos) { firstPos_ = pos; }

void GameShip::setDigest(const QString &digest) { digest_ = digest; }

void GameShip::setDestroyed(bool destr) { destroyed_ = destr; }

int GameShip::nextPosition(int prev)
{
    if (prev == -1)
        return firstPos_;
    int offs = (direction_ == ShipHorizontal) ? 1 : 10;
    if ((prev - firstPos_) >= (length_ - 1) * offs)
        return -1;
    return prev + offs;
}
