/* This file is part of the KDE project
 * Copyright (C) 2009, 2010, 2011, 2012 Dag Andersen <danders@get2net.dk>
 * Copyright (C) 2016 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 "PlanTJScheduler.h"

#include "kptproject.h"
#include "kptschedule.h"
#include "kptresource.h"
#include "kpttask.h"
#include "kptrelation.h"
#include "kptduration.h"
#include "kptcalendar.h"
#include "kptdebug.h"

#include "taskjuggler/taskjuggler.h"
#include "taskjuggler/Project.h"
#include "taskjuggler/Scenario.h"
#include "taskjuggler/Resource.h"
#include "taskjuggler/Task.h"
#include "taskjuggler/Interval.h"
#include "taskjuggler/Allocation.h"
#include "taskjuggler/Utility.h"
#include "taskjuggler/UsageLimits.h"
#include "taskjuggler/CoreAttributes.h"
#include "taskjuggler/TjMessageHandler.h"
#include "taskjuggler/debug.h"

#include <QString>
#include <QTime>
#include <QMutexLocker>
#include <QMap>
#include <QLocale>

#include <KLocalizedString>
#include <KFormat>

#include <iostream>

#define PROGRESS_MAX_VALUE 100


PlanTJScheduler::PlanTJScheduler(Project *project, ScheduleManager *sm, ulong granularity, QObject *parent)
    : SchedulerThread(project, sm, parent),
    result(-1),
    m_schedule(0),
    m_recalculate(false),
    m_usePert(false),
    m_backward(false),
    m_granularity(granularity)
{
    TJ::TJMH.reset();
    connect(&TJ::TJMH, &TJ::TjMessageHandler::message, this, &PlanTJScheduler::slotMessage);

    connect(this, &PlanTJScheduler::sigCalculationStarted, project, &KPlato::Project::sigCalculationStarted);
    emit sigCalculationStarted(project, sm);

    connect(this, &PlanTJScheduler::sigCalculationFinished, project, &KPlato::Project::sigCalculationFinished);
}

PlanTJScheduler::~PlanTJScheduler()
{
    delete m_tjProject;
}

void PlanTJScheduler::slotMessage(int type, const QString &msg, TJ::CoreAttributes *object)
{
//     debugPlan<<"PlanTJScheduler::slotMessage:"<<msg;
    Schedule::Log log;
    if (object &&  object->getType() == CA_Task && m_taskmap.contains(static_cast<TJ::Task*>(object))) {
        log = Schedule::Log(static_cast<Node*>(m_taskmap[ static_cast<TJ::Task*>(object) ]), type, msg);
    } else if (object && object->getType() == CA_Resource && m_resourcemap.contains(static_cast<TJ::Resource*>(object))) {
        log = Schedule::Log(0, m_resourcemap[ static_cast<TJ::Resource*>(object) ], type, msg);
    } else if (object && ! object->getName().isEmpty()) {
        log = Schedule::Log(static_cast<Node*>(m_project), type, QString("%1: %2").arg(object->getName()).arg(msg));
    } else {
        log = Schedule::Log(static_cast<Node*>(m_project), type, msg);
    }
    slotAddLog(log);
}

void PlanTJScheduler::run()
{
    if (m_haltScheduling) {
        deleteLater();
        return;
    }
    if (m_stopScheduling) {
        return;
    }
    setMaxProgress(PROGRESS_MAX_VALUE);
    { // mutex -->
        m_projectMutex.lock();
        m_managerMutex.lock();

        m_project = new Project();
        loadProject(m_project, m_pdoc);
        m_project->setName("Schedule: " + m_project->name()); //Debug
        m_project->stopcalculation = false;
        m_manager = m_project->scheduleManager(m_mainmanagerId);
        Q_CHECK_PTR(m_manager);
        Q_ASSERT(m_manager->expected());
        Q_ASSERT(m_manager != m_mainmanager);
        Q_ASSERT(m_manager->scheduleId() == m_mainmanager->scheduleId());
        Q_ASSERT(m_manager->expected() != m_mainmanager->expected());
        m_manager->setName("Schedule: " + m_manager->name()); //Debug
        m_schedule = m_manager->expected();

        bool x = connect(m_manager, SIGNAL(sigLogAdded(KPlato::Schedule::Log)), this, SLOT(slotAddLog(KPlato::Schedule::Log)));
        Q_ASSERT(x);
        Q_UNUSED(x);

        m_project->initiateCalculation(*m_schedule);
        m_project->initiateCalculationLists(*m_schedule);

        m_usePert = m_manager->usePert();
        m_recalculate = m_manager->recalculate();
        if (m_recalculate) {
            m_backward = false;
        } else {
            m_backward = m_manager->schedulingDirection();
        }
        m_project->setCurrentSchedule(m_manager->expected()->id());

        m_schedule->setPhaseName(0, xi18nc("@info/plain" , "Init"));
        QLocale locale;
        KFormat format(locale);
        if (! m_backward) {
            logDebug(m_project, 0, QString("Schedule project using TJ Scheduler, starting at %1, granularity %2").arg(QDateTime::currentDateTime().toString()).arg(format.formatDuration(m_granularity)), 0);
            if (m_recalculate) {
                logInfo(m_project, 0, xi18nc("@info/plain" , "Re-calculate project from start time: %1", locale.toString(m_project->constraintStartTime(), QLocale::ShortFormat)), 0);
            } else {
                logInfo(m_project, 0, xi18nc("@info/plain" , "Schedule project from start time: %1", locale.toString(m_project->constraintStartTime(), QLocale::ShortFormat)), 0);
            }
            logInfo(m_project, 0, xi18nc("@info/plain" , "Project target finish time: %1", locale.toString(m_project->constraintEndTime(), QLocale::ShortFormat)), 0);
        } else {
            logDebug(m_project, 0, QString("Schedule project backward using TJ Scheduler, starting at %1, granularity %2").arg(locale.toString(QDateTime::currentDateTime(), QLocale::ShortFormat)).arg(format.formatDuration(m_granularity)), 0);
            logInfo(m_project, 0, xi18nc("@info/plain" , "Schedule project from end time: %1", locale.toString(m_project->constraintEndTime(), QLocale::ShortFormat)), 0);
        }

        m_managerMutex.unlock();
        m_projectMutex.unlock();
    } // <--- mutex
    setProgress(2);
    if (! kplatoToTJ()) {
        result = 1;
        setProgress(PROGRESS_MAX_VALUE);
        return;
    }
    setMaxProgress(PROGRESS_MAX_VALUE);
    connect(m_tjProject, SIGNAL(updateProgressBar(int,int)), this, SLOT(setProgress(int)));

    m_schedule->setPhaseName(1, xi18nc("@info/plain" , "Schedule"));
    logInfo(m_project, 0, "Start scheduling", 1);
    bool r = solve();
    if (! r) {
        debugPlan<<"Scheduling failed";
        result = 2;
        logError(m_project, 0, xi18nc("@info/plain" , "Failed to schedule project"));
        setProgress(PROGRESS_MAX_VALUE);
        return;
    }
    if (m_haltScheduling) {
        debugPlan<<"Scheduling halted";
        logInfo(m_project, 0, "Scheduling halted");
        deleteLater();
        return;
    }
    m_schedule->setPhaseName(2, xi18nc("@info/plain" , "Update"));
    logInfo(m_project, 0, "Scheduling finished, update project", 2);
    if (! kplatoFromTJ()) {
        logError(m_project, 0, "Project update failed");
    }
    setProgress(PROGRESS_MAX_VALUE);
    m_schedule->setPhaseName(3, xi18nc("@info/plain" , "Finish"));
}

bool PlanTJScheduler::check()
{
    DebugCtrl.setDebugMode(0);
    DebugCtrl.setDebugLevel(1000);
    return m_tjProject->pass2(true);
}

bool PlanTJScheduler::solve()
{
    debugPlan<<"PlanTJScheduler::solve()";
    TJ::Scenario *sc = m_tjProject->getScenario(0);
    if (! sc) {
        logError(m_project, 0, xi18nc("@info/plain" , "Failed to find scenario to schedule"));
        return false;
    }
    DebugCtrl.setDebugLevel(0);
    DebugCtrl.setDebugMode(PSDEBUG+TSDEBUG+RSDEBUG+PADEBUG);

    return m_tjProject->scheduleScenario(sc);
}

bool PlanTJScheduler::kplatoToTJ()
{
    m_tjProject = new TJ::Project();
    m_tjProject->setPriority(m_project->priority());
    m_tjProject->setScheduleGranularity(m_granularity / 1000);
    m_tjProject->getScenario(0)->setMinSlackRate(0.0); // Do not calculate critical path

    m_tjProject->setNow(m_project->constraintStartTime().toTime_t());
    m_tjProject->setStart(m_project->constraintStartTime().toTime_t());
    m_tjProject->setEnd(m_project->constraintEndTime().toTime_t());

    m_tjProject->setDailyWorkingHours(m_project->standardWorktime()->day());

    // Set working days for the project, it is used for tasks with a length specification
    // FIXME: Plan has task specific calendars for this estimate type
    KPlato::Calendar *cal = m_project->defaultCalendar();
    if (! cal) {
        m_project->calendars().value(0);
    }
    if (cal) {
        int days[ 7 ] = { Qt::Sunday, Qt::Monday, Qt::Tuesday, Qt::Wednesday, Qt::Thursday, Qt::Friday, Qt::Saturday };
        for (int i = 0; i < 7; ++i) {
            CalendarDay *d = 0;
            for (Calendar *c = cal; c; c = c->parentCal()) {
                QTime t; t.start();
                d = c->weekday(days[ i ]);
                Q_ASSERT(d);
                if (d == 0 || d->state() != CalendarDay::Undefined) {
                    break;
                }
            }
            if (d && d->state() == CalendarDay::Working) {
                QList<TJ::Interval*> lst;
                foreach (const TimeInterval *ti, d->timeIntervals()) {
                    TJ::Interval *tji = new TJ::Interval(toTJInterval(ti->startTime(), ti->endTime(),tjGranularity()));
                    lst << tji;
                }
                m_tjProject->setWorkingHours(i, lst);
                qDeleteAll(lst);
            }
        }
    }
    addTasks();
    setConstraints();
    addDependencies();
    addRequests();
    addStartEndJob();

    if (result != -1) {
        return false;
    }
    return check();
}

void PlanTJScheduler::addStartEndJob()
{
    TJ::Task *start = new TJ::Task(m_tjProject, "TJ::StartJob", "TJ::StartJob", 0, QString(), 0);
    start->setMilestone(true);
    if (! m_backward) {
        start->setSpecifiedStart(0, m_tjProject->getStart());
        start->setPriority(999);
    } else {
        // backwards: insert a new ms before start and make start an ALAP to push all other jobs ALAP
        TJ::Task *bs = new TJ::Task(m_tjProject, "TJ::StartJob-B", "TJ::StartJob-B", 0, QString(), 0);
        bs->setMilestone(true);
        bs->setSpecifiedStart(0, m_tjProject->getStart());
        bs->setPriority(999);
        bs->addPrecedes(start->getId());
        start->addDepends(bs->getId());
        start->setScheduling(TJ::Task::ALAP);
    }
    TJ::Task *end = new TJ::Task(m_tjProject, "TJ::EndJob", "TJ::EndJob", 0, QString(), 0);
    end->setMilestone(true);
    if (m_backward) {
        end->setSpecifiedEnd(0, m_tjProject->getEnd() - 1);
        end->setScheduling(TJ::Task::ALAP);
    }
    for (QMap<TJ::Task*, Task*>::ConstIterator it = m_taskmap.constBegin(); it != m_taskmap.constEnd(); ++it) {
        if (it.value()->isStartNode()) {
            it.key()->addDepends(start->getId());
//             logDebug(m_project, 0, QString("'%1' depends on: '%2'").arg(it.key()->getName()).arg(start->getName()));
            if (start->getScheduling() == TJ::Task::ALAP) {
                start->addPrecedes(it.key()->getId());
//                 logDebug(m_project, 0, QString("'%1' precedes: '%2'").arg(start->getName()).arg(it.key()->getName()));
            }
        }
        if (it.value()->isEndNode()) {
            end->addDepends(it.key()->getId());
            if (it.key()->getScheduling() == TJ::Task::ALAP) {
                it.key()->addPrecedes(end->getId());
            }
        }
    }
}

// static
int PlanTJScheduler::toTJDayOfWeek(int day)
{
    return day == 7 ? 0 : day;
}

// static
DateTime PlanTJScheduler::fromTime_t(time_t t, const QTimeZone &tz) {
    return DateTime (QDateTime::fromTime_t(t).toTimeZone(tz));
}

time_t PlanTJScheduler::toTJTime_t(const QDateTime &dt, ulong granularity)
{
    int secs = QTime(0, 0, 0).secsTo(dt.time());
    secs -= secs % granularity;
    return QDateTime(dt.date(), QTime(0, 0, 0).addSecs(secs), dt.timeZone()).toTime_t();
}

// static
AppointmentInterval PlanTJScheduler::fromTJInterval(const TJ::Interval &tji, const QTimeZone &tz) {
    AppointmentInterval a(fromTime_t(tji.getStart(), tz), fromTime_t(tji.getEnd(), tz).addSecs(1));
    return a;
}

// static
TJ::Interval PlanTJScheduler::toTJInterval(const QDateTime &start, const QDateTime &end, ulong granularity) {
    int secs = QTime(0, 0, 0).secsTo(start.time());
    secs -= secs % granularity;
    QDateTime s(start.date(), QTime(0, 0, 0).addSecs(secs), start.timeZone());
    secs = QTime(0, 0, 0).secsTo(end.time());
    secs -= secs % granularity;
    QDateTime e(end.date(), QTime(0, 0, 0).addSecs(secs), end.timeZone());
    TJ::Interval ti(s.toTime_t(), e.addSecs(-1).toTime_t());
    return ti;
}

// static
TJ::Interval PlanTJScheduler::toTJInterval(const QTime &start, const QTime &end, ulong granularity) {
    int secs = QTime(0, 0, 0).secsTo(start);
    time_t s =  secs - (secs % granularity);
    secs = (end == QTime(0, 0, 0)) ? 86399 : QTime(0, 0, 0).secsTo(end);
    time_t e = secs - (secs % granularity) - 1;
    TJ::Interval ti(s, e);
    return ti;
}

ulong PlanTJScheduler::tjGranularity() const
{
    return m_tjProject->getScheduleGranularity();
}

bool PlanTJScheduler::kplatoFromTJ()
{
    MainSchedule *cs = static_cast<MainSchedule*>(m_project->currentSchedule());

    QDateTime start;
    QDateTime end;
    for (QMap<TJ::Task*, Task*>::ConstIterator it = m_taskmap.constBegin(); it != m_taskmap.constEnd(); ++it) {
        if (! taskFromTJ(it.key(), it.value())) {
            return false;
        }
        if (! start.isValid() || it.value()->startTime() < start) {
            start = it.value()->startTime();
        }
        if (! end.isValid() || it.value()->endTime() > end) {
            end = it.value()->endTime();
        }
    }
    m_project->setStartTime(start.isValid() ? start : m_project->constraintStartTime());
    m_project->setEndTime(end.isValid() ? end : m_project->constraintEndTime());

    adjustSummaryTasks(m_schedule->summaryTasks());

    foreach (Task *task, m_taskmap) {
        calcPertValues(task);
    }

    m_project->calcCriticalPathList(m_schedule);
    // calculate positive float
    foreach (Task* t, m_taskmap) {
        if (! t->inCriticalPath() && t->isStartNode()) {
            calcPositiveFloat(t);
        }
    }

    QLocale locale;
    logInfo(m_project, 0, xi18nc("@info/plain" , "Project scheduled to start at %1 and finish at %2", locale.toString(m_project->startTime(), QLocale::ShortFormat), locale.toString(m_project->endTime(), QLocale::ShortFormat)));

    if (m_manager) {
        logDebug(m_project, 0, QString("Project scheduling finished at %1").arg(locale.toString(QDateTime::currentDateTime(), QLocale::ShortFormat)));
        m_project->finishCalculation(*m_manager);
        m_manager->scheduleChanged(cs);
    }
    return true;
}

bool PlanTJScheduler::taskFromTJ(TJ::Task *job, Task *task)
{
    if (m_haltScheduling || m_manager == 0) {
        return true;
    }
    Schedule *cs = task->currentSchedule();
    Q_ASSERT(cs);
    QTimeZone tz = m_project->timeZone();
    debugPlan<<"taskFromTJ:"<<task<<task->name()<<cs->id()<<tz;
    time_t s = job->getStart(0);
    if (s < m_tjProject->getStart() || s > m_tjProject->getEnd()) {
        m_project->currentSchedule()->setSchedulingError(true);
        cs->setSchedulingError(true);
        s = m_tjProject->getStart();
    }
    time_t e = job->getEnd(0);
    if (job->isMilestone()) {
        Q_ASSERT(s = (e + 1));
        e = s - 1;
    } else if (e <= s || e > m_tjProject->getEnd()) {
        m_project->currentSchedule()->setSchedulingError(true);
        cs->setSchedulingError(true);
        e = s + (8*60*60);
    }
    task->setStartTime(fromTime_t(s, tz));
    task->setEndTime(fromTime_t(e + 1, tz));
    task->setDuration(task->endTime() - task->startTime());

    debugPlan<<TJ::time2ISO(s)<<task->startTime()<<"-- "<<TJ::time2ISO(e+1)<<task->endTime();
    if (! task->startTime().isValid()) {
        logError(task, 0, xi18nc("@info/plain", "Invalid start time"));
        return false;
    }
    if (! task->endTime().isValid()) {
        logError(task, 0, xi18nc("@info/plain", "Invalid end time"));
        return false;
    }
    if (m_project->startTime() > task->startTime()) {
        m_project->setStartTime(task->startTime());
    }
    if (task->endTime() > m_project->endTime()) {
        m_project->setEndTime(task->endTime());
    }
    foreach (TJ::CoreAttributes *a, job->getBookedResources(0)) {
        TJ::Resource *r = static_cast<TJ::Resource*>(a);
        Resource *res = m_resourcemap[ r ];
        const QVector<TJ::Interval> lst = r->getBookedIntervals(0, job);
        foreach (const TJ::Interval &tji, lst) {
            AppointmentInterval ai = fromTJInterval(tji, tz);
            double load = res->type() == Resource::Type_Material ? res->units() : ai.load() * r->getEfficiency();
            res->addAppointment(cs, ai.startTime(), ai.endTime(), load);
            logDebug(task, 0, '\'' + res->name() + "' added appointment: " +  ai.startTime().toString(Qt::ISODate) + " - " + ai.endTime().toString(Qt::ISODate));
        }
    }
    cs->setScheduled(true);
    QLocale locale;
    if (task->type() == Node::Type_Milestone) {
        logInfo(task, 0, xi18nc("@info/plain" , "Scheduled milestone: %1", locale.toString(task->startTime(), QLocale::ShortFormat)));
    } else {
        logInfo(task, 0, xi18nc("@info/plain" , "Scheduled task: %1 - %2", locale.toString(task->startTime(), QLocale::ShortFormat), locale.toString(task->endTime(), QLocale::ShortFormat)));
    }
    return true;
}

void PlanTJScheduler::adjustSummaryTasks(const QList<Node*> &nodes)
{
    foreach (Node *n, nodes) {
        adjustSummaryTasks(n->childNodeIterator());
        if (n->parentNode()->type() == Node::Type_Summarytask) {
            DateTime pt = n->parentNode()->startTime();
            DateTime nt = n->startTime();
            if (! pt.isValid() || pt > nt) {
                n->parentNode()->setStartTime(nt);
            }
            pt = n->parentNode()->endTime();
            nt = n->endTime();
            if (! pt.isValid() || pt < nt) {
                n->parentNode()->setEndTime(nt);
            }
        }
    }
}

Duration PlanTJScheduler::calcPositiveFloat(Task *task)
{
    if (task->positiveFloat() != 0) {
        return task->positiveFloat();
    }
    Duration x;
    if (task->dependChildNodes().isEmpty() && task->childProxyRelations().isEmpty()) {
        x = m_project->endTime() - task->endTime();
    } else {
        foreach (const Relation *r, task->dependChildNodes() + task->childProxyRelations()) {
            if (! r->child()->inCriticalPath()) {
                Duration f = calcPositiveFloat(static_cast<Task*>(r->child()));
                if (x == 0 || f < x) {
                    x = f;
                }
            }
        }
    }
    Duration totfloat = task->freeFloat() + x;
    task->setPositiveFloat(totfloat);
    return totfloat;
}

void PlanTJScheduler::calcPertValues(Task *t)
{
    switch (t->constraint()) {
    case Node::MustStartOn:
        if (t->constraintStartTime() != t->startTime()) {
            t->setNegativeFloat(t->startTime() - t->constraintStartTime());
        }
        break;
    case Node::StartNotEarlier:
        if (t->startTime() < t->constraintStartTime()) {
            t->setNegativeFloat(t->constraintStartTime() - t->startTime());
        }
        break;
    case Node::MustFinishOn:
        if (t->constraintEndTime() != t->endTime()) {
            t->setNegativeFloat(t->endTime() - t->constraintEndTime());
        }
        break;
    case Node::FinishNotLater:
        if (t->endTime() > t->constraintEndTime()) {
            t->setNegativeFloat(t->endTime() - t->constraintEndTime());
        }
        break;
    case Node::FixedInterval:
        if (t->constraintStartTime() != t->startTime()) {
            t->setNegativeFloat(t->startTime() - t->constraintStartTime());
        } else if (t->endTime() != t->constraintEndTime()) {
            t->setNegativeFloat(t->endTime() - t->constraintEndTime());
        }
        break;
    default:
        break;
    }
    if (t->negativeFloat() != 0) {
        t->currentSchedule()->setConstraintError(true);
        m_project->currentSchedule()->setSchedulingError(true);
        logError(t, 0, i18nc("1=type of constraint", "%1: Failed to meet constraint. Negative float=%2", t->constraintToString(true), t->negativeFloat().toString(Duration::Format_i18nHour)));
    }
    debugPlan<<t->name()<<t->startTime()<<t->endTime();
    Duration negativefloat;
    foreach (const Relation *r, t->dependParentNodes() + t->parentProxyRelations()) {
        if (r->parent()->endTime() + r->lag() > t->startTime()) {
            Duration f = r->parent()->endTime() + r->lag() - t->startTime();
            if (f > negativefloat) {
                negativefloat = f;
            }
        }
    }
    if (negativefloat > 0) {
        t->currentSchedule()->setSchedulingError(true);
        m_project->currentSchedule()->setSchedulingError(true);
        logError(t, 0, xi18nc("@info/plain", "Failed to meet dependency. Negative float=%1", negativefloat.toString(Duration::Format_i18nHour)));
        if (t->negativeFloat() < negativefloat) {
            t->setNegativeFloat(negativefloat);
        }
    }
    Duration freefloat;
    foreach (const Relation *r, t->dependChildNodes() + t->childProxyRelations()) {
        if (t->endTime() + r->lag() <  r->child()->startTime()) {
            Duration f = r->child()->startTime() - r->lag() - t->endTime();
            if (f > 0 && (freefloat == 0 || freefloat > f)) {
                freefloat = f;
            }
        }
    }
    t->setFreeFloat(freefloat);
}

bool PlanTJScheduler::exists(QList<CalendarDay*> &lst, CalendarDay *day)
{
    foreach (CalendarDay *d, lst) {
        if (d->date() == day->date() && day->state() != CalendarDay::Undefined && d->state() != CalendarDay::Undefined) {
            return true;
        }
    }
    return false;
}

TJ::Resource *PlanTJScheduler::addResource(KPlato::Resource *r)
{
    if (m_resourcemap.values().contains(r)) {
        debugPlan<<r->name()<<"already exist";
        return m_resourcemap.key(r);
    }
    TJ::Resource *res = new TJ::Resource(m_tjProject, r->id(), r->name(), 0);
    if (r->type() == Resource::Type_Material) {
        res->setEfficiency(0.0);
    } else {
        res->setEfficiency((double)(r->units()) / 100.);
    }
    Calendar *cal = r->calendar();
    Q_ASSERT(cal);
    DateTime start = qMax(r->availableFrom(), m_project->constraintStartTime());
    DateTime end = m_project->constraintEndTime();
    if (r->availableUntil().isValid() && end > r->availableUntil()) {
        end = r->availableUntil();
    }
    AppointmentIntervalList lst = cal->workIntervals(start, end, 1.0);
//    qDebug()<<r->name()<<lst;
    const QMultiMap<QDate, AppointmentInterval> &map = lst.map();
    QMultiMap<QDate, AppointmentInterval>::const_iterator mapend = map.constEnd();
    QMultiMap<QDate, AppointmentInterval>::const_iterator it = map.constBegin();
    TJ::Shift *shift = new TJ::Shift(m_tjProject, r->id(), r->name(), 0, QString(), 0);
    for (; it != mapend; ++it) {
        shift->addWorkingInterval(toTJInterval(it.value().startTime(), it.value().endTime(), m_granularity/1000));
    }
    res->addShift(toTJInterval(start, end, m_granularity/1000), shift);
    m_resourcemap[res] = r;
    logDebug(m_project, 0, "Added resource: " + r->name());
/*    QListIterator<TJ::Interval*> it = res->getVacationListIterator();
    while (it.hasNext()) {
        TJ::Interval *i = it.next();
        logDebug(m_project, 0, "Vacation: " + TJ::time2ISO(i->getStart()) + " - " + TJ::time2ISO(i->getEnd()));
    }*/
    return res;
}

