#pragma once

#ifndef TRACK_INCLUDED
#define TRACK_INCLUDED

// TnzTools includes
#include <tools/inputstate.h>

// TnzCore includes
#include <tcommon.h>
#include <tgeometry.h>

// Qt headers
#include <Qt>

// std includes
#include <vector>
#include <algorithm>


#undef DVAPI
#undef DVVAR
#ifdef TNZTOOLS_EXPORTS
#define DVAPI DV_EXPORT_API
#define DVVAR DV_EXPORT_VAR
#else
#define DVAPI DV_IMPORT_API
#define DVVAR DV_IMPORT_VAR
#endif


//===================================================================

//    Forward Declarations

class TTrack;
class TTrackPoint;
class TTrackTangent;
class TTrackTransform;
class TTrackHandler;
class TSubTrackHandler;
class TMultiTrackHandler;
class TTrackInterpolator;

typedef TSmartPointerT<TTrack> TTrackP;
typedef TSmartPointerT<TTrackHandler> TTrackHandlerP;
typedef TSmartPointerT<TSubTrackHandler> TSubTrackHandlerP;
typedef TSmartPointerT<TMultiTrackHandler> TMultiTrackHandlerP;
typedef TSmartPointerT<TTrackInterpolator> TTrackInterpolatorP;

typedef std::vector<TTrackPoint> TTrackPointList;
typedef std::vector<TTrackTangent> TTrackTangentList;
typedef std::vector<TTrackTransform> TTrackTransformList;
typedef std::vector<TTrackP> TTrackList;

//===================================================================


//*****************************************************************************************
//    export template implementations for win32
//*****************************************************************************************

#ifdef _WIN32
template class DVAPI TSmartPointerT<TTrack>;
template class DVAPI TSmartPointerT<TTrackHandler>;
template class DVAPI TSmartPointerT<TSubTrackHandler>;
template class DVAPI TSmartPointerT<TMultiTrackHandler>;
template class DVAPI TSmartPointerT<TTrackInterpolator>;
#endif


//*****************************************************************************************
//    TTrackPoint definition
//*****************************************************************************************

class DVAPI TTrackPoint {
public:
  TPointD position;
  double pressure;
  TPointD tilt;

  double originalIndex;
  double time;
  double length;

  bool final;

  explicit TTrackPoint(
    const TPointD &position = TPointD(),
    double pressure = 0.5,
    const TPointD &tilt = TPointD(),
    double originalIndex = 0.0,
    double time = 0.0,
    double length = 0.0,
    bool final = false
  ):
    position(position),
    pressure(pressure),
    tilt(tilt),
    originalIndex(originalIndex),
    time(time),
    length(length),
    final(final)
  { }
};


//*****************************************************************************************
//    TTrackTangent definition
//*****************************************************************************************

class DVAPI TTrackTangent {
public:
  TPointD position;
  double pressure;
  TPointD tilt;

  inline explicit TTrackTangent(
    const TPointD &position = TPointD(),
    double pressure = 0.0,
    const TPointD &tilt = TPointD()
  ):
    position(position),
    pressure(pressure),
    tilt(tilt)
  { }
};


//*****************************************************************************************
//    TTrackHandler definition
//*****************************************************************************************

class DVAPI TTrackTransform {
public:
  TAffine transform;
  TAffine tiltTransform;
  double pressureScale;
  double pressureOffset;

  inline TTrackTransform(
    const TAffine &transform,
    const TAffine &tiltTransform,
    double pressureScale = 1,
    double pressureOffset = 0
  ):
    transform(transform),
    tiltTransform(tiltTransform),
    pressureScale(pressureScale),
    pressureOffset(pressureOffset) { }

  inline explicit TTrackTransform(
    const TAffine &transform,
    double pressureScale = 1,
    double pressureOffset = 0
  ):
    transform(transform),
    tiltTransform(makeTiltTransform(transform)),
    pressureScale(pressureScale),
    pressureOffset(pressureOffset) { }

  inline explicit TTrackTransform(
    double pressureScale = 1,
    double pressureOffset = 0
  ):
    pressureScale(pressureScale),
    pressureOffset(pressureOffset) { }
  
