#include "airplaneroute.h"
#include "probleminstance.h"
#include "airplanepool.h"
#include "drawingarea.h"
#include "geomutils.h"
#include "assertions.h"
#include <QTime>
#include <cmath>
#include <algorithm>

AirplaneRoute::Vertex::Vertex(QPointF const& pos)
    : _position(pos), _directionBefore(0, 0), _directionAfter(0, 0),
      _distanceFromStart(0), _turnedness(0)
{
}

bool AirplaneRoute::Vertex::isLeftTurn() const
{
    return ::isLeftTurn(_turnedness);
}
bool AirplaneRoute::Vertex::isRightTurn() const
{
    return ::isRightTurn(_turnedness);
}
QPointF AirplaneRoute::Vertex::position() const
{
    return _position;
}
QPointF AirplaneRoute::Vertex::directionBefore() const
{
    return _directionBefore;
}
QPointF AirplaneRoute::Vertex::directionAfter() const
{
    return _directionAfter;
}
qreal AirplaneRoute::Vertex::distanceFromStart() const
{
    return _distanceFromStart;
}
qreal AirplaneRoute::Vertex::timeFromStart(qreal airplaneSpeed) const
{
    return distanceFromStart() / airplaneSpeed;
}

void AirplaneRoute::Vertex::appendVertex(AirplaneRoute::Vertex* next)
{
    QPointF edge = next->_position - this->_position;
    this->_directionAfter = next->_directionBefore = edge;
    next->_distanceFromStart = this->_distanceFromStart + vectorLength(edge);
    this->recomputeTurnedness();
}
void AirplaneRoute::Vertex::reverse()
{
    std::swap(_directionBefore, _directionAfter);
    _directionBefore *= -1;
    _directionAfter  *= -1;
    _turnedness      *= -1;
}

void AirplaneRoute::Vertex::recomputeTurnedness()
{
    if (isNullVector(_directionBefore) || isNullVector(_directionAfter)) {
        _turnedness = 0;
    } else {
        _turnedness = turnedness(_position - _directionBefore, _position, _position + _directionAfter);
    }
}




AirplaneRouteID AirplaneRoute::_nextID = 0;

AirplaneRoute::AirplaneRoute(ProblemInstance& problemInstance, AirplanePool& pool)
    : _expDist(1), _nextSpawnTime(-1), _usesPoisson(true),
      _airplanePool(&pool), _nextAirplaneID(0), _uniqueID(_nextID++),
      _labelTemplate("label"), _airplaneSpeed(1), _problemInstance(&problemInstance)
{
    rngSeed(randomSeed());
}

void AirplaneRoute::appendVertex(QPointF const& position)
{
    std::size_t i = _vertices.size();
    _vertices.emplace_back(position);
    if (i >= 1) {
        _vertices[i-1].appendVertex(&_vertices[i]);
    }
}

void AirplaneRoute::reverse()
{
    std::reverse(_vertices.begin(), _vertices.end());

    std::size_t n = _vertices.size();
    qreal distanceFromStart = 0;
    for (std::size_t i = 0; i < n; ++i) {
        qreal edge = (i == n-1) ? 0 : _vertices[i]._distanceFromStart - _vertices[i+1]._distanceFromStart;

        _vertices[i]._distanceFromStart = distanceFromStart;
        _vertices[i].reverse();

        distanceFromStart += edge;
    }
}

void AirplaneRoute::draw(DrawingArea* drawing) const
{
    auto from = _vertices.begin();
    for (auto to = std::next(from); to != _vertices.end(); ++from, ++to) {
        drawing->drawExistingRouteArrow(from->position(), to->position());
    }
    drawing->drawExistingRouteAnchor( _vertices.front().position() );
    drawing->drawExistingRouteAnchor( _vertices.back().position() );
}

void AirplaneRoute::findRouteSegment(qreal time, AirplaneRoute::const_iterator& beforeOrAt, qreal& alpha) const
{
    qreal totalLength = _vertices.back().distanceFromStart();
    qreal distance = std::min(time * airplaneSpeed(), totalLength);

    auto after = std::upper_bound(_vertices.cbegin(), _vertices.cend(), distance,
                                  [](qreal distance, Vertex const& v) { return distance < v.distanceFromStart(); });
    assert(after != _vertices.cbegin());
    beforeOrAt = std::prev(after);

    if (after == _vertices.cend()) {
        alpha = 0.0;
    } else {
        qreal lengthBefore2 = beforeOrAt->distanceFromStart();
        qreal lengthAfter2 = after->distanceFromStart();
        alpha = std::min(1.0, (distance - lengthBefore2) / (lengthAfter2 - lengthBefore2));
        assert(!std::isnan(alpha));
    }
}

QPointF AirplaneRoute::positionAtTime(qreal time) const
{
    const_iterator beforeOrAt; qreal alpha;
    findRouteSegment(time, beforeOrAt, alpha);
    if (alpha == 0.0) {
        return beforeOrAt->position();
    } else {
        const_iterator after = std::next(beforeOrAt);
        assert(after != _vertices.cend());
        return (1-alpha)*beforeOrAt->position() + alpha*after->position();
    }
}
QPointF AirplaneRoute::directionAtTime(qreal time) const
{
    const_iterator beforeOrAt; qreal alpha;
    findRouteSegment(time, beforeOrAt, alpha);
    if (std::next(beforeOrAt) == _vertices.cend()) {
        return beforeOrAt->directionBefore();
    } else {
        return beforeOrAt->directionAfter();
    }
}

