/*
 * Copyright 2010-2015 Institut Pasteur.
 * 
 * This file is part of Icy.
 * 
 * Icy 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 3 of the License, or
 * (at your option) any later version.
 * 
 * Icy is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
 * GNU General Public License for more details.
 * 
 * You should have received a copy of the GNU General Public License
 * along with Icy. If not, see <http://www.gnu.org/licenses/>.
 */
package icy.math;

import icy.math.SmoothMover.SmoothMoveType;

import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.util.ArrayList;

import javax.swing.Timer;

/**
 * @author Stephane
 */
public class MultiSmoothMover implements ActionListener
{
    public static interface MultiSmoothMoverListener
    {
        public void moveStarted(MultiSmoothMover source, int index, double start, double end);

        public void moveModified(MultiSmoothMover source, int index, double start, double end);

        public void moveEnded(MultiSmoothMover source, int index, double value);

        public void valueChanged(MultiSmoothMover source, int index, double newValue, int pourcent);
    }

    public static class MultiSmoothMoverAdapter implements MultiSmoothMoverListener
    {
        @Override
        public void moveStarted(MultiSmoothMover source, int index, double start, double end)
        {
        }

        @Override
        public void moveModified(MultiSmoothMover source, int index, double start, double end)
        {
        }

        @Override
        public void moveEnded(MultiSmoothMover source, int index, double value)
        {
        }

        @Override
        public void valueChanged(MultiSmoothMover source, int index, double newValue, int pourcent)
        {
        }
    }

    /**
     * current value
     */
    protected double[] currentValues;
    /**
     * smooth movement type
     */
    protected SmoothMoveType type;
    /**
     * time to do move (in ms)
     */
    protected int moveTime;

    /**
     * internals
     */
    protected final Timer timer;
    protected boolean[] moving;
    protected double[] destValues;
    protected double[][] stepValues;
    // private int stepIndex;
    protected long[] startTime;
    protected final ArrayList<MultiSmoothMoverListener> listeners;

    public MultiSmoothMover(int size, SmoothMoveType type)
    {
        super();

        currentValues = new double[size];
        moving = new boolean[size];
        destValues = new double[size];
        startTime = new long[size];

        this.type = type;
        // 60 updates per second by default
        timer = new Timer(1000 / 60, this);
        // no initial delay
        timer.setInitialDelay(0);
        timer.setRepeats(true);
        // timer always running here
        timer.start();
        // default : 1 second to reach destination
        moveTime = 1000;
        // default
        stepValues = new double[size][0];

        listeners = new ArrayList<MultiSmoothMoverListener>();
    }

    public MultiSmoothMover(int size)
    {
        this(size, SmoothMoveType.LINEAR);
    }

    /**
     * Move the value at specified index to 'value'
     */
    public void moveTo(int index, double value)
    {
        if (destValues[index] != value)
        {
            destValues[index] = value;
            // start movement
            start(index, System.currentTimeMillis());
        }
    }

    /**
     * Move all values
     */
    public void moveTo(double[] values)
    {
        final int maxInd = Math.min(values.length, destValues.length);

        // first we check we have at least one value which had changed
        boolean changed = false;
        for (int index = 0; index < maxInd; index++)
        {
            if (destValues[index] != values[index])
            {
                changed = true;
                break;
            }
        }

        // value changed ?
        if (changed)
        {
            // better synchronization for multiple changes
            final long time = System.currentTimeMillis();

            for (int index = 0; index < maxInd; index++)
            {
                destValues[index] = values[index];
                // start movement
                start(index, time);
            }
        }
    }

    public boolean isMoving(int index)
    {
        return moving[index];
    }

    public boolean isMoving()
    {
        for (boolean b : moving)
            if (b)
                return true;

        return false;
    }

    protected void start(int index, long time)
    {
        final double current = currentValues[index];
        final double dest = destValues[index];

        // number of step to reach final value
        final int size = Math.max(moveTime / timer.getDelay(), 1);

        // calculate interpolation
        switch (type)
        {
            case NONE:
                stepValues[index] = new double[2];
                stepValues[index][0] = current;
                stepValues[index][1] = dest;
                break;

            case LINEAR:
                stepValues[index] = Interpolator.doLinearInterpolation(current, dest, size);
                break;

            case LOG:
                stepValues[index] = Interpolator.doLogInterpolation(current, dest, size);
                break;

            case EXP:
                stepValues[index] = Interpolator.doExpInterpolation(current, dest, size);
                break;
        }

        // notify and start
        if (!isMoving(index))
        {
            moveStarted(index, time);
            moving[index] = true;
        }
        else
            moveModified(index, time);
    }

