#include "table_zone.h"

#include "../../client/settings/cache_settings.h"
#include "../../game/player/player_actions.h"
#include "../../game/player/player_logic.h"
#include "../../game/zones/table_zone_logic.h"
#include "../../interface/theme_manager.h"
#include "../board/arrow_item.h"
#include "../board/card_drag_item.h"
#include "../board/card_item.h"
#include "../z_values.h"

#include <QGraphicsScene>
#include <QPainter>
#include <libcockatrice/card/card_info.h>
#include <libcockatrice/protocol/pb/command_move_card.pb.h>
#include <libcockatrice/protocol/pb/command_set_card_attr.pb.h>
#include <libcockatrice/utility/zone_names.h>

const QColor TableZone::BACKGROUND_COLOR = QColor(100, 100, 100);
const QColor TableZone::FADE_MASK = QColor(0, 0, 0, 80);
const QColor TableZone::GRADIENT_COLOR = QColor(255, 255, 255, 150);
const QColor TableZone::GRADIENT_COLORLESS = QColor(255, 255, 255, 0);

TableZone::TableZone(TableZoneLogic *_logic, bool _mirrored, QGraphicsItem *parent)
    : SelectZone(_logic, parent), active(false), mirrored(_mirrored)
{
    connect(_logic, &TableZoneLogic::contentSizeChanged, this, &TableZone::resizeToContents);
    connect(_logic, &TableZoneLogic::toggleTapped, this, &TableZone::toggleTapped);
    connect(themeManager, &ThemeManager::themeChanged, this, &TableZone::updateBg);
    connect(&SettingsCache::instance(), &SettingsCache::invertVerticalCoordinateChanged, this,
            &TableZone::reorganizeCards);

    updateBg();

    height = MARGIN_TOP + MARGIN_BOTTOM + TABLEROWS * CardDimensions::HEIGHT + (TABLEROWS - 1) * PADDING_Y;
    width = MIN_WIDTH;
    currentMinimumWidth = width;

    setCacheMode(DeviceCoordinateCache);
    setAcceptHoverEvents(true);
}

void TableZone::updateBg()
{
    update();
}

QRectF TableZone::boundingRect() const
{
    return QRectF(0, 0, width, height);
}

void TableZone::setMirrored(bool isMirrored)
{
    mirrored = isMirrored;
    update();
}

bool TableZone::isInverted() const
{
    return ((mirrored && !SettingsCache::instance().getInvertVerticalCoordinate()) ||
            (!mirrored && SettingsCache::instance().getInvertVerticalCoordinate()));
}

void TableZone::paint(QPainter *painter, const QStyleOptionGraphicsItem * /*option*/, QWidget * /*widget*/)
{
    QBrush brush = themeManager->getExtraBgBrush(ThemeManager::Table, getLogic()->getPlayer()->getZoneId());
    painter->fillRect(boundingRect(), brush);

    if (active) {
        paintZoneOutline(painter);
    } else {
        // inactive player gets a darker table zone with a semi transparent black mask
        // this means if the user provides a custom background it will fade
        painter->fillRect(boundingRect(), FADE_MASK);
    }

    paintLandDivider(painter);
}

/**
   Render a soft outline around the edge of the TableZone.

   @param painter QPainter object
 */
void TableZone::paintZoneOutline(QPainter *painter)
{
    QLinearGradient grad1(0, 0, 0, 1);
    grad1.setCoordinateMode(QGradient::ObjectBoundingMode);
    grad1.setColorAt(0, GRADIENT_COLOR);
    grad1.setColorAt(1, GRADIENT_COLORLESS);
    painter->fillRect(QRectF(0, 0, width, BOX_LINE_WIDTH), QBrush(grad1));

    grad1.setFinalStop(1, 0);
    painter->fillRect(QRectF(0, 0, BOX_LINE_WIDTH, height), QBrush(grad1));

    grad1.setStart(0, 1);
    grad1.setFinalStop(0, 0);
    painter->fillRect(QRectF(0, height - BOX_LINE_WIDTH, width, BOX_LINE_WIDTH), QBrush(grad1));

    grad1.setStart(1, 0);
    painter->fillRect(QRectF(width - BOX_LINE_WIDTH, 0, BOX_LINE_WIDTH, height), QBrush(grad1));
}

