/* This file is part of the KDE project
  Copyright (C) 2009, 2010, 2012 Dag Andersen <danders@get2net.dk>

  This library is free software; you can redistribute it and/or
  modify it under the terms of the GNU Library General Public
  License as published by the Free Software Foundation; either
  version 2 of the License, or (at your option) any later version.

  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.
*/

// clazy:excludeall=qstring-arg
#include "kptschedulerplugin.h"

#include "kptproject.h"
#include "kptschedule.h"
#include "kptxmlloaderobject.h"
#include "XmlSaveContext.h"
#include "kptdebug.h"

#include "KoXmlReader.h"


namespace KPlato
{

class Q_DECL_HIDDEN SchedulerPlugin::Private
{
public:
    Private() {}

    QString name;
    QString comment;
};

SchedulerPlugin::SchedulerPlugin(QObject *parent)
    : QObject(parent),
      d(new SchedulerPlugin::Private()),
      m_granularity(0)
{
    // register Schedule::Log so it can be used in queued connections
    qRegisterMetaType<Schedule::Log>("Schedule::Log");

    m_synctimer.setInterval(500);
    connect(&m_synctimer, &QTimer::timeout, this, &SchedulerPlugin::slotSyncData);
}

SchedulerPlugin::~SchedulerPlugin()
{
    foreach (SchedulerThread *s, m_jobs) {
        s->mainManager()->setScheduling(false);
        s->haltScheduling();
        s->wait();
    }
    delete d;
}

void SchedulerPlugin::setName(const QString &name)
{
    d->name = name;
}

QString SchedulerPlugin::name() const
{
    return d->name;
}

void SchedulerPlugin::setComment(const QString &comment)
{
    d->comment = comment;
}

QString SchedulerPlugin::comment() const
{
    return d->comment;
}

int SchedulerPlugin::capabilities() const
{
    return AvoidOverbooking | AllowOverbooking | ScheduleForward | ScheduleBackward;
}

void SchedulerPlugin::stopCalculation(ScheduleManager *sm)
{
    foreach (SchedulerThread *j, m_jobs) {
        if (sm == j->mainManager()) {
            j->stopScheduling();
        }
    }
}

void SchedulerPlugin::haltCalculation(ScheduleManager *sm)
{
    debugPlan<<sm;
    foreach (SchedulerThread *j, m_jobs) {
        if (sm == j->mainManager()) {
            haltCalculation(j);
            break;
        }
    }
    sm->setCalculationResult(ScheduleManager::CalculationCanceled);
    sm->setScheduling(false);
}

void SchedulerPlugin::stopCalculation(SchedulerThread *job)
{
    job->stopScheduling();
}

void SchedulerPlugin::haltCalculation(SchedulerThread *job)
{
    debugPlan<<job<<m_jobs.contains(job);
    disconnect(this, 0, job, 0);
    job->haltScheduling();
    if (m_jobs.contains(job)) {
        debugPlan<<"SchedulerPlugin::haltCalculation: remove"<<job;
        m_jobs.removeAt(m_jobs.indexOf(job));
    }
}

QList<long unsigned int> SchedulerPlugin::granularities() const
{
    return m_granularities;
}

int SchedulerPlugin::granularity() const
{
    return m_granularity;
}

void SchedulerPlugin::setGranularity(int index)
{
    m_granularity = qMin(index, m_granularities.count() - 1);
}

void SchedulerPlugin::slotSyncData()
{
    updateProgress();
    updateLog();
}

void SchedulerPlugin::updateProgress()
{
    foreach (SchedulerThread *j, m_jobs) {
        ScheduleManager *sm = j->mainManager();
        if (sm->maxProgress() != j->maxProgress()) {
            sm->setMaxProgress(j->maxProgress());
        }
        sm->setProgress(j->progress());
    }
}

void SchedulerPlugin::updateLog()
{
    foreach (SchedulerThread *j, m_jobs) {
        updateLog(j);
    }
}

void SchedulerPlugin::updateLog(SchedulerThread *j)
{
    ScheduleManager *sm = j->mainManager();
    Project *p = j->mainProject();
    Q_ASSERT(p == &(sm->project()));
#ifdef NDEBUG
    Q_UNUSED(p)
#endif

    if (j->manager()) {
        sm->setPhaseNames(j->phaseNames());
    }

    QVector<Schedule::Log> logs = j->takeLog();
    QMutableVectorIterator<Schedule::Log> i(logs);
    while (i.hasNext()) {
        Schedule::Log &l = i.next();
        // map log from temporary project to real project
        if (l.resource) {
            l.resource = sm->project().findResource(l.resource->id());
            // resource may have been deleted while scheduling is running
            if (!l.resource) {
                i.remove();
                continue;
            }
            Q_ASSERT(l.resource->project() == p);
        }
        if (l.node) {
            const Node *n = l.node;
            if (l.node->type() == Node::Type_Project) {
                l.node = &(sm->project());
            } else {
                l.node = sm->project().findNode(l.node->id());
                // task may have been deleted while scheduling is running
                if (!l.node) {
                    i.remove();
                    continue;
                }
            }
            Q_ASSERT(n != l.node);
#ifdef NDEBUG
            Q_UNUSED(n)
#endif
            Q_ASSERT(l.node->projectNode() == p);
        }
    }
    if (! logs.isEmpty()) {
        sm->slotAddLog(logs);
    }
}

void SchedulerPlugin::updateProject(const Project *tp, const ScheduleManager *tm, Project *mp, ScheduleManager *sm) const
{
    Q_CHECK_PTR(tp);
    Q_CHECK_PTR(tm);
    Q_CHECK_PTR(mp);
    Q_CHECK_PTR(sm);
    //debugPlan<<"SchedulerPlugin::updateProject:"<<tp<<tp->name()<<"->"<<mp<<mp->name()<<sm;
    Q_ASSERT(tp != mp && tm != sm);
    long sid = tm->scheduleId();
    Q_ASSERT(sid == sm->scheduleId());

    XMLLoaderObject status;
    status.setVersion(PLAN_FILE_SYNTAX_VERSION);
    status.setProject(mp);
    status.setProjectTimeZone(mp->timeZone());

    foreach (const Node *tn, tp->allNodes()) {
        Node *mn = mp->findNode(tn->id());
        if (mn) {
            updateNode(tn, mn, sid, status);
        }
    }
    foreach (const Resource *tr, tp->resourceList()) {
        Resource *r = mp->findResource(tr->id());
        if (r) {
            updateResource(tr, r, status);
        }
    }
    // update main schedule and appointments
    updateAppointments(tp, tm, mp, sm, status);
    sm->scheduleChanged(sm->expected());
}

void SchedulerPlugin::updateNode(const Node *tn, Node *mn, long sid, XMLLoaderObject &status) const
{
    //debugPlan<<"SchedulerPlugin::updateNode:"<<tn<<tn->name()<<"->"<<mn<<mn->name();
    NodeSchedule *s = static_cast<NodeSchedule*>(tn->schedule(sid));
    if (s == 0) {
        warnPlan<<"SchedulerPlugin::updateNode:"<<"Task:"<<tn->name()<<"could not find schedule with id:"<<sid;
        return;
    }
    QDomDocument doc("tmp");
    QDomElement e = doc.createElement("schedules");
    doc.appendChild(e);
    s->saveXML(e);

    Q_ASSERT(! mn->findSchedule(sid));
    s = static_cast<NodeSchedule*>(mn->schedule(sid));
    Q_ASSERT(s == 0);
    s = new NodeSchedule();

    KoXmlDocument xd;
    xd.setContent(doc.toString());
    KoXmlElement se = xd.documentElement().namedItem("schedule").toElement();
    Q_ASSERT(! se.isNull());

    s->loadXML(se, status);
    s->setDeleted(false);
    s->setNode(mn);
    mn->addSchedule(s);
}

void SchedulerPlugin::updateResource(const Resource *tr, Resource *r, XMLLoaderObject &status) const
{
    QDomDocument doc("tmp");
    QDomElement e = doc.createElement("cache");
    doc.appendChild(e);
    tr->saveCalendarIntervalsCache(e);

    KoXmlDocument xd;
    QString err;
    xd.setContent(doc.toString(), &err);
    KoXmlElement se = xd.documentElement();
    Q_ASSERT(! se.isNull());
    r->loadCalendarIntervalsCache(se, status);

    Calendar *cr = tr->calendar();
    Calendar *c = r->calendar();
    if (cr == 0 || c == 0) {
        return;
    }
    debugPlan<<"cr:"<<cr->cacheVersion()<<"c"<<c->cacheVersion();
    c->setCacheVersion(cr->cacheVersion());
}

void SchedulerPlugin::updateAppointments(const Project *tp, const ScheduleManager *tm, Project *mp, ScheduleManager *sm, XMLLoaderObject &status) const
{
    MainSchedule *sch = tm->expected();
    Q_ASSERT(sch);
    Q_ASSERT(sch != sm->expected());
    Q_ASSERT(sch->id() == sm->expected()->id());

    QDomDocument doc("tmp");
    QDomElement e = doc.createElement("schedule");
    doc.appendChild(e);
    sch->saveXML(e);
    tp->saveAppointments(e, sch->id());

    KoXmlDocument xd;
    xd.setContent(doc.toString());
    KoXmlElement se = xd.namedItem("schedule").toElement();
    Q_ASSERT(! se.isNull());

    bool ret = sm->loadMainSchedule(sm->expected(), se, status); // also loads appointments
    Q_ASSERT(ret);
#ifdef NDEBUG
    Q_UNUSED(ret)
#endif
    mp->setCurrentSchedule(sch->id());
    sm->expected()->setPhaseNames(sch->phaseNames());
    mp->changed(sm);
}

//----------------------
SchedulerThread::SchedulerThread(Project *project, ScheduleManager *manager, QObject *parent)
    : QThread(parent),
    m_mainproject(project),
    m_mainmanager(manager),
    m_mainmanagerId(manager->managerId()),
    m_project(0),
    m_manager(0),
    m_stopScheduling(false),
    m_haltScheduling(false),
    m_progress(0)
{
    manager->createSchedules(); // creates expected() to get log messages during calculation

    QDomDocument document("kplato");
    saveProject(project, document);

    m_pdoc.setContent(document.toString());


    connect(this, &QThread::started, this, &SchedulerThread::slotStarted);
    connect(this, &QThread::finished, this, &SchedulerThread::slotFinished);
}

SchedulerThread::~SchedulerThread()
{
    debugPlan<<"SchedulerThread::~SchedulerThread:"<<QThread::currentThreadId();
    delete m_project;
    m_project = 0;
    wait();
}

void SchedulerThread::setMaxProgress(int value)
{
    m_maxprogressMutex.lock();
    m_maxprogress = value;
    m_maxprogressMutex.unlock();
    emit maxProgressChanged(value, m_mainmanager);
}

int SchedulerThread::maxProgress() const
{
    QMutexLocker m(&m_maxprogressMutex);
    return m_maxprogress;
}

void SchedulerThread::setProgress(int value)
{
    m_progressMutex.lock();
    m_progress = value;
    m_progressMutex.unlock();
    emit progressChanged(value, m_mainmanager);
}

int SchedulerThread::progress() const
{
    QMutexLocker m(&m_progressMutex);
    return m_progress;
}

void SchedulerThread::slotAddLog(const KPlato::Schedule::Log &log)
{
//     debugPlan<<log;
    QMutexLocker m(&m_logMutex);
    m_logs << log;
}

QVector<Schedule::Log> SchedulerThread::takeLog()
{
    QMutexLocker m(&m_logMutex);
    const QVector<KPlato::Schedule::Log> l = m_logs;
    m_logs.clear();
    return l;
}

QMap<int, QString> SchedulerThread::phaseNames() const
{
    QMutexLocker m(&m_managerMutex);
    return m_manager->phaseNames();
}


void SchedulerThread::slotStarted()
{
    emit jobStarted(this);
}

void SchedulerThread::slotFinished()
{
    if (m_haltScheduling) {
        deleteLater();
    } else {
        emit jobFinished(this);
    }
}

void SchedulerThread::doRun()
{
    slotStarted();
    run();
    slotFinished();
}

ScheduleManager *SchedulerThread::manager() const
{
    QMutexLocker m(&m_managerMutex);
    return m_manager;
}

Project *SchedulerThread::project() const
{
    QMutexLocker m(&m_projectMutex);
    return m_project;
}

void SchedulerThread::stopScheduling()
{
    debugPlan;
    m_stopScheduling = true;
}

void SchedulerThread::haltScheduling()
{
    debugPlan;
    m_haltScheduling = true;
}

void SchedulerThread::logError(Node *n, Resource *r, const QString &msg, int phase)
{
    Schedule::Log log;
    if (r == 0) {
        log = Schedule::Log(n, Schedule::Log::Type_Error, msg, phase);
    } else {
        log = Schedule::Log(n, r, Schedule::Log::Type_Error, msg, phase);
    }
    slotAddLog(log);
}

void SchedulerThread::logWarning(Node *n, Resource *r, const QString &msg, int phase)
{
    Schedule::Log log;
    if (r == 0) {
        log = Schedule::Log(n, Schedule::Log::Type_Warning, msg, phase);
    } else {
        log = Schedule::Log(n, r, Schedule::Log::Type_Warning, msg, phase);
    }
    slotAddLog(log);
}

void SchedulerThread::logInfo(Node *n, Resource *r, const QString &msg, int phase)
{
    Schedule::Log log;
    if (r == 0) {
        log = Schedule::Log(n, Schedule::Log::Type_Info, msg, phase);
    } else {
        log = Schedule::Log(n, r, Schedule::Log::Type_Info, msg, phase);
    }
    slotAddLog(log);
}

void SchedulerThread::logDebug(Node *n, Resource *r, const QString &msg, int phase)
{
    Schedule::Log log;
    if (r == 0) {
        log = Schedule::Log(n, Schedule::Log::Type_Debug, msg, phase);
    } else {
        log = Schedule::Log(n, r, Schedule::Log::Type_Debug, msg, phase);
    }
    slotAddLog(log);
}

//static
void SchedulerThread::saveProject(Project *project, QDomDocument &document)
{
    document.appendChild(document.createProcessingInstruction("xml", "version=\"1.0\" encoding=\"UTF-8\""));

    QDomElement doc = document.createElement("kplato");
    doc.setAttribute("editor", "Plan");
    doc.setAttribute("mime", "application/x-vnd.kde.plan");
    doc.setAttribute("version", PLAN_FILE_SYNTAX_VERSION);
    document.appendChild(doc);
    project->save(doc, XmlSaveContext());
}

//static
bool SchedulerThread::loadProject(Project *project, const KoXmlDocument &doc)
{
    KoXmlElement pel = doc.documentElement().namedItem("project").toElement();
    if (pel.isNull()) {
        return false;
    }
    XMLLoaderObject status;
    status.setVersion(PLAN_FILE_SYNTAX_VERSION);
    status.setProject(project);
    return project->load(pel, status);
}


} //namespace KPlato