    /**
     * Stop specified index
     */
    public void stop(int index)
    {
        // stop and notify
        if (isMoving(index))
        {
            moving[index] = false;
            moveEnded(index);
        }
    }

    /**
     * Stop all
     */
    public void stopAll()
    {
        // stop all
        for (int index = 0; index < moving.length; index++)
            if (moving[index])
                moveEnded(index);
    }

    /**
     * Shutdown the mover object (this actually stop internal timer and remove all listeners)
     */
    public void shutDown()
    {
        timer.stop();
        timer.removeActionListener(this);
        listeners.clear();
    }

    /**
     * @return the update delay (in ms)
     */
    public int getUpdateDelay()
    {
        return timer.getDelay();
    }

    /**
     * @param updateDelay
     *        the update delay (in ms) to set
     */
    public void setUpdateDelay(int updateDelay)
    {
        timer.setDelay(updateDelay);
    }

    /**
     * @return the smooth type
     */
    public SmoothMoveType getType()
    {
        return type;
    }

    /**
     * @param type
     *        the smooth type to set
     */
    public void setType(SmoothMoveType type)
    {
        this.type = type;
    }

    /**
     * @return the moveTime
     */
    public int getMoveTime()
    {
        return moveTime;
    }

    /**
     * @param moveTime
     *        the moveTime to set
     */
    public void setMoveTime(int moveTime)
    {
        // can't be < 1
        this.moveTime = Math.max(moveTime, 1);
    }

    /**
     * Immediately set the value
     */
    public void setValue(int index, double value)
    {
        // stop current movement
        stop(index);
        // directly set value
        destValues[index] = value;
        setCurrentValue(index, value, 100);
    }

    /**
     * Immediately set all values
     */
    public void setValues(double[] values)
    {
        final int maxInd = Math.min(values.length, destValues.length);

        for (int index = 0; index < maxInd; index++)
        {
            final double value = values[index];
            // stop current movement
            stop(index);
            // directly set value
            destValues[index] = value;
            setCurrentValue(index, value, 100);
        }
    }

    /**
     * @return the value
     */
    public double getValue(int index)
    {
        return currentValues[index];
    }

    /**
     * @return the destValue
     */
    public double getDestValue(int index)
    {
        return destValues[index];
    }

    /**
     * update current value from elapsed time
     */
    protected void updateCurrentValue(int index, long time)
    {
        final int elapsedMsTime = (int) (time - startTime[index]);

        // move completed ?
        if ((type == SmoothMoveType.NONE) || (elapsedMsTime >= moveTime))
        {
            setCurrentValue(index, destValues[index], 100);
            // stop
            stop(index);
        }
        else
        {
            final int len = stepValues[index].length;
            final int ind = Math.min((elapsedMsTime * len) / moveTime, len - 2) + 1;

            // set value
            if ((ind >= 0) && (ind < stepValues[index].length))
                setCurrentValue(index, stepValues[index][ind], (elapsedMsTime * 100) / moveTime);
        }
    }

    protected void setCurrentValue(int index, double value, int pourcent)
    {
        if (currentValues[index] != value)
        {
            currentValues[index] = value;
            // notify value changed
            changed(index, value, pourcent);
        }
    }

    public void addListener(MultiSmoothMoverListener listener)
    {
        listeners.add(listener);
    }

    public void removeListener(MultiSmoothMoverListener listener)
    {
        listeners.remove(listener);
    }

    /**
     * Move started event
     */
    protected void moveStarted(int index, long time)
    {
        startTime[index] = time;

        for (MultiSmoothMoverListener listener : listeners)
            listener.moveStarted(this, index, currentValues[index], destValues[index]);
    }

    /**
     * Move modified event.
     */
    protected void moveModified(int index, long time)
    {
        startTime[index] = time;

        for (MultiSmoothMoverListener listener : listeners)
            listener.moveModified(this, index, currentValues[index], destValues[index]);
    }

    /**
     * Move ended event.
     */
    protected void moveEnded(int index)
    {
        for (MultiSmoothMoverListener listener : listeners)
            listener.moveEnded(this, index, currentValues[index]);
    }

    /**
     * Value changed event.
     */
    protected void changed(int index, double newValue, int pourcent)
    {
        for (MultiSmoothMoverListener listener : listeners)
            listener.valueChanged(this, index, newValue, pourcent);
    }

    @Override
    public void actionPerformed(ActionEvent e)
    {
        // better synchronization for multiple changes
        final long time = System.currentTimeMillis();

        // process only moving values
        for (int index = 0; index < moving.length; index++)
            if (moving[index])
                updateCurrentValue(index, time);
    }

}