// spice2xyzv.cpp
//
// Copyright (C) 2008-2009, Chris Laurel <claurel@gmail.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.
//
// Create a Celestia xyzv file from a pool of SPICE SPK files

#include "SpiceUsr.h"
#include <string>
#include <vector>
#include <iostream>
#include <fstream>
#include <sstream>
#include <iomanip>
#include <cmath>
#include <ctime>

#ifndef min
#define min(a, b) ((a) < (b) ? (a) : (b))
#endif

using namespace std;


const double J2000 = 2451545.0;


// Default values
// Units are seconds
const double MIN_STEP_SIZE = 60.0;
const double MAX_STEP_SIZE = 5 * 86400.0;

// Units are kilometers
const double TOLERANCE     = 20.0;


class Configuration
{
public:
    Configuration() :
        kernelDirectory("."),
        frameName("eclipJ2000"),
        minStepSize(MIN_STEP_SIZE),
        maxStepSize(MAX_STEP_SIZE),
        tolerance(TOLERANCE)
    {
    }

    string kernelDirectory;
    vector<string> kernelList;
    string startDate;
    string endDate;
    string observerName;
    string targetName;
    string frameName;
    double minStepSize;
    double maxStepSize;
    double tolerance;
};


// Very basic 3-vector class
class Vec3d
{
public:
    Vec3d() : x(0.0), y(0.0), z(0.0) {}
    Vec3d(double _x, double _y, double _z) : x(_x), y(_y), z(_z) {}
    Vec3d(const double v[]) : x(v[0]), y(v[1]), z(v[2]) {}

    double length() const { return std::sqrt(x * x + y * y + z * z); }

    double x, y, z;
};

// Vector add
Vec3d operator+(const Vec3d& v0, const Vec3d& v1)
{
    return Vec3d(v0.x + v1.x, v0.y + v1.y, v0.z + v1.z);
}

// Vector subtract
Vec3d operator-(const Vec3d& v0, const Vec3d& v1)
{
    return Vec3d(v0.x - v1.x, v0.y - v1.y, v0.z - v1.z);
}

// Additive inverse
Vec3d operator-(const Vec3d& v)
{
    return Vec3d(-v.x, -v.y, -v.z);
}

// Scalar multiply
Vec3d operator*(const Vec3d& v, double d)
{
    return Vec3d(v.x * d, v.y * d, v.z * d);
}

// Scalar multiply
Vec3d operator*(double d, const Vec3d& v)
{
    return Vec3d(v.x * d, v.y * d, v.z * d);
}

ostream& operator<<(ostream& o, const Vec3d& v)
{
    return o << v.x << ' ' << v.y << ' ' << v.z;
}


// The StateVector object just contains the position and velocity
// in 3D.
class StateVector
{
public:
    // Construct a new StateVector from an array of 6 doubles
    // (as used by SPICE.)
    StateVector(const double v[]) :
        position(v), velocity(v + 3) {};

    Vec3d position;
    Vec3d velocity;
};


// QuotedString is used read a double quoted string from a C++ input
// stream.
class QuotedString
{
public:
    string value;
};

istream& operator>>(istream& in, QuotedString& qs)
{
    char c = '\0';

    in >> c;
    while (in && std::isspace(static_cast<unsigned char>(c)))
    {
        in >> c;
    }

    if (c != '"')
    {
        in.setstate(ios::failbit);
        return in;
    }

    string s;

    in >> c;
    while (in && c != '"')
    {
        s += c;
        in.get(c);
    }

    if (in)
        qs.value = s;

    return in;
}



// QuoteStringList is used to read a list of double quoted strings from
// a C++ input stream. The string list must be enclosed by square brackets.
class QuotedStringList
{
public:
    vector<string> value;
};

istream& operator>>(istream& in, QuotedStringList& qsl)
{
    qsl.value.clear();
    char c = '\0';

    in >> c;
    if (c != '[')
    {
        in.setstate(ios::failbit);
        return in;
    }

    in >> c;
    while (in && c == '"')
    {
        in.unget();

        QuotedString qs;
        if (in >> qs)
        {
            qsl.value.push_back(qs.value);
            in >> c;
        }
    }

    if (c != ']')
        in.setstate(ios::failbit);

    return in;
}



static Vec3d cubicInterpolate(const Vec3d& p0, const Vec3d& v0,
                              const Vec3d& p1, const Vec3d& v1,
                              double t)
{
    return p0 + (((2.0 * (p0 - p1) + v1 + v0) * (t * t * t)) +
                 ((3.0 * (p1 - p0) - 2.0 * v0 - v1) * (t * t)) +
                 (v0 * t));
}


