#include "labelmodel.h"
#include "portals.h"
#include "hourglass.h"
#include "geomutils.h"
#include "assertions.h"
#include <cmath>

LabelModel::LabelModel()
    : _labelWidth(0), _labelHeight(0)
{
}

LabelModel::~LabelModel()
{
}

void LabelModel::setLabelSize(int width, int height)
{
    _labelWidth = width;
    _labelHeight = height;
}

bool LabelModel::labelsAlmostIntersect(QPointF const& center1, QPointF const& center2, qreal epsilon) const
{
    bool xSeparated = std::abs(center2.x() - center1.x()) >= _labelWidth - epsilon;
    bool ySeparated = std::abs(center2.y() - center1.y()) >= _labelHeight - epsilon;
    return !xSeparated && !ySeparated;
}

LabelTrajectory LabelModel::slowestLabelTrajectory(
    Airplane const& airplane, qreal time1, qreal label1, qreal time2, qreal label2) const
{
    Q_UNUSED(airplane);
    TrajectoryType type;
    if (label1 == ANY_LABEL && label2 == ANY_LABEL) {
        PerimeterRange candidates = perimeterRange(airplane, time1);
        label1 = label2 = candidates.first; // TODO: is there a more intelligent choice to be made here?
        type = EdgeToEdge;
    } else if (label1 == ANY_LABEL) {
        label1 = label2;
        type = EdgeToPoint;
    } else if (label2 == ANY_LABEL) {
        label2 = label1;
        type = PointToEdge;
    } else {
        label2 = label1 + smallestLabelMovement(label1, label2, labelPerimeter());
        type = PointToPoint;
    }
    return LabelTrajectory({{time1, label1}, {time2, label2}}, type);
}

std::unique_ptr<LabelModel> LabelModel::fromName(QString const& name)
{
    std::unique_ptr<LabelModel> p;
    if (name == "LeftSlider")
        p.reset(new OneSliderModel(LeftSlider));
    else if (name == "RightSlider")
        p.reset(new OneSliderModel(RightSlider));
    else if (name == "TopSlider")
        p.reset(new OneSliderModel(TopSlider));
    else if (name == "BottomSlider")
        p.reset(new OneSliderModel(BottomSlider));
    else if (name == "FourSlider")
        p.reset(new FourSliderModel());
    else if (name == "BehindSlider")
        p.reset(new BehindSliderModel());
    return std::move(p);
}

std::unique_ptr<LabelModel> LabelModel::defaultModel()
{
    std::unique_ptr<LabelModel> p(new BehindSliderModel);
    return std::move(p);
}

qreal LabelModel::labelToPerimeterValue(QPointF const& relativeLabelCenter) const
{
    // NOTE: the two "vector extension" statements below are unnecessary if
    // we're really given a relative label center.  However, that is not the
    // case when this function is called by BehindSliderModel::computePerimeterRange.
    // Then we're given a vector in the same direction as a relative label
    // center, but possibly having the wrong length.
    qreal result;
    qreal criticalSlope = labelHeight() / (qreal)labelWidth();
    QPointF dir = relativeLabelCenter;
    bool dirSlopeSteeper = dir.x() == 0 || std::abs(dir.y() / dir.x()) >= criticalSlope;
    if (dirSlopeSteeper) {
        dir *= std::abs(0.5*labelHeight() / dir.y()); // extend dir vector to top or bottom segment of label candidates
        qreal distanceFromLeft = dir.x() + 0.5*labelWidth();
        if (dir.y() > 0) {
            result = labelHeight() + distanceFromLeft; // bottom segment
        } else {
            result = labelPerimeter() - distanceFromLeft; // top segment
        }
    } else {
        dir *= std::abs(0.5*labelWidth() / dir.x()); // extend dir vector to left or right segment of label candidates
        qreal distanceFromTop = dir.y() + 0.5*labelHeight();
        if (dir.x() > 0) {
            result = labelHalfPerimeter() + labelHeight() - distanceFromTop; // right segment
        } else {
            result = distanceFromTop; // left segment
        }
    }
    return result;
}