/**
   Render a division line for land placement

   @painter QPainter object
 */
void TableZone::paintLandDivider(QPainter *painter)
{
    // Place the line 2 grid heights down then back it off just enough to allow
    // some space between a 3-card stack and the land area.
    qreal separatorY = MARGIN_TOP + 2 * (CardDimensions::HEIGHT + PADDING_Y) - STACKED_CARD_OFFSET_Y / 2;
    if (isInverted()) {
        separatorY = height - separatorY;
    }
    painter->setPen(QColor(255, 255, 255, 40));
    painter->drawLine(QPointF(0, separatorY), QPointF(width, separatorY));
}

void TableZone::handleDropEvent(const QList<CardDragItem *> &dragItems,
                                CardZoneLogic *startZone,
                                const QPoint &dropPoint)
{
    handleDropEventByGrid(dragItems, startZone, mapToGrid(dropPoint));
}

void TableZone::handleDropEventByGrid(const QList<CardDragItem *> &dragItems,
                                      CardZoneLogic *startZone,
                                      const QPoint &gridPoint)
{
    Command_MoveCard cmd;
    cmd.set_start_player_id(startZone->getPlayer()->getPlayerInfo()->getId());
    cmd.set_start_zone(startZone->getName().toStdString());
    cmd.set_target_player_id(getLogic()->getPlayer()->getPlayerInfo()->getId());
    cmd.set_target_zone(getLogic()->getName().toStdString());
    cmd.set_x(gridPoint.x());
    cmd.set_y(gridPoint.y());

    for (const auto &item : dragItems) {
        CardToMove *ctm = cmd.mutable_cards_to_move()->add_card();
        ctm->set_card_id(item->getId());
        if (item->isForceFaceDown()) {
            ctm->set_face_down(true);
        }
        if (startZone->getName() != getLogic()->getName() && !item->isForceFaceDown()) {
            const auto &card = item->getItem()->getCard();
            if (card) {
                ctm->set_pt(card.getInfo().getPowTough().toStdString());
            }
        }
    }

    startZone->getPlayer()->getPlayerActions()->sendGameCommand(cmd);
}

void TableZone::reorganizeCards()
{
    // Calculate card stack widths so mapping functions work properly
    computeCardStackWidths();

    for (int i = 0; i < getLogic()->getCards().size(); ++i) {
        QPoint gridPoint = getLogic()->getCards()[i]->getGridPos();
        if (gridPoint.x() == -1) {
            continue;
        }

        QPointF mapPoint = mapFromGrid(gridPoint);
        qreal x = mapPoint.x();
        qreal y = mapPoint.y();

        int numberAttachedCards = getLogic()->getCards()[i]->getAttachedCards().size();
        qreal actualX = x + numberAttachedCards * STACKED_CARD_OFFSET_X;
        qreal actualY = y;
        if (numberAttachedCards) {
            actualY += 15;
        }

        getLogic()->getCards()[i]->setPos(actualX, actualY);
        getLogic()->getCards()[i]->setRealZValue(ZValues::tableCardZValue(actualX, actualY));

        QListIterator<CardItem *> attachedCardIterator(getLogic()->getCards()[i]->getAttachedCards());
        int j = 0;
        while (attachedCardIterator.hasNext()) {
            ++j;
            CardItem *attachedCard = attachedCardIterator.next();
            qreal childX = actualX - j * STACKED_CARD_OFFSET_X;
            qreal childY = y + 5;
            attachedCard->setPos(childX, childY);
            attachedCard->setRealZValue(ZValues::tableCardZValue(childX, childY));
        }
    }

    resizeToContents();
    update();
}

