/*
 * Copyright © 2013 Canonical Ltd.
 *
 * This program is free software: you can redistribute it and/or modify
 * it under the terms of the GNU Lesser General Public License version 3 as
 * published by the Free Software Foundation.
 *
 * 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 Lesser General Public License for more details.
 *
 * You should have received a copy of the GNU Lesser General Public License
 * along with this program.  If not, see <http://www.gnu.org/licenses/>.
 *
 * Authored by: Thomas Voß <thomas.voss@canonical.com>
 */

#include "debug.h"
#include "transfer.h"
#include "utils.cpp"

#include <QFileInfo>
#include <com/lomiri/content/hub.h>
#include <com/lomiri/content/store.h>
#include <com/lomiri/content/transfer.h>
#include <lomiri/download_manager/download.h>
#include <lomiri/download_manager/manager.h>

namespace cuc = com::lomiri::content;
namespace cucd = com::lomiri::content::detail;

struct cucd::Transfer::Private
{
    Private(const int id,
            const QString& source,
            const QString& destination,
            const int direction,
            const QString& content_type):
        state(cuc::Transfer::created),
            id(id),
            source(source),
            destination(destination),
            direction(direction),
            store_scope(Scope::transient),
            store_type(Type::unknown().id()),
            selection_type(cuc::Transfer::single),
            source_started_by_content_hub(false),
            should_be_started_by_content_hub(true),
            content_type(content_type)
    {
    }
    
    cuc::Transfer::State state;
    const int id;
    const QString source;
    const QString destination;
    int direction;
    Scope store_scope;
    QString store_type;
    QString store_path;
    int selection_type;
    QVariantList items;
    bool source_started_by_content_hub;
    bool should_be_started_by_content_hub;
    QString download_id;
    const QString content_type;
};

cucd::Transfer::Transfer(const int id,
                         const QString& source,
                         const QString& destination,
                         const int direction,
                         const QString& content_type,
                         QObject* parent) :
    QObject(parent), d(new Private(id, source, destination, direction, content_type))
{
    TRACE() << __PRETTY_FUNCTION__;

    d->store_path = calculateStorePath(d->store_scope, d->store_type);
}

cucd::Transfer::~Transfer()
{
    TRACE() << __PRETTY_FUNCTION__;
    if (d->store_scope == Scope::transient)
        purge_store_cache(d->store_path);
}

/* unique id of the transfer */
int cucd::Transfer::Id()
{
    TRACE() << __PRETTY_FUNCTION__;
    return d->id;
}

/* returns the peer_id of the requested export handler */
QString cucd::Transfer::source()
{
    TRACE() << __PRETTY_FUNCTION__;
    return d->source;
}

/* returns the peer_id of the application requesting the import */
QString cucd::Transfer::destination()
{
    TRACE() << __PRETTY_FUNCTION__;
    return d->destination;
}

int cucd::Transfer::Direction()
{
    TRACE() << __PRETTY_FUNCTION__;
    return d->direction;
}

int cucd::Transfer::State()
{
    TRACE() << __PRETTY_FUNCTION__;
    return d->state;
}

void cucd::Transfer::Abort()
{
    TRACE() << __PRETTY_FUNCTION__;

    if (d->state == cuc::Transfer::aborted)
        return;

    if (d->store_scope == Scope::transient)
        purge_store_cache(d->store_path);

    d->items.clear();
    d->state = cuc::Transfer::aborted;
    Q_EMIT(StateChanged(d->state));
}

void cucd::Transfer::Start()
{
    TRACE() << __PRETTY_FUNCTION__;

    if (d->state == cuc::Transfer::initiated)
        return;

    d->state = cuc::Transfer::initiated;
    Q_EMIT(StateChanged(d->state));
}

void cucd::Transfer::Handled()
{
    TRACE() << __PRETTY_FUNCTION__;

    if (d->state == cuc::Transfer::in_progress)
        return;

    d->state = cuc::Transfer::in_progress;
    Q_EMIT(StateChanged(d->state));
}

void cucd::Transfer::Charge(const QVariantList& items)
{
    TRACE() << __PRETTY_FUNCTION__;

    if (d->state == cuc::Transfer::charged)
        return;

    if (d->state == cuc::Transfer::downloading)
    {
        qWarning() << "Unable to charge, download still in progress.";
        return;
    }

    if (d->state == cuc::Transfer::downloaded)
    {
        d->state = cuc::Transfer::charged;
        Q_EMIT(StateChanged(d->state));
        return;
    } 

    QString profile = aa_profile(message().service());
    TRACE() << Q_FUNC_INFO << "PROFILE:" << profile;

    QVariantList ret;
    Q_FOREACH(QVariant iv, items) {
        cuc::Item item = qdbus_cast<Item>(iv);
        if (item.url().isEmpty()) {
            ret.append(QVariant::fromValue(item));
        } else {
            TRACE() << Q_FUNC_INFO;
            if (profile.toStdString() != QString("unconfined").toStdString() &&
                item.url().isLocalFile()) {
                TRACE() << Q_FUNC_INFO << "IS LOCAL FILE";
                QString file(item.url().toLocalFile());
                TRACE() << Q_FUNC_INFO << "FILE:" << file;
                // Verify app has read access to local file before transfer
                if (not check_profile_read(profile, file)) {
                    // If failed to access file, abort
                    ret.clear();
                    goto abort;
                }
            }
            QString newUrl = copy_to_store(
                item.url().toString(),
                d->store_path, d->store_scope != Scope::transient);
            if (!newUrl.isEmpty()) {
                item.setUrl(QUrl(newUrl));
                TRACE() << Q_FUNC_INFO << "Item:" << item.url();
                ret.append(QVariant::fromValue(item));
            } else {
                ret.clear();
                goto abort;
            }
        }
    }

abort:
    if (ret.count() <= 0)
    {
        qWarning() << "Failed to charge items, aborting";
        d->state = cuc::Transfer::aborted;
    }
    else
    {
        d->items = ret;
        d->state = cuc::Transfer::charged;
    }
    Q_EMIT(StateChanged(d->state));
}

