/*
 * IMPLEMENTATION NOTES
 * 
 * NOTE 0. Earth motions accounted in this implementation:
 * 1. Rotation around the polar axis.
 * 2. Precession of the polar axis due to the Moon; nutations negligible.
 * 3. Revolution around the Sun on an elliptic orbit at varying speed and
 *    varying distance.
 * 4. Magic constant added to the time to account for light speed propagation
 *    and errors on the solstice date and perihelion date.
 * Comparison with published ephemeris of years 2010 and 2017 matches within
 * 0.1 DEG of latitude and 0.2 DEG of longitude - see test program.
 * 
 * NOTE 1. Computing the position of the Sun in the geodetic Earth reference.
 * We start the whole reasoning from a Sun-centric reference "S". The orbit of
 * the Earth is the xy plane; the initial position of the Earth with components
 * on the S reference is T_S = [r,0,0] being r the mean distance of the Earth
 * from the Sun.
 * We assume the Earth's polar axis be inclined toward the Sun by an angle sigma.
 * 
 * The Earth rotates around the Sun on an ellipse; be r is initial distance from
 * the Sun; the components of its position over time given in the S reference are:
 * 
 *     T_S(t) = [r*cos(omegaY*t + phiY), r*sin(omegaY*t + phiA), 0]
 * 
 * where:
 * 
 *     omegaA = 2*PI / SIDERAL_YEAR, being SIDERAL_YEAR the revolution time of
 *     the Earth around the Sun in the S reference;
 * 
 *     phiA = phase to account for the actual start of the simulation.
 * 
 * The distance from the Sun and the orbital angle of revolution depend on the
 * time according to the Kepler laws; specific interpolation tables are computed
 * just to solve this problem.
 * 
 * For any point P, its components P_S in the S reference and its components T_G
 * in the geodetic rotating Earth are related by:
 * 
 *     P_G = Rz(omegaD*t + phiD) * Ry(sigma) * (P_S - T_S(t))
 * 
 * where:
 * 
 *     Ry() and Rz() are "active" rotations around the y and z axis respectively,
 *     that is, the reference system rotates in the opposite direction;
 * 
 *     omegaD = 2*PI / SIDERAL_DAY, being SIDERAL_DAY the rotation period of the
 *     Earth around its polar axis measured in the S reference;
 *     phiA = phase to account for the actual start of the simulation.
 * 
 * If the point P is the Sun itself P_S = [0,0,0] the formula above gives the
 * position of the Sun with components in G:
 * 
 *     SUN_G(t) = Rz(omegaD*t + phiD) * Ry(sigma) * (- T_S(t))
 * 
 * Setting the correct values for phiY and phiD for any given departure time is
 * a bit tricky, but note that with both values set to zero the date period is
 * around the Summer solstice (21 of June) and the midnight is at the zero
 * meridian.
 * By setting the phase angle phiY we may set any other date, and this mainly
 * affects the elevation of the Sun over the horizon.
 * By setting the phase angle phiD we may set the current UTC on the simulated
 * Earth.
 * 
 * Equinox precession can be introduced by simply replacing the Ry(sigma)
 * factor with the Rz(tss/(26000 years))Ry(sigma) where tss is the time elapsed
 * since the summer solstice of reference and 26000 years is the precession
 * period of the Earth axis.
 * 
 * NOTE 2. The summer solstice of the year 2017 is assumed as a reference point
 * for all the calculations. At this exact time the polar axis of the Earth was
 * inclined toward the Sun, and from here we compute the state of the Earth for
 * any future or past date, including the equinox precession.
 */

#include <stdio.h>
#include <stdlib.h>
#include <assert.h>
#include <math.h>

#include "../util/error.h"

// LINKER_OPTIONS -lm

#define astro_IMPORT
#include "astro.h"

/** Earth sidereal polar axis rotation period (s). */
#define astro_EARTH_SPIN_PERIOD (23.9344699 * 3600)

/** Earth sidereal Sun orbit period (s). */
#define astro_EARTH_ORBIT_PERIOD (365.256363004 * 86400.0)

/** Minimal distance Earth to Sun (m). */
#define astro_EARTH_ORBIT_PERIHELION (147.098291e9)

/** Earth orbit eccentricity. */
#define astro_EARTH_ORBIT_ECCENTRICITY (0.0167086)

/**
 * Earth polar axis angular inclination from the perpendicular to the orbit
 * plane (RAD). This value refers to year 2017; the following parameters must
 * refer to this same year.
 */