TJ::Task *PlanTJScheduler::addTask(KPlato::Task *task, TJ::Task *parent)
{
/*    if (m_backward && task->isStartNode()) {
        Relation *r = new Relation(m_backwardTask, task);
        m_project->addRelation(r);
    }*/
    TJ::Task *t = new TJ::Task(m_tjProject, task->id(), task->name(), parent, QString(), 0);
    m_taskmap[ t ] = task;
    t->setPriority(task->priority());
//     logDebug(m_project, 0, "Added task: " + task->name());
    addWorkingTime(task, t);
    return t;
}

void PlanTJScheduler::addWorkingTime(KPlato::Task *task, TJ::Task *job)
{
    if (task->type() != Node::Type_Task || task->estimate()->type() != Estimate::Type_Duration || ! task->estimate()->calendar()) {
        return;
    }
    int id = 0;
    Calendar *cal = task->estimate()->calendar();
    DateTime start = m_project->constraintStartTime();
    DateTime end = m_project->constraintEndTime();

    AppointmentIntervalList lst = cal->workIntervals(start, end, 1.0);
    const QMultiMap<QDate, AppointmentInterval> &map = lst.map();
    QMultiMap<QDate, AppointmentInterval>::const_iterator mapend = map.constEnd();
    QMultiMap<QDate, AppointmentInterval>::const_iterator it = map.constBegin();
    TJ::Shift *shift = new TJ::Shift(m_tjProject, task->id() + QString("-%1").arg(++id), task->name(), 0, QString(), 0);
    for (; it != mapend; ++it) {
        shift->addWorkingInterval(toTJInterval(it.value().startTime(), it.value().endTime(), m_granularity/1000));
    }
    job->addShift(toTJInterval(start, end, m_granularity/1000), shift);
}

