/***************************************************************************
								  gamelist.cpp  -  Game List window
									  -------------------
	 begin                : Sun 23 Jul 2006
	 copyright            : (C) 2006 Michal Rudolf <mrudolf@kdewebdev.org>
 ***************************************************************************/

/***************************************************************************
 *                                                                         *
 *   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.                                   *
 *                                                                         *
 ***************************************************************************/

#include "database.h"
#include "filter.h"
#include "filtermodel.h"
#include "gamelist.h"
#include "gamelistsortmodel.h"
#include "GameMimeData.h"
#include "numbersearch.h"
#include "qt6compat.h"
#include "quicksearch.h"
#include "settings.h"
#include "tags.h"
#include "tagsearch.h"

#include "gamex.h"
#include "output.h"
#include "boardview.h"

#include <qevent.h>
#include <QDrag>
#include <QHeaderView>
#include <QMenu>
#include <QPixmap>
#include <QRandomGenerator>

#if defined(_MSC_VER) && defined(_DEBUG)
#define DEBUG_NEW new( _NORMAL_BLOCK, __FILE__, __LINE__ )
#define new DEBUG_NEW
#endif // _MSC_VER

void GameListDelegate::setModelData(QWidget *editor, QAbstractItemModel *model,
        const QModelIndex &index) const
{
    QStyledItemDelegate::setModelData(editor, model, index);
}

GameList::GameList(FilterX* filter, QWidget* parent) : TableView(parent)
{
    setSelectionMode(QAbstractItemView::ExtendedSelection);
    setObjectName("GameList");
    setWindowTitle(tr("Game list"));
    m_model = new FilterModel(filter);
    connect(m_model, SIGNAL(searchProgress(int)), SIGNAL(searchProgress(int)));
    connect(m_model, SIGNAL(searchFinished()), SIGNAL(searchFinished()));
    connect(m_model, SIGNAL(dataChanged(QModelIndex,QModelIndex)), SLOT(itemDataChanged(QModelIndex,QModelIndex)));
    sortModel = new GameListSortModel(nullptr);
    sortModel->setFilter(filter);
    sortModel->setSourceModel(m_model);
    sortModel->setDynamicSortFilter(true);
    sortModel->setSortRole(Qt::UserRole);
    setModel(sortModel);

    connect(this, SIGNAL(clicked(QModelIndex)), SLOT(itemSelected(QModelIndex)));
    connect(this, SIGNAL(activated(QModelIndex)), SLOT(itemSelected(QModelIndex)));
    connect(this, SIGNAL(customContextMenuRequested(QPoint)), SLOT(slotContextMenu(QPoint)));
    connect(selectionModel(), SIGNAL(currentChanged(QModelIndex,QModelIndex)),
            this, SLOT(slotItemSelected(QModelIndex)));
    horizontalHeader()->setSectionsClickable(true);

    horizontalHeader()->setSortIndicatorShown(true);
    setSortingEnabled(true);

    setDragEnabled(true);
    setAcceptDrops(true);

    setEditTriggers(QTableView::DoubleClicked);
    setItemDelegate(new GameListDelegate);
}

GameList::~GameList()
{
    setModel(nullptr);
    delete sortModel;
    delete m_model;
}

void GameList::startUpdate()
{
    if (m_model)
    {
        m_model->startUpdate();
    }
}

void GameList::endUpdate()
{
    if (m_model)
    {
        m_model->endUpdate();
    }
}

void GameList::itemDataChanged(const QModelIndex & topLeft, const QModelIndex & bottomRight)
{
    if (m_model && topLeft.column()==bottomRight.column())
    {
        GameId n = topLeft.row();
        int column = topLeft.column();
        QString tag = m_model->GetColumnTags().at(column);

        emit gameTagChanged(n, tag);
    }
}

void GameList::slotReconfigure()
{
    if (m_model)
    {
        m_model->updateColumns();
    }
    TableView::slotReconfigure();
}

