/*  This file is part of the KDE project
    Copyright (C) 2007 Kevin Ottens <ervin@kde.org>
    Copyright (C) 2007 David Faure <faure@kde.org>

    This library is free software; you can redistribute it and/or
    modify it under the terms of the GNU Library General Public
    License version 2 as published by the Free Software Foundation.

    This library 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
    Library General Public License for more details.

    You should have received a copy of the GNU Library General Public License
    along with this library; see the file COPYING.LIB.  If not, write to
    the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor,
    Boston, MA 02110-1301, USA.

*/
#include "kfileplacesmodel.h"
#include "kfileplacesitem_p.h"
#include "kfileplacessharedbookmarks_p.h"

#include <QtCore/QMimeData>
#include <QtCore/QTimer>
#include <QtCore/QFile>
#include <QtGui/QColor>
#include <QtGui/QAction>

#include <kglobal.h>
#include <klocale.h>
#include <kuser.h>
#include <kstandarddirs.h>
#include <kcomponentdata.h>
#include <kicon.h>
#include <kmimetype.h>
#include <kdebug.h>

#include <kbookmarkmanager.h>
#include <kbookmark.h>

#include <kio/netaccess.h>

#include <solid/devicenotifier.h>
#include <solid/storageaccess.h>
#include <solid/storagedrive.h>
#include <solid/storagevolume.h>
#include <solid/opticaldrive.h>
#include <solid/opticaldisc.h>
#include <solid/predicate.h>

class KFilePlacesModel::Private
{
public:
    Private(KFilePlacesModel *self) : q(self), bookmarkManager(0), sharedBookmarks(0) {}
    ~Private()
    { 
        delete sharedBookmarks;
        qDeleteAll(items);
    }

    KFilePlacesModel *q;

    QList<KFilePlacesItem*> items;
    QSet<QString> availableDevices;
    QMap<QObject*, QPersistentModelIndex> setupInProgress;

    Solid::Predicate predicate;
    KBookmarkManager *bookmarkManager;
    KFilePlacesSharedBookmarks * sharedBookmarks;
    
    void reloadAndSignal();
    QList<KFilePlacesItem *> loadBookmarkList();

    void _k_initDeviceList();
    void _k_deviceAdded(const QString &udi);
    void _k_deviceRemoved(const QString &udi);
    void _k_itemChanged(const QString &udi);
    void _k_reloadBookmarks();
    void _k_storageSetupDone(Solid::ErrorType error, QVariant errorData);
    void _k_storageTeardownDone(Solid::ErrorType error, QVariant errorData);
};