QPointF LabelModel::relativeLabelPosition(qreal perimeterValue) const
{
    perimeterValue = std::fmod(perimeterValue, labelPerimeter());
    if (perimeterValue < 0) perimeterValue += labelPerimeter();

    QPointF topleft(-0.5*labelWidth(), -0.5*labelHeight());
    QPointF result = topleft;
    if (perimeterValue < labelHeight()) { // left segment
        qreal y = perimeterValue;
        result += QPointF(0, y);
    } else if (perimeterValue < labelHalfPerimeter()) { // bottom segment
        qreal x = perimeterValue - labelHeight();
        result += QPointF(x, labelHeight());
    } else if (perimeterValue < labelPerimeter() - labelWidth()) { // right segment
        qreal y = perimeterValue - labelHalfPerimeter();
        result += QPointF(labelWidth(), labelHeight() - y);
    } else { // top segment
        assert(perimeterValue < labelPerimeter());
        qreal x = perimeterValue - labelHalfPerimeter() - labelHeight();
        result += QPointF(labelWidth() - x, 0);
    }
    return result;
}

LabelModel::SliderLocation LabelModel::locateOnSliders(LabelCandidateSegment* sliders[], qreal perimeterValue) const
{
    perimeterValue = std::fmod(perimeterValue, labelPerimeter());
    if (perimeterValue < 0) perimeterValue += labelPerimeter();

    qreal low = 0; qreal high = 0;
    for (int i = 0; i < 4; ++i) {
        qreal sliderLength = (i % 2) ? labelWidth() : labelHeight();
        low = high;
        high += sliderLength;
        if (perimeterValue <= high) {
            return {sliders[i], i, {low, high}, perimeterValue - low, high - perimeterValue};
        }
    }
    assert(false);
    return {sliders[0], 0, {0, 0}, 0, 0};
}

namespace {

void adjustSegment(LabelCandidateSegment* segment, int i, qreal adjustment, bool adjustFirstPoint)
{
    qreal* low; qreal* high;
    if (i % 2) {
        low  = &segment->leftmostPoint().rx();
        high = &segment->rightmostPoint().rx();
    } else {
        low  = &segment->topmostPoint().ry();
        high = &segment->bottommostPoint().ry();
    }

    bool adjustLow = (i / 2) ? !adjustFirstPoint : adjustFirstPoint;
    if (adjustLow) {
        *low += adjustment;
    } else {
        *high -= adjustment;
    }
}
inline void removePrefix(LabelCandidateSegment* segment, int i, qreal prefixLength)
{ adjustSegment(segment, i, prefixLength, true); }
inline void removeSuffix(LabelCandidateSegment* segment, int i, qreal suffixLength)
{ adjustSegment(segment, i, suffixLength, false); }

} // unnamed namespace

LabelCandidates LabelModel::labelCandidates(QPointF pointPosition, PerimeterRange const& range) const
{
    LabelCandidates result = LabelCandidates::fourSliders(pointPosition, labelWidth(), labelHeight());
    qreal length = range.second - range.first;
    if (length >= labelPerimeter()) { return result; }

    qreal low = std::fmod(range.first, labelPerimeter());
    if (low < 0) low += labelPerimeter();
    qreal high = low + length;

    LabelCandidateSegment* sliders[4] = { &result.leftmostCandidates(),
                                          &result.bottommostCandidates(),
                                          &result.rightmostCandidates(),
                                          &result.topmostCandidates() };

    SliderLocation loc1 = locateOnSliders(sliders, low);
    SliderLocation loc2 = locateOnSliders(sliders, high);

    // If `range` is only missing a little piece out of a 4-slider then this creates two disconnected
    // components along one of the sliders (namely, `loc1.slider` which then equals `loc2.slider`).
    // The `LabelCandidateSegment` class is not set up to handle that, so we make sure it never occurs.
    assert(loc1.sliderIndex != loc2.sliderIndex || loc1.distanceFromStart <= loc2.distanceFromStart);

    removePrefix(loc1.slider, loc1.sliderIndex, loc1.distanceFromStart);
    removeSuffix(loc2.slider, loc2.sliderIndex, loc2.distanceToEnd);
    for (int i = loc2.sliderIndex + 1; (i % 4) != loc1.sliderIndex; ++i) {
        sliders[i%4]->makeEmpty();
    }

    return std::move(result);
}


QString OneSliderModel::name() const
{
    static char const* names[] = {
        "LeftSlider",
        "BottomSlider",
        "RightSlider",
        "TopSlider"
    };
    return QString(names[_slider]);
}

