/*
* Copyright (c) 2019 Eric Christoffersen (impolexg@outlook.com)
*
* This program is free software; you can redistribute it and/or modify it
* under the terms of the GNU General Public License as published by the Free
* Software Foundation; either version 2 of the License, or (at your option)
* any later version.
*
* This program is distributed in the hope that it will be useful, but WITHOUT
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
* FITNESS FOR A PARTICULAR PURPOSE.  See the GNU General Public License for
* more details.
*
* You should have received a copy of the GNU General Public License along
* with this program; if not, write to the Free Software Foundation, Inc., 51
* Franklin Street, Fifth Floor, Boston, MA  02110-1301  USA
*/

#include <assert.h>
#include <algorithm>
#include "LocationInterpolation.h"
#include "BlinnSolver.h"

static double radianstodegrees(double r) { return r * (360.0 / (2 * M_PI)); }
static double degreestoradians(double d) { return d * (2 * M_PI / 360.0); }

xyz geolocation::toxyz() const
{
    // Approach developed by:
    //
    // (Olson, D.K. (1996).
    // "Converting earth-Centered, Earth-Fixed Coordinates to Geodetic Coordinates,"
    // IEEE Transactions on Aerospace and Electronic Systems, Vol. 32, No. 1, January 1996, pp. 473 - 476).
    //
    //
    // Java implementation:
    //
    // D. Rose (2014).
    // "Converting between Earth-Centered, Earth Fixed and Geodetic Coordinates"
    //

    //Convert Lat, Lon, Altitude to Earth-Centered-Earth-Fixed (ECEF)
    //Input is a three element array containing lat, lon (rads) and alt (m)
    //Returned array contains x, y, z in meters
    static const double  a = 6378137.0;                  //WGS-84 semi-major axis
    static const double e2 = 6.6943799901377997e-3;      //WGS-84 first eccentricity squared

    double lat = degreestoradians(Lat());
    double lon = degreestoradians(Long());
    double alt = Alt();
    double n = a / sqrt(1 - e2 * sin(lat)*sin(lat));
    double dx = ((n + alt)*cos(lat)*cos(lon));           //ECEF x
    double dy = ((n + alt)*cos(lat)*sin(lon));           //ECEF y
    double dz = ((n*(1 - e2) + alt)*sin(lat));           //ECEF z
    return(xyz(dx, dy, dz));                             //Return x, y, z in ECEF
}

// Compute initial bearing from this geoloc to another, in RADIANS.
// Note that unless bearing is 0 or pi it will vary on the path from one to the other.
double geolocation::BearingTo(const geolocation& to) const
{
    if (this->Lat() == to.Lat() && this->Long() == to.Long())
        return 0.;

    double phi0   = degreestoradians(this->Lat());
    double phi1   = degreestoradians(to.Lat());
    double lamda0 = degreestoradians(this->Long());
    double lamda1 = degreestoradians(to.Long());

    double y = sin(lamda1 - lamda0) * cos(phi1);
    double x = cos(phi0) * sin(phi1) - sin(phi0) * cos(phi1) * cos(lamda1 - lamda0);

    double bearing = atan2(y, x);

    return bearing;
}


geolocation xyz::togeolocation() const
{
    // Approach developed by:
    // (Olson, D.K. (1996).
    // "Converting earth-Centered, Earth-Fixed Coordinates to Geodetic Coordinates,"
    // IEEE Transactions on Aerospace and Electronic Systems, Vol. 32, No. 1, January 1996, pp. 473 - 476).
    //
    // Java implementation:
    //
    // D. Rose (2014).
    // "Converting between Earth-Centered, Earth Fixed and Geodetic Coordinates"
    //

    //Convert Earth-Centered-Earth-Fixed (ECEF) to lat, Lon, Altitude
    //Input is a three element array containing x, y, z in meters
    //Returned array contains lat and lon in radians, and altitude in meters

    static const double  a = 6378137.0;              //WGS-84 semi-major axis
    static const double e2 = 6.6943799901377997e-3;  //WGS-84 first eccentricity squared
    static const double a1 = 4.2697672707157535e+4;  //a1 = a*e2
    static const double a2 = 1.8230912546075455e+9;  //a2 = a1*a1
    static const double a3 = 1.4291722289812413e+2;  //a3 = a1*e2/2
    static const double a4 = 4.5577281365188637e+9;  //a4 = 2.5*a2
    static const double a5 = 4.2840589930055659e+4;  //a5 = a1+a3
    static const double a6 = 9.9330562000986220e-1;  //a6 = 1-e2

    double x = xyz::x();
    double y = xyz::y();
    double z = xyz::z();
    double zp = std::abs(z);
    double w2 = x * x + y * y;
    double w = sqrt(w2);
    double r2 = w2 + z * z;
    double r = sqrt(r2);

    double retLong;
    double retLat;
    double retAlt;

    retLong = atan2(y, x);    // Lon (final)
    double s2 = z * z / r2;
    double c2 = w2 / r2;
    double u = a2 / r;
    double v = a3 - a4 / r;
    double s, ss, c;
    if (c2 > 0.3) {
        s = (zp / r)*(1.0 + c2 * (a1 + u + s2 * v) / r);
        retLat = asin(s);      //Lat
        ss = s * s;
        c = sqrt(1.0 - ss);
    }
    else {
        c = (w / r)*(1.0 - s2 * (a5 - u - c2 * v) / r);
        retLat = acos(c);      //Lat
        ss = 1.0 - c * c;
        s = sqrt(ss);
    }
    double g = 1.0 - e2 * ss;
    double rg = a / sqrt(g);
    double rf = a6 * rg;
    u = w - rg * c;
    v = zp - rf * s;
    double f = c * u + s * v;
    double m = c * v - s * u;
    double p = m / (rf / g + f);
    retLat = retLat + p;            //Lat
    retAlt = f + m * p / 2.0;         //Altitude
    if (z < 0.0) {
        retLat = retLat * -1.0;     //Lat
    }

    return geolocation(radianstodegrees(retLat), radianstodegrees(retLong), retAlt);
}