KFilePlacesModel::KFilePlacesModel(QObject *parent)
    : QAbstractItemModel(parent), d(new Private(this))
{
    const QString file = KStandardDirs::locateLocal("data", "kfileplaces/bookmarks.xml");
    d->bookmarkManager = KBookmarkManager::managerForFile(file, "kfilePlaces");

    // Let's put some places in there if it's empty. We have a corner case here:
    // Given you have bookmarked some folders (which have been saved on
    // ~/.local/share/user-places.xbel (according to freedesktop bookmarks spec), and
    // deleted the home directory ~/.kde, the call managerForFile() will return the
    // bookmark manager for the fallback "kfilePlaces", making root.first().isNull() being
    // false (you have your own items bookmarked), resulting on only being added your own
    // bookmarks, and not the default ones too. So, we also check if kfileplaces/bookmarks.xml
    // file exists, and if it doesn't, we also add the default places. (ereslibre)
    KBookmarkGroup root = d->bookmarkManager->root();
    if (root.first().isNull() || !QFile::exists(file)) {
        KFilePlacesItem::createSystemBookmark(d->bookmarkManager,
                                        i18nc("Home Directory", "Home"), KUrl(KUser().homeDir()), "user-home");
        KFilePlacesItem::createSystemBookmark(d->bookmarkManager,
                                        i18n("Network"), KUrl("remote:/"), "network-workgroup");
#ifdef Q_OS_WIN
	//C:/ as root for windows...forward slashes are valid too and are used in much/most of the KDE code on Windows
        KFilePlacesItem::createSystemBookmark(d->bookmarkManager,
                                        i18n("Root"), KUrl("C:/"), "folder-red");
#else
        KFilePlacesItem::createSystemBookmark(d->bookmarkManager,
                                        i18n("Root"), KUrl("/"), "folder-red");
#endif
        KFilePlacesItem::createSystemBookmark(d->bookmarkManager,
                                        i18n("Trash"), KUrl("trash:/"), "user-trash");

        // Force bookmarks to be saved. If on open/save dialog and the bookmarks are not saved, QFile::exists
        // will always return false, which opening/closing all the time the open/save dialog would case the
        // bookmarks to be added once each time, having lots of times each bookmark. This forces the defaults
        // to be saved on the bookmarks.xml file. Of course, the complete list of bookmarks (those that come from
        // user-places.xbel will be filled later). (ereslibre)
        d->bookmarkManager->saveAs(file);
    }

    // create after, so if we have own places, they are added afterwards, in case of equal priorities
    d->sharedBookmarks = new KFilePlacesSharedBookmarks(d->bookmarkManager);

    d->predicate = Solid::Predicate::fromString(
        "[[ StorageVolume.ignored == false AND [ StorageVolume.usage == 'FileSystem' OR StorageVolume.usage == 'Encrypted' ]]"
        " OR "
        "[ IS StorageAccess AND StorageDrive.driveType == 'Floppy' ]]");

    connect(d->bookmarkManager, SIGNAL(changed(const QString&, const QString&)),
            this, SLOT(_k_reloadBookmarks()));
    connect(d->bookmarkManager, SIGNAL(bookmarksChanged(const QString&)),
            this, SLOT(_k_reloadBookmarks()));

    d->_k_reloadBookmarks();
    QTimer::singleShot(0, this, SLOT(_k_initDeviceList()));
}

KFilePlacesModel::~KFilePlacesModel()
{
    delete d;
}

KUrl KFilePlacesModel::url(const QModelIndex &index) const
{
    return KUrl(data(index, UrlRole).toUrl());
}

bool KFilePlacesModel::setupNeeded(const QModelIndex &index) const
{
    return data(index, SetupNeededRole).toBool();
}

KIcon KFilePlacesModel::icon(const QModelIndex &index) const
{
    return KIcon(data(index, Qt::DecorationRole).value<QIcon>());
}

QString KFilePlacesModel::text(const QModelIndex &index) const
{
    return data(index, Qt::DisplayRole).toString();
}

bool KFilePlacesModel::isHidden(const QModelIndex &index) const
{
    return data(index, HiddenRole).toBool();
}

bool KFilePlacesModel::isDevice(const QModelIndex &index) const
{
    if (!index.isValid())
        return false;

    KFilePlacesItem *item = static_cast<KFilePlacesItem*>(index.internalPointer());

    return item->isDevice();
}

Solid::Device KFilePlacesModel::deviceForIndex(const QModelIndex &index) const
{
    if (!index.isValid())
        return Solid::Device();

    KFilePlacesItem *item = static_cast<KFilePlacesItem*>(index.internalPointer());

    if (item->isDevice()) {
        return item->device();
    } else {
        return Solid::Device();
    }
}

KBookmark KFilePlacesModel::bookmarkForIndex(const QModelIndex &index) const
{
    if (!index.isValid())
        return KBookmark();

    KFilePlacesItem *item = static_cast<KFilePlacesItem*>(index.internalPointer());

    if (!item->isDevice()) {
        return item->bookmark();
    } else {
        return KBookmark();
    }
}

QVariant KFilePlacesModel::data(const QModelIndex &index, int role) const
{
    if (!index.isValid())
        return QVariant();

    KFilePlacesItem *item = static_cast<KFilePlacesItem*>(index.internalPointer());
    return item->data(role);
}

QModelIndex KFilePlacesModel::index(int row, int column, const QModelIndex &parent) const
{
    if (row<0 || column!=0 || row>=d->items.size())
        return QModelIndex();

    if (parent.isValid())
        return QModelIndex();

    return createIndex(row, column, d->items.at(row));
}

QModelIndex KFilePlacesModel::parent(const QModelIndex &child) const
{
    Q_UNUSED(child);
    return QModelIndex();
}