void PlanTJScheduler::addTasks()
{
    debugPlan;
    QList<Node*> list = m_project->allNodes();
    for (int i = 0; i < list.count(); ++i) {
        Node *n = list.at(i);
        TJ::Task *parent = 0;
        switch (n->type()) {
            case Node::Type_Summarytask:
                m_schedule->insertSummaryTask(n);
                break;
            case Node::Type_Task:
            case Node::Type_Milestone:
                switch (n->constraint()) {
                    case Node::StartNotEarlier:
                        parent = addStartNotEarlier(n);
                        break;
                case Node::FinishNotLater:
                    parent = addFinishNotLater(n);
                    break;
                }
                addTask(static_cast<Task*>(n), parent);
                break;
            default:
                break;
        }
    }
}

void PlanTJScheduler::addDepends(const Relation *rel)
{
    TJ::Task *child = m_tjProject->getTask(rel->child()->id());
    TJ::TaskDependency *d = child->addDepends(rel->parent()->id());
    d->setGapDuration(0, rel->lag().seconds());
}

void PlanTJScheduler::addPrecedes(const Relation *rel)
{
    TJ::Task *parent = m_tjProject->getTask(rel->parent()->id());
    TJ::TaskDependency *d = parent->addPrecedes(rel->child()->id());
    d->setGapDuration(0, rel->lag().seconds());
}

