
package org.orekit.frames;

import java.io.Serializable;
import java.util.Collection;
import java.util.List;

import org.apache.commons.math3.analysis.interpolation.HermiteInterpolator;
import org.orekit.errors.OrekitException;
import org.orekit.errors.OrekitMessages;
import org.orekit.errors.TimeStampedCacheException;
import org.orekit.time.AbsoluteDate;
import org.orekit.time.TimeFunction;
import org.orekit.time.TimeStamped;
import org.orekit.utils.IERSConventions;
import org.orekit.utils.ImmutableTimeStampedCache;

/** This class loads any kind of Earth Orientation Parameter data throughout a large time range.
 * @author Pascal Parraud
 */
public class EOPHistory implements Serializable {

    /** Serializable UID. */
    private static final long serialVersionUID = 20131010L;

    /** Number of points to use in interpolation. */
    private static final int INTERPOLATION_POINTS = 4;

    /**
     * If this history has any EOP data.
     *
     * @see #hasDataFor(AbsoluteDate)
     */
    private final boolean hasData;

    /** EOP history entries. */
    private final transient ImmutableTimeStampedCache<EOPEntry> cache;

    /** IERS conventions to which EOP refers. */
    private final IERSConventions conventions;

    /** Correction to apply to EOP (may be null). */
    private final transient TimeFunction<double[]> tidalCorrection;

    /** Simple constructor.
     * @param conventions IERS conventions to which EOP refers
     * @param data the EOP data to use
     * @param simpleEOP if true, tidal effects are ignored when interpolating EOP
     * @exception OrekitException if tidal correction model cannot be loaded
     */
    protected EOPHistory(final IERSConventions conventions,
                         final Collection<EOPEntry> data,
                         final boolean simpleEOP)
        throws OrekitException {
        this.conventions = conventions;
        tidalCorrection = simpleEOP ? null : conventions.getEOPTidalCorrection();
        if (data.size() >= INTERPOLATION_POINTS) {
            // enough data to interpolate
            cache = new ImmutableTimeStampedCache<EOPEntry>(INTERPOLATION_POINTS, data);
            hasData = true;
        } else {
            // not enough data to interpolate -> always use null correction
            cache = ImmutableTimeStampedCache.emptyCache();
            hasData = false;
        }
    }

    /** Get the IERS conventions to which these EOP apply.
     * @return IERS conventions to which these EOP apply
     */
    public IERSConventions getConventions() {
        return conventions;
    }

    /** Get the date of the first available Earth Orientation Parameters.
     * @return the start date of the available data
     */
    public AbsoluteDate getStartDate() {
        return this.cache.getEarliest().getDate();
    }

    /** Get the date of the last available Earth Orientation Parameters.
     * @return the end date of the available data
     */
    public AbsoluteDate getEndDate() {
        return this.cache.getLatest().getDate();
    }

    /** Get the UT1-UTC value.
     * <p>The data provided comes from the IERS files. It is smoothed data.</p>
     * @param date date at which the value is desired
     * @return UT1-UTC in seconds (0 if date is outside covered range)
     */
    public double getUT1MinusUTC(final AbsoluteDate date) {
        //check if there is data for date
        if (!this.hasDataFor(date)) {
            // no EOP data available for this date, we use a default 0.0 offset
            return (tidalCorrection == null) ? 0.0 : tidalCorrection.value(date)[2];
        }
        //we have EOP data -> interpolate offset
        try {
            final List<EOPEntry> neighbors = getNeighbors(date);
            final HermiteInterpolator interpolator = new HermiteInterpolator();
            final double firstDUT = neighbors.get(0).getUT1MinusUTC();
            boolean beforeLeap = true;
            for (final EOPEntry neighbor : neighbors) {
                final double dut;
                if (neighbor.getUT1MinusUTC() - firstDUT > 0.9) {
                    // there was a leap second between the entries
                    dut = neighbor.getUT1MinusUTC() - 1.0;
                    if (neighbor.getDate().compareTo(date) <= 0) {
                        beforeLeap = false;
                    }
                } else {
                    dut = neighbor.getUT1MinusUTC();
                }
                interpolator.addSamplePoint(neighbor.getDate().durationFrom(date),
                                            new double[] {
                                                dut
                                            });
            }
            double interpolated = interpolator.value(0)[0];
            if (tidalCorrection != null) {
                interpolated += tidalCorrection.value(date)[2];
            }
            return beforeLeap ? interpolated : interpolated + 1.0;
        } catch (TimeStampedCacheException tce) {
            //this should not happen because of date check above
            throw OrekitException.createInternalError(tce);
        }
    }