void cucd::Transfer::Download()
{
    TRACE() << __PRETTY_FUNCTION__;
    if(d->download_id.isEmpty()) 
    {
        return;
    }

    Manager *downloadManager = Manager::createSessionManager();
    auto download = downloadManager->getDownloadForId(d->download_id);
    if (download == nullptr) 
    {
        TRACE() << downloadManager->lastError();
    }
    else
    {
        QDir dir;
        dir.mkpath(d->store_path);
        download->setDestinationDir(d->store_path);
        connect(download, SIGNAL(finished(QString)), this, SLOT(DownloadComplete(QString)));
        connect(download, SIGNAL(error(Lomiri::DownloadManager::Error*)), this, SLOT(DownloadError(Lomiri::DownloadManager::Error*)));
        QVariantMap metadata = download->metadata();
        metadata["app-id"] = d->destination;
        download->setMetadata(metadata);
        download->start();
        d->state = cuc::Transfer::downloading;
        Q_EMIT(StateChanged(d->state));
    }
}

void cucd::Transfer::AddItemsFromDir(QDir dir) {
    QFileInfoList files = dir.entryInfoList(QDir::NoDotAndDotDot | QDir::Dirs | QDir::Files);
    Q_FOREACH(const QFileInfo &fileInfo, files) {
        QString path = fileInfo.absoluteFilePath();
        if(fileInfo.isDir()) {
            AddItemsFromDir(QDir(path));
        } else {
            cuc::Item item = cuc::Item{QUrl::fromLocalFile(path).toString()};
            d->items.append(QVariant::fromValue(item));
        }
    }
}

void cucd::Transfer::DownloadComplete(QString destFilePath)
{
    TRACE() << __PRETTY_FUNCTION__;
    QFileInfo fileInfo(destFilePath);
    if(fileInfo.isDir()) {
        // When downloading and deflating zip files, download manager may
        // send us the path of the directory that multiple files have been
        // unpacked into.
        AddItemsFromDir(QDir(destFilePath));
    } else {
        cuc::Item item = cuc::Item{QUrl::fromLocalFile(destFilePath).toString()};
        d->items.append(QVariant::fromValue(item));
    }
    d->state = cuc::Transfer::downloaded;
    Q_EMIT(StateChanged(d->state));
}

void cucd::Transfer::DownloadError(Lomiri::DownloadManager::Error* error)
{
    TRACE() << __PRETTY_FUNCTION__;
    qWarning() << "Download manager error: " << error->errorString();

    d->state = cuc::Transfer::aborted;
    Q_EMIT(DownloadManagerError(error->errorString()));
    Q_EMIT(StateChanged(d->state));
}

QVariantList cucd::Transfer::Collect()
{
    TRACE() << __PRETTY_FUNCTION__;

    if (d->state != cuc::Transfer::collected)
    {
        d->state = cuc::Transfer::collected;
        Q_EMIT(StateChanged(d->state));
    }

    return d->items;
}

void cucd::Transfer::Finalize()
{
    TRACE() << __PRETTY_FUNCTION__;

    if (d->state == cuc::Transfer::finalized)
        return;

    if (d->store_scope == Scope::transient)
        purge_store_cache(d->store_path);

    d->items.clear();
    d->state = cuc::Transfer::finalized;
    Q_EMIT(StateChanged(d->state));
}

int cucd::Transfer::Store(QString &out_type_id, QString &out_uri)
{
    TRACE() << __PRETTY_FUNCTION__;

    out_type_id = d->store_type;
    out_uri = d->store_path;
    return d->store_scope;
}