void PlanTJScheduler::addDependencies(KPlato::Task *task)
{
    foreach (Relation *r, task->dependParentNodes() + task->parentProxyRelations()) {
        Node *n = r->parent();
        if (n == 0 || n->type() == Node::Type_Summarytask) {
            continue;
        }
        switch (r->type()) {
            case Relation::FinishStart:
                break;
            case Relation::FinishFinish:
            case Relation::StartStart:
                warnPlan<<"Dependency type not handled. Using FinishStart.";
                logWarning(task, 0, xi18nc("@info/plain" , "Dependency type '%1' not handled. Using FinishStart.", r->typeToString(true)));
                break;
        }
        switch (task->constraint()) {
            case Node::ASAP:
            case Node::ALAP:
                addPrecedes(r);
                addDepends(r);
                break;
            case Node::MustStartOn:
            case Node::StartNotEarlier:
                addPrecedes(r);
                if (task->constraintStartTime() < m_project->constraintStartTime()) {
                    addDepends(r);
                }
                break;
            case Node::MustFinishOn:
            case Node::FinishNotLater:
                addDepends(r);
                if (task->constraintEndTime() < m_project->constraintEndTime()) {
                    addPrecedes(r);
                }
                break;
            case Node::FixedInterval:
                break;
        }
    }
}