  inline TTrackPoint apply(TTrackPoint p) const {
    p.position = transform * p.position;
    
    TPointD t = tiltTransform * p.tilt;
    p.tilt.x = t.x > -1 ? (t.x < 1 ? t.x : 1) : -1;
    p.tilt.y = t.y > -1 ? (t.y < 1 ? t.y : 1) : -1;
    
    double pr = p.pressure*pressureScale + pressureOffset;
    p.pressure = pr > 0 ? (pr < 1 ? pr : 1) : 0;
    
    return p;
  }

  inline void recalcTiltTransform()
    { tiltTransform = makeTiltTransform(transform); }

  static TAffine makeTiltTransform(const TAffine &a);
};


//*****************************************************************************************
//    TTrackHandler definition
//*****************************************************************************************

class DVAPI TTrackHandler : public TSmartObject { };


//*****************************************************************************************
//    TSubTrackHandler definition
//*****************************************************************************************

class DVAPI TSubTrackHandler: public TTrackHandler {
public:
  TTrackP track;
};


//*****************************************************************************************
//    TMultiTrackHandler definition
//*****************************************************************************************

class DVAPI TMultiTrackHandler: public TTrackHandler {
public:
  std::vector<TTrackP> tracks;
};


//*****************************************************************************************
//    TTrackInterpolator definition
//*****************************************************************************************

class DVAPI TTrackInterpolator : public TSmartObject {
public:
  TTrack &track;
  inline explicit TTrackInterpolator(TTrack &track);
  virtual TTrackPoint interpolate(double index) = 0;
};


//*****************************************************************************************
//    TTrack definition
//*****************************************************************************************

class DVAPI TTrack : public TSmartObject {
public:
  typedef long long Id;

private:
  static Id m_lastId;

public:
  const Id id;
  const TInputState::DeviceId deviceId;
  const TInputState::TouchId touchId;
  const TInputState::KeyHistory::Holder keyHistory;
  const TInputState::ButtonHistory::Holder buttonHistory;
  const bool hasPressure;
  const bool hasTilt;

  const TTrack* const original;
  const double timeOffset;
  const double rootTimeOffset;
  
  mutable TTrackHandlerP handler;
  mutable int pointsRemoved;
  mutable int pointsAdded;
  mutable int fixedPointsAdded;

private:
  friend class TTrackInterpolator;
  TTrackInterpolatorP interpolator;
  TTrackPointList m_points;
  const TTrackPoint m_none;
  int m_pointsFixed;

public:

  explicit TTrack(
    TInputState::DeviceId deviceId = TInputState::DeviceId(),
    TInputState::TouchId touchId = TInputState::TouchId(),
    const TInputState::KeyHistory::Holder &keyHistory = TInputState::KeyHistory::Holder(),
    const TInputState::ButtonHistory::Holder &buttonHistory = TInputState::ButtonHistory::Holder(),
    bool hasPressure = false,
    bool hasTilt = false,
    double timeOffset = 0
  );

  explicit TTrack(const TTrack &original, double timeOffset = 0);

  const TTrackInterpolatorP& getInterpolator() const
    { return interpolator; }
  void removeInterpolator()
    { interpolator.reset(); }

  inline TTimerTicks ticks() const
    { return keyHistory.ticks(); }
  inline bool changed() const
    { return pointsRemoved || pointsAdded || fixedPointsAdded; }

  const TTrack* root() const;
  int level() const;

  inline int clampIndex(int index) const
    { return std::min(std::max(index, 0), size() - 1); }
  inline double clampIndexFloat(double index) const
    { return std::min(std::max(index, 0.0), (double)(size() - 1)); }
  inline int floorIndexNoClamp(double index) const
    { return (int)floor(index + TConsts::epsilon); }
  inline int floorIndex(double index) const
    { return clampIndex(floorIndexNoClamp(index)); }
  inline int ceilIndexNoClamp(double index) const
    { return floorIndexNoClamp(index) + 1; }
  inline int ceilIndex(double index) const
    { return clampIndex(ceilIndexNoClamp(index)); }

  int floorIndex(double index, double *outFrac) const;

  inline const TTrackPoint& floorPoint(double index, double *outFrac = NULL) const
    { return point(floorIndex(index, outFrac)); }
  inline const TTrackPoint& ceilPoint(double index) const
    { return point(ceilIndex(index)); }