xyz Slerper::Slerp(double frac)
{
    if (m_sin_angle != 0.0)
    {
        double scale = sin(frac * m_angle) / m_sin_angle;
        return m_x0_norm.scale(sin((1 - frac) * m_angle) / m_sin_angle).add(m_x1_norm.scale(scale));
    }
    return m_x0_norm;
}

// Precompute invariant values needed to geoslerp
Slerper::Slerper(geolocation g0, geolocation g1) :
    m_g0(g0),
    m_g1(g1),
    m_x0(m_g0.toxyz()),
    m_x1(m_g1.toxyz()),
    m_x0_norm(m_x0.normalize()),
    m_x1_norm(m_x1.normalize()),
    m_x0_magnitude(m_x0.magnitude())
{
    m_angle = atan2(m_x0_norm.cross(m_x1_norm).magnitude(), m_x0_norm.dot(m_x1_norm));
    m_sin_angle = sin(m_angle);
    m_altitude_delta = (m_g1.Alt() - m_g0.Alt());
}

// Interpolate fractional arc between two wgs84 vectors.
// This models the earths ellipsoid.
geolocation Slerper::GeoSlerp(double frac)
{
    // Generate normalized ECEF vector
    xyz slerp = Slerp(frac);

    // Scale ecef unit vector so its roughly at earth surface
    double interpaltitudestep = m_altitude_delta * frac;
    slerp = slerp.scale(m_x0_magnitude + interpaltitudestep);

    // Convert ecef back to wgs84
    geolocation slerpgeo = slerp.togeolocation();

    // Set linear interpolated altitude on slerped wgs84 vector
    double interpaltitude = m_altitude_delta * frac;
    slerpgeo.Alt() = m_g0.Alt() + interpaltitude;

    return slerpgeo;
}

xyz LinearTwoPointInterpolator::InterpolateNext(xyz p0, xyz p1)
{
    xyz delta = p1.subtract(p0);
    xyz retval = p1.add(delta);
    return retval;
}

xyz SphericalTwoPointInterpolator::InterpolateNext(xyz p0, xyz p1)
{
    Slerper slerper(p0.togeolocation(), p1.togeolocation());

    geolocation geo = slerper.GeoSlerp(2.0);

    return geo.toxyz();
}

void UnitCatmullRomInterpolator::Init(double pm1, double p0, double p1, double p2)
{
    std::get<0>(m_baseCoefs) = pm1;
    std::get<1>(m_baseCoefs) = p0;
    std::get<2>(m_baseCoefs) = p1;
    std::get<3>(m_baseCoefs) = p2;

    m_locCoefs.Invalidate();
    m_tanCoefs.Invalidate();
}

UnitCatmullRomInterpolator::UnitCatmullRomInterpolator()
{
    Init(0.0, 0.0, 0.0, 0.0);
}

UnitCatmullRomInterpolator::UnitCatmullRomInterpolator(double pm1, double p0, double p1, double p2)
{
    Init(pm1, p0, p1, p2);
}

double UnitCatmullRomInterpolator::Location(double u) const
{
    double A, B, C, D;
    m_locCoefs.Get(m_baseCoefs, A, B, C, D);

    double retval = (u * u * u) * A +
                    (u * u)     * B +
                    (u)         * C +
                                  D;

    return retval;
}