void PlanTJScheduler::addDependencies()
{
    foreach (Task *t, m_taskmap) {
        addDependencies(t);
    }
}

void PlanTJScheduler::setConstraints()
{
    QMap<TJ::Task*, Task*> ::const_iterator it = m_taskmap.constBegin();
    for (; it != m_taskmap.constEnd(); ++it) {
        setConstraint(it.key(), it.value());
    }
}

void PlanTJScheduler::setConstraint(TJ::Task *job, KPlato::Task *task)
{
    switch (task->constraint()) {
        case Node::ASAP:
            if (! job->isMilestone()) {
                job->setScheduling(m_backward ? TJ::Task::ALAP : TJ::Task::ASAP);
            }
            break;
        case Node::ALAP:
            job->setScheduling(TJ::Task::ALAP);
            break;
        case Node::MustStartOn:
            if (task->constraintStartTime() >= m_project->constraintStartTime()) {
                job->setPriority(600);
                job->setSpecifiedStart(0, task->constraintStartTime().toTime_t());
                logDebug(task, 0, QString("MSO: set specified start: %1").arg(TJ::time2ISO(task->constraintStartTime().toTime_t())));
            } else {
                logWarning(task, 0, xi18nc("@info/plain", "%1: Invalid start constraint", task->constraintToString(true)));
            }
            break;
        case Node::StartNotEarlier: {
            break;
        }
        case Node::MustFinishOn:
            if (task->constraintEndTime() <= m_project->constraintEndTime()) {
                job->setPriority(600);
                job->setScheduling(TJ::Task::ALAP);
                job->setSpecifiedEnd(0, task->constraintEndTime().toTime_t() - 1);
                logDebug(task, 0, QString("MFO: set specified end: %1").arg(TJ::time2ISO(task->constraintEndTime().toTime_t())));
            } else {
                logWarning(task, 0, xi18nc("@info/plain", "%1: Invalid end constraint", task->constraintToString(true)));
            }
            break;
        case Node::FinishNotLater: {
            break;
        }
        case Node::FixedInterval: {
            job->setPriority(700);
            TJ::Interval i(toTJInterval(task->constraintStartTime(), task->constraintEndTime(), tjGranularity()));
            job->setSpecifiedPeriod(0, i);
            // estimate not allowed
            job->setDuration(0, 0.0);
            job->setLength(0, 0.0);
            job->setEffort(0, 0.0);
            logDebug(task, 0, QString("FI: set specified: %1 - %2 -> %3 - %4 (%5)")
                      .arg(TJ::time2ISO(task->constraintStartTime().toTime_t()))
                      .arg(TJ::time2ISO(task->constraintEndTime().toTime_t()))
                      .arg(TJ::time2ISO(i.getStart()))
                      .arg(TJ::time2ISO(i.getEnd()))
                      .arg(tjGranularity()));
            break;
        }
        default:
            logWarning(task, 0, xi18nc("@info/plain", "Unhandled time constraint type"));
            break;
    }
}

