/***************************************************************************
 *   Copyright (c) 2009  Thomas Lübking <thomas.luebking@web.de>           *
 *                                                                         *
 *   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, write to the                         *
 *   Free Software Foundation, Inc.,                                       *
 *   51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.         *
 ***************************************************************************/

#include "TokenDropTarget.h"

#include "Token.h"

#include <QDrag>
#include <QtDebug>
#include <QMimeData>
#include <QDropEvent>
#include <QVBoxLayout>

/** TokenDragger - eventfilter that drags a token, designed to be a child of TokenDropTarget
This is necessary, as if TokenDropTarget would QDrag::exec() itself, the eventFilter would be blocked
and thus not be able to handle other events for the parenting widget, like e.g. dragEnter... */

class TokenDragger : public QObject
{
public:
    TokenDragger(const QString &mimeType, TokenDropTarget *parent) : QObject(parent)
        , m_mimeType(mimeType)
    {
    }

protected:
    bool eventFilter(QObject *o, QEvent *e) Q_DECL_OVERRIDE
    {
        if (e->type() == QEvent::MouseMove) {
            if (static_cast<QMouseEvent *>(e)->buttons() & Qt::LeftButton) {
                return drag(qobject_cast<Token *>(o));
            }
        } else if (e->type() == QEvent::MouseButtonPress) {
            if (static_cast<QMouseEvent *>(e)->buttons() & Qt::LeftButton) {
                setCursor(qobject_cast<QWidget *>(o), Qt::ClosedHandCursor);
//                 m_startPos = me->pos(); // not sure whether i like this...
//             else if ( event->button() == Qt::MidButton ) // TODO: really kick item on mmbc?
            }
            return false;
        } else if (e->type() == QEvent::MouseButtonRelease) {
            if (static_cast<QMouseEvent *>(e)->buttons() & Qt::LeftButton) {
                setCursor(qobject_cast<QWidget *>(o), Qt::OpenHandCursor);
            }
            return false;
        } else if (e->type() == QEvent::FocusIn) {
            emit static_cast<TokenDropTarget *>(parent())->focussed(qobject_cast<QWidget *>(o));
        } else if (e->type() == QEvent::Hide) {
            setCursor(qobject_cast<QWidget *>(o), Qt::OpenHandCursor);
            return false;
        }
        return false;
    }

private:
    bool drag(Token *token)
    {
        if (!token) {
            return false;
        }

        bool ret = false;
        bool stacked = token->parentWidget() && qobject_cast<TokenDropTarget *>(token->parentWidget());
        if (stacked) {
            token->hide();
        }

        QPixmap pixmap(token->size());
        token->render(&pixmap);
        QDrag *drag = new QDrag(token);
        QMimeData *data = new QMimeData;

        QByteArray itemData;
        QDataStream dataStream(&itemData, QIODevice::WriteOnly);
//         dataStream << child->name() << child->iconName() << child->value();

        data->setData(m_mimeType, itemData);
        drag->setMimeData(data);
        drag->setPixmap(pixmap);
        drag->setHotSpot(pixmap.rect().center());

        Qt::DropAction dropAction = drag->exec(Qt::CopyAction | Qt::MoveAction, Qt::CopyAction);

        if (stacked) {
            if (dropAction != Qt::MoveAction && dropAction != Qt::CopyAction) { // dragged out
                // TODO: nice poof animation? ;-)
                delete token;
                emit static_cast<TokenDropTarget *>(parent())->changed();
                ret = true; // THIS IS IMPORTANT
            }
            // anyway, tell daddy to wipe empty rows NOW
            static_cast<TokenDropTarget *>(parent())->deleteEmptyRows();
        }
        return ret;
    }

    inline void setCursor(QWidget *w, Qt::CursorShape shape)
    {
        if (!w) {
            return;
        }
        w->setCursor(shape);
    }

private:
    QString m_mimeType;
    QPoint m_startPos;
};

TokenDropTarget::TokenDropTarget(const QString &mimeType, QWidget *parent) : QWidget(parent)
    , m_tokenDragger(new TokenDragger(mimeType, this))
    , m_tokenFactory(new TokenFactory())
{
    new QVBoxLayout(this);
    m_mimeType = mimeType;
    m_limits[0] = m_limits[1] = 0;
    // let daddy widget be droppable... ;)
    parent->setAcceptDrops(true);
    // ...and handle drop events for him
    parent->removeEventFilter(this);
    parent->installEventFilter(this);

    // visual, maybe there should be spacing? however, frames etc. can have contentmargin.
    layout()->setSpacing(0);
    // top-align content
    layout()->addItem(new QSpacerItem(1, 1, QSizePolicy::Expanding, QSizePolicy::MinimumExpanding));
}

