/***************************************************************************
 *   Copyright (C) 2010-2012 by Daniel Nicoletti                           *
 *   dantti12@gmail.com                                                    *
 *                                                                         *
 *   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; see the file COPYING. If not, write to       *
 *   the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor,  *
 *   Boston, MA 02110-1301, USA.                                           *
 ***************************************************************************/

#include "JobModel.h"

#include <KCupsRequest.h>
#include <KCupsPrinter.h>
#include <KCupsJob.h>

#include <QDateTime>
#include <QMimeData>
#include <QPointer>

#include <KUser>
#include <KDebug>
#include <KLocale>
#include <KMessageBox>

#include <QStringBuilder>

JobModel::JobModel(QObject *parent) :
    QStandardItemModel(parent),
    m_jobRequest(0),
    m_whichjobs(CUPS_WHICHJOBS_ACTIVE),
    m_parentId(0)
{
    setHorizontalHeaderItem(ColStatus,        new QStandardItem(i18n("Status")));
    setHorizontalHeaderItem(ColName,          new QStandardItem(i18n("Name")));
    setHorizontalHeaderItem(ColUser,          new QStandardItem(i18n("User")));
    setHorizontalHeaderItem(ColCreated,       new QStandardItem(i18n("Created")));
    setHorizontalHeaderItem(ColCompleted,     new QStandardItem(i18n("Completed")));
    setHorizontalHeaderItem(ColPages,         new QStandardItem(i18n("Pages")));
    setHorizontalHeaderItem(ColProcessed,     new QStandardItem(i18n("Processed")));
    setHorizontalHeaderItem(ColSize,          new QStandardItem(i18n("Size")));
    setHorizontalHeaderItem(ColStatusMessage, new QStandardItem(i18n("Status Message")));
    setHorizontalHeaderItem(ColPrinter,       new QStandardItem(i18n("Printer")));
    setHorizontalHeaderItem(ColFromHost,      new QStandardItem(i18n("From Hostname")));

    // Setup the attributes we want from jobs
    m_jobAttributes << KCUPS_JOB_ID;
    m_jobAttributes << KCUPS_JOB_NAME;
    m_jobAttributes << KCUPS_JOB_K_OCTETS;
    m_jobAttributes << KCUPS_JOB_K_OCTETS_PROCESSED;
    m_jobAttributes << KCUPS_JOB_STATE;
    m_jobAttributes << KCUPS_TIME_AT_COMPLETED;
    m_jobAttributes << KCUPS_TIME_AT_CREATION;
    m_jobAttributes << KCUPS_TIME_AT_PROCESSING;
    m_jobAttributes << KCUPS_JOB_PRINTER_URI;
    m_jobAttributes << KCUPS_JOB_ORIGINATING_USER_NAME;
    m_jobAttributes << KCUPS_JOB_ORIGINATING_HOST_NAME;
    m_jobAttributes << KCUPS_JOB_MEDIA_PROGRESS;
    m_jobAttributes << KCUPS_JOB_MEDIA_SHEETS;
    m_jobAttributes << KCUPS_JOB_MEDIA_SHEETS_COMPLETED;
    m_jobAttributes << KCUPS_JOB_PRINTER_STATE_MESSAGE;
    m_jobAttributes << KCUPS_JOB_PRESERVED;

    QHash<int, QByteArray> roles = roleNames();
    roles[RoleJobId] = "jobId";
    roles[RoleJobState] = "jobState";
    roles[RoleJobName] = "jobName";
    roles[RoleJobPages] = "jobPages";
    roles[RoleJobSize] = "jobSize";
    roles[RoleJobOwner] = "jobOwner";
    roles[RoleJobCreatedAt] = "jobCreatedAt";
    roles[RoleJobIconName] = "jobIconName";
    roles[RoleJobCancelEnabled] = "jobCancelEnabled";
    roles[RoleJobHoldEnabled] = "jobHoldEnabled";
    roles[RoleJobReleaseEnabled] = "jobReleaseEnabled";
    roles[RoleJobRestartEnabled] = "jobRestartEnabled";
    roles[RoleJobPrinter] = "jobPrinter";
    roles[RoleJobOriginatingHostName] = "jobFrom";
    setRoleNames(roles);

    // This is emitted when a job change it's state
    connect(KCupsConnection::global(),
            SIGNAL(jobState(QString,QString,QString,uint,QString,bool,uint,uint,QString,QString,uint)),
            this,
            SLOT(insertUpdateJob(QString,QString,QString,uint,QString,bool,uint,uint,QString,QString,uint)));

    // This is emitted when a job is created
    connect(KCupsConnection::global(),
            SIGNAL(jobCreated(QString,QString,QString,uint,QString,bool,uint,uint,QString,QString,uint)),
            this,
            SLOT(insertUpdateJob(QString,QString,QString,uint,QString,bool,uint,uint,QString,QString,uint)));

    // This is emitted when a job is stopped
    connect(KCupsConnection::global(),
            SIGNAL(jobStopped(QString,QString,QString,uint,QString,bool,uint,uint,QString,QString,uint)),
            this,
            SLOT(insertUpdateJob(QString,QString,QString,uint,QString,bool,uint,uint,QString,QString,uint)));

    // This is emitted when a job has it's config changed
    connect(KCupsConnection::global(),
            SIGNAL(jobConfigChanged(QString,QString,QString,uint,QString,bool,uint,uint,QString,QString,uint)),
            this,
            SLOT(insertUpdateJob(QString,QString,QString,uint,QString,bool,uint,uint,QString,QString,uint)));

    // This is emitted when a job change it's progress
    connect(KCupsConnection::global(),
            SIGNAL(jobProgress(QString,QString,QString,uint,QString,bool,uint,uint,QString,QString,uint)),
            this,
            SLOT(insertUpdateJob(QString,QString,QString,uint,QString,bool,uint,uint,QString,QString,uint)));

    // This is emitted when a printer is removed
    connect(KCupsConnection::global(),
            SIGNAL(jobCompleted(QString,QString,QString,uint,QString,bool,uint,uint,QString,QString,uint)),
            this,
            SLOT(jobCompleted(QString,QString,QString,uint,QString,bool,uint,uint,QString,QString,uint)));

    connect(KCupsConnection::global(), SIGNAL(serverAudit(QString)),
            SLOT(getJobs()));
    connect(KCupsConnection::global(), SIGNAL(serverStarted(QString)),
            SLOT(getJobs()));
    connect(KCupsConnection::global(), SIGNAL(serverStopped(QString)),
            SLOT(getJobs()));
    connect(KCupsConnection::global(), SIGNAL(serverRestarted(QString)),
            SLOT(getJobs()));
}