TJ::Task *PlanTJScheduler::addStartNotEarlier(Node *task)
{
    DateTime time = task->constraintStartTime();
    if (task->estimate()->type() == Estimate::Type_Duration && task->estimate()->calendar() != 0) {
        Calendar *cal = task->estimate()->calendar();
        if (cal != m_project->defaultCalendar() && cal != m_project->calendars().value(0)) {
            logWarning(task, 0, xi18nc("@info/plain", "Could not use the correct calendar for calculation of task duration"));
        } else {
            time = cal->firstAvailableAfter(time, m_project->constraintEndTime());
        }
    }
    TJ::Task *p = new TJ::Task(m_tjProject, QString("%1-sne").arg(m_tjProject->taskCount() + 1), task->name() + "-sne", 0, QString(), 0);
    p->setSpecifiedStart(0, toTJTime_t(time, tjGranularity()));
    p->setSpecifiedEnd(0, m_tjProject->getEnd() - 1);
    qDebug()<<"PlanTJScheduler::addStartNotEarlier:"<<time<<TJ::time2ISO(toTJTime_t(time, tjGranularity()));
    return p;
}

TJ::Task *PlanTJScheduler::addFinishNotLater(Node *task)
{
    DateTime time = task->constraintEndTime();
    if (task->estimate()->type() == Estimate::Type_Duration && task->estimate()->calendar() != 0) {
        Calendar *cal = task->estimate()->calendar();
        if (cal != m_project->defaultCalendar() && cal != m_project->calendars().value(0)) {
            logWarning(task, 0, xi18nc("@info/plain", "Could not use the correct calendar for calculation of task duration"));
        } else {
            time = cal->firstAvailableBefore(time, m_project->constraintStartTime());
        }
    }
    TJ::Task *p = new TJ::Task(m_tjProject, QString("%1-fnl").arg(m_tjProject->taskCount() + 1), task->name() + "-fnl", 0, QString(), 0);
    p->setSpecifiedEnd(0, toTJTime_t(time, tjGranularity()) - 1);
    p->setSpecifiedStart(0, m_tjProject->getStart());
    return p;
}