int KFilePlacesModel::rowCount(const QModelIndex &parent) const
{
    if (parent.isValid())
        return 0;
    else
        return d->items.size();
}

int KFilePlacesModel::columnCount(const QModelIndex &parent) const
{
    Q_UNUSED(parent)
    // We only know 1 piece of information for a particular entry
    return 1;
}

QModelIndex KFilePlacesModel::closestItem(const KUrl &url) const
{
    int foundRow = -1;
    int maxLength = 0;

    // Search the item which is equal to the URL or at least is a parent URL.
    // If there are more than one possible item URL candidates, choose the item
    // which covers the bigger range of the URL.
    for (int row = 0; row<d->items.size(); ++row) {
        KFilePlacesItem *item = d->items[row];
        KUrl itemUrl = KUrl(item->data(UrlRole).toUrl());

        if (itemUrl.isParentOf(url)) {
            const int length = itemUrl.prettyUrl().length();
            if (length > maxLength) {
                foundRow = row;
                maxLength = length;
            }
        }
    }

    if (foundRow==-1)
        return QModelIndex();
    else
        return createIndex(foundRow, 0, d->items[foundRow]);
}

void KFilePlacesModel::Private::_k_initDeviceList()
{
    Solid::DeviceNotifier *notifier = Solid::DeviceNotifier::instance();

    connect(notifier, SIGNAL(deviceAdded(const QString&)),
            q, SLOT(_k_deviceAdded(const QString&)));
    connect(notifier, SIGNAL(deviceRemoved(const QString&)),
            q, SLOT(_k_deviceRemoved(const QString&)));

    const QList<Solid::Device> &deviceList = Solid::Device::listFromQuery(predicate);

    foreach(const Solid::Device &device, deviceList) {
        availableDevices << device.udi();
    }

    _k_reloadBookmarks();
}

void KFilePlacesModel::Private::_k_deviceAdded(const QString &udi)
{
    Solid::Device d(udi);

    if (predicate.matches(d)) {
        availableDevices << udi;
        _k_reloadBookmarks();
    }
}

void KFilePlacesModel::Private::_k_deviceRemoved(const QString &udi)
{
    if (availableDevices.contains(udi)) {
        availableDevices.remove(udi);
        _k_reloadBookmarks();
    }
}

void KFilePlacesModel::Private::_k_itemChanged(const QString &id)
{
    for (int row = 0; row<items.size(); ++row) {
        if (items.at(row)->id()==id) {
            QModelIndex index = q->index(row, 0);
            emit q->dataChanged(index, index);
        }
    }
}

void KFilePlacesModel::Private::_k_reloadBookmarks()
{
    QList<KFilePlacesItem*> currentItems = loadBookmarkList();

    QList<KFilePlacesItem*>::Iterator it_i = items.begin();
    QList<KFilePlacesItem*>::Iterator it_c = currentItems.begin();

    QList<KFilePlacesItem*>::Iterator end_i = items.end();
    QList<KFilePlacesItem*>::Iterator end_c = currentItems.end();

    while (it_i!=end_i || it_c!=end_c) {
        if (it_i==end_i && it_c!=end_c) {
            int row = items.count();

            q->beginInsertRows(QModelIndex(), row, row);
            it_i = items.insert(it_i, *it_c);
            ++it_i;
            it_c = currentItems.erase(it_c);

            end_i = items.end();
            end_c = currentItems.end();
            q->endInsertRows();

        } else if (it_i!=end_i && it_c==end_c) {
            int row = items.indexOf(*it_i);

            q->beginRemoveRows(QModelIndex(), row, row);
            delete *it_i;
            it_i = items.erase(it_i);

            end_i = items.end();
            end_c = currentItems.end();
            q->endRemoveRows();

        } else if ((*it_i)->id()==(*it_c)->id()) {
            bool shouldEmit = !((*it_i)->bookmark()==(*it_c)->bookmark());
            (*it_i)->setBookmark((*it_c)->bookmark());
            if (shouldEmit) {
                int row = items.indexOf(*it_i);
                QModelIndex idx = q->index(row, 0);
                emit q->dataChanged(idx, idx);
            }
            ++it_i;
            ++it_c;
        } else if ((*it_i)->id()!=(*it_c)->id()) {
            int row = items.indexOf(*it_i);

            if (it_i+1!=end_i && (*(it_i+1))->id()==(*it_c)->id()) { // if the next one matches, it's a remove
                q->beginRemoveRows(QModelIndex(), row, row);
                delete *it_i;
                it_i = items.erase(it_i);

                end_i = items.end();
                end_c = currentItems.end();
                q->endRemoveRows();
            } else {
                q->beginInsertRows(QModelIndex(), row, row);
                it_i = items.insert(it_i, *it_c);
                ++it_i;
                it_c = currentItems.erase(it_c);

                end_i = items.end();
                end_c = currentItems.end();
                q->endInsertRows();
            }
        }
    }

    qDeleteAll(currentItems);
    currentItems.clear();
}