double et2jd(double et)
{
    return J2000 + et / 86400.0;
}


string etToString(double et)
{
    char buf[200];
    et2utc_c(et, "C", 3, sizeof(buf), buf);
    return string(buf);
}


void printRecord(ostream& out, double et, const StateVector& state)
{
    // < 1 second error around J2000
    out << setprecision(12) << et2jd(et) << " ";

    // < 1 meter error at 1 billion km
    out << setprecision(12) << state.position << " ";

    // < 0.1 mm/s error at 10 km/s
    out << setprecision(8) << state.velocity << endl;
}


StateVector getStateVector(SpiceInt targetID,
                           double et,
                           const string& frameName,
                           SpiceInt observerID)
{
    double stateVector[6];
    double lightTime = 0.0;

    spkgeo_c(targetID, et, frameName.c_str(), observerID,
             stateVector, &lightTime);

    return StateVector(stateVector);
}


// Convert a body name to a NAIF ID. Return true if the ID was
// found, and false otherwise.
bool bodyNameToId(const std::string& name, SpiceInt* naifId)
{
    SpiceBoolean found = SPICEFALSE;

    bodn2c_c(name.c_str(), naifId, &found);
    if (found)
    {
        return true;
    }

    // Check to see whether the string is actually a numeric ID
    istringstream str(name, istringstream::in);
    int id = 0;
    str >> id;
    if (str.eof())
    {
        *naifId = id;
        return true;
    }
    else
    {
        return false;
    }
}


// Dump information about the xyzv file in the comment header
void writeCommentHeader(const Configuration& config,
                        ostream& out)
{
    SpiceInt observerID = 0;
    SpiceInt targetID = 0;

    if (!bodyNameToId(config.observerName, &observerID))
        return;
    if (!bodyNameToId(config.targetName, &targetID))
        return;

    out << "# Celestia xyzv file generated by spice2xyzv\n";
    out << "#\n";

    time_t now = 0;
    time(&now);
    out << "# Creation date: " << ctime(&now);
    out << "#\n";

    out << "# SPICE kernel files used:\n";
    for (vector<string>::const_iterator iter = config.kernelList.begin();
         iter != config.kernelList.end(); iter++)
    {
        out << "#   " << *iter << endl;
    }
    out << "#\n";

    out << "# Start date: " << config.startDate << endl;
    out << "# End date:   " << config.endDate << endl;
    out << "# Observer:   " << config.observerName << " (" << observerID << ")" << endl;
    out << "# Target:     " << config.targetName << " (" << targetID << ")" << endl;
    out << "# Frame:      " << config.frameName << endl;
    out << "#\n";

    out << "# Min step size: " << config.minStepSize << " s" << endl;
    out << "# Max step size: " << config.maxStepSize << " s" << endl;
    out << "# Tolerance:     " << config.tolerance << " km" << endl;
    out << "#\n";

    out << "# Records are <jd> <x> <y> <z> <vel x> <vel y> <vel z>\n";
    out << "#   Time is a TDB Julian date\n";
    out << "#   Position in km\n";
    out << "#   Velocity in km/sec\n";

    out << endl;
}