void PlanTJScheduler::addRequests()
{
    debugPlan;
    QMap<TJ::Task*, Task*> ::const_iterator it = m_taskmap.constBegin();
    for (; it != m_taskmap.constEnd(); ++it) {
        addRequest(it.key(), it.value());
    }
}

void PlanTJScheduler::addRequest(TJ::Task *job, Task *task)
{
    debugPlan;
    if (task->type() == Node::Type_Milestone || task->estimate() == 0 || (m_recalculate && task->completion().isFinished())) {
        job->setMilestone(true);
        job->setDuration(0, 0.0);
        return;
    }
    // Note: FI tasks can never have an estimate set (duration, length or effort)
    if (task->constraint() != Node::FixedInterval) {
        if (task->estimate()->type() == Estimate::Type_Duration && task->estimate()->calendar() == 0) {
            job->setDuration(0, task->estimate()->value(Estimate::Use_Expected, m_usePert).toDouble(Duration::Unit_d));
            return;
        }
        if (task->estimate()->type() == Estimate::Type_Duration && task->estimate()->calendar() != 0) {
            job->setLength(0, task->estimate()->value(Estimate::Use_Expected, m_usePert).toDouble(Duration::Unit_d) * 24.0 / m_tjProject->getDailyWorkingHours());
            return;
        }
        if (m_recalculate && task->completion().isStarted()) {
            job->setEffort(0, task->completion().remainingEffort().toDouble(Duration::Unit_d));
        } else {
            Estimate *estimate = task->estimate();
            double e = estimate->scale(estimate->value(Estimate::Use_Expected, m_usePert), Duration::Unit_d, estimate->scales());
            job->setEffort(0, e);
        }
    }
    if (task->requests().isEmpty()) {
        return;
    }
    foreach (ResourceRequest *rr, task->requests().resourceRequests(true /*resolveTeam*/)) {
        if (!rr->resource()->calendar()) {
            result = 1; // stops scheduling
            logError(task, 0, i18n("No working hours defined for resource: %1",rr->resource()->name()));
            continue; // may happen if no calendar is set, and no default calendar
        }
        TJ::Resource *tjr = addResource(rr->resource());
        TJ::Allocation *a = new TJ::Allocation();
        a->setSelectionMode(TJ::Allocation::order);
        if (rr->units() != 100) {
            TJ::UsageLimits *l = new TJ::UsageLimits();
            l->setDailyUnits(rr->units());
            a->setLimits(l);
        }
        a->addCandidate(tjr);
        job->addAllocation(a);
        logDebug(task, 0, "Added resource candidate: " + rr->resource()->name());
        foreach (Resource *r, rr->requiredResources()) {
            TJ::Resource *tr = addResource(r);
            a->addRequiredResource(tjr, tr);
            logDebug(task, 0, "Added required resource: " + r->name());
        }
    }
}