void GameList::ShowContextMenu(const QPoint& pos)
{
    QMenu headerMenu;
    QAction* filterTag = headerMenu.addAction(tr("Find tag..."));
    headerMenu.addSeparator();
    QAction* hide = headerMenu.addAction(tr("Hide Column"));
    headerMenu.addSeparator();
    QAction* resizeAll = headerMenu.addAction(tr("Resize visible Columns"));
    QAction* showAll = headerMenu.addAction(tr("Show all Columns"));

    QAction* selectedItem = headerMenu.exec(mapToGlobal(pos));
    int column = columnAt(pos.x());
    if (selectedItem == filterTag)
    {
        simpleSearch(column);
    }
    else if(selectedItem == hide)
    {
        if(column > 0)
        {
            hideColumn(column);
        }
    }
    else if(selectedItem == showAll)
    {
        for(int i = 0; i < model()->columnCount(); ++i)
        {
            showColumn(i);
        }
    }
    else if (selectedItem == resizeAll)
    {
        resizeColumnsToContents();
    }
}

void GameList::removeSelection()
{
    clearSelection();
    emit signalFirstGameLoaded(true);
    emit signalLastGameLoaded(true);
}

void GameList::slotItemSelected(const QModelIndex& index)
{
    scrollTo(index, EnsureVisible);
}

void GameList::filterInvert()
{
    if (m_model)
    {
        m_model->invert();
    }
}

void GameList::filterSetAll(int value)
{
    if (m_model)
    {
        m_model->setAll(value);
    }
}

QModelIndex GameList::NewSortIndex(int row) const
{
    QModelIndex m = sortModel->index(row,0);
    return m;
}

QModelIndex GameList::GetSourceIndex(const QModelIndex& index) const
{
    QModelIndex m = sortModel->mapToSource(index);
    return m;
}

QModelIndex GameList::GetSortModelIndex(const QModelIndex& index) const
{
    QModelIndex m = sortModel->mapFromSource(index);
    return m;
}

void GameList::itemSelected(const QModelIndex& index)
{
    if (selectionModel()->selectedRows().size() == 1)
    {
        QModelIndex m = GetSourceIndex(index);
        GameId n = m.row();
        if (VALID_INDEX(n))
        {
            emit gameSelected(n);
        }
    }
}

bool GameList::triggerGameSelection(int sortRow)
{
    QModelIndex sortIndex = NewSortIndex(sortRow);
    QModelIndex sourceIndex = GetSourceIndex(sortIndex);
    GameId game = sourceIndex.row();
    if (VALID_INDEX(game))
    {
        emit gameSelected(sourceIndex.row());
        return true;
    }
    return false;
}

void GameList::selectPreviousGame()
{
    QModelIndex sortIndex = currentIndex();
    int oldRow = sortIndex.row();
    int row = std::max(0, sortIndex.row()-1);
    if (row != oldRow)
    {
        triggerGameSelection(row);
    }
}

bool GameList::selectNextGame()
{
    QModelIndex sortIndex = currentIndex();
    int oldRow = sortIndex.row();
    int row = std::min(m_model->filter()->count()-1, oldRow+1);
    if (row != oldRow)
    {
        return triggerGameSelection(row);
    }
    return false;
}

void GameList::selectRandomGame()
{
    if(m_model->filter()->count()>1)
    {
        QModelIndex sortIndex = currentIndex();
        int oldRow = sortIndex.row();
        int randomSortRow = QRandomGenerator::global()->bounded(0,m_model->filter()->count()-2); // The last game is represented by current game
        if (oldRow == randomSortRow)
        {
            randomSortRow = m_model->filter()->count()-1;
        }
        QModelIndex sourceIndex = GetSourceIndex(NewSortIndex(randomSortRow));
        GameId game = sourceIndex.row();
        if (VALID_INDEX(game))
        {
            emit gameSelected(game);
        }
    }
}