    /**
     * Get the entries surrounding a central date.
     * <p>
     * See {@link #hasDataFor(AbsoluteDate)} to determine if the cache has data
     * for {@code central} without throwing an exception.
     *
     * @param central central date
     * @return array of cached entries surrounding specified date
     * @exception TimeStampedCacheException if EOP data cannot be retrieved
     */
    protected List<EOPEntry> getNeighbors(final AbsoluteDate central) throws TimeStampedCacheException {
        return cache.getNeighbors(central);
    }

    /** Get the LoD (Length of Day) value.
     * <p>The data provided comes from the IERS files. It is smoothed data.</p>
     * @param date date at which the value is desired
     * @return LoD in seconds (0 if date is outside covered range)
     */
    public double getLOD(final AbsoluteDate date) {
        //check if there is data for date
        if (!this.hasDataFor(date)) {
            // no EOP data available for this date, we use a default null correction
            return (tidalCorrection == null) ? 0.0 : tidalCorrection.value(date)[3];
        }
        //we have EOP data for date -> interpolate correction
        try {
            final HermiteInterpolator interpolator = new HermiteInterpolator();
            for (final EOPEntry entry : getNeighbors(date)) {
                interpolator.addSamplePoint(entry.getDate().durationFrom(date),
                                            new double[] {
                                                entry.getLOD()
                                            });
            }
            double interpolated = interpolator.value(0)[0];
            if (tidalCorrection != null) {
                interpolated += tidalCorrection.value(date)[3];
            }
            return interpolated;
        } catch (TimeStampedCacheException tce) {
            // this should not happen because of date check above
            throw OrekitException.createInternalError(tce);
        }
    }

    /** Get the pole IERS Reference Pole correction.
     * <p>The data provided comes from the IERS files. It is smoothed data.</p>
     * @param date date at which the correction is desired
     * @return pole correction ({@link PoleCorrection#NULL_CORRECTION
     * PoleCorrection.NULL_CORRECTION} if date is outside covered range)
     */
    public PoleCorrection getPoleCorrection(final AbsoluteDate date) {
        // check if there is data for date
        if (!this.hasDataFor(date)) {
            // no EOP data available for this date, we use a default null correction
            if (tidalCorrection == null) {
                return PoleCorrection.NULL_CORRECTION;
            } else {
                final double[] correction = tidalCorrection.value(date);
                return new PoleCorrection(correction[0], correction[1]);
            }
        }
        //we have EOP data for date -> interpolate correction
        try {
            final HermiteInterpolator interpolator = new HermiteInterpolator();
            for (final EOPEntry entry : getNeighbors(date)) {
                interpolator.addSamplePoint(entry.getDate().durationFrom(date),
                                            new double[] {
                                                entry.getX(), entry.getY()
                                            });
            }
            final double[] interpolated = interpolator.value(0);
            if (tidalCorrection != null) {
                final double[] correction = tidalCorrection.value(date);
                interpolated[0] += correction[0];
                interpolated[1] += correction[1];
            }
            return new PoleCorrection(interpolated[0], interpolated[1]);
        } catch (TimeStampedCacheException tce) {
            // this should not happen because of date check above
            throw OrekitException.createInternalError(tce);
        }
    }

    /** Get the correction to the nutation parameters for equinox-based paradigm.
     * <p>The data provided comes from the IERS files. It is smoothed data.</p>
     * @param date date at which the correction is desired
     * @return nutation correction in longitude &Delta;&Psi; and in obliquity &Delta;&epsilon;
     * (zero if date is outside covered range)
     */
    public double[] getEquinoxNutationCorrection(final AbsoluteDate date) {
        // check if there is data for date
        if (!this.hasDataFor(date)) {
            // no EOP data available for this date, we use a default null correction
            return new double[2];
        }
        //we have EOP data for date -> interpolate correction
        try {
            final HermiteInterpolator interpolator = new HermiteInterpolator();
            for (final EOPEntry entry : getNeighbors(date)) {
                interpolator.addSamplePoint(entry.getDate().durationFrom(date),
                                            new double[] {
                                                entry.getDdPsi(), entry.getDdEps()
                                            });
            }
            return interpolator.value(0);
        } catch (TimeStampedCacheException tce) {
            // this should not happen because of date check above
            throw OrekitException.createInternalError(tce);
        }
    }

