/*
Copyright (C) 2001, 2006 United States Government as represented by
the Administrator of the National Aeronautics and Space Administration.
All Rights Reserved.
*/
package gov.nasa.worldwind.awt;

import gov.nasa.worldwind.*;
import gov.nasa.worldwind.geom.*;

/**
 * @author dcollins
 * @version $Id: InterpolatorTimer.java 1818 2007-05-10 15:59:46Z dcollins $
 */
class InterpolatorTimer
{
    static class ViewProperties
    {
        public LatLon latLon;
        public Angle heading;
        public Angle pitch;
        public Double zoom;
    }

    private java.util.TimerTask timerTask;
    private volatile boolean isRunning;
    private java.beans.PropertyChangeListener listener;
    private double stepCoefficient;
    private double errorThreshold;
    private Object begin;
    private Object end;

    public InterpolatorTimer(final int period)
    {
        if (period < 0)
        {
            String message = WorldWind.retrieveErrMsg("awt.InterpolatorTimer.PeriodLessThanZero");
            WorldWind.logger().log(java.util.logging.Level.FINE, message);
            throw new IllegalArgumentException(message);
        }
        java.util.Timer timer = new java.util.Timer();
        this.timerTask = new java.util.TimerTask()
        {
            public void run()
            {
                long time = System.currentTimeMillis();
                if (time - this.scheduledExecutionTime() >= 2 * period)
                    return;
                if (InterpolatorTimer.this.listener == null)
                    return;
                InterpolatorTimer.this.updateAndNotify(InterpolatorTimer.this.listener);
            }
        };
        timer.schedule(timerTask, 0, period);
    }

    public synchronized boolean isRunning()
    {
        return this.isRunning;
    }

    private static double error(Object x, Object end)
    {
        if (x instanceof Angle)
            return error((Angle) x, (Angle) end);
        else if (x instanceof Double)
            return error((Double) x, (Double) end);
        else if (x instanceof LatLon)
            return error((LatLon) x, (LatLon) end);
        else if (x instanceof Point)
            return error((Point) x, (Point) end);
        else if (x instanceof ViewProperties)
            return error((ViewProperties) x, (ViewProperties) end);
        else
            return 0;
    }

    private static double error(Double x, Double end)
    {
        return Math.abs(end - x);
    }

    private static double error(Angle x, Angle end)
    {
        return Math.abs(end.getDegrees() - x.getDegrees()) % 360d;
    }

    private static double error(LatLon x, LatLon end)
    {
        double latError = Math.abs(end.getLatitude().getRadians() - x.getLatitude().getRadians()) % (Math.PI / 2);
        double lonError = Math.abs(end.getLongitude().getRadians() - x.getLongitude().getRadians()) % Math.PI;
        return latError > lonError ? latError : lonError;
    }

    private static double error(Point x, Point end)
    {
        double maxError = 0;
        maxError = Math.max(maxError, error(x.x(), end.x()));
        maxError = Math.max(maxError, error(x.y(), end.y()));
        maxError = Math.max(maxError, error(x.z(), end.z()));
        maxError = Math.max(maxError, error(x.w(), end.w()));
        return maxError;
    }

    private static double error(ViewProperties x, ViewProperties end)
    {
        double maxError = 0;
        if (x.latLon != null && end.latLon != null)
            maxError = Math.max(maxError, error(x.latLon, end.latLon));
        if (x.heading != null && end.heading != null)
            maxError = Math.max(maxError, error(x.heading, end.heading));
        if (x.pitch != null && end.pitch != null)
            maxError = Math.max(maxError, error(x.pitch, end.pitch));
        if (x.zoom != null && end.zoom != null)
            maxError = Math.max(maxError, error(x.zoom, end.zoom));
        return maxError;
    }

    private static Object mix(double t, Object begin, Object end)
    {
        if (begin instanceof Angle)
            return mix(t, (Angle) begin, (Angle) end);
        else if (begin instanceof Double)
            return mix(t, (Double) begin, (Double) end);
        else if (begin instanceof LatLon)
            return mix(t, (LatLon) begin, (LatLon) end);
        else if (begin instanceof Point)
            return mix(t, (Point) begin, (Point) end);
        else if (begin instanceof ViewProperties)
            return mix(t, (ViewProperties) begin, (ViewProperties) end);
        else
            return null;
    }

    private static Angle mix(double t, Angle begin, Angle end)
    {
        if (t < 0)
            return begin;
        else if (t > 1)
            return end;
        Quaternion beginQuat = Quaternion.EulerToQuaternion(begin.getRadians(), 0, 0);
        Quaternion endQuat = Quaternion.EulerToQuaternion(end.getRadians(), 0, 0);
        Quaternion q = Quaternion.Slerp(beginQuat, endQuat, t);
        Point v = Quaternion.QuaternionToEuler(q);

        if (Double.isNaN(v.x()))
            return null;

        return Angle.fromRadians(v.x());
    }

    private static Double mix(double t, Double begin, Double end)
    {
        if (t < 0)
            return begin;
        else if (t > 1)
            return end;
        return (1 - t) * begin + t * end;
    }