void GameList::setFilter(FilterX* filter)
{
	if (filter)
	{
        m_model->startUpdate();
        sortModel->setFilter(filter);
		m_model->setFilter(filter);
        sortByColumn(0, Qt::AscendingOrder); // Hack to ensure fast opening after loading DB
        m_model->endUpdate();
    }
    if (AppSettings->value("/MainWindow/AutoRaise").toBool())
    {
        emit raiseRequest();
    }
}

void GameList::keyPressEvent(QKeyEvent* event)
{
    if ((event->modifiers() & Qt::KeyboardModifierMask) == Qt::MetaModifier)
    {
        switch (event->key())
        {
            case Qt::Key_A: selectAll();      break;
            case Qt::Key_C: slotCopyGame();   break;
            case Qt::Key_X: slotDeleteGame(); break;
            case Qt::Key_H: slotHideGame(); break;
        }
        update();
    }
    TableView::keyPressEvent(event);
}

void GameList::slotContextMenu(const QPoint& pos)
{
    QModelIndex cell = indexAt(pos);
    QModelIndexList selection = selectedIndexes();
    QMenu menu(tr("Game list"), this);
    if(cell.isValid() && selection.contains(cell))
    {
        // Right click occured on a cell!
        menu.addAction(tr("Copy games..."), this, SLOT(slotCopyGame()));
        menu.addAction(tr("Filter exact twins"), this, SLOT(slotFindDuplicate()));
        menu.addAction(tr("Filter twins"), this, SLOT(slotFindIdentical()));
        QMenu* mergeMenu = menu.addMenu(tr("Merge into current game"));
        mergeMenu->addAction(tr("All Games"), this, SLOT(slotMergeAllGames()));
        mergeMenu->addAction(tr("Filter"), this, SLOT(slotMergeFilter()));
        mergeMenu->addAction(tr("Selected games"), this, SLOT(slotMergeSelectedGames()));
        menu.addSeparator();

        int deleted = 0;
        int activated = 0;
        QModelIndexList list = selectionModel()->selectedRows();
        foreach(QModelIndex index, list)
        {
            QModelIndex source = GetSourceIndex(index);
            GameId n = source.row();
            if (VALID_INDEX(n))
            {
                if (m_model->filter()->database()->deleted(n))
                {
                   ++deleted;
                }
                else
                {
                    ++activated;
                }
            }
            if (activated && deleted)
            {
                break;
            }
        }

        QAction* deleteAction;
        if (activated && deleted)
        {
            deleteAction = menu.addAction(tr("Toggle deletions"), this, SLOT(slotDeleteGame()));
        }
        else
        {
            QString deleteText;
            if (deleted)
            {
                deleteText = selection.count()>1 ? tr("Undelete games") : tr("Undelete game");
            }
            else
            {
                deleteText = selection.count()>1 ? tr("Delete games") : tr("Delete game");
            }
            deleteAction = menu.addAction(deleteText, this, SLOT(slotDeleteGame()));
        }
        deleteAction->setEnabled(!m_model->filter()->database()->isReadOnly());

        menu.addSeparator();
        QString hideText = selection.count()>1 ? tr("Hide games") : tr("Hide game");
        menu.addAction(hideText, this, SLOT(slotHideGame()));
        deleteAction = menu.addAction(tr("Hide deleted games"), this, SLOT(slotHideDeletedGames()));
        deleteAction->setEnabled(activated && deleted);
        menu.addAction(tr("Select All"), this, SLOT(selectAll()));
        menu.addSeparator();
    }
    menu.addAction(tr("Reset filter"), this, SLOT(filterSetAll()));
    menu.addAction(tr("Reverse filter"), this, SLOT(filterInvert()));
    menu.exec(mapToGlobal(pos));
}

void GameList::executeSearch(Search* search, FilterOperator searchOperator)
{
    m_model->executeSearch(search, searchOperator, 0);
}