QList<KFilePlacesItem *> KFilePlacesModel::Private::loadBookmarkList()
{
    QList<KFilePlacesItem*> items;

    KBookmarkGroup root = bookmarkManager->root();
    KBookmark bookmark = root.first();
    QSet<QString> devices = availableDevices;

    while (!bookmark.isNull()) {
        QString udi = bookmark.metaDataItem("UDI");
        QString appName = bookmark.metaDataItem("OnlyInApp");
        bool deviceAvailable = devices.remove(udi);

        bool allowedHere = appName.isEmpty() || (appName==KGlobal::mainComponent().componentName());

        if ((udi.isEmpty() && allowedHere) || deviceAvailable) {
            KFilePlacesItem *item;
            if (deviceAvailable) {
                item = new KFilePlacesItem(bookmarkManager, bookmark.address(), udi);
                // TODO: Update bookmark internal element
            } else {
                item = new KFilePlacesItem(bookmarkManager, bookmark.address());
            }
            connect(item, SIGNAL(itemChanged(const QString&)),
                    q, SLOT(_k_itemChanged(const QString&)));
            items << item;
        }

        bookmark = root.next(bookmark);
    }

    // Add bookmarks for the remaining devices, they were previously unknown
    foreach (const QString &udi, devices) {
        bookmark = KFilePlacesItem::createDeviceBookmark(bookmarkManager, udi);
        if (!bookmark.isNull()) {
            KFilePlacesItem *item = new KFilePlacesItem(bookmarkManager,
                                                        bookmark.address(), udi);
            connect(item, SIGNAL(itemChanged(const QString&)),
                    q, SLOT(_k_itemChanged(const QString&)));
            // TODO: Update bookmark internal element
            items << item;
        }
    }

    return items;
}

void KFilePlacesModel::Private::reloadAndSignal()
{
    bookmarkManager->emitChanged(bookmarkManager->root()); // ... we'll get relisted anyway
}

Qt::DropActions KFilePlacesModel::supportedDropActions() const
{
    return Qt::ActionMask;
}

Qt::ItemFlags KFilePlacesModel::flags(const QModelIndex &index) const
{
    Qt::ItemFlags res = Qt::ItemIsSelectable|Qt::ItemIsEnabled;

    if (index.isValid())
        res|= Qt::ItemIsDragEnabled;

    if (!index.isValid())
        res|= Qt::ItemIsDropEnabled;

    return res;
}

static QString _k_internalMimetype(const KFilePlacesModel * const self)
{
    return QString("application/x-kfileplacesmodel-")+QString::number((long)self);
}

QStringList KFilePlacesModel::mimeTypes() const
{
    QStringList types;

    types << _k_internalMimetype(this) << "text/uri-list";

    return types;
}

QMimeData *KFilePlacesModel::mimeData(const QModelIndexList &indexes) const
{
    KUrl::List urls;
    QByteArray itemData;

    QDataStream stream(&itemData, QIODevice::WriteOnly);

    foreach (const QModelIndex &index, indexes) {
        KUrl itemUrl = url(index);
        if (itemUrl.isValid())
            urls << itemUrl;
        stream << index.row();
    }

    QMimeData *mimeData = new QMimeData();

    if (!urls.isEmpty())
        urls.populateMimeData(mimeData);

    mimeData->setData(_k_internalMimetype(this), itemData);

    return mimeData;
}