void JobModel::setParentWId(WId parentId)
{
    m_parentId = parentId;
}

void JobModel::init(const QString &destName)
{
    m_destName = destName;

    // Get all jobs
    getJobs();
}

void JobModel::hold(const QString &printerName, int jobId)
{
    QPointer<KCupsRequest> request = new KCupsRequest;
    request->holdJob(printerName, jobId);
    request->waitTillFinished();
    if (request) {
        request->deleteLater();
    }
}

void JobModel::release(const QString &printerName, int jobId)
{
    QPointer<KCupsRequest> request = new KCupsRequest;
    request->releaseJob(printerName, jobId);
    request->waitTillFinished();
    if (request) {
        request->deleteLater();
    }
}

void JobModel::cancel(const QString &printerName, int jobId)
{
    QPointer<KCupsRequest> request = new KCupsRequest;
    request->cancelJob(printerName, jobId);
    request->waitTillFinished();
    if (request) {
        request->deleteLater();
    }
}

void JobModel::move(const QString &printerName, int jobId, const QString &toPrinterName)
{
    QPointer<KCupsRequest> request = new KCupsRequest;
    request->moveJob(printerName, jobId, toPrinterName);
    request->waitTillFinished();
    if (request) {
        request->deleteLater();
    }
}

void JobModel::getJobs()
{
    if (m_jobRequest) {
        return;
    }

    m_jobRequest = new KCupsRequest;
    connect(m_jobRequest, SIGNAL(finished()), this, SLOT(getJobFinished()));

    m_jobRequest->getJobs(m_destName, false, m_whichjobs, m_jobAttributes);

    m_processingJob.clear();
}