    private static LatLon mix(double t, LatLon begin, LatLon end)
    {
        if (t < 0)
            return begin;
        else if (t > 1)
            return end;
        Quaternion beginQuat = Quaternion.EulerToQuaternion(begin.getLongitude().getRadians(),
            begin.getLatitude().getRadians(), 0);
        Quaternion endQuat = Quaternion.EulerToQuaternion(end.getLongitude().getRadians(),
            end.getLatitude().getRadians(), 0);
        Quaternion q = Quaternion.Slerp(beginQuat, endQuat, t);
        Point v = Quaternion.QuaternionToEuler(q);
        if (Double.isNaN(v.x()) || Double.isNaN(v.y()))
            return null;
        return LatLon.fromRadians(v.y(), v.x());
    }

    private static Point mix(double t, Point begin, Point end)
    {
        return new Point(mix(t, begin.x(), end.x()), mix(t, begin.y(), end.y()),
            mix(t, begin.z(), end.z()), mix(t, begin.w(), end.w()));
    }

    private static ViewProperties mix(double t, ViewProperties begin, ViewProperties end)
    {
        ViewProperties x = new ViewProperties();
        if (begin.latLon != null && end.latLon != null)
            x.latLon = mix(t, begin.latLon, end.latLon);
        if (begin.heading != null && end.heading != null)
        {
            if (begin.pitch != null && end.pitch != null)
            {
                // TODO: this doesn't pan heading & pitch equally
                Quaternion beginQuat = Quaternion.EulerToQuaternion(begin.heading.getRadians(),
                    begin.pitch.getRadians(), 0);
                Quaternion endQuat = Quaternion.EulerToQuaternion(end.heading.getRadians(),
                    end.pitch.getRadians(), 0);
                Quaternion q = Quaternion.Slerp(beginQuat, endQuat, t);
                Point v = Quaternion.QuaternionToEuler(q);
                if (!Double.isNaN(v.x()) && !Double.isNaN(v.y()))
                {
                    x.heading = Angle.fromRadians(v.x());
                    x.pitch = Angle.fromRadians(v.y());
                }
                else
                {
                    x.heading = null;
                    x.pitch = null;
                }
            }
            else
            {
                x.heading = mix(t, begin.heading, end.heading);
            }
        }
        else if (begin.pitch != null && end.pitch != null)
            x.pitch = mix(t, begin.pitch, end.pitch);
        if (begin.zoom != null && end.zoom != null)
            x.zoom = mix(t, begin.zoom, end.zoom);
        return x;
    }

    public synchronized void start(double stepCoefficient, double errorThreshold, Object begin, Object end,
        java.beans.PropertyChangeListener listener)
    {
        if (stepCoefficient < 0)
        {
            String message = WorldWind.retrieveErrMsg("awt.InterpolatorTimer.StepCoefficientLessThanZero");
            WorldWind.logger().log(java.util.logging.Level.FINE, message);
            throw new IllegalArgumentException(message);
        }
        if (errorThreshold < 0)
        {
            String message = WorldWind.retrieveErrMsg("awt.InterpolatorTimer.ErrorThresholdLessThanZero");
            WorldWind.logger().log(java.util.logging.Level.FINE, message);
            throw new IllegalArgumentException(message);
        }
        if (begin == null || end == null)
        {
            String message = WorldWind.retrieveErrMsg("nullValue.ObjectIsNull");
            WorldWind.logger().log(java.util.logging.Level.FINE, message);
            throw new IllegalArgumentException(message);
        }
        if (!end.getClass().isInstance(begin))
        {
            String message = WorldWind.retrieveErrMsg("awt.InterpolatorTimer.DifferentMixTypes");
            WorldWind.logger().log(java.util.logging.Level.FINE, message);
            throw new IllegalArgumentException(message);
        }
        if (listener == null)
        {
            String message = WorldWind.retrieveErrMsg("nullValue.ListenerIsNull");
            WorldWind.logger().log(java.util.logging.Level.FINE, message);
            throw new IllegalArgumentException(message);
        }

        if (this.isRunning && this.listener != null && !this.listener.equals(listener))
            this.listener.propertyChange(new java.beans.PropertyChangeEvent(this, null, null, null));

        this.listener = listener;
        this.stepCoefficient = stepCoefficient;
        this.errorThreshold = errorThreshold;
        this.begin = begin;
        this.end = end;
        this.isRunning = true;
    }

    public synchronized void stop()
    {
        if (this.isRunning)
        {
            if (this.listener != null)
                listener.propertyChange(new java.beans.PropertyChangeEvent(this, null, null, null));
            this.listener = null;
            this.stepCoefficient = errorThreshold = -1;
            this.begin = end = null;
            this.isRunning = false;
        }
    }

    private synchronized void updateAndNotify(java.beans.PropertyChangeListener listener)
    {
        if (!this.isRunning || this.begin == null || listener == null)
            return;

        Object newValue = mix(this.stepCoefficient, this.begin, this.end);
        if (newValue == null)
        {
            this.stop();
            return;
        }

        double error = error(newValue, this.end);
        if (error < this.errorThreshold)
        {
            this.stop();
            return;
        }

        listener.propertyChange(new java.beans.PropertyChangeEvent(this, null, this.begin, newValue));
        this.begin = newValue;
    }
}