void TableZone::toggleTapped()
{
    QList<QGraphicsItem *> selectedItemsRaw = scene()->selectedItems();
    QList<QGraphicsItem *> selectedItems;

    auto isCardOnTable = [](const QGraphicsItem *item) {
        if (auto card = qgraphicsitem_cast<const CardItem *>(item)) {
            return card->getZone()->getName() == ZoneNames::TABLE;
        }
        return false;
    };

    std::copy_if(selectedItemsRaw.begin(), selectedItemsRaw.end(), std::back_inserter(selectedItems), isCardOnTable);

    bool tapAll = std::any_of(selectedItems.begin(), selectedItems.end(), [](const QGraphicsItem *item) {
        return !qgraphicsitem_cast<const CardItem *>(item)->getTapped();
    });
    QList<const ::google::protobuf::Message *> cmdList;
    for (const auto &selectedItem : selectedItems) {
        CardItem *temp = qgraphicsitem_cast<CardItem *>(selectedItem);
        if (temp->getTapped() != tapAll) {
            Command_SetCardAttr *cmd = new Command_SetCardAttr;
            cmd->set_zone(getLogic()->getName().toStdString());
            cmd->set_card_id(temp->getId());
            cmd->set_attribute(AttrTapped);
            cmd->set_attr_value(tapAll ? "1" : "0");
            cmdList.append(cmd);
        }
    }
    getLogic()->getPlayer()->getPlayerActions()->sendGameCommand(
        getLogic()->getPlayer()->getPlayerActions()->prepareGameCommand(cmdList));
}

void TableZone::resizeToContents()
{
    int xMax = 0;

    // Find rightmost card position, which includes the left margin amount.
    for (int i = 0; i < getLogic()->getCards().size(); ++i) {
        if (getLogic()->getCards()[i]->pos().x() > xMax) {
            xMax = (int)getLogic()->getCards()[i]->pos().x();
        }
    }

    // Minimum width is the rightmost card position plus enough room for
    // another card with padding, then margin.
    currentMinimumWidth = xMax + (2 * CardDimensions::WIDTH) + PADDING_X + MARGIN_RIGHT;

    if (currentMinimumWidth < MIN_WIDTH) {
        currentMinimumWidth = MIN_WIDTH;
    }

    if (currentMinimumWidth != width) {
        prepareGeometryChange();
        width = currentMinimumWidth;
        emit sizeChanged();
    }
}

CardItem *TableZone::getCardFromGrid(const QPoint &gridPoint) const
{
    for (int i = 0; i < getLogic()->getCards().size(); i++) {
        if (getLogic()->getCards().at(i)->getGridPoint() == gridPoint) {
            return getLogic()->getCards().at(i);
        }
    }
    return 0;
}

CardItem *TableZone::getCardFromCoords(const QPointF &point) const
{
    QPoint gridPoint = mapToGrid(point);
    return getCardFromGrid(gridPoint);
}

void TableZone::computeCardStackWidths()
{
    // Each card stack is three grid points worth of card locations.
    // First pass: compute the number of cards at each card stack.
    QMap<int, int> cardStackCount;
    for (int i = 0; i < getLogic()->getCards().size(); ++i) {
        const QPoint &gridPoint = getLogic()->getCards()[i]->getGridPos();
        if (gridPoint.x() == -1) {
            continue;
        }

        const int key = getCardStackMapKey(gridPoint.x() / 3, gridPoint.y());
        cardStackCount.insert(key, cardStackCount.value(key, 0) + 1);
    }

    // Second pass: compute the width at each card stack.
    cardStackWidth.clear();
    for (int i = 0; i < getLogic()->getCards().size(); ++i) {
        const QPoint &gridPoint = getLogic()->getCards()[i]->getGridPos();
        if (gridPoint.x() == -1) {
            continue;
        }

        const int key = getCardStackMapKey(gridPoint.x() / 3, gridPoint.y());
        const int stackCount = cardStackCount.value(key, 0);
        if (stackCount == 1) {
            cardStackWidth.insert(key, CardDimensions::WIDTH + getLogic()->getCards()[i]->getAttachedCards().size() *
                                                                   STACKED_CARD_OFFSET_X);
        } else {
            cardStackWidth.insert(key, CardDimensions::WIDTH + (stackCount - 1) * STACKED_CARD_OFFSET_X);
        }
    }
}