LabelCandidates OneSliderModel::labelCandidates(QPointF const& pointPosition, QPointF const&) const
{
    typedef LabelCandidates (*Constructor)(QPointF const&, int, int);
    static Constructor constructors[] = {
        &LabelCandidates::leftSlider,
        &LabelCandidates::bottomSlider,
        &LabelCandidates::rightSlider,
        &LabelCandidates::topSlider
    };
    return constructors[_slider](pointPosition, labelWidth(), labelHeight());

}
PerimeterRange OneSliderModel::perimeterRange(QPointF const&, QPointF const&) const
{
    int start = 0;
    for (int i = 0; i < _slider; ++i) {
        start += (i % 2) ? labelWidth() : labelHeight();
    }
    int length = (_slider % 2) ? labelWidth() : labelHeight();
    return PerimeterRange(start, start+length);
}

LabelCandidates FourSliderModel::labelCandidates(QPointF const& pos, QPointF const&) const
{
    return LabelCandidates::fourSliders(pos, labelWidth(), labelHeight());
}
PerimeterRange FourSliderModel::perimeterRange(QPointF const&, QPointF const&) const
{
    return PerimeterRange(0, labelPerimeter());
}

LabelTrajectory BehindSliderModel::slowestLabelTrajectory(
    Airplane const& airplane, qreal time1, qreal label1, qreal time2, qreal label2) const
{
    Portals portals(airplane, *this, time1, time2);
    if (label1 == ANY_LABEL && label2 == ANY_LABEL) {
        return portals.shortestEdgeToEdgePath();
    } else if (label1 == ANY_LABEL) {
        return portals.shortestEdgeToPointPath(label2);
    } else if (label2 == ANY_LABEL) {
        return portals.shortestPointToEdgePath(label1);
    } else {
        return portals.shortestPointToPointPath(label1, label2);
    }
}

namespace {

qreal solveForX(QPointF const& pos, QPointF const& dir, qreal targetY) {
    qreal alpha = (targetY - pos.y()) / dir.y();
    return pos.x() + alpha*dir.x();
}
qreal solveForY(QPointF const& pos, QPointF const& dir, qreal targetX) {
    qreal alpha = (targetX - pos.x()) / dir.x();
    return pos.y() + alpha*dir.y();
}

} // anonymous namespace

LabelCandidates BehindSliderModel::labelCandidates(QPointF const& pos, QPointF const& dir) const
{
    LabelCandidates result = LabelCandidates::fourSliders(pos, labelWidth(), labelHeight());
    if (isNullVector(dir)) {
        return result; // degenerate into 4-slider for non-moving points
    }

    QPointF perp = perpendicular(dir);
    LabelCandidateSegment* segments[4] = { &result.leftmostCandidates(),
                                           &result.rightmostCandidates(),
                                           &result.topmostCandidates(),
                                           &result.bottommostCandidates() };
    for (int i = 0; i < 2; ++i) { // vertical segments
        QPointF& p1 = segments[i]->topmostPoint();
        QPointF& p2 = segments[i]->bottommostPoint();
        bool infront1 = isInfrontOf(p1, pos, dir);
        bool infront2 = isInfrontOf(p2, pos, dir);
        if (infront1 && infront2) {
            segments[i]->makeEmpty();
        } else if (infront1) {
            p1.ry() = solveForY(pos, perp, p1.x());
        } else if (infront2) {
            p2.ry() = solveForY(pos, perp, p2.x());
        }
    }
    for (int i = 2; i < 4; ++i) { // horizontal segments
        QPointF& p1 = segments[i]->leftmostPoint();
        QPointF& p2 = segments[i]->rightmostPoint();
        bool infront1 = isInfrontOf(p1, pos, dir);
        bool infront2 = isInfrontOf(p2, pos, dir);
        if (infront1 && infront2) {
            segments[i]->makeEmpty();
        } else if (infront1) {
            p1.rx() = solveForX(pos, perp, p1.y());
        } else if (infront2) {
            p2.rx() = solveForX(pos, perp, p2.y());
        }
    }
    return result;
}

PerimeterRange BehindSliderModel::perimeterRange(QPointF const&, QPointF const& dir) const
{
    if (isNullVector(dir)) {
        return PerimeterRange(0, labelPerimeter()); // degenerate into 4-slider for non-moving points
    } else {
        qreal low = labelToPerimeterValue(perpendicular(dir));
        qreal high = low + labelHalfPerimeter();
        return PerimeterRange(low, high);
    }
}