void JobModel::getJobFinished()
{
    KCupsRequest *request = static_cast<KCupsRequest *>(sender());
    if (request) {
        if (request->hasError()) {            
            // clear the model after so that the proper widget can be shown
            clear();
        } else {
            KCupsJobs jobs = request->jobs();
            kDebug() << jobs.size();
            for (int i = 0; i < jobs.size(); ++i) {
                if (jobs.at(i).state() == IPP_JOB_PROCESSING) {
                    m_processingJob = jobs.at(i).name();
                }

                // try to find the job row
                int job_row = jobRow(jobs.at(i).id());
                if (job_row == -1) {
                    // not found, insert new one
                    insertJob(i, jobs.at(i));
                } else if (job_row == i) {
                    // update the job
                    updateJob(i, jobs.at(i));
                } else {
                    // found at wrong position
                    // take it and insert on the right position
                    QList<QStandardItem *> row = takeRow(job_row);
                    insertRow(i, row);
                    updateJob(i, jobs.at(i));
                }
            }

            // remove old printers
            // The above code starts from 0 and make sure
            // dest == modelIndex(x) and if it's not the
            // case it either inserts or moves it.
            // so any item > num_jobs can be safely deleted
            while (rowCount() > jobs.size()) {
                removeRow(rowCount() - 1);
            }
        }
        request->deleteLater();
    } else {
        kWarning() << "Should not be called from a non KCupsRequest class" << sender();
    }
    m_jobRequest = 0;
}

void JobModel::jobCompleted(const QString &text,
                                   const QString &printerUri,
                                   const QString &printerName,
                                   uint printerState,
                                   const QString &printerStateReasons,
                                   bool printerIsAcceptingJobs,
                                   uint jobId,
                                   uint jobState,
                                   const QString &jobStateReasons,
                                   const QString &jobName,
                                   uint jobImpressionsCompleted)
{
    // REALLY? all these parameters just to say foo was deleted??
    Q_UNUSED(text)
    Q_UNUSED(printerUri)
    Q_UNUSED(printerName)
    Q_UNUSED(printerState)
    Q_UNUSED(printerStateReasons)
    Q_UNUSED(printerIsAcceptingJobs)
    Q_UNUSED(jobId)
    Q_UNUSED(jobState)
    Q_UNUSED(jobStateReasons)
    Q_UNUSED(jobName)
    Q_UNUSED(jobImpressionsCompleted)

    // We grab all jobs again
    getJobs();
}

void JobModel::insertUpdateJob(const QString &text,
                                      const QString &printerUri,
                                      const QString &printerName,
                                      uint printerState,
                                      const QString &printerStateReasons,
                                      bool printerIsAcceptingJobs,
                                      uint jobId,
                                      uint jobState,
                                      const QString &jobStateReasons,
                                      const QString &jobName,
                                      uint jobImpressionsCompleted)
{
    // REALLY? all these parameters just to say foo was created??
    Q_UNUSED(text)
    Q_UNUSED(printerUri)
    Q_UNUSED(printerName)
    Q_UNUSED(printerState)
    Q_UNUSED(printerStateReasons)
    Q_UNUSED(printerIsAcceptingJobs)
    Q_UNUSED(jobId)
    Q_UNUSED(jobState)
    Q_UNUSED(jobStateReasons)
    Q_UNUSED(jobName)
    Q_UNUSED(jobImpressionsCompleted)

    // We grab all jobs again
    getJobs();
}

