// rotation.h
//
// Copyright (C) 2004-2009, the Celestia Development Team
// Original version by 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.

#pragma once

#include <memory>

#include <Eigen/Core>
#include <Eigen/Geometry>

namespace celestia::ephem
{

/*! A RotationModel object describes the orientation of an object
 *  over some time range.
 */
class RotationModel
{
 public:
    virtual ~RotationModel() = default;

    /*! Return the orientation of an object in its reference frame at the
     * specified time (TDB). Some rotations can be decomposed into two parts:
     * a fixed or slowly varying part, and a much more rapidly varying part.
     * The rotation of a planet is such an example. The rapidly varying part
     * is referred to as spin; the slowly varying part determines the
     * equatorial plane. When the rotation of an object can be decomposed
     * in this way, the overall orientation = spin * equator. Otherwise,
     * orientation = spin.
     */
    Eigen::Quaterniond orientationAtTime(double tjd) const
    {
        return spin(tjd) * equatorOrientationAtTime(tjd);
    }

    virtual Eigen::Vector3d angularVelocityAtTime(double tjd) const;

    /*! Return the orientation of the equatorial plane (normal to the primary
     *  axis of rotation.) The overall orientation of the object is
     *  spin * equator. If there is no primary axis of rotation, equator = 1
     *  and orientation = spin.
     */
    virtual Eigen::Quaterniond equatorOrientationAtTime(double /*tjd*/) const
    {
        return Eigen::Quaterniond::Identity();
    }

    /*! Return the rotation about primary axis of rotation (if there is one.)
     *  The overall orientation is spin * equator. For objects without a
     *  primary axis of rotation, spin *is* the orientation.
     */
    virtual Eigen::Quaterniond spin(double tjd) const = 0;

    virtual double getPeriod() const = 0;

    virtual bool isPeriodic() const = 0;

    // Return the time range over which the orientation model is valid;
    // if the model is always valid, begin and end should be equal.
    virtual void getValidRange(double& begin, double& end) const
    {
        begin = 0.0;
        end = 0.0;
    };
};


/*! CachingRotationModel is an abstract base class for complicated rotation
 *  models that are computationally expensive. The last calculated spin,
 *  equator orientation, and angular velocity are all cached and reused in
 *  order to avoid redundant calculation. Subclasses must override computeSpin(),
 *  computeEquatorOrientation(), and getPeriod(). The default implementation
 *  of computeAngularVelocity uses differentiation to approximate the
 *  the instantaneous angular velocity. It may be overridden if there is some
 *  better means to calculate the angular velocity for a specific rotation
 *  model.
 */
class CachingRotationModel : public RotationModel
{
 public:
    CachingRotationModel();
    ~CachingRotationModel() override = default;

    Eigen::Quaterniond spin(double tjd) const override;
    Eigen::Quaterniond equatorOrientationAtTime(double tjd) const override;
    Eigen::Vector3d angularVelocityAtTime(double tjd) const override;

    virtual Eigen::Quaterniond computeEquatorOrientation(double tjd) const = 0;
    virtual Eigen::Quaterniond computeSpin(double tjd) const = 0;
    virtual Eigen::Vector3d computeAngularVelocity(double tjd) const;
    double getPeriod() const override = 0;
    bool isPeriodic() const override = 0;

private:
    mutable Eigen::Quaterniond lastSpin;
    mutable Eigen::Quaterniond lastEquator;
    mutable Eigen::Vector3d lastAngularVelocity;
    mutable double lastTime;
    mutable bool spinCacheValid;
    mutable bool equatorCacheValid;
    mutable bool angularVelocityCacheValid;
};


/*! The simplest rotation model is ConstantOrientation, which describes
 *  an orientation that is fixed within a reference frame.
 */
class ConstantOrientation : public RotationModel
{
 public:
    ConstantOrientation() = default;
    ConstantOrientation(const Eigen::Quaterniond& q);
    ~ConstantOrientation() override = default;

    Eigen::Quaterniond spin(double tjd) const override;
    Eigen::Vector3d angularVelocityAtTime(double tjd) const override;

    double getPeriod() const override { return 0.0; }
    bool isPeriodic() const override { return false; }

    static std::shared_ptr<const RotationModel> identity();

 private:
    Eigen::Quaterniond orientation{ Eigen::Quaterniond::Identity() };
};


/*! UniformRotationModel describes an object that rotates with a constant
 *  angular velocity.
 */
class UniformRotationModel : public RotationModel
{
 public:
    UniformRotationModel(double _period,
                         float _offset,
                         double _epoch,
                         float _inclination,
                         float _ascendingNode);
    ~UniformRotationModel() override = default;

    bool isPeriodic() const override;
    double getPeriod() const override;
    Eigen::Quaterniond equatorOrientationAtTime(double tjd) const override;
    Eigen::Quaterniond spin(double tjd) const override;
    Eigen::Vector3d angularVelocityAtTime(double tjd) const override;

 private:
    double period;       // sidereal rotation period
    float offset;        // rotation at epoch
    double epoch;
    float inclination;   // tilt of rotation axis w.r.t. reference plane
    float ascendingNode; // longitude of ascending node of equator on the refernce plane
};


/*! PrecessingRotationModel describes an object with a spin axis that
 *  precesses at a constant rate about some axis.
 */
class PrecessingRotationModel : public RotationModel
{
 public:
    PrecessingRotationModel(double _period,
                            float _offset,
                            double _epoch,
                            float _inclination,
                            float _ascendingNode,
                            double _precPeriod);
    ~PrecessingRotationModel() override = default;

    bool isPeriodic() const override;
    double getPeriod() const override;
    Eigen::Quaterniond equatorOrientationAtTime(double tjd) const override;
    Eigen::Quaterniond spin(double tjd) const override;

 private:
    double period;       // sidereal rotation period (in Julian days)
    float offset;        // rotation at epoch
    double epoch;
    float inclination;   // tilt of rotation axis w.r.t. reference plane
    float ascendingNode; // longitude of ascending node of equator on the refernce plane

    double precessionPeriod; // period of precession (in Julian days)
};

} // end namespace celestia::ephem