QString cucd::Transfer::SetStore(int scope, const QString &type_id)
{
    TRACE() << Q_FUNC_INFO;

    if (d->store_scope == scope && d->store_type == type_id)
        return d->store_path;

    switch (scope) {
        case Scope::transient:
        case Scope::app:
        case Scope::system:
        case Scope::user:
            break;
        default:
            sendErrorReply(QDBusError::InvalidArgs,
                QStringLiteral("Invalid scope value %1.").arg(scope));
            return QString();
    }

    if (type_id != Type::unknown().id() &&
            type_id != Type::Known::documents().id() &&
            type_id != Type::Known::pictures().id() &&
            type_id != Type::Known::music().id() &&
            type_id != Type::Known::contacts().id() &&
            type_id != Type::Known::videos().id() &&
            type_id != Type::Known::links().id() &&
            type_id != Type::Known::ebooks().id() &&
            type_id != Type::Known::text().id() &&
            type_id != Type::Known::events().id()) {
        sendErrorReply(QDBusError::InvalidArgs,
            QStringLiteral("Invalid type ID %1.").arg(type_id));
        return QString();
    }

    setStoreInternal(
        static_cast<Scope>(scope),
        type_id,
        calculateStorePath(static_cast<Scope>(scope), type_id));

    return d->store_path;
}

QString cucd::Transfer::calculateStorePath(Scope scope, const QString & type_id)
{
    if (scope == Scope::transient) {
        return QStandardPaths::writableLocation(QStandardPaths::GenericCacheLocation)
             + QDir::separator()
             + d->destination.split('_')[0]
             + QStringLiteral("/HubIncoming/")
             + QString::number(d->id, /* base */ 10);
    } else if (scope == Scope::user) {
        if (type_id == Type::Known::pictures().id())
            return QStandardPaths::writableLocation(QStandardPaths::PicturesLocation);
        else if (type_id == Type::Known::music().id())
            return QStandardPaths::writableLocation(QStandardPaths::MusicLocation);
        else if (type_id == Type::Known::documents().id())
            return QStandardPaths::writableLocation(QStandardPaths::DocumentsLocation);
        else
            // This isn't exactly right, but I guess it's the best we can do.
            return QStandardPaths::writableLocation(QStandardPaths::DocumentsLocation);
    } else {
        auto prefix = scope == Scope::system
            ? QStringLiteral("/content")
            : QStandardPaths::writableLocation(QStandardPaths::GenericDataLocation);

        auto app_pkg = scope == Scope::app
            ? QStringLiteral("/") + d->destination.split('_')[0]
            : QString();

        auto suffix =
              type_id == Type::Known::pictures().id() ? QStringLiteral("/Pictures")
            : type_id == Type::Known::music().id() ? QStringLiteral("/Music")
            : type_id == Type::Known::documents().id() ? QStringLiteral("/Documents")
            : QStringLiteral("/Contents");

        return prefix + app_pkg + suffix;
    }
}

void cucd::Transfer::setStoreInternal(Scope scope, const QString &type_id, const QString & store)
{
    d->store_scope = scope;
    d->store_type = type_id;
    d->store_path = store;

    Q_EMIT(StoreChanged());
}

int cucd::Transfer::SelectionType()
{
    TRACE() << __PRETTY_FUNCTION__;
    return d->selection_type;
}

void cucd::Transfer::SetSelectionType(int type)
{
    TRACE() << Q_FUNC_INFO;
    if (d->state != cuc::Transfer::created)
        return;
    if (d->selection_type == type)
        return;

    d->selection_type = type;
    Q_EMIT(SelectionTypeChanged(d->selection_type));
}

QString cucd::Transfer::DownloadId()
{
    TRACE() << Q_FUNC_INFO;
    return d->download_id;
}

void cucd::Transfer::SetDownloadId(QString DownloadId)
{
    TRACE() << Q_FUNC_INFO;
    if (d->download_id == DownloadId)
        return;

    d->download_id = DownloadId;
    Q_EMIT(DownloadIdChanged(d->download_id));
}

/* returns the object path for the export */
QString cucd::Transfer::export_path()
{
    TRACE() << Q_FUNC_INFO << "source:" << d->source;
    static const QString exporter_path_pattern{"/transfers/%1/export/%2"};
    QString source = exporter_path_pattern
            .arg(sanitize_id(d->source))
            .arg(d->id);
    return source;
}

/* returns the object path for the import */
QString cucd::Transfer::import_path()
{
    TRACE() << Q_FUNC_INFO << "destination:" << d->destination;
    static const QString importer_path_pattern{"/transfers/%1/import/%2"};
    QString destination = importer_path_pattern
            .arg(sanitize_id(d->destination))
            .arg(d->id);
    return destination;
}

/* sets, if the source app is freshly started by the content hub */
void cucd::Transfer::SetSourceStartedByContentHub(bool started)
{
    d->source_started_by_content_hub = started;
}

/* returns if the source app was started by the content hub */
bool com::lomiri::content::detail::Transfer::WasSourceStartedByContentHub() const
{
    return d->source_started_by_content_hub;
}

/* sets, if the dest app should be invoked by the content hub */
void cucd::Transfer::SetShouldBeStartedByContentHub(bool start)
{
    d->should_be_started_by_content_hub = start;
}

/* returns if the dest app should be invoked by the content hub */
bool com::lomiri::content::detail::Transfer::ShouldBeStartedByContentHub() const
{
    return d->should_be_started_by_content_hub;
}

QString cucd::Transfer::ContentType()
{
    TRACE() << __PRETTY_FUNCTION__;
    return d->content_type;
}