bool
TokenDropTarget::accept(QDropEvent *de)
{
    if (!de->mimeData()->hasFormat(m_mimeType)) {
        de->ignore();
        return false;
    }

    //FIXME : Find the apt replacement for : de->source()->parentWidget()
    if (de->source() && parentWidget() && de->source()->findChild<QWidget *>()->parentWidget() == parentWidget()) { // move
        de->setDropAction(Qt::MoveAction);
        de->accept();
    } else {
        de->acceptProposedAction();
    }
    return true;
}

QHBoxLayout *
TokenDropTarget::appendRow()
{
    QHBoxLayout *box = new QHBoxLayout;
    box->setSpacing(0);
    box->addStretch();
    static_cast<QVBoxLayout *>(layout())->insertLayout(layout()->count() - 1, box);  // last item is a spacer
    return box;
}

QWidget *
TokenDropTarget::childAt(const QPoint &pos) const
{
    for (int row = 0; row <= rows(); ++row) {
        if (QHBoxLayout *rowBox = qobject_cast<QHBoxLayout *>(layout()->itemAt(row)->layout())) {
            for (int col = 0; col < rowBox->count(); ++col) {
                if (QWidget *kid = rowBox->itemAt(col)->widget()) {
                    if (kid->geometry().contains(pos)) {
                        return kid;
                    }
                }
            }
        }
    }
    return NULL;
}

void
TokenDropTarget::clear()
{
    QLayoutItem *row, *col;
    while ((row = layout()->takeAt(0)))
    {
        if (QLayout *layout = row->layout()) {
            while ((col = layout->takeAt(0)))
            {
                delete col->widget();
                delete col;
            }
        }
        delete row;
    }
    //readd our spacer
    layout()->addItem(new QSpacerItem(1, 1, QSizePolicy::Expanding, QSizePolicy::MinimumExpanding));
}

int
TokenDropTarget::count(int row) const
{
    int lower = 0, upper = rows();
    if (row > -1 && row < rows()) {
        lower = row;
        upper = row + 1;
    }

    int c = 0;
    for (row = lower; row < upper; ++row) {
        if (QHBoxLayout *rowBox = qobject_cast<QHBoxLayout *>(layout()->itemAt(row)->layout())) {
            c += rowBox->count() - 1;
        }
    }
    return c;
}

void
TokenDropTarget::deleteEmptyRows()
{
    QBoxLayout *box = 0;
    for (int row = 0; row <= rows();) {
        box = qobject_cast<QBoxLayout *>(layout()->itemAt(row)->layout());
        if (box && box->count() < 2) { // sic! last is spacer
            layout()->removeItem(box);
            delete box;
        } else {
            ++row;
        }
    }
}

QList< Token *>
TokenDropTarget::drags(int row)
{
    int lower = 0, upper = rows();
    if (row > -1 && row < rows()) {
        lower = row;
        upper = row + 1;
    }

    QList< Token *> list;
    Token *token;
    for (row = lower; row < upper; ++row) {
        if (QHBoxLayout *rowBox = qobject_cast<QHBoxLayout *>(layout()->itemAt(row)->layout())) {
            for (int col = 0; col < rowBox->count() - 1; ++col) {
                if ((token = qobject_cast<Token *>(rowBox->itemAt(col)->widget()))) {
                    list << token;
                }
            }
        }
    }

    return list;
}