double UnitCatmullRomInterpolator::Tangent(double u) const
{
    double A, B, C;
    m_tanCoefs.Get(m_baseCoefs, A, B, C);

    double retval = (u * u) * A +
                    (u)     * B +
                              C;

    return retval;
}

// Given interpolated value r, provides u that would yield r.
// Only successful if spline is invertable (which is true for
// the distance spline which is monotonic, etc.)
bool UnitCatmullRomInterpolator::Inverse(double r, double &u) const
{
    double a, b, c, d;
    m_locCoefs.Get(m_baseCoefs, a, b, c, d);

    d -= r; // "- r" because root finder solves expressions that equal zero.

    Roots roots = BlinnCubicSolver(a, b, c, d);

    // There are 0, 1, 2 or 3 roots.

    // In general it is possible that there are multiple roots in range... but should never happen
    // for monotonic distance mapping.
    bool ret = false;    
    for (unsigned i = 0; i < roots.resultcount(); i++) {
        double rt = roots.result(i).x / roots.result(i).w;
        // Take the first root we find in range 0..1.
        if (rt >= 0. && rt <= 1.) {
            u = rt;
            ret = true;
            break;
        }
    }

    return ret;
}

void UnitCatmullRomInterpolator3D::Init(xyz pm1, xyz p0, xyz p1, xyz p2)
{
    x.Init(pm1.x(), p0.x(), p1.x(), p2.x());
    y.Init(pm1.y(), p0.y(), p1.y(), p2.y());
    z.Init(pm1.z(), p0.z(), p1.z(), p2.z());
}

UnitCatmullRomInterpolator3D::UnitCatmullRomInterpolator3D(xyz pm1, xyz p0, xyz p1, xyz p2)
{
    Init(pm1, p0, p1, p2);
}

xyz UnitCatmullRomInterpolator3D::Location(double frac) const
{
    return xyz(x.Location(frac), y.Location(frac), z.Location(frac));
}

xyz UnitCatmullRomInterpolator3D::Tangent(double frac) const
{
    return xyz(x.Tangent(frac), y.Tangent(frac), z.Tangent(frac));
}

geolocation GeoPointInterpolator::Location(double distance)
{
    xyz l0xyz = DistancePointInterpolator<SphericalTwoPointInterpolator>::Location(distance);
    geolocation l0 = l0xyz.togeolocation();
    return l0;
}

geolocation GeoPointInterpolator::Location(double distance, double &slope)
{
    xyz tangentVector;
    xyz l0xyz = DistancePointInterpolator<SphericalTwoPointInterpolator>::Location(distance, tangentVector);

    // First step, construct 2 geo locations that are separated by a unit-length tangent veocity vector.
    geolocation l1 = xyz(l0xyz.add(tangentVector.normalize())).togeolocation();
    geolocation l0 = l0xyz.togeolocation();

    // - Route distance is independent of geometric distance.
    //   - Route distance is fixed in stone because it must match video
    //     synchronization files.
    //   - Geometric distance is also fixed since it is a fact of gps location.
    //
    // - Altitude is always in route distance units (so is computable
    //   using gradient and route distance.)
    //
    // - Path length on spline is gps-based so uses geometric units.
    //
    // - Catmull-Rom spline is non-uniform, meaning speed of interpolated point can
    //   vary across bracket which distorts the tangent vector.

    // Vertical velocity of unit tangent vector (altitude is always kept in in route distance units.)
    double rise = l1.Alt() - l0.Alt();

    // Tangent vector's magnitude is velocity in terms of distance spline.
    // Will be unit vector when distance spline has constant rate. Can have
    // zero length when processing points without motion.
    double hyp = tangentVector.magnitude();

    // Compute adjacent speed.
    double adj = fabs((hyp * hyp) - (rise * rise));

    // Gradient.
    slope = (adj > 0.) ? rise / sqrt(adj) : 0.;

    // No matter what we still don't permit slopes above 40%.
    slope = std::min(slope,  .4);
    slope = std::max(slope, -.4);

    // Clear out location if we are an altitude only spline.
    if (!HasLocation()) {
        l0.Lat() = 0;
        l0.Long() = 0;
    }

    return l0;
}

void GeoPointInterpolator::Push(double distance, geolocation point)
{
    if (m_locationState == Unset)
        m_locationState = YesLocation;
    else
        assert(m_locationState == YesLocation);

    DistancePointInterpolator<SphericalTwoPointInterpolator>::Push(distance, point.toxyz());
}

// Special form for case where altitude exists but no location.
void GeoPointInterpolator::Push(double distance, double altitude)
{
    if (m_locationState == Unset)
        m_locationState = NoLocation;
    else
        assert(m_locationState == NoLocation);

    geolocation geo(0, 0, altitude);
    xyz point = geo.toxyz();

    DistancePointInterpolator<SphericalTwoPointInterpolator>::Push(distance, point);
}