void GameList::simpleSearch(int tagid)
{
    QuickSearchDialog dlg(this);
    for (int section = 0; section < m_model->columnCount(); ++section)
    {
        QString tag = m_model->headerData(section, Qt::Horizontal).toString();
        dlg.addTag(tag);
    }

    dlg.setTag(tagid);
    dlg.setMode(1);

    if(dlg.exec() != QDialog::Accepted)
    {
        return;
    }

    QString tag = m_model->GetColumnTags().at(dlg.tag());
    QString value = dlg.value();
    if(value.isEmpty())
    {
        m_model->filter()->setAll(1);
    }
    else if(dlg.tag() == 0)
    {
        // filter by number
        Search* ns = new NumberSearch(m_model->filter()->database(), value);
        m_model->executeSearch(ns, FilterOperator(dlg.mode()));
    }
    else
    {
        QStringList list = value.split("-", SkipEmptyParts);
        if ((list.size() > 1) && (dlg.tag() != 9)) // Tag 9 is the Result
        {
            // Filter a range
            Search* ts = (dlg.tag() == 11) ? // Tag 11 is number of moves
                    new TagSearch(m_model->filter()->database(), tag, list.at(0).toInt(), list.at(1).toInt()) :
                    new TagSearch(m_model->filter()->database(), tag, list.at(0), list.at(1));
            if(dlg.mode())
            {
                m_model->executeSearch(ts, FilterOperator(dlg.mode()));
            }
            else
            {
                m_model->executeSearch(ts);
            }
        }
        else
        {
            // Filter tag using partial values
            Search* ts = new TagSearch(m_model->filter()->database(), tag, value);
            m_model->executeSearch(ts, FilterOperator(dlg.mode()));
        }
    }
    if (AppSettings->value("/MainWindow/AutoRaise").toBool())
    {
        emit raiseRequest();
    }
}

void GameList::slotFilterListByPlayer(QString s)
{
    FilterOperator op = FilterOperator::NullOperator;
    if (s.startsWith("+"))
    {
        s.remove(0,1);
        op = FilterOperator::Or;
    }
    QUrl url(s);
    QString fragment = url.fragment();

    if (fragment.isEmpty())
    {
        Search* ts = new TagSearch(m_model->filter()->database(),  TagNameWhite, url.path());
        Search* ts2 = new TagSearch(m_model->filter()->database(), TagNameBlack, url.path());
        ts->AddSearch(ts2, FilterOperator::Or);
        m_model->executeSearch(ts, op);
    }
    else
    {
        Search* ts = new TagSearch(m_model->filter()->database(),  fragment, url.path());
        m_model->executeSearch(ts, op);
    }
    if (AppSettings->value("/MainWindow/AutoRaise").toBool())
    {
        emit raiseRequest();
    }
}

void GameList::slotFilterListByEcoPlayer(QString tag, QString eco, QString player, QString result)
{
    m_model->filter()->setAll(1); // ??
    Search* ts = nullptr;
    if (!player.isEmpty())
    {
        ts = new TagSearch(m_model->filter()->database(),  tag, player);
    }
    Search* ts2 = ts;
    if (!eco.isEmpty())
    {
        ts2 = new TagSearch(m_model->filter()->database(), TagNameECO, eco);
        if (ts)
        {
            ts->AddSearch(ts2, FilterOperator::And);
        }
        else
        {
            ts = ts2;
        }
    }
    if (!result.isEmpty())
    {
        Search* ts3 = new TagSearch(m_model->filter()->database(), TagNameResult, result);
        if (ts2)
        {
            ts2->AddSearch(ts3, FilterOperator::And);
        }
        else // ts is also 0, as otherwise t2 is non-null
        {
            ts = ts3;
        }
    }
    m_model->executeSearch(ts);
    if (AppSettings->value("/MainWindow/AutoRaise").toBool())
    {
        emit raiseRequest();
    }
}