qreal AirplaneRoute::length() const
{
    return _vertices.back().distanceFromStart();
}

AirplaneRoute::const_range AirplaneRoute::vertices() const
{
    return boost::make_iterator_range(_vertices.cbegin(), _vertices.cend());
}
AirplaneRoute::const_range AirplaneRoute::verticesBetween(qreal time1, qreal time2) const
{
    if (time1 == time2) {
        return boost::make_iterator_range(_vertices.cbegin()+1,
                                          _vertices.cbegin()+1);
    } else {
        return boost::make_iterator_range(firstVertexAfter(time1),
                                          firstVertexAtOrAfter(time2));
    }
}

AirplaneRoute::const_iterator AirplaneRoute::firstVertexAtOrAfter(qreal time) const
{
    const_iterator beforeOrAt; qreal alpha;
    findRouteSegment(time, beforeOrAt, alpha);
    if (beforeOrAt->timeFromStart(airplaneSpeed()) < time) {
        ++beforeOrAt;
    }
    return beforeOrAt;
}
AirplaneRoute::const_iterator AirplaneRoute::firstVertexAfter(qreal time) const
{
    const_iterator beforeOrAt; qreal alpha;
    findRouteSegment(time, beforeOrAt, alpha);
    if (beforeOrAt->timeFromStart(airplaneSpeed()) <= time) {
        ++beforeOrAt;
    }
    return beforeOrAt;
}

unsigned int AirplaneRoute::randomSeed()
{
    static QTime midnight(0, 0, 0);
    return midnight.msecsTo(QTime::currentTime());
}

void AirplaneRoute::rngSeed(unsigned int seed)
{
    _initialSeed = seed;
    resetSpawnedAirplanes();
}

void AirplaneRoute::rerollSeed()
{
    _rng.seed(_initialSeed);
    _initialSeed = _rng();
    resetSpawnedAirplanes();
}

qreal AirplaneRoute::firstArrivalTime() const {
    return _usesPoisson ? nextInterarrivalTime() : 0;
}
qreal AirplaneRoute::nextInterarrivalTime() const
{
    return _usesPoisson ? _expDist(_rng) : averageInterarrivalTime();
}

void AirplaneRoute::labelTemplate(QString s)
{
    if (_labelTemplate != s) {
        _labelTemplate = s;
        _problemInstance->recomputeLabelSize();
    }
}

void AirplaneRoute::airplaneSpeed(qreal v)
{
    if (_airplaneSpeed != v) {
        _airplaneSpeed = v;
        _problemInstance->notifyObserversOfAirplaneScheduleChange();
    }
}

void AirplaneRoute::usesPoissonProcess(bool b)
{
    if (_usesPoisson != b) {
        _usesPoisson = b;
        resetSpawnedAirplanes();
    }
}

qreal AirplaneRoute::averageInterarrivalTime() const
{
    return 1.0/_expDist.lambda();
}
void AirplaneRoute::averageInterarrivalTime(qreal t)
{
    assert(t != 0);
    qreal newLambda = 1.0/t;
    if (_expDist.lambda() != newLambda) {
        _expDist.param(lambda(newLambda));
        resetSpawnedAirplanes();
    }
}

void AirplaneRoute::resetSpawnedAirplanes()
{
    _nextSpawnTime = -1;
    _rng.seed(_initialSeed);
    for (Airplane* airplane : _myAirplanes) {
        _airplanePool->recycleAirplane(*airplane);
    }
    _myAirplanes.clear();
    _nextAirplaneID = 0;
    _problemInstance->notifyObserversOfAirplaneScheduleChange();
}

void AirplaneRoute::spawnAirplanesUpto(qreal time) const
{
    if (_nextSpawnTime < 0) {
        _nextSpawnTime = firstArrivalTime();
    }
    while (_nextSpawnTime <= time) {
        Airplane& airplane = _airplanePool->spawnAirplane(*this, _nextSpawnTime, _nextAirplaneID++);
        _myAirplanes.emplace_front(&airplane);
        _nextSpawnTime += nextInterarrivalTime();
    }
}

namespace {
    QString substituteIds(QString label, AirplaneID a, AirplaneID i, AirplaneRouteID r)
    {
        QString result;
        result.reserve(label.size()+10);
        for (int j = 0; j < label.size();) {
            QChar c1 = label[j];
            if (c1 != '%' || j+1 >= label.size()) {
                result.append(c1);
                ++j;
            } else {
                QChar fillChar = '0';
                int fillWidth = 0;
                int start = j++;
                QChar c2;
                while (j < label.size() && (c2 = label[j++]).isDigit()) {
                    fillWidth = fillWidth*10 + c2.digitValue();
                }
                switch (c2.toAscii()) {
                case 'a': result.append( QString("%1").arg(a, fillWidth, 10, fillChar) ); break;
                case 'i': result.append( QString("%1").arg(i, fillWidth, 10, fillChar) ); break;
                case 'r': result.append( QString("%1").arg(r, fillWidth, 10, fillChar) ); break;
                default:
                    if (c2 == '%' && j == start+2)
                        result.append('%');                           // "%%|..." -> "%|..."
                    else
                        result.append(label.mid(start, (--j)-start)); // "%123x|..." -> "%123|x..."
                }
            }
        }
        return result;
    }
} // unnamed namespace

QString AirplaneRoute::labelText(AirplaneID id, AirplaneID rid) const
{
    return substituteIds(_labelTemplate, id+1, rid+1, _uniqueID+1);
}