#define astro_EARTH_INCL (23.435*M_PI/180.0)

/**
 * Date and time of Earth solstice (ISO 8601 format). Allows to establish the
 * orientation of the polar axis: in fact, at this exact time the polar axis
 * is inclined toward the Sun. It should be the absolute time not affected by
 * the light travel time and NOT the time the event can be observed from the
 * Earth. I don't know if this is the case of the value I reported here, though.
 */
#define astro_EARTH_SUMMER_SOLSTICE_ZULU "2017-06-21T04:24"

/**
 * Date and time of Earth perihelion (ISO 8601 format). Allows to determine
 * where the perihelion point of the orbit is in reference to our main reference
 * point, the summer solstice, so that the varying orbital speed of the Earth
 * and the correct distance of the Sun can be calculated. It should be the
 * absolute time not affected by the light travel time and NOT the time the
 * event can be observed from the Earth. I don't know if this is the case of the
 * value I reported here, though.
 */
#define astro_EARTH_PERIHELION_ZULU "2017-01-04T14:18"

/** If this module has been initialized. */
static int initialized;
/** Offset to the simulator time to get time since summer solstice (s). */
static double year_offset;
/** Offset to the simulator time to get angular rotation around polar axis. */
static double day_offset;
/**
 * Offset to the simulation time to get elapsed time since reference summer
 * solstice. Used to calculate the Earth equinox precession.
 */
static double precession_offset;
static double perihelion_to_solstice_time;
static double theta_solstice;

// Cached last position of the Sun.
static int last_available;
static double last_timestamp;
static VPoint last_sun;
#define astro_LAST_SUN_POSITION_EXPIRE_PERIOD (30)

#define INTERPOLATION_TABLE_LEN (99)

/**
 * Interpolation table giving the Sun distance from Earth. The first value refers
 * to the perihelion; the other values are computed at time steps of 1/INTERPOLATION_TABLE_LEN
 * of the Earth sidereal period. Last entry duplicates the first one.
 */
static double rho_table[INTERPOLATION_TABLE_LEN];

/**
 * Interpolation table giving the angular position of the Earth respect to the
 * Sun measured from the perihelion. Each steps by 1/INTERPOLATION_TABLE_LEN
 * ofthe Earth sidereal year, which means about 10 DEG. Last entry is
 * (should be...) 360 DEG.
 */
static double theta_table[INTERPOLATION_TABLE_LEN];


/** Converts ISO 8601 into Unix timestamp. */
static int astro_zuluToTimestamp(char *zulu)
{
	zulu_Date d;
	zulu_dateParse(zulu, &d);
	return zulu_dateToTimestamp(&d);
}


/**
 * Generates the interpolation tables distance and orbital angle of the Earth
 * vs. time.
 */
static void astro_computeEarthTables()
{
	double p = (1 + astro_EARTH_ORBIT_ECCENTRICITY) * astro_EARTH_ORBIT_PERIHELION;
	double e = astro_EARTH_ORBIT_ECCENTRICITY;
	double a = astro_EARTH_ORBIT_PERIHELION / (1-e); // major axis
	double b = astro_EARTH_ORBIT_PERIHELION * sqrt((1+e)/(1-e)); // minor axis
	// dTheta = h/rho^2 * dt (Kepler law of equal areas)
	double h = 2*M_PI*a*b/astro_EARTH_ORBIT_PERIOD;
	double theta = 0; // initial theta
	rho_table[0] = 0; // initial rho
	theta_table[0] = theta;
	double dt = astro_EARTH_ORBIT_PERIOD / (INTERPOLATION_TABLE_LEN - 1.0);
	int i;
	for(i = 1; i < INTERPOLATION_TABLE_LEN; i++){
		double rho = p / (1+e*cos(theta));
		theta += h * dt / (rho * rho);
		rho_table[i] = rho;
		theta_table[i] = theta;
	}
}


/**
 * Determines the orbital angle and distance of the Earth vs. Sun by
 * interpolation of the tables.
 * @param t Time elapsed since the perihelion (s). Multiple of the sidereal
 * orbit period allowed as well. For time zero, for example, the minimal
 * distance from the Sun and the angle zero are returned.
 * @param rho Here returns the distance from the Sun (m).
 * @param theta Here returns the orbital angle vs. summer solstice (RAD in [0,2*PI[).
 */