bool KFilePlacesModel::dropMimeData(const QMimeData *data, Qt::DropAction action,
                                    int row, int column, const QModelIndex &parent)
{
    if (action == Qt::IgnoreAction)
        return true;

    if (column > 0)
        return false;

    if (row==-1 && parent.isValid()) {
        return false; // Don't allow to move an item onto another one,
                      // too easy for the user to mess something up
                      // If we really really want to allow copying files this way,
                      // let's do it in the views to get the good old drop menu
    }


    KBookmark afterBookmark;

    if (row==-1) {
        // The dropped item is moved or added to the last position

        KFilePlacesItem *lastItem = d->items.last();
        afterBookmark = lastItem->bookmark();

    } else {
        // The dropped item is moved or added before position 'row', ie after position 'row-1'

        if (row>0) {
            KFilePlacesItem *afterItem = d->items[row-1];
            afterBookmark = afterItem->bookmark();
        }
    }

    if (data->hasFormat(_k_internalMimetype(this))) {
        // The operation is an internal move
        QByteArray itemData = data->data(_k_internalMimetype(this));
        QDataStream stream(&itemData, QIODevice::ReadOnly);
        int itemRow;

        stream >> itemRow;

        KFilePlacesItem *item = d->items[itemRow];
        KBookmark bookmark = item->bookmark();

        d->bookmarkManager->root().moveBookmark(bookmark, afterBookmark);

    } else if (data->hasFormat("text/uri-list")) {
        // The operation is an add
        KUrl::List urls = KUrl::List::fromMimeData(data);

        KBookmarkGroup group = d->bookmarkManager->root();

        foreach (const KUrl &url, urls) {
            KMimeType::Ptr mimetype = KMimeType::mimeType(KIO::NetAccess::mimetype(url, 0));

            if (!mimetype->is("inode/directory")) {
                // Only directories are allowed
                continue;
            }

            KBookmark bookmark = KFilePlacesItem::createBookmark(d->bookmarkManager,
                                                                 url.fileName(), url,
                                                                 mimetype->iconName());
            group.moveBookmark(bookmark, afterBookmark);
            afterBookmark = bookmark;
        }

    } else {
        // Oops, shouldn't happen thanks to mimeTypes()
        kWarning() << ": received wrong mimedata, " << data->formats();
        return false;
    }

    d->reloadAndSignal();

    return true;
}

void KFilePlacesModel::addPlace(const QString &text, const KUrl &url,
                                const QString &iconName, const QString &appName)
{
    KBookmark bookmark = KFilePlacesItem::createBookmark(d->bookmarkManager,
                                                         text, url, iconName);

    if (!appName.isEmpty()) {
        bookmark.setMetaDataItem("OnlyInApp", appName);
    }

    d->reloadAndSignal();
}

void KFilePlacesModel::editPlace(const QModelIndex &index, const QString &text, const KUrl &url,
                                 const QString &iconName, const QString &appName)
{
    if (!index.isValid()) return;

    KFilePlacesItem *item = static_cast<KFilePlacesItem*>(index.internalPointer());

    if (item->isDevice()) return;

    KBookmark bookmark = item->bookmark();

    if (bookmark.isNull()) return;

    bookmark.setFullText(text);
    bookmark.setUrl(url);
    bookmark.setIcon(iconName);
    bookmark.setMetaDataItem("OnlyInApp", appName);

    d->reloadAndSignal();
    emit dataChanged(index, index);
}

void KFilePlacesModel::removePlace(const QModelIndex &index) const
{
    if (!index.isValid()) return;

    KFilePlacesItem *item = static_cast<KFilePlacesItem*>(index.internalPointer());

    if (item->isDevice()) return;

    KBookmark bookmark = item->bookmark();

    if (bookmark.isNull()) return;

    d->bookmarkManager->root().deleteBookmark(bookmark);
    d->reloadAndSignal();
}