void
TokenDropTarget::drop(Token *token, const QPoint &pos)
{
    if (!token) {
        return;
    }

    // unlayout in case of move
    if (QBoxLayout *box = rowBox(token)) {
        box->removeWidget(token);
    }
    token->setParent(parentWidget());

    QBoxLayout *box = 0;
    if (Token *brother = qobject_cast<Token *>(childAt(pos))) { // we hit a sibling, -> prepend
        QPoint idx;
        box = rowBox(brother, &idx);
        if (pos.x() > brother->geometry().x() + 2*brother->width()/3) {
            box->insertWidget(idx.x() + 1, token);
        } else {
            box->insertWidget(idx.x(), token);
        }
    } else {
        if (rowLimit() && rows() >= (int)rowLimit()) { // we usually don't want more rows
            box = qobject_cast<QBoxLayout *>(layout()->itemAt(rows() - 1)->layout());
        }

        if (!box) {
            box = rowBox(pos);   // maybe this is on an existing row
            if (!box) {
                box = appendRow();
            }
        }
        int idx = (box->count() > 1 && box->itemAt(0)->widget()
                   && pos.x() < box->itemAt(0)->widget()->geometry().x()) ? 0 : box->count() - 1;
        box->insertWidget(idx, token);   // append to existing row
    }
    token->show();
    emit changed();
}

bool
TokenDropTarget::eventFilter(QObject *o, QEvent *ev)
{
    Q_UNUSED(o)

    if (ev->type() == QEvent::DragMove
        || ev->type() == QEvent::DragEnter) {
        accept(static_cast<QDropEvent *>(ev));
        return false; // TODO: return accept boolean ?
    }

//     if ( ev->type() == QEvent::DragLeave )
    if (ev->type() == QEvent::Drop) {
        QDropEvent *de = static_cast<QDropEvent *>(ev);
        if (accept(de)) {
            Token *token = qobject_cast<Token *>(de->source());
            if (!token) {
                QByteArray itemData = de->mimeData()->data(m_mimeType);
                QDataStream dataStream(&itemData, QIODevice::ReadOnly);

                QString tokenName;
                QString tokenIconName;
                int tokenValue;
                dataStream >> tokenName;
                dataStream >> tokenIconName;
                dataStream >> tokenValue;

                token = m_tokenFactory->createToken(tokenName, tokenIconName, tokenValue, this);
                token->removeEventFilter(m_tokenDragger);
                token->installEventFilter(m_tokenDragger);
                token->setCursor(Qt::OpenHandCursor);
            }
            drop(token, de->pos());
        }
        return false;
    }
    return false;
}

void
TokenDropTarget::insertToken(Token *token, int row, int col)
{
    QBoxLayout *box = 0;
    if (row > rows() - 1) {
        box = appendRow();
    } else {
        box = qobject_cast<QBoxLayout *>(layout()->itemAt(row)->layout());
    }
    token->setParent(parentWidget());
    if (col < 0 || col > box->count() - 2) {
        col = box->count() - 1;
    }
    box->insertWidget(col, token);
    token->removeEventFilter(m_tokenDragger);
    token->installEventFilter(m_tokenDragger);
    token->setCursor(Qt::OpenHandCursor);
}

int
TokenDropTarget::row(Token *token) const
{
    for (int row = 0; row <= rows(); ++row) {
        QBoxLayout *box = qobject_cast<QBoxLayout *>(layout()->itemAt(row)->layout());
        if (box && (box->indexOf(token)) > -1) {
            return row;
        }
    }
    return -1;
}

int
TokenDropTarget::rows() const
{
    return layout()->count() - 1;
}

QBoxLayout *
TokenDropTarget::rowBox(QWidget *w, QPoint *idx) const
{
    QBoxLayout *box = 0;
    int col;
    for (int row = 0; row <= rows(); ++row) {
        box = qobject_cast<QBoxLayout *>(layout()->itemAt(row)->layout());
        if (box && (col = box->indexOf(w)) > -1) {
            if (idx) {
                idx->setX(col);
                idx->setY(row);
            }
            return box;
        }
    }
    return NULL;
}

QBoxLayout *
TokenDropTarget::rowBox(const QPoint &pt) const
{
    QBoxLayout *box = 0;
    for (int row = 0; row <= rows(); ++row) {
        box = qobject_cast<QBoxLayout *>(layout()->itemAt(row)->layout());
        if (!box) {
            continue;
        }
        for (int col = 0; col < box->count(); ++col) {
            if (QWidget *w = box->itemAt(col)->widget()) {
                const QRect &geo = w->geometry();
                if (geo.y() <= pt.y() && geo.bottom() >= pt.y()) {
                    return box;
                }
                break; // yes - all widgets are supposed of equal height. we checked on, we checked all
            }
        }
    }
    return NULL;
}

void
TokenDropTarget::setCustomTokenFactory(TokenFactory *factory)
{
    delete m_tokenFactory;
    m_tokenFactory = factory;
}