void JobModel::insertJob(int pos, const KCupsJob &job)
{
    // insert the first column which has the job state and id
    QList<QStandardItem*> row;
    ipp_jstate_e jobState = job.state();
    QStandardItem *statusItem = new QStandardItem(jobStatus(jobState));
    statusItem->setData(jobState, RoleJobState);
    statusItem->setData(job.id(), RoleJobId);
    statusItem->setData(job.name(), RoleJobName);
    statusItem->setData(job.originatingUserName(), RoleJobOwner);
    statusItem->setData(job.originatingHostName(), RoleJobOriginatingHostName);
    QString size = KGlobal::locale()->formatByteSize(job.size());
    statusItem->setData(size, RoleJobSize);
    QString createdAt = KGlobal::locale()->formatDateTime(job.createdAt());
    statusItem->setData(createdAt, RoleJobCreatedAt);

    // TODO move the update code before the insert and reuse some code...
    statusItem->setData(KCupsJob::iconName(jobState), RoleJobIconName);
    statusItem->setData(KCupsJob::cancelEnabled(jobState), RoleJobCancelEnabled);
    statusItem->setData(KCupsJob::holdEnabled(jobState), RoleJobHoldEnabled);
    statusItem->setData(KCupsJob::releaseEnabled(jobState), RoleJobReleaseEnabled);
    statusItem->setData(job.reprintEnabled(), RoleJobRestartEnabled);

    QString pages = QString::number(job.pages());
    if (job.processedPages()) {
        pages = QString::number(job.processedPages()) % QLatin1Char('/') % QString::number(job.processedPages());
    }
    if (statusItem->data(RoleJobPages) != pages) {
        statusItem->setData(pages, RoleJobPages);
    }

    row << statusItem;
    for (int i = ColName; i < LastColumn; i++) {
        // adds all Items to the model
        row << new QStandardItem;
    }

    // insert the whole row
    insertRow(pos, row);

    // update the items
    updateJob(pos, job);
}