  inline const TTrackPoint& point(int index) const
    { return empty() ? m_none : m_points[clampIndex(index)]; }

  inline int size() const
    { return (int)m_points.size(); }
  inline int fixedSize() const
    { return m_pointsFixed; }
  inline int previewSize() const
    { return size() - fixedSize(); }
  inline bool empty() const
    { return m_points.empty(); }
  inline const TTrackPoint& front() const
    { return point(0); }
  inline const TTrackPoint& back() const
    { return point(size() - 1); }
  inline bool finished() const
    { return !m_points.empty() && back().final; }
  inline bool fixedFinished() const
    { return finished() && !previewSize(); }
  inline const TTrackPoint& operator[] (int index) const
    { return point(index); }
  inline const TTrackPointList& points() const
    { return m_points; }

  inline void resetRemoved() const
    { pointsRemoved = 0; }
  inline void resetAdded() const
    { pointsAdded = 0; }
  inline void resetFixedAdded() const
    { fixedPointsAdded = 0; }
  inline void resetChanges() const
    { resetRemoved(); resetAdded(); resetFixedAdded(); }

  void push_back(const TTrackPoint &point, bool fixed);
  void pop_back(int count = 1);
  void fix_points(int count = 1);

  inline void truncate(int count)
    { pop_back(size() - count); }
  inline void fix_to(int count)
    { fix_points(count - fixedSize()); }
  inline void fix_all()
    { fix_to(size()); }

  inline const TTrackPoint& current() const
    { return point(size() - pointsAdded); }
  inline const TTrackPoint& previous() const
    { return point(size() - pointsAdded - 1); }
  inline const TTrackPoint& next() const
    { return point(size() - pointsAdded + 1); }

  inline TInputState::KeyState::Holder getKeyState(double time) const
    { return keyHistory.get(time); }
  inline TInputState::KeyState::Holder getKeyState(const TTrackPoint &point) const
    { return getKeyState(rootTimeOffset + point.time); }
  inline TInputState::KeyState::Holder getCurrentKeyState() const
    { return getKeyState(rootTimeOffset + current().time); }
  inline TInputState::ButtonState::Holder getButtonState(double time) const
    { return buttonHistory.get(time); }
  inline TInputState::ButtonState::Holder getButtonState(const TTrackPoint &point) const
    { return getButtonState(rootTimeOffset + point.time); }
  inline TInputState::ButtonState::Holder getCurrentButtonState() const
    { return getButtonState(rootTimeOffset + current().time); }

private:
  template<double TTrackPoint::*Field>
  double binarySearch(double value) const {
    // points_[a].value <= value < points_[b].value
    if (m_points.empty()) return 0.0;
    int a = 0;
    double aa = m_points[a].*Field;
    if (value - aa <= 0.5*TConsts::epsilon) return (double)a;
    int b = (int)m_points.size() - 1;
    double bb = m_points[b].*Field;
    if (bb - value <= 0.5*TConsts::epsilon) return (double)b;
    while(true) {
      int c = (a + b)/2;
      if (a == c) break;
      double cc = m_points[c].*Field;
      if (cc - value > 0.5*TConsts::epsilon)
        { b = c; bb = cc; } else { a = c; aa = cc; }
    }
    return bb - aa >= 0.5*TConsts::epsilon ? (double)a + (value - aa)/(bb - aa) : (double)a;
  }

public:
  inline double indexByOriginalIndex(double originalIndex) const
    { return binarySearch<&TTrackPoint::originalIndex>(originalIndex); }
  inline double indexByTime(double time) const
    { return binarySearch<&TTrackPoint::time>(time); }
  inline double indexByLength(double length) const
    { return binarySearch<&TTrackPoint::length>(length); }

  inline double originalIndexByIndex(double index) const {
    double frac;
    const TTrackPoint &p0 = floorPoint(index, &frac);
    const TTrackPoint &p1 = ceilPoint(index);
    return interpolationLinear(p0.originalIndex, p1.originalIndex, frac);
  }
  inline double timeByIndex(double index) const {
    double frac;
    const TTrackPoint &p0 = floorPoint(index, &frac);
    const TTrackPoint &p1 = ceilPoint(index);
    return interpolationLinear(p0.time, p1.time, frac);
  }
  inline double lengthByIndex(double index) const {
    double frac;
    const TTrackPoint &p0 = floorPoint(index, &frac);
    const TTrackPoint &p1 = ceilPoint(index);
    return interpolationLinear(p0.length, p1.length, frac);
  }