void KFilePlacesModel::setPlaceHidden(const QModelIndex &index, bool hidden)
{
    if (!index.isValid()) return;

    KFilePlacesItem *item = static_cast<KFilePlacesItem*>(index.internalPointer());

    KBookmark bookmark = item->bookmark();

    if (bookmark.isNull()) return;

    bookmark.setMetaDataItem("IsHidden", (hidden ? "true" : "false"));

    d->reloadAndSignal();
    emit dataChanged(index, index);
}

int KFilePlacesModel::hiddenCount() const
{
    int rows = rowCount();
    int hidden = 0;

    for (int i=0; i<rows; ++i) {
        if (isHidden(index(i, 0))) {
            hidden++;
        }
    }

    return hidden;
}

QAction *KFilePlacesModel::teardownActionForIndex(const QModelIndex &index) const
{
    Solid::Device device = deviceForIndex(index);

    if ( (device.is<Solid::StorageAccess>() && device.as<Solid::StorageAccess>()->isAccessible())
     || device.is<Solid::OpticalDisc>()) {

        Solid::StorageDrive *drive = device.as<Solid::StorageDrive>();

        if (drive==0) {
            drive = device.parent().as<Solid::StorageDrive>();
        }

        bool hotpluggable = false;
        bool removable = false;

        if (drive!=0) {
            hotpluggable = drive->isHotpluggable();
            removable = drive->isRemovable();
        }

        QString text;
        QString label = data(index, Qt::DisplayRole).toString().replace('&',"&&");

        if (device.is<Solid::OpticalDisc>()) {
            text = i18n("&Eject '%1'", label);
        } else if (removable || hotpluggable) {
            text = i18n("&Safely Remove '%1'", label);
        } else {
            text = i18n("&Unmount '%1'", label);
        }

        return new QAction(KIcon("media-eject"), text, 0);
    }

    return 0;
}

void KFilePlacesModel::requestTeardown(const QModelIndex &index)
{
    Solid::Device device = deviceForIndex(index);

    Solid::OpticalDrive *drive = device.parent().as<Solid::OpticalDrive>();

    if (drive!=0) {
        connect(drive, SIGNAL(ejectDone(Solid::ErrorType, QVariant, const QString &)),
                this, SLOT(_k_storageTeardownDone(Solid::ErrorType, QVariant)));

        drive->eject();
    } else {
        Solid::StorageAccess *access = device.as<Solid::StorageAccess>();

        connect(access, SIGNAL(teardownDone(Solid::ErrorType, QVariant, const QString &)),
                this, SLOT(_k_storageTeardownDone(Solid::ErrorType, QVariant)));

        device.as<Solid::StorageAccess>()->teardown();
    }
}

void KFilePlacesModel::requestSetup(const QModelIndex &index)
{
    Solid::Device device = deviceForIndex(index);

    if (device.is<Solid::StorageAccess>()
     && !d->setupInProgress.contains(device.as<Solid::StorageAccess>())
     && !device.as<Solid::StorageAccess>()->isAccessible()) {

        Solid::StorageAccess *access = device.as<Solid::StorageAccess>();

        d->setupInProgress[access] = index;

        connect(access, SIGNAL(setupDone(Solid::ErrorType, QVariant, const QString &)),
                this, SLOT(_k_storageSetupDone(Solid::ErrorType, QVariant)));

        access->setup();
    }
}

void KFilePlacesModel::Private::_k_storageSetupDone(Solid::ErrorType error, QVariant errorData)
{
    QPersistentModelIndex index = setupInProgress.take(q->sender());

    if (!index.isValid()) {
        return;
    }

    if (!error) {
        emit q->setupDone(index, true);
    } else {
        if (errorData.isValid()) {
            emit q->errorMessage(i18n("An error occurred while accessing '%1', the system responded: %2",
                                      q->text(index),
                                      errorData.toString()));
        } else {
            emit q->errorMessage(i18n("An error occurred while accessing '%1'",
                                      q->text(index)));
        }
        emit q->setupDone(index, false);
    }

}

void KFilePlacesModel::Private::_k_storageTeardownDone(Solid::ErrorType error, QVariant errorData)
{
    if (error && errorData.isValid()) {
        emit q->errorMessage(errorData.toString());
    }
}

#include "kfileplacesmodel.moc"