void GameList::slotFilterListByEvent(QString s)
{
    FilterOperator op = FilterOperator::NullOperator;
    if (s.startsWith("+"))
    {
        s.remove(0,1);
        op = FilterOperator::Or;
    }

    Search* ts = new TagSearch(m_model->filter()->database(), TagNameEvent, s);
    m_model->executeSearch(ts, op);
    if (AppSettings->value("/MainWindow/AutoRaise").toBool())
    {
        emit raiseRequest();
    }
}

void GameList::slotFilterListByEventPlayer(QString player, QString event)
{
    m_model->filter()->setAll(1);
    Search* ts = new TagSearch(m_model->filter()->database(),  TagNameWhite, player);
    Search* ts2 = new TagSearch(m_model->filter()->database(), TagNameBlack, player);
    Search* ts3 = new TagSearch(m_model->filter()->database(), TagNameEvent, event);
    ts->AddSearch(ts2, FilterOperator::Or);
    ts2->AddSearch(ts3, FilterOperator::And);
    m_model->executeSearch(ts);
    if (AppSettings->value("/MainWindow/AutoRaise").toBool())
    {
        emit raiseRequest();
    }
}

void GameList::slotFilterListByEco(QString s)
{
    FilterOperator op = FilterOperator::NullOperator;
    if (s.startsWith("+"))
    {
        s.remove(0,1);
        op = FilterOperator::Or;
    }
    Search* ts = new TagSearch(m_model->filter()->database(), TagNameECO, s);
    m_model->executeSearch(ts, op);
}

void GameList::endSearch()
{
    if (AppSettings->value("/MainWindow/AutoRaise").toBool())
    {
        emit raiseRequest();
    }
}

void GameList::selectGame(GameId index)
{
    if (VALID_INDEX(index))
    {
        QModelIndex filterModelIndex = m_model->index(index, 0);
        QModelIndex sortModelIndex = GetSortModelIndex(filterModelIndex);
        setCurrentIndex(sortModelIndex);
        emit signalFirstGameLoaded(!m_model->filter()->count() || (sortModelIndex.row()==0));
        emit signalLastGameLoaded(sortModelIndex.row()+1 >= m_model->filter()->count());
    }
    else
    {
        emit signalFirstGameLoaded(true);
        emit signalLastGameLoaded(true);
    }
    emit signalFilterSize(m_model->filter()->count());
}

void GameList::updateFilter(GameId index, int value)
{
    if (m_model->filter()->database()) // ?
    {
        if (VALID_INDEX(index))
        {
            m_model->set(index, value);
        }
    }
}

QList<GameId> GameList::selectedGames(bool skipDeletedGames)
{
    QList<GameId> gameIndexList;
    Database* db = m_model->filter()->database();

    foreach(QModelIndex index, selectionModel()->selectedRows())
    {
        QModelIndex m = GetSourceIndex(index);
        GameId n = m.row();
        if (VALID_INDEX(n) && (!skipDeletedGames || !db->deleted(n)))
        {
            gameIndexList.append(n);
        }
    }
    return gameIndexList;
}

void GameList::slotCopyGame()
{
    QList<GameId> gameIndexList = selectedGames(true);
    emit requestCopyGame(gameIndexList);
}

void GameList::slotFindDuplicate()
{
    QList<GameId> gameIndexList = selectedGames(true);
    emit requestFindDuplicates(gameIndexList);
}

void GameList::slotFindIdentical()
{
    QList<GameId> gameIndexList = selectedGames(true);
    emit requestFindIdenticals(gameIndexList);
}

void GameList::slotMergeAllGames()
{
    emit requestMergeAllGames();
}

void GameList::slotMergeFilter()
{
    emit requestMergeFilter();
}

void GameList::slotMergeSelectedGames()
{
    QList<GameId> gameIndexList = selectedGames(true);
    emit requestMergeGame(gameIndexList);
}

void GameList::slotDeleteGame()
{
    QList<GameId> gameIndexList = selectedGames(false);
    emit requestDeleteGame(gameIndexList);
}

void GameList::slotHideGame()
{
    QList<GameId> gameIndexList = selectedGames(false);
    foreach(GameId game, gameIndexList)
    {
        m_model->set(game, 0);
    }
}

