/*
 * 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.image.lut;

import icy.common.CollapsibleEvent;
import icy.common.UpdateEventHandler;
import icy.common.listener.ChangeListener;
import icy.file.xml.XMLPersistent;
import icy.image.colormap.IcyColorMap;
import icy.image.colormodel.IcyColorModel;
import icy.image.colorspace.IcyColorSpace;
import icy.image.colorspace.IcyColorSpaceEvent;
import icy.image.colorspace.IcyColorSpaceListener;
import icy.image.lut.LUT.LUTChannelEvent.LUTChannelEventType;
import icy.image.lut.LUTEvent.LUTEventType;
import icy.math.Scaler;
import icy.math.ScalerEvent;
import icy.math.ScalerListener;
import icy.type.DataType;
import icy.util.XMLUtil;

import java.util.ArrayList;
import java.util.EventListener;
import java.util.List;

import org.w3c.dom.Node;

public class LUT implements IcyColorSpaceListener, ScalerListener, ChangeListener, XMLPersistent
{
    private final static String ID_NUM_CHANNEL = "numChannel";
    private final static String ID_SCALER = "scaler";
    private final static String ID_COLORMAP = "colormap";

    public static interface LUTChannelListener extends EventListener
    {
        public void lutChannelChanged(LUTChannelEvent e);
    }

    public static class LUTChannelEvent
    {
        public enum LUTChannelEventType
        {
            SCALER_CHANGED, COLORMAP_CHANGED
        }

        private final LUTChannel lutChannel;
        private final LUTChannelEventType type;

        public LUTChannelEvent(LUTChannel lutChannel, LUTChannelEventType type)
        {
            super();

            this.lutChannel = lutChannel;
            this.type = type;
        }

        /**
         * @return the lutChannel
         */
        public LUTChannel getLutChannel()
        {
            return lutChannel;
        }

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

    public class LUTChannel
    {
        /**
         * band index
         */
        private final int channel;

        /**
         * listeners
         */
        private final List<LUTChannelListener> channelListeners;

        public LUTChannel(int channel)
        {
            this.channel = channel;

            channelListeners = new ArrayList<LUT.LUTChannelListener>();
        }

        public LUT getLut()
        {
            return LUT.this;
        }

        /**
         * Copy the colormap and scaler data from the specified source {@link LUTChannel}
         */
        public void copyFrom(LUTChannel source)
        {
            setColorMap(source.getColorMap(), true);
            setScaler(source.getScaler());
        }

        public Scaler getScaler()
        {
            return getScalers()[channel];
        }

        /**
         * Set the specified scaler (do a copy).
         * 
         * @param source
         *        source scaler to copy from
         */
        public void setScaler(Scaler source)
        {
            final Scaler scaler = getScaler();

            scaler.beginUpdate();
            try
            {
                scaler.setAbsLeftRightIn(source.getAbsLeftIn(), source.getAbsRightIn());
                scaler.setLeftRightIn(source.getLeftIn(), source.getRightIn());
                scaler.setLeftRightOut(source.getLeftOut(), source.getRightOut());
            }
            finally
            {
                scaler.endUpdate();
            }
        }

        public IcyColorMap getColorMap()
        {
            return getColorSpace().getColorMap(channel);
        }

        /**
         * Set the specified colormap (do a copy).
         * 
         * @param colorMap
         *        source colorspace to copy
         * @param setAlpha
         *        also set the alpha information
         */
        public void setColorMap(IcyColorMap colorMap, boolean setAlpha)
        {
            getColorSpace().setColorMap(channel, colorMap, setAlpha);
        }

        /**
         * @deprecated Use {@link #setColorMap(IcyColorMap, boolean)} instead.
         */
        @Deprecated
        public void setColorMap(IcyColorMap colorMap)
        {
            setColorMap(colorMap, true);
        }

        /**
         * @deprecated Use {@link #setColorMap(IcyColorMap, boolean)} instead.
         */
        @Deprecated
        public void copyColorMap(IcyColorMap colorMap)
        {
            setColorMap(colorMap, true);
        }

        public double getMin()
        {
            return getScaler().getLeftIn();
        }

        public void setMin(double value)
        {
            getScaler().setLeftIn(value);
        }

        public double getMax()
        {
            return getScaler().getRightIn();
        }

        public void setMax(double value)
        {
            getScaler().setRightIn(value);
        }

        public void setMinMax(double min, double max)
        {
            getScaler().setLeftRightIn(min, max);
        }

        public double getMinBound()
        {
            return getScaler().getAbsLeftIn();
        }

        public double getMaxBound()
        {
            return getScaler().getAbsRightIn();
        }

        public void setMinBound(double value)
        {
            getScaler().setAbsLeftIn(value);
        }

        public void setMaxBound(double value)
        {
            getScaler().setAbsRightIn(value);
        }

        /**
         * Returns the <i>enabled</i> state of this channel LUT
         */
        public boolean isEnabled()
        {
            return getColorMap().isEnabled();
        }

        /**
         * Enable/disable specified channel LUT
         */
        public void setEnabled(boolean value)
        {
            getColorMap().setEnabled(value);
        }

        /**
         * @deprecated Use {@link #getChannel()} instead.
         */
        @Deprecated
        public int getComponent()
        {
            return getChannel();
        }

        /**
         * @return the component
         */
        public int getChannel()
        {
            return channel;
        }

        /**
         * Add a listener.
         */
        public void addListener(LUTChannelListener listener)
        {
            channelListeners.add(listener);
        }

        /**
         * Remove a listener.
         */
        public void removeListener(LUTChannelListener listener)
        {
            channelListeners.remove(listener);
        }

        /**
         * Fire change event.
         */
        public void fireEvent(LUTChannelEvent e)
        {
            for (LUTChannelListener listener : new ArrayList<LUTChannelListener>(channelListeners))
                listener.lutChannelChanged(e);
        }
    }

    private List<LUTChannel> lutChannels = new ArrayList<LUTChannel>();

    final IcyColorSpace colorSpace;
    final Scaler[] scalers;
    final int numChannel;

    private boolean enabled = true;

    /**
     * listeners
     */
    private final List<LUTListener> listeners;

    /**
     * internal updater
     */
    private final UpdateEventHandler updater;

    public LUT(IcyColorModel cm)
    {
        colorSpace = cm.getIcyColorSpace();
        scalers = cm.getColormapScalers();
        numChannel = colorSpace.getNumComponents();

        if (scalers.length != numChannel)
        {
            throw new IllegalArgumentException("Incorrect size for scalers : " + scalers.length + ".  Expected : "
                    + numChannel);
        }

        final DataType dataType = cm.getDataType_();

        for (int channel = 0; channel < numChannel; channel++)
        {
            // BYTE data type --> fix bounds to data type bounds
            if (dataType == DataType.UBYTE)
                scalers[channel].setLeftRightIn(dataType.getMinValue(), dataType.getMaxValue());

            lutChannels.add(new LUTChannel(channel));
        }

        listeners = new ArrayList<LUTListener>();
        updater = new UpdateEventHandler(this, false);

        // add listener
        for (Scaler scaler : scalers)
            scaler.addListener(this);
        colorSpace.addListener(this);

    }

    protected int indexOf(Scaler scaler)
    {
        for (int i = 0; i < scalers.length; i++)
            if (scalers[i] == scaler)
                return i;

        return -1;
    }

    public IcyColorSpace getColorSpace()
    {
        return colorSpace;
    }

    public Scaler[] getScalers()
    {
        return scalers;
    }

    public boolean isEnabled()
    {
        return enabled;
    }

    public void setEnabled(boolean enabled)
    {
        this.enabled = enabled;
    }

    public ArrayList<LUTChannel> getLutChannels()
    {
        return new ArrayList<LUTChannel>(lutChannels);
    }

    /**
     * Return the {@link LUTChannel} for specified channel index.
     */
    public LUTChannel getLutChannel(int channel)
    {
        return lutChannels.get(channel);
    }

    /**
     * @deprecated Use {@link #getLutChannels()} instead.
     */
    @Deprecated
    public ArrayList<LUTBand> getLutBands()
    {
        final ArrayList<LUTBand> result = new ArrayList<LUTBand>();

        for (LUTChannel lutChannel : lutChannels)
            result.add(new LUTBand(this, lutChannel.getChannel()));

        return result;
    }

    /**
     * @deprecated Use {@link #getLutChannel(int)} instead.
     */
    @Deprecated
    public LUTBand getLutBand(int band)
    {
        return getLutBands().get(band);
    }

    /**
     * @return the number of channel.
     */
    public int getNumChannel()
    {
        return numChannel;
    }

    /**
     * @deprecated Use {@link #getNumChannel()} instead.
     */
    @Deprecated
    public int getNumComponents()
    {
        return getNumChannel();
    }

    /**
     * Copy LUT from the specified source lut
     */
    public void copyFrom(LUT lut)
    {
        beginUpdate();
        try
        {
            setColorMaps(lut, true);
            setScalers(lut);
        }
        finally
        {
            endUpdate();
        }
    }

    /**
     * Set the scalers from the specified source lut (do a copy)
     */
    public void setScalers(LUT lut)
    {
        final Scaler[] srcScalers = lut.getScalers();
        final int len = Math.min(scalers.length, srcScalers.length);

        beginUpdate();
        try
        {
            for (int i = 0; i < len; i++)
            {
                final Scaler src = srcScalers[i];
                final Scaler dst = scalers[i];

                dst.setAbsLeftRightIn(src.getAbsLeftIn(), src.getAbsRightIn());
                dst.setLeftRightIn(src.getLeftIn(), src.getRightIn());
                dst.setLeftRightOut(src.getLeftOut(), src.getRightOut());
            }
        }
        finally
        {
            endUpdate();
        }
    }

    /**
     * @deprecated Use {@link #setScalers(LUT)} instead.
     */
    @Deprecated
    public void copyScalers(LUT lut)
    {
        setScalers(lut);
    }

    /**
     * Set colormaps from the specified source lut (do a copy).
     * 
     * @param lut
     *        source lut to use
     * @param setAlpha
     *        also set the alpha information
     */
    public void setColorMaps(LUT lut, boolean setAlpha)
    {
        getColorSpace().setColorMaps(lut.getColorSpace(), setAlpha);
    }

    /**
     * @deprecated USe {@link #setColorMaps(LUT, boolean)} instead.
     */
    @Deprecated
    public void setColormaps(LUT lut)
    {
        setColorMaps(lut, true);
    }

    /**
     * @deprecated Use {@link #setColorMaps(LUT, boolean)} instead.
     */
    @Deprecated
    public void copyColormaps(LUT lut)
    {
        setColorMaps(lut, true);
    }

    /**
     * Set the alpha channel to full opaque for all LUT channel
     */
    public void setAlphaToOpaque()
    {
        beginUpdate();
        try
        {
            for (LUTChannel lutChannel : getLutChannels())
                lutChannel.getColorMap().setAlphaToOpaque();
        }
        finally
        {
            endUpdate();
        }
    }

    /**
     * Set the alpha channel to linear opacity (0 to 1) for all LUT channel
     */
    public void setAlphaToLinear()
    {
        beginUpdate();
        try
        {
            for (LUTChannel lutChannel : getLutChannels())
                lutChannel.getColorMap().setAlphaToLinear();
        }
        finally
        {
            endUpdate();
        }
    }

    /**
     * Set the alpha channel to an optimized linear transparency for 3D volume display on all LUT
     * channel
     */
    public void setAlphaToLinear3D()
    {
        beginUpdate();
        try
        {
            for (LUTChannel lutChannel : getLutChannels())
                lutChannel.getColorMap().setAlphaToLinear3D();
        }
        finally
        {
            endUpdate();
        }
    }

    /**
     * Return true if LUT is compatible with specified ColorModel.<br>
     * (Same number of channels with same data type)
     */
    public boolean isCompatible(LUT lut)
    {
        if (numChannel != lut.getNumChannel())
            return false;

        final Scaler[] cmScalers = lut.getScalers();

        // check that data type is compatible
        for (int channel = 0; channel < numChannel; channel++)
            if (scalers[channel].isIntegerData() != cmScalers[channel].isIntegerData())
                return false;

        return true;
    }

    /**
     * Return true if LUT is compatible with specified ColorModel.<br>
     * (Same number of channels with same data type)
     */
    public boolean isCompatible(IcyColorModel colorModel)
    {
        if (numChannel != colorModel.getNumComponents())
            return false;

        final Scaler[] cmScalers = colorModel.getColormapScalers();

        // check that data type is compatible
        for (int comp = 0; comp < numChannel; comp++)
            if (scalers[comp].isIntegerData() != cmScalers[comp].isIntegerData())
                return false;

        return true;
    }

    /**
     * Add a listener
     * 
     * @param listener
     */
    public void addListener(LUTListener listener)
    {
        listeners.add(listener);
    }

    /**
     * Remove a listener
     * 
     * @param listener
     */
    public void removeListener(LUTListener listener)
    {
        listeners.remove(listener);
    }

    public void fireLUTChanged(LUTEvent e)
    {
        for (LUTListener lutListener : new ArrayList<LUTListener>(listeners))
            lutListener.lutChanged(e);
    }

    @Override
    public void onChanged(CollapsibleEvent compare)
    {
        final LUTEvent event = (LUTEvent) compare;

        // notify listener we have changed
        fireLUTChanged(event);

        // propagate event to LUTChannel
        final int channel = event.getComponent();
        final LUTChannelEventType type = (event.getType() == LUTEventType.COLORMAP_CHANGED) ? LUTChannelEventType.COLORMAP_CHANGED
                : LUTChannelEventType.SCALER_CHANGED;

        if (channel == -1)
        {
            for (LUTChannel lutChannel : lutChannels)
                lutChannel.fireEvent(new LUTChannelEvent(lutChannel, type));
        }
        else
        {
            final LUTChannel lutChannel = getLutChannel(channel);
            lutChannel.fireEvent(new LUTChannelEvent(lutChannel, type));
        }
    }

    @Override
    public void colorSpaceChanged(IcyColorSpaceEvent e)
    {
        // notify LUT colormap changed
        updater.changed(new LUTEvent(this, e.getComponent(), LUTEventType.COLORMAP_CHANGED));
    }

    @Override
    public void scalerChanged(ScalerEvent e)
    {
        // notify LUTBand changed
        updater.changed(new LUTEvent(this, indexOf(e.getScaler()), LUTEventType.SCALER_CHANGED));
    }

    public void beginUpdate()
    {
        updater.beginUpdate();
    }

    public void endUpdate()
    {
        updater.endUpdate();
    }

    public boolean isUpdating()
    {
        return updater.isUpdating();
    }

    @Override
    public boolean loadFromXML(Node node)
    {
        if (node == null)
            return false;

        // different channel number --> exit
        if (numChannel != XMLUtil.getElementIntValue(node, ID_NUM_CHANNEL, 1))
            return false;

        beginUpdate();
        try
        {
            for (int ch = 0; ch < numChannel; ch++)
            {
                Node n;

                n = XMLUtil.getElement(node, ID_SCALER + ch);
                if (n != null)
                    scalers[ch].loadFromXML(n);
                n = XMLUtil.getElement(node, ID_COLORMAP + ch);
                if (n != null)
                    colorSpace.getColorMap(ch).loadFromXML(n);
            }
        }
        finally
        {
            endUpdate();
        }

        return true;
    }

    @Override
    public boolean saveToXML(Node node)
    {
        if (node == null)
            return false;

        XMLUtil.setElementIntValue(node, ID_NUM_CHANNEL, numChannel);

        for (int ch = 0; ch < numChannel; ch++)
        {
            Node n;

            n = XMLUtil.setElement(node, ID_SCALER + ch);
            if (n != null)
                scalers[ch].saveToXML(n);
            n = XMLUtil.setElement(node, ID_COLORMAP + ch);
            if (n != null)
                colorSpace.getColorMap(ch).saveToXML(n);
        }

        return true;
    }
}