QPointF TableZone::mapFromGrid(QPoint gridPoint) const
{
    qreal x, y;

    // Start with margin plus stacked card offset
    x = MARGIN_LEFT + (gridPoint.x() % 3) * STACKED_CARD_OFFSET_X;

    // Add in width of card stack plus padding for each column
    for (int i = 0; i < gridPoint.x() / 3; ++i) {
        const int key = getCardStackMapKey(i, gridPoint.y());
        x += cardStackWidth.value(key, CardDimensions::WIDTH) + PADDING_X;
    }

    if (isInverted()) {
        gridPoint.setY(TABLEROWS - 1 - gridPoint.y());
    }

    // Start with margin plus stacked card offset
    y = MARGIN_TOP + (gridPoint.x() % 3) * STACKED_CARD_OFFSET_Y;

    // Add in card size and padding for each row
    for (int i = 0; i < gridPoint.y(); ++i) {
        y += CardDimensions::HEIGHT + PADDING_Y;
    }

    return QPointF(x, y);
}

QPoint TableZone::mapToGrid(const QPointF &mapPoint) const
{
    // Begin by calculating the y-coordinate of the grid space, which will be
    // used for the x-coordinate.

    // Offset point by the margin amount to reference point within grid area.
    int y = mapPoint.y() - MARGIN_TOP;

    // Below calculation effectively rounds to the nearest grid point.
    const int gridPointHeight = CardDimensions::HEIGHT + PADDING_Y;
    int gridPointY = (y + PADDING_Y / 2) / gridPointHeight;

    gridPointY = clampValidTableRow(gridPointY);

    if (isInverted()) {
        gridPointY = TABLEROWS - 1 - gridPointY;
    }

    // Calculating the x-coordinate of the grid space requires adding up the
    // widths of each card stack along the row.

    // Offset point by the margin amount to reference point within grid area.
    int x = mapPoint.x() - MARGIN_LEFT + PADDING_X / 2;

    // Maximum value is a card width from the right margin, referenced to the
    // grid area.
    const int xMax = width - MARGIN_LEFT - MARGIN_RIGHT - CardDimensions::WIDTH;

    int xStack = 0;
    int xNextStack = 0;
    int nextStackCol = 0;
    while ((xNextStack <= x) && (xNextStack <= xMax)) {
        xStack = xNextStack;
        const int key = getCardStackMapKey(nextStackCol, gridPointY);
        xNextStack += cardStackWidth.value(key, CardDimensions::WIDTH) + PADDING_X;
        nextStackCol++;
    }
    int stackCol = qMax(nextStackCol - 1, 0);

    // Have the stack column, need to refine to the grid column.  Take the
    // difference between the point and the stack point and divide by stacked
    // card offsets.
    int xDiff = x - xStack;
    int gridPointX = stackCol * 3 + qMin(xDiff / STACKED_CARD_OFFSET_X, 2);

    return QPoint(gridPointX, gridPointY);
}

QPointF TableZone::closestGridPoint(const QPointF &point)
{
    QPoint gridPoint = mapToGrid(point);
    gridPoint.setX((gridPoint.x() / 3) * 3);
    if (getCardFromGrid(gridPoint)) {
        gridPoint.setX(gridPoint.x() + 1);
    }
    if (getCardFromGrid(gridPoint)) {
        gridPoint.setX(gridPoint.x() + 1);
    }
    return mapFromGrid(gridPoint);
}

int TableZone::clampValidTableRow(const int row)
{
    if (row < 0) {
        return 0;
    }
    if (row >= TABLEROWS) {
        return TABLEROWS - 1;
    }
    return row;
}

int TableZone::tableRowToGridY(int tableRow)
{
    if (tableRow > 2) {
        tableRow = 1;
    }
    return clampValidTableRow(2 - tableRow);
}
