#ifndef HOURGLASS_H
#define HOURGLASS_H

#include "perimeterrange.h"
#include <boost/range/join.hpp>
#include <deque>
#include <QPointF>
#include <cassert>

class Hourglass;

class Funnel {
public:
    template <typename Portals>
    Funnel(qreal startLabel, Portals const& portals)
    {
        assert( portals.size() >= 2 );
        assert( inPerimeterRange(startLabel, portals.front().range()) );

        qreal startTime = portals.front().time();
        qreal endTime = portals.back().time();
        assert(startTime != endTime);

        _initialChain.emplace_back(startTime, startLabel);
        auto p = std::next(portals.begin()), pend = portals.end();
        if (startTime < endTime) {
            for (; p != pend; ++p) { extendRightward(*p); }
        } else {
            for (; p != pend; ++p) { extendLeftward(*p); }
        }
    }

    Funnel(Funnel const&) = delete;
    Funnel& operator=(Funnel const&) = delete;
    Funnel(Funnel&&) = default;
    Funnel& operator=(Funnel&&) = default;

    // NOTE: both "getPath" functions cannibalize the Funnel so it becomes unusable afterwards
    std::vector<QPointF> getPathToPoint(qreal label);
    std::vector<QPointF> getPathToEdge();

    qreal startTime() const;
    qreal endTime() const;

    qreal startLabel() const;
    PerimeterRange endRange() const;

    QPointF const& cusp() const { return _initialChain.back(); }

    void printForIpe() const;

private:
    // From an upper and a lower funnel for a closed hourglass, construct its left- and right-facing funnels.
    friend class Hourglass;
    static void constructClosedHourglass(Funnel& upperIn_rightOut, Funnel& lowerIn_leftOut);

private:
    void reset(std::vector<QPointF>&& initialChain,
               std::deque<QPointF>&& leftChain, std::deque<QPointF>&& rightChain)
    {
        _initialChain = std::move(initialChain);
        _leftChain    = std::move(leftChain);
        _rightChain   = std::move(rightChain);
    }
    static QPointF splitAtFirstSharedPoint(Funnel& funnel1, Funnel& funnel2,
                                           std::deque<QPointF>& remainder1, std::deque<QPointF>& remainder2);

    void extendLeftward(Portal const& portal)
    {
        assert(portal.time() < endTime());
        extendRightChain(portal.time(), portal.range().first);
        extendLeftChain(portal.time(), portal.range().second);
        assert(!_leftChain.empty() && !_rightChain.empty());
    }
    void extendRightward(Portal const& portal)
    {
        assert(portal.time() > endTime());
        extendLeftChain(portal.time(), portal.range().first);
        extendRightChain(portal.time(), portal.range().second);
        assert(!_leftChain.empty() && !_rightChain.empty());
    }

    void extendLeftChain(qreal time, qreal label);
    void extendRightChain(qreal time, qreal label);

    std::deque<QPointF> const& upperChain() const;
    std::deque<QPointF>&       upperChain();
    std::deque<QPointF> const& lowerChain() const;
    std::deque<QPointF>&       lowerChain();

private:
    // QPointF.x maps to time, .y maps to label-perimeter value
    std::vector<QPointF> _initialChain; // nodes upto and including cusp
    std::deque<QPointF>  _leftChain;    // nodes when going left after the cusp
    std::deque<QPointF>  _rightChain;   // nodes when going right after the cusp

    auto cuspOnlyPath() const -> decltype(boost::make_iterator_range(std::prev(_initialChain.end()), _initialChain.end()))
    { return boost::make_iterator_range(std::prev(_initialChain.end()), _initialChain.end()); }
public:
    // NOTE: these must be down here, because _initialChain, upperChain, and lowerChain must be declared
    //   before they can be used in decltype(...)
    auto upperPath() const -> decltype(boost::join(_initialChain, upperChain()))
    { return boost::join(_initialChain, upperChain()); }
    auto lowerPath() const -> decltype(boost::join(_initialChain, lowerChain()))
    { return boost::join(_initialChain, lowerChain()); }

    auto upperPathFromCusp() const -> decltype(boost::join(cuspOnlyPath(), upperChain()))
    { return boost::join(cuspOnlyPath(), upperChain()); }
    auto lowerPathFromCusp() const -> decltype(boost::join(cuspOnlyPath(), lowerChain()))
    { return boost::join(cuspOnlyPath(), lowerChain()); }
};

class Hourglass
{
public:
    template <typename Portals>
    Hourglass(Portals const& portals)
        : _rightOrUpper(portals.front().range().first, portals),
          _leftOrLower(portals.front().range().second, portals),
          _isClosed(false)
    {
        finalizeHourglass();
    }

    Hourglass(Hourglass const&) = delete;
    Hourglass& operator=(Hourglass const&) = delete;
    Hourglass(Hourglass&&) = default;
    Hourglass& operator=(Hourglass&&) = default;

    // NOTE: "getPath" cannibalizes the Hourglass so it becomes unusable afterwards
    std::vector<QPointF> getPath() { return _isClosed ? getPathFromClosedHourglass() : getPathFromOpenHourglass(); }

    qreal startTime() const { return _isClosed ? leftFacingFunnel().endTime()  : upperFunnel().startTime(); }
    qreal endTime() const   { return _isClosed ? rightFacingFunnel().endTime() : upperFunnel().endTime();   }

    PerimeterRange startRange() const;
    PerimeterRange endRange() const;

    PerimeterRange trimmedStartRange(qreal trimming) const { return trimmedRange(Start, trimming); }
    PerimeterRange trimmedEndRange(qreal trimming) const { return trimmedRange(End, trimming); }

    void printForIpe() const;
private:
    void finalizeHourglass();

    Funnel&       upperFunnel()       { assert(!_isClosed); return _rightOrUpper; }
    Funnel const& upperFunnel() const { assert(!_isClosed); return _rightOrUpper; }
    Funnel&       lowerFunnel()       { assert(!_isClosed); return _leftOrLower; }
    Funnel const& lowerFunnel() const { assert(!_isClosed); return _leftOrLower; }
    std::vector<QPointF> getPathFromOpenHourglass();

    Funnel&       rightFacingFunnel()       { assert(_isClosed); return _rightOrUpper; }
    Funnel const& rightFacingFunnel() const { assert(_isClosed); return _rightOrUpper; }
    Funnel&       leftFacingFunnel()        { assert(_isClosed); return _leftOrLower; }
    Funnel const& leftFacingFunnel() const  { assert(_isClosed); return _leftOrLower; }
    std::vector<QPointF> getPathFromClosedHourglass();

    QPointF firstUpperTangency(QPointF const& direction) const;
    QPointF firstLowerTangency(QPointF const& direction) const;
    QPointF lastUpperTangency(QPointF const& direction) const;
    QPointF lastLowerTangency(QPointF const& direction) const;
    enum WhichSide { Start, End };
    enum WhichChain { Upper, Lower };
    QPointF pointOfTangency(WhichSide side, WhichChain chain, QPointF const& direction) const
    {
        if (side == Start) {
            if (chain == Upper) {
                return firstUpperTangency(direction);
            } else {
                return firstLowerTangency(direction);
            }
        } else {
            if (chain == Upper) {
                return lastUpperTangency(direction);
            } else {
                return lastLowerTangency(direction);
            }
        }
    }
    PerimeterRange trimmedRange(WhichSide side, qreal trimming) const;

private:
    Funnel _rightOrUpper, _leftOrLower;
    bool _isClosed;
};

#endif // HOURGLASS_H