bool convertSpkToXyzv(const Configuration& config,
                      ostream& out)
{
    // Load the required SPICE kernels
    for (vector<string>::const_iterator iter = config.kernelList.begin();
         iter != config.kernelList.end(); iter++)
    {
        string pathname = config.kernelDirectory + "/" + *iter;
        furnsh_c(pathname.c_str());
    }

    double startET = 0.0;
    double endET = 0.0;

    str2et_c(config.startDate.c_str(), &startET);
    str2et_c(config.endDate.c_str(),   &endET);

    SpiceInt observerID = 0;
    SpiceInt targetID = 0;
    if (!bodyNameToId(config.observerName, &observerID))
    {
        cerr << "Observer object " << config.observerName << " not found. Aborting.\n";
        return false;
    }

    if (!bodyNameToId(config.targetName, &targetID))
    {
        cerr << "Target object " << config.targetName << " not found. Aborting.\n";
        return false;
    }

    double startStepSize = config.minStepSize; // samplingParams.startStep;
    double maxStepSize   = config.maxStepSize; // samplingParams.maxStep;
    double minStepSize   = config.minStepSize; // samplingParams.minStep;
    double tolerance     = config.tolerance;   // samplingParams.tolerance;
    double t = startET;
    const double stepFactor = 1.25;

    StateVector lastState = getStateVector(targetID, startET, config.frameName, observerID);
    double et = startET;

    printRecord(out, et, lastState);

    while (t < endET)
    {
        // Make sure that we don't go past the end of the sample interval
        maxStepSize = min(maxStepSize, endET - t);
        double dt = min(maxStepSize, startStepSize * 2.0);

        StateVector s1 = getStateVector(targetID, t + dt, config.frameName, observerID);

        double tmid = t + dt / 2.0;
        Vec3d pTest = getStateVector(targetID, tmid, config.frameName, observerID).position;
        Vec3d pInterp = cubicInterpolate(lastState.position,
                                         lastState.velocity * dt,
                                         s1.position,
                                         s1.velocity * dt,
                                         0.5);

        double positionError = (pInterp - pTest).length();

        // Error is greater than tolerance; decrease the step until the
        // error is within the tolerance.
        if (positionError > tolerance)
        {
            while (positionError > tolerance && dt > minStepSize)
            {
                dt /= stepFactor;

                s1 = getStateVector(targetID, t + dt, config.frameName, observerID);

                tmid = t + dt / 2.0;
                Vec3d pTest = getStateVector(targetID, tmid, config.frameName, observerID).position;
                pInterp = cubicInterpolate(lastState.position,
                                           lastState.velocity * dt,
                                           s1.position,
                                           s1.velocity * dt,
                                           0.5);

                positionError = (pInterp - pTest).length();
            }
        }
        else
        {
            // Error is less than the tolerance; increase the step size until
            // the tolerance is just exceeded.
            while (positionError < tolerance && dt < maxStepSize)
            {
                dt *= stepFactor;
                dt = min(maxStepSize, dt);

                s1 = getStateVector(targetID, t + dt, config.frameName, observerID);

                tmid = t + dt / 2.0;
                Vec3d pTest = getStateVector(targetID, tmid, config.frameName, observerID).position;
                pInterp = cubicInterpolate(lastState.position,
                                           lastState.velocity * dt,
                                           s1.position,
                                           s1.velocity * dt,
                                           0.5);

                positionError = (pInterp - pTest).length();
            }
        }

        t = t + dt;
        lastState = s1;

        printRecord(out, t, lastState);
    }

    return true;
}


bool readConfig(istream& in, Configuration& config)
{
    QuotedString qs;

    while (in && !in.eof())
    {
        string key;

        in >> key;
        if (in.eof())
            return true;

        if (!in.eof())
        {
            if (key == "StartDate")
            {
                if (in >> qs)
                    config.startDate = qs.value;
            }
            else if (key == "EndDate")
            {
                if (in >> qs)
                    config.endDate = qs.value;
            }
            else if (key == "Observer")
            {
                if (in >> qs)
                    config.observerName = qs.value;
            }
            else if (key == "Target")
            {
                if (in >> qs)
                    config.targetName = qs.value;
            }
            else if (key == "Frame")
            {
                if (in >> qs)
                    config.frameName = qs.value;
            }
            else if (key == "MinStep")
            {
                in >> config.minStepSize;
            }
            else if (key == "MaxStep")
            {
                in >> config.maxStepSize;
            }
            else if (key == "Tolerance")
            {
                in >> config.tolerance;
            }
            else if (key == "KernelDirectory")
            {
                if (in >> qs)
                    config.kernelDirectory = qs.value;
            }
            else if (key == "Kernels")
            {
                QuotedStringList qsl;
                if (in >> qsl)
                {
                    config.kernelList = qsl.value;
                }
            }
        }
    }

    return in.good();
}


int main(int argc, char* argv[])
{
    if (argc < 2)
    {
        cerr << "Usage: spice2xyzv <config filename> [output filename]\n";
        return 1;
    }


    ifstream configFile(argv[1]);
    if (!configFile)
    {
        cerr << "Error opening configuration file.\n";
        return 1;
    }


    Configuration config;
    if (!readConfig(configFile, config))
    {
        cerr << "Error in configuration file.\n";
        return 1;
    }

    // Check that all required parameters are present.
    if (config.startDate.empty())
    {
        cerr << "StartDate missing from configuration file.\n";
        return 1;
    }

    if (config.endDate.empty())
    {
        cerr << "EndDate missing from configuration file.\n";
        return 1;
    }

    if (config.targetName.empty())
    {
        cerr << "Target missing from configuration file.\n";
        return 1;
    }

    if (config.observerName.empty())
    {
        cerr << "Observer missing from configuration file.\n";
        return 1;
    }

    if (config.kernelList.empty())
    {
        cerr << "Kernels missing from configuration file.\n";
        return 1;
    }

    // Load the leap second kernel
#if defined(_WIN32)
    furnsh_c("naif0012.tls");
#else
    furnsh_c(CONFIG_DATA_DIR "/" "naif0012.tls");
#endif

    writeCommentHeader(config, cout);
    convertSpkToXyzv(config, cout);

    return 0;
}
