#include "portals.h"
#include "labelmodel.h"
#include "airplaneroute.h"
#include <boost/range/iterator_range.hpp>
#include <boost/range/adaptor/reversed.hpp>
#include "hourglass.h"
#include "assertions.h"
#include <iostream>

using namespace boost::adaptors;

namespace {

qreal clampToLifetime(qreal time, Airplane const& airplane)
{
    return std::max(airplane.timeOfBirth(), std::min(time, airplane.timeOfDeath()));
}

} // unnamed namespace

Portal::Portal(qreal time, Airplane const& airplane, LabelModel const& labelModel)
    : _range(labelModel.perimeterRange(airplane, time)), _time(time)
{}

Portals::Portals(Airplane const& airplane, LabelModel const& labelModel, qreal time1, qreal time2)
    : _labelPerimeter(labelModel.labelPerimeter())
{
    computePortals(airplane, labelModel,
                   clampToLifetime(time1, airplane),
                   clampToLifetime(time2, airplane));
    if (airplane.id() == 0) { // 5
        //printForIpe();
    }
}

LabelTrajectory Portals::shortestPointToPointPath(qreal label1, qreal label2) const
{
    shiftValueOntoPerimeterRange(label1, range1(), _labelPerimeter);
    shiftValueOntoPerimeterRange(label2, range2(), _labelPerimeter);

    if (_portals.size() == 1) {
        return LabelTrajectory({{time1(), label1}, {time2(), label2}}, PointToPoint);
    } else {
        Funnel funnel(label1, _portals);
        return LabelTrajectory(funnel.getPathToPoint(label2), PointToPoint);
    }
}
LabelTrajectory Portals::shortestPointToEdgePath(qreal label1) const
{
    shiftValueOntoPerimeterRange(label1, range1(), _labelPerimeter);

    if (_portals.size() == 1) {
        return LabelTrajectory({{time1(), label1}, {time2(), label1}}, PointToPoint);
    } else {
        Funnel funnel(label1, _portals);
        return LabelTrajectory(funnel.getPathToEdge(), PointToEdge);
    }
}
LabelTrajectory Portals::shortestEdgeToPointPath(qreal label2) const
{
    shiftValueOntoPerimeterRange(label2, range2(), _labelPerimeter);

    if (_portals.size() == 1) {
        return LabelTrajectory({{time1(), label2}, {time2(), label2}}, PointToPoint);
    } else {
        Funnel funnel(label2, _portals | reversed);
        std::vector<QPointF>&& vertices = funnel.getPathToEdge();
        std::reverse(vertices.begin(), vertices.end());
        return LabelTrajectory(std::move(vertices), EdgeToPoint);
    }
}
LabelTrajectory Portals::shortestEdgeToEdgePath() const
{
    if (_portals.size() == 1) {
        qreal label = range1().first;
        return LabelTrajectory({{time1(), label}, {time2(), label}}, PointToPoint);
    } else {
        Hourglass hourglass(_portals);
        return LabelTrajectory(hourglass.getPath(), EdgeToEdge);
    }
}

void Portals::computePortals(Airplane const& airplane, LabelModel const& labelModel, qreal time1, qreal time2)
{
    AirplaneRoute const& route = airplane.route();
    qreal speed = route.airplaneSpeed();
    qreal routeTime1 = airplane.timeOnRoute(time1);
    qreal routeTime2 = airplane.timeOnRoute(time2);
    assert(routeTime1 <= routeTime2);
    auto routeVertices = route.verticesBetween(routeTime1, routeTime2);

    //            ,--- routeVertices --.
    //            :            :       :
    //          begin         last    end
    //    +-------+            +-------+
    //    |       |      +-----+       |
    //    |   *   +------+     |   *   |
    //    |(t1,l1)|      |     |(t2,l2)|
    //    +-------+      |     +-------+
    //            |      +-----+
    //            +------+

    // To get portals that fit together, and properly model the configuration space, we need
    // to shift them upwards and/or downwards as appropriate by multiples of perimeter.
    PerimeterRange rangeAfter = labelModel.perimeterRange(airplane, time1);
    _portals.emplace_back(time1, rangeAfter);
    for (auto&& v : routeVertices) {
        PerimeterRange rangeBefore = rangeAfter;
        rangeAfter = labelModel.perimeterRange(v.position(), v.directionAfter());
        if (v.isRightTurn()) {
            shiftPerimeterRangeOntoValue(rangeAfter, rangeBefore.first, _labelPerimeter);
        } else if (v.isLeftTurn()){
            shiftPerimeterRangeOntoValue(rangeAfter, rangeBefore.second, _labelPerimeter);
        } else {
            qreal mid = 0.5*(rangeBefore.first + rangeBefore.second);
            shiftPerimeterRangeOntoValue(rangeAfter, mid, _labelPerimeter);
        }
        assert(perimeterRangesOverlap(rangeBefore, rangeAfter));
        qreal time = v.timeFromStart(speed) + airplane.timeOfBirth();
        _portals.emplace_back(time, intersectRanges(rangeBefore, rangeAfter));
    }
    if (_portals.back().time() != time2) {
        _portals.emplace_back(time2, rangeAfter);
    }
}

PerimeterRange Portals::trimmedRange1(qreal trimAmount) const
{
    if (_portals.size() == 1) {
        return range1();
    } else {
        Hourglass hourglass(_portals);
        return hourglass.trimmedStartRange(trimAmount);
    }
}
PerimeterRange Portals::trimmedRange2(qreal trimAmount) const
{
    if (_portals.size() == 1) {
        return range2();
    } else {
        Hourglass hourglass(_portals);
        return hourglass.trimmedEndRange(trimAmount);
    }
}

void Portals::printPassage(Portal const& p1, Portal const& p2) const
{
    std::cout << "<path stroke=\"lightgray\" fill=\"lightyellow\">\n";
    std::cout << 10*p1.time() << " " << p1.range().first << " m\n";
    std::cout << 10*p1.time() << " " << p1.range().second << " l\n";
    std::cout << 10*p2.time() << " " << p2.range().second << " l\n";
    std::cout << 10*p2.time() << " " << p2.range().first << " l\n";
    std::cout << "h\n</path>\n";
}

void Portals::printForIpe() const
{
    std::cout << "<group>\n";
    Portal const* prev = &_portals.front();
    for (Portal const& curr : _portals) {
        printPassage(*prev, curr);
        prev = &curr;
    }
    std::cout << "</group>" << std::endl;
}