static void astro_earthRhoAndTheta(double t, double *rho, double *theta)
{
	t -= astro_EARTH_ORBIT_PERIOD * floor(t / astro_EARTH_ORBIT_PERIOD);
	int i = t * (INTERPOLATION_TABLE_LEN - 1.0) / astro_EARTH_ORBIT_PERIOD;
	double f = (INTERPOLATION_TABLE_LEN - 1.0) / astro_EARTH_ORBIT_PERIOD * t - i;
	assert(0 <= i && i < INTERPOLATION_TABLE_LEN);
	*rho   = rho_table[i]   + f * (rho_table[i+1]   - rho_table[i]);
	*theta = theta_table[i] + f * (theta_table[i+1] - theta_table[i]);
}


void astro_init(double simulation_time, zulu_Date *departure)
{
	/*
	 * Account here once for all for the light propagation delay Sun-to-Earth
	 * and error in the summer solstice and perihelion time we are currently using.
	 */
	simulation_time += 3.25*60; // 3.25 min seems to fit with data available
	
	int solstice_timestamp = astro_zuluToTimestamp(astro_EARTH_SUMMER_SOLSTICE_ZULU);
	double since_solstice = zulu_dateToTimestamp(departure) - solstice_timestamp;
	
	year_offset = since_solstice;
	// Normalize to [0,astro_EARTH_ORBIT_PERIOD[ range:
	year_offset -= astro_EARTH_ORBIT_PERIOD * floor(year_offset / astro_EARTH_ORBIT_PERIOD);
	year_offset -= simulation_time;
	
	zulu_Date solstice_date;
	zulu_dateParse(astro_EARTH_SUMMER_SOLSTICE_ZULU, &solstice_date);
	day_offset = 3600.0 * solstice_date.hour
		+ 60.0 * solstice_date.minutes
		+ solstice_date.seconds
		+ since_solstice;
	// Normalize to [0,astro_EARTH_ROTATION_PERIOD[ range:
	day_offset -= astro_EARTH_SPIN_PERIOD * floor(day_offset / astro_EARTH_SPIN_PERIOD);
	day_offset -= simulation_time;
	
	precession_offset = - simulation_time + since_solstice;
	
	astro_computeEarthTables();

	perihelion_to_solstice_time = astro_zuluToTimestamp(astro_EARTH_SUMMER_SOLSTICE_ZULU)
		- astro_zuluToTimestamp(astro_EARTH_PERIHELION_ZULU);
	
	double dummy;
	astro_earthRhoAndTheta(perihelion_to_solstice_time, &dummy, &theta_solstice);
	
	initialized = 1;
	last_available = 0;
}


void astro_getSun(double simulation_time, VPoint *sun)
{
	if( ! initialized )
		error_internal("module not initialized", 0);
	
	// Return cached value most of the times:
	if( last_available && simulation_time - last_timestamp < astro_LAST_SUN_POSITION_EXPIRE_PERIOD ){
		*sun = last_sun;
		return;
	}

	// Compute:
	// rho = current distance from the Sun;
	// year_angle = orbital angle since perihelion.
	double year_angle, rho;
	astro_earthRhoAndTheta(simulation_time + year_offset + perihelion_to_solstice_time, &rho, &year_angle);
	// Convert angle to angle from summer solstice (polar axis facing the Sun):
	year_angle -= theta_solstice;
	year_angle -= 2*M_PI*floor(year_angle / (2*M_PI)); // normalize [0,2*PI[
	
	VPoint p;
	// (Minus sign actually needed next, not here)
	p.x = - rho * cos(year_angle);
	p.y = - rho * sin(year_angle);
	p.z = 0;
	
	// FIXME: this matrix is constant. Or Earth precession should be accounted.
	VMatrix m;
	VIdentMatrix(&m);
	// Equinox precession, 26000 years period:
	VRotate(&m, ZRotation, (simulation_time + precession_offset)
		/(26000.0*86400.0*365.25));
	VRotate(&m, YRotation, astro_EARTH_INCL);
	VTransform_(&p, &m, &p);
	
	double day_angle = 2*M_PI/astro_EARTH_SPIN_PERIOD * (simulation_time + day_offset);
	day_angle -= 2*M_PI*floor(day_angle / (2*M_PI));
	VIdentMatrix(&m);
	VRotate(&m, ZRotation, day_angle);
	VReverseTransform_(&p, &m, &p);
	*sun = p;
	last_sun = p;
	last_available = 1;
}