void JobModel::updateJob(int pos, const KCupsJob &job)
{
    // Job Status & internal dataipp_jstate_e
    ipp_jstate_e jobState = job.state();
    if (item(pos, ColStatus)->data(RoleJobState).toInt() != jobState) {
        item(pos, ColStatus)->setText(jobStatus(jobState));
        item(pos, ColStatus)->setData(static_cast<int>(jobState), RoleJobState);

        item(pos, ColStatus)->setData(KCupsJob::iconName(jobState), RoleJobIconName);
        item(pos, ColStatus)->setData(KCupsJob::cancelEnabled(jobState), RoleJobCancelEnabled);
        item(pos, ColStatus)->setData(KCupsJob::holdEnabled(jobState), RoleJobHoldEnabled);
        item(pos, ColStatus)->setData(KCupsJob::releaseEnabled(jobState), RoleJobReleaseEnabled);
        item(pos, ColStatus)->setData(job.reprintEnabled(), RoleJobRestartEnabled);
    }

    QString pages = QString::number(job.pages());
    if (job.processedPages()) {
        pages = QString::number(job.processedPages()) % QLatin1Char('/') % QString::number(job.processedPages());
    }
    if (item(pos, ColStatus)->data(RoleJobPages) != pages) {
        item(pos, ColStatus)->setData(pages, RoleJobPages);
    }

    // internal dest name & column
    QString destName = job.printer();
    if (item(pos, ColStatus)->data(RoleJobPrinter).toString() != destName) {
        item(pos, ColStatus)->setData(destName, RoleJobPrinter);
        // Column job printer Name
        item(pos, ColPrinter)->setText(destName);
    }

    // job name
    QString jobName = job.name();
    if (item(pos, ColName)->text() != jobName) {
        item(pos, ColStatus)->setData(jobName, RoleJobName);
        item(pos, ColName)->setText(jobName);
    }

    // owner of the job
    // try to get the full user name
    QString userString = job.originatingUserName();
    KUser user(userString);
    if (user.isValid() && !user.property(KUser::FullName).toString().isEmpty()) {
        userString = user.property(KUser::FullName).toString();
    }

    // user name
    if (item(pos, ColUser)->text() != userString) {
        item(pos, ColUser)->setText(userString);
    }

    // when it was created
    QDateTime timeAtCreation = job.createdAt();
    if (item(pos, ColCreated)->data(Qt::DisplayRole).toDateTime() != timeAtCreation) {
        item(pos, ColCreated)->setData(timeAtCreation, Qt::DisplayRole);
    }

    // when it was completed
    QDateTime completedAt = job.completedAt();
    if (item(pos, ColCompleted)->data(Qt::DisplayRole).toDateTime() != completedAt) {
        if (!completedAt.isNull()) {
            item(pos, ColCompleted)->setData(completedAt, Qt::DisplayRole);
        } else {
            // Clean the data might happen when the job is restarted
            item(pos, ColCompleted)->setText(QString());
        }
    }

    // job pages
    int completedPages = job.processedPages();
    if (item(pos, ColPages)->data(Qt::UserRole) != completedPages) {
        item(pos, ColPages)->setData(completedPages, Qt::UserRole);
        item(pos, ColPages)->setText(QString::number(completedPages));
    }

    // when it was precessed
    QDateTime timeAtProcessing = job.processedAt();
    if (item(pos, ColProcessed)->data(Qt::DisplayRole).toDateTime() != timeAtProcessing) {
        if (!timeAtProcessing.isNull()) {
            item(pos, ColProcessed)->setData(timeAtProcessing, Qt::DisplayRole);
        } else {
            // Clean the data might happen when the job is restarted
            item(pos, ColCompleted)->setText(QString());
        }
    }

    // job size TODO use kde converter
    int jobSize = job.size();
    if (item(pos, ColSize)->data(Qt::UserRole) != jobSize) {
        item(pos, ColSize)->setData(jobSize, Qt::UserRole);
        item(pos, ColSize)->setText(KGlobal::locale()->formatByteSize(jobSize));
    }

    // job printer state message
    QString stateMessage = job.stateMsg();
    if (item(pos, ColStatusMessage)->text() != stateMessage) {
        item(pos, ColStatusMessage)->setText(stateMessage);
    }

    // owner of the job
    // try to get the full user name
    QString originatingHostName = job.originatingHostName();
    if (item(pos, ColFromHost)->text() != originatingHostName) {
        item(pos, ColFromHost)->setText(originatingHostName);
    }
}

QStringList JobModel::mimeTypes() const
{
    return QStringList("application/x-cupsjobs");
}

Qt::DropActions JobModel::supportedDropActions() const
{
    return Qt::MoveAction;
}

QMimeData* JobModel::mimeData(const QModelIndexList &indexes) const
{
    QMimeData *mimeData = new QMimeData();
    QByteArray encodedData;

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

    foreach (const QModelIndex &index, indexes) {
        if (index.isValid() && index.column() == 0) {
            // serialize the jobId and fromDestName
            stream << data(index, RoleJobId).toInt()
                   << data(index, RoleJobPrinter).toString()
                   << item(index.row(), ColName)->text();
        }
    }

    mimeData->setData("application/x-cupsjobs", encodedData);
    return mimeData;
}

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

    if (!data->hasFormat("application/x-cupsjobs")) {
        return false;
    }

    QByteArray encodedData = data->data("application/x-cupsjobs");
    QDataStream stream(&encodedData, QIODevice::ReadOnly);

    bool ret = false;
    while (!stream.atEnd()) {
        QString fromDestName, displayName;
        int jobId;
        // get the jobid and the from dest name
        stream >> jobId >> fromDestName >> displayName;
        if (fromDestName == m_destName) {
            continue;
        }

        QPointer<KCupsRequest> request = new KCupsRequest;
        request->moveJob(fromDestName, jobId, m_destName);
        request->waitTillFinished();
        if (request) {
            if (request->hasError()) {
                // failed to move one job
                // we return here to avoid more password tries
                KMessageBox::detailedSorryWId(m_parentId,
                                              i18n("Failed to move '%1' to '%2'",
                                                   displayName, m_destName),
                                              request->errorMsg(),
                                              i18n("Failed"));
            }
            request->deleteLater();
            ret = !request->hasError();
        }
    }
    return ret;
}