  inline TTrackPoint calcPoint(double index) const
    { return interpolator ? interpolator->interpolate(index) : interpolateLinear(index); }
  TPointD calcTangent(double index, double distance = 0.1) const;
  double rootIndexByIndex(double index) const;
  TTrackPoint calcRootPoint(double index) const;

  inline TTrackPoint pointFromOriginal(const TTrackPoint &originalPoint, double originalIndex) const {
    TTrackPoint p = originalPoint;
    p.originalIndex = original ? original->clampIndexFloat(originalIndex) : originalIndex;
    p.time -= timeOffset;
    return p;
  }
  
  inline TTrackPoint pointFromOriginal(int originalIndex) const
    { return original ? pointFromOriginal(original->point(originalIndex), originalIndex) : TTrackPoint(); }
  
  inline TTrackPoint calcPointFromOriginal(double originalIndex) const
    { return original ? pointFromOriginal(original->calcPoint(originalIndex), originalIndex) : TTrackPoint(); }

  inline TTrackPoint interpolateLinear(double index) const {
    double frac;
    const TTrackPoint &p0 = floorPoint(index, &frac);
    const TTrackPoint &p1 = ceilPoint(index);
    return interpolationLinear(p0, p1, frac);
  }

  template<typename T>
  static inline T interpolationLinear(const T &p0, const T &p1, double l)
    { return p0*(1.0 - l) + p1*l; }

  template<typename T>
  static T interpolationSpline(const T &p0, const T &p1, const T &t0, const T &t1, double l) {
    double ll = l*l;
    double lll = ll*l;
    return p0*( 2.0*lll - 3.0*ll + 1.0)
         + p1*(-2.0*lll + 3.0*ll      )
         + t0*(     lll - 2.0*ll + l  )
         + t1*(     lll - 1.0*ll      );
  }

  static inline TTrackPoint interpolationLinear(const TTrackPoint &p0, const TTrackPoint &p1, double l) {
    if (l <= TConsts::epsilon) return p0;
    if (l >= 1.0 - TConsts::epsilon) return p1;
    return TTrackPoint(
      interpolationLinear(p0.position      , p1.position      , l),
      interpolationLinear(p0.pressure      , p1.pressure      , l),
      interpolationLinear(p0.tilt          , p1.tilt          , l),
      interpolationLinear(p0.originalIndex , p1.originalIndex , l),
      interpolationLinear(p0.time          , p1.time          , l),
      interpolationLinear(p0.length        , p1.length        , l),
      p0.final && p1.final );
  }

  static inline TTrackPoint interpolationSpline(
    const TTrackPoint &p0,
    const TTrackPoint &p1,
    const TTrackTangent &t0,
    const TTrackTangent &t1,
    double l )
  {
    if (l <= TConsts::epsilon) return p0;
    if (l >= 1.0 - TConsts::epsilon) return p1;
    return TTrackPoint(
      interpolationSpline(p0.position      , p1.position      , t0.position , t1.position , l),
      interpolationLinear(p0.pressure      , p1.pressure      , l),
    //interpolationSpline(p0.pressure      , p1.pressure      , t0.pressure , t1.pressure , l),
      interpolationLinear(p0.tilt          , p1.tilt          , l),
    //interpolationSpline(p0.tilt          , p1.tilt          , t0.tilt     , t1.tilt     , l),
      interpolationLinear(p0.originalIndex , p1.originalIndex , l),
      interpolationLinear(p0.time          , p1.time          , l),
      interpolationLinear(p0.length        , p1.length        , l),
      p0.final && p1.final );
  }
};



//*****************************************************************************************
//    TTrackInterpolator implementation
//*****************************************************************************************

inline TTrackInterpolator::TTrackInterpolator(TTrack &track):
  track(track) { track.interpolator = this; }


//*****************************************************************************************
//    TTrackIntrOrig definition
//*****************************************************************************************

class DVAPI TTrackIntrOrig : public TTrackInterpolator {
public:
  using TTrackInterpolator::TTrackInterpolator;
  TTrackPoint interpolate(double index) override;
};


#endif