void GameList::slotHideDeletedGames()
{
    QList<GameId> gameIndexList = selectedGames(false);
    if (!gameIndexList.isEmpty())
    {
        m_model->startUpdate();
        foreach(GameId game, gameIndexList)
        {
            if (m_model->filter()->database()->deleted(game))
            {
                m_model->set(game,0);
            }
        }
        m_model->endUpdate();
    }
}

void GameList::startDrag(Qt::DropActions supportedActions)
{
    TableView::startDrag(supportedActions);

    GameMimeData *mimeData = new GameMimeData;
    Database* db = m_model->filter()->database();
    mimeData->source = db->filename();
    mimeData->m_indexList = selectedGames(true);

    if (mimeData->m_indexList.count() < 1000) // Avoid excessive size of clipboard
    {
        QString text;
        foreach(GameId gameIndex, mimeData->m_indexList)
        {
            GameX g;
            if(m_model->filter()->database()->loadGame(gameIndex, g))
            {
                Output textWriter(Output::Pgn, &BoardView::renderImageForBoard);
                QString pgn = textWriter.output(&g);
                if (!text.isEmpty())
                {
                    text.append("\r\n");
                }
                text.append(pgn);
                text.append("\r\n");
            }
        }
        mimeData->setText(text);
    }

    QPixmap pixmap = style()->standardPixmap(QStyle::SP_FileIcon);

    QDrag* pDrag = new QDrag(this);
    pDrag->setMimeData(mimeData);
    pDrag->setPixmap(pixmap);

    pDrag->exec(Qt::CopyAction | Qt::MoveAction, Qt::CopyAction);
}

void GameList::dragEnterEvent(QDragEnterEvent *event)
{
    TableView::dragEnterEvent(event);
    const QMimeData *mimeData = event->mimeData();
    const DbMimeData* dbMimeData = qobject_cast<const DbMimeData*>(mimeData);
    const GameMimeData* gameMimeData = qobject_cast<const GameMimeData*>(mimeData);

    if (gameMimeData && (m_model->filter()->database()->filename() == gameMimeData->source))
    {
        event->acceptProposedAction();
    }
    else if(dbMimeData || gameMimeData || (mimeData && mimeData->hasUrls()))
    {
        event->acceptProposedAction();
    }
}

void GameList::dragMoveEvent(QDragMoveEvent *event)
{
    TableView::dragMoveEvent(event);
    const QMimeData *mimeData = event->mimeData();
    const DbMimeData* dbMimeData = qobject_cast<const DbMimeData*>(mimeData);
    const GameMimeData* gameMimeData = qobject_cast<const GameMimeData*>(mimeData);

    if (gameMimeData && (m_model->filter()->database()->filename() == gameMimeData->source))
    {
        event->ignore();
    }
    else if(dbMimeData || gameMimeData || (mimeData && mimeData->hasUrls()))
    {
        event->acceptProposedAction();
    }
    else
    {
        event->ignore();
    }
}

void GameList::dragLeaveEvent(QDragLeaveEvent *event)
{
    TableView::dragLeaveEvent(event);
    event->accept();
}

void GameList::dropEvent(QDropEvent *event)
{
    TableView::dropEvent(event);
    if (event->dropAction())
    {
        const QMimeData *mimeData = event->mimeData();
        const DbMimeData* dbMimeData = qobject_cast<const DbMimeData*>(mimeData);
        const GameMimeData* gameMimeData = qobject_cast<const GameMimeData*>(mimeData);

        if(dbMimeData || (mimeData && mimeData->hasUrls()))
        {
            m_model->startUpdate();
            emit signalDropEvent(event);
            m_model->endUpdate();
        }
        else if (gameMimeData)
        {
            emit requestAppendGames(m_model->filter()->database()->filename(), gameMimeData->m_indexList, gameMimeData->source);
        }
        else
        {
            event->ignore();
        }
    }
}