KCupsRequest* JobModel::modifyJob(int row, JobAction action, const QString &newDestName, const QModelIndex &parent)
{
    Q_UNUSED(parent)

    if (row < 0 || row >= rowCount()) {
        kWarning() << "Row number is invalid:" << row;
        return 0;
    }

    QStandardItem *job = item(row, ColStatus);
    int jobId = job->data(RoleJobId).toInt();
    QString destName = job->data(RoleJobPrinter).toString();

    // ignore some jobs
    ipp_jstate_t state = static_cast<ipp_jstate_t>(job->data(RoleJobState).toInt());
    if ((state == IPP_JOB_HELD && action == Hold) ||
        (state == IPP_JOB_CANCELED && action == Cancel) ||
        (state != IPP_JOB_HELD && action == Release)) {
        return 0;
    }

    KCupsRequest *request = new KCupsRequest;
    switch (action) {
    case Cancel:
        request->cancelJob(destName, jobId);
        break;
    case Hold:
        request->holdJob(destName, jobId);
        break;
    case Release:
        request->releaseJob(destName, jobId);
        break;
    case Reprint:
        request->restartJob(destName, jobId);
        break;
    case Move:
        request->moveJob(destName, jobId, newDestName);
        break;
    default:
        kWarning() << "Unknown ACTION called!!!" << action;
        return 0;
    }

    return request;
}

int JobModel::jobRow(int jobId)
{
    // find the position of the jobId inside the model
    for (int i = 0; i < rowCount(); i++) {
        if (jobId == item(i)->data(RoleJobId).toInt())
        {
            return i;
        }
    }
    // -1 if not found
    return -1;
}

QString JobModel::jobStatus(ipp_jstate_e job_state)
{
  switch (job_state)
  {
    case IPP_JOB_PENDING    : return i18n("Pending");
    case IPP_JOB_HELD       : return i18n("On hold");
    case IPP_JOB_PROCESSING : return "-";
    case IPP_JOB_STOPPED    : return i18n("Stopped");
    case IPP_JOB_CANCELED   : return i18n("Canceled");
    case IPP_JOB_ABORTED    : return i18n("Aborted");
    case IPP_JOB_COMPLETED  : return i18n("Completed");
  }
  return "-";
}

void JobModel::clear()
{
    removeRows(0, rowCount());
}

void JobModel::setWhichJobs(WhichJobs whichjobs)
{
    switch (whichjobs) {
    case WhichActive:
        m_whichjobs = CUPS_WHICHJOBS_ACTIVE;
        break;
    case WhichCompleted:
        m_whichjobs = CUPS_WHICHJOBS_COMPLETED;
        break;
    case WhichAll:
        m_whichjobs = CUPS_WHICHJOBS_ALL;
        break;
    }

    getJobs();
}

Qt::ItemFlags JobModel::flags(const QModelIndex &index) const
{
    if (index.isValid()) {
        ipp_jstate_t state = static_cast<ipp_jstate_t>(item(index.row(), ColStatus)->data(RoleJobState).toInt());
        if (state == IPP_JOB_PENDING ||
            state == IPP_JOB_PROCESSING) {
            return Qt::ItemIsSelectable | Qt::ItemIsEnabled | Qt::ItemIsDragEnabled | Qt::ItemIsDropEnabled;
        }
    }
    return Qt::ItemIsSelectable | Qt::ItemIsEnabled | Qt::ItemIsDropEnabled;
}

QString JobModel::processingJob() const
{
    return m_processingJob;
}

#include "JobModel.moc"