    /** Get the correction to the nutation parameters for Non-Rotating Origin paradigm.
     * <p>The data provided comes from the IERS files. It is smoothed data.</p>
     * @param date date at which the correction is desired
     * @return nutation correction in Celestial Intermediat Pole coordinates
     * &delta;X and &delta;Y (zero if date is outside covered range)
     */
    public double[] getNonRotatinOriginNutationCorrection(final AbsoluteDate date) {
        // check if there is data for date
        if (!this.hasDataFor(date)) {
            // no EOP data available for this date, we use a default null correction
            return new double[2];
        }
        //we have EOP data for date -> interpolate correction
        try {
            final HermiteInterpolator interpolator = new HermiteInterpolator();
            for (final EOPEntry entry : getNeighbors(date)) {
                interpolator.addSamplePoint(entry.getDate().durationFrom(date),
                                            new double[] {
                                                entry.getDx(), entry.getDy()
                                            });
            }
            return interpolator.value(0);
        } catch (TimeStampedCacheException tce) {
            // this should not happen because of date check above
            throw OrekitException.createInternalError(tce);
        }
    }

    /** Check Earth orientation parameters continuity.
     * @param maxGap maximal allowed gap between entries (in seconds)
     * @exception OrekitException if there are holes in the data sequence
     */
    public void checkEOPContinuity(final double maxGap) throws OrekitException {
        TimeStamped preceding = null;
        for (final TimeStamped current : this.cache.getAll()) {

            // compare the dates of preceding and current entries
            if ((preceding != null) && ((current.getDate().durationFrom(preceding.getDate())) > maxGap)) {
                throw new OrekitException(OrekitMessages.MISSING_EARTH_ORIENTATION_PARAMETERS_BETWEEN_DATES,
                                          preceding.getDate(), current.getDate());
            }

            // prepare next iteration
            preceding = current;

        }
    }

    /**
     * Check if the cache has data for the given date using
     * {@link #getStartDate()} and {@link #getEndDate()}.
     *
     * @param date the requested date
     * @return true if the {@link #cache} has data for the requested date, false
     *         otherwise.
     */
    protected boolean hasDataFor(final AbsoluteDate date) {
        /*
         * when there is no EOP data, short circuit getStartDate, which will
         * throw an exception
         */
        return this.hasData && this.getStartDate().compareTo(date) <= 0 &&
               date.compareTo(this.getEndDate()) <= 0;
    }

    /** Get a non-modifiable view of the EOP entries.
     * @return non-modifiable view of the EOP entries
     */
    List<EOPEntry> getEntries() {
        return cache.getAll();
    }

    /** Replace the instance with a data transfer object for serialization.
     * <p>
     * This intermediate class serializes only the frame key.
     * </p>
     * @return data transfer object that will be serialized
     */
    private Object writeReplace() {
        return new DataTransferObject(conventions, getEntries(), tidalCorrection == null);
    }

    /** Internal class used only for serialization. */
    private static class DataTransferObject implements Serializable {

        /** Serializable UID. */
        private static final long serialVersionUID = 20131010L;

        /** IERS conventions. */
        private final IERSConventions conventions;

        /** EOP entries. */
        private final List<EOPEntry> entries;

        /** Indicator for simple interpolation without tidal effects. */
        private final boolean simpleEOP;

        /** Simple constructor.
         * @param conventions IERS conventions to which EOP refers
         * @param entries the EOP data to use
         * @param simpleEOP if true, tidal effects are ignored when interpolating EOP
         */
        public DataTransferObject(final IERSConventions conventions,
                                  final List<EOPEntry> entries,
                                  final boolean simpleEOP) {
            this.conventions = conventions;
            this.entries     = entries;
            this.simpleEOP   = simpleEOP;
        }

        /** Replace the deserialized data transfer object with a {@link EOPHistory}.
         * @return replacement {@link EOPHistory}
         */
        private Object readResolve() {
            try {
                // retrieve a managed frame
                return new EOPHistory(conventions, entries, simpleEOP);
            } catch (OrekitException oe) {
                throw OrekitException.createInternalError(oe);
            }
        }

    }

}
