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

import icy.common.CollapsibleEvent;
import icy.common.UpdateEventHandler;
import icy.common.listener.ChangeListener;
import icy.image.colormap.IcyColorMap;
import icy.image.colormodel.IcyColorModelEvent.IcyColorModelEventType;
import icy.image.colorspace.IcyColorSpace;
import icy.image.colorspace.IcyColorSpaceEvent;
import icy.image.colorspace.IcyColorSpaceListener;
import icy.image.lut.LUT;
import icy.math.Scaler;
import icy.math.ScalerEvent;
import icy.math.ScalerListener;
import icy.type.DataType;
import icy.type.TypeUtil;
import icy.type.collection.array.Array1DUtil;
import icy.util.ReflectionUtil;

import java.awt.image.BandedSampleModel;
import java.awt.image.ColorModel;
import java.awt.image.ComponentSampleModel;
import java.awt.image.DataBufferByte;
import java.awt.image.DataBufferDouble;
import java.awt.image.DataBufferFloat;
import java.awt.image.DataBufferInt;
import java.awt.image.DataBufferShort;
import java.awt.image.DataBufferUShort;
import java.awt.image.Raster;
import java.awt.image.SampleModel;
import java.awt.image.WritableRaster;
import java.lang.reflect.Field;
import java.util.ArrayList;
import java.util.List;

/**
 * @author stephane
 */
public abstract class IcyColorModel extends ColorModel implements ScalerListener, IcyColorSpaceListener, ChangeListener
{
    /**
     * scalers for normalization
     */
    protected final Scaler[] normalScalers;
    /**
     * scalers for colorMap
     */
    protected final Scaler[] colormapScalers;

    /**
     * data type
     */
    protected final DataType dataType;

    /**
     * overridden variables
     */
    protected final int numComponents;

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

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

    /**
     * Default constructor
     */
    IcyColorModel(int numComponents, DataType dataType, int[] bits)
    {
        super(dataType.getBitSize(), bits, new IcyColorSpace(numComponents), true, false, TRANSLUCENT, dataType
                .toDataBufferType());

        if (numComponents == 0)
            throw new IllegalArgumentException("Number of components should be > 0");

        // overridden variable
        this.numComponents = numComponents;

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

        // data type information
        this.dataType = dataType;

        // get default min and max for datatype
        final double[] defaultBounds = dataType.getDefaultBounds();
        final double min = defaultBounds[0];
        final double max = defaultBounds[1];
        // float type flag
        final boolean isFloat = dataType.isFloat();

        // allocating scalers
        normalScalers = new Scaler[numComponents];
        colormapScalers = new Scaler[numComponents];
        // defining scalers
        for (int i = 0; i < numComponents; i++)
        {
            // scale for normalization
            normalScalers[i] = new Scaler(min, max, 0f, 1f, !isFloat);
            // scale for colormap
            colormapScalers[i] = new Scaler(min, max, 0f, IcyColorMap.MAX_INDEX, !isFloat);
            // add listener to the colormap scalers only
            colormapScalers[i].addListener(this);
        }

        // add the listener to colorSpace
        getIcyColorSpace().addListener(this);
    }

    /**
     * @deprecated use {@link #IcyColorModel(int, DataType, int[])} instead
     */
    @Deprecated
    IcyColorModel(int numComponents, int dataType, boolean signed, int[] bits)
    {
        this(numComponents, DataType.getDataType(dataType, signed), bits);
    }

    /**
     * Creates a new ColorModel with the given color component and image data type
     * 
     * @param numComponents
     *        number of component
     * @param dataType
     *        the type of image data (see {@link DataType})
     * @return a IcyColorModel object
     */
    public static IcyColorModel createInstance(int numComponents, DataType dataType)
    {
        // define bits size
        final int bits = dataType.getBitSize();
        // we have to fake one more extra component for alpha in ColorModel class
        final int numComponentFixed = numComponents + 1;
        final int[] componentBits = new int[numComponentFixed];

        for (int i = 0; i < numComponentFixed; i++)
            componentBits[i] = bits;

        switch (dataType)
        {
            case UBYTE:
                return new UByteColorModel(numComponents, componentBits);
            case BYTE:
                return new ByteColorModel(numComponents, componentBits);
            case USHORT:
                return new UShortColorModel(numComponents, componentBits);
            case SHORT:
                return new ShortColorModel(numComponents, componentBits);
            case UINT:
                return new UIntColorModel(numComponents, componentBits);
            case INT:
                return new IntColorModel(numComponents, componentBits);
            case ULONG:
                return new ULongColorModel(numComponents, componentBits);
            case LONG:
                return new LongColorModel(numComponents, componentBits);
            case FLOAT:
                return new FloatColorModel(numComponents, componentBits);
            case DOUBLE:
                return new DoubleColorModel(numComponents, componentBits);
            default:
                throw new IllegalArgumentException("Unsupported data type !");
        }
    }

    /**
     * @deprecated use {@link #createInstance(int, DataType)} instead
     */
    @Deprecated
    public static IcyColorModel createInstance(int numComponents, int dataType, boolean signed)
    {
        return createInstance(numComponents, DataType.getDataType(dataType, signed));
    }

    /**
     * Creates a new ColorModel from a given icyColorModel
     * 
     * @param colorModel
     *        icyColorModel
     * @param copyColormap
     *        flag to indicate if we want to copy colormaps from the given icyColorModel
     * @param copyBounds
     *        flag to indicate if we want to copy bounds from the given icyColorModel
     * @return a IcyColorModel object
     */
    public static IcyColorModel createInstance(IcyColorModel colorModel, boolean copyColormap, boolean copyBounds)
    {
        final IcyColorModel result = IcyColorModel.createInstance(colorModel.getNumComponents(),
                colorModel.getDataType_());

        result.beginUpdate();
        try
        {
            // copy colormaps from colorModel ?
            if (copyColormap)
                result.setColorMaps(colorModel);
            // copy bounds from colorModel ?
            if (copyBounds)
                result.setBounds(colorModel);
        }
        finally
        {
            result.endUpdate();
        }

        return result;
    }

    /**
     * Create default ColorModel : 4 components, unsigned byte data type
     */
    public static IcyColorModel createInstance()
    {
        return createInstance(4, DataType.UBYTE);
    }

    @Override
    public SampleModel createCompatibleSampleModel(int w, int h)
    {
        return new BandedSampleModel(transferType, w, h, getNumComponents());
    }

    @Override
    public WritableRaster createCompatibleWritableRaster(int w, int h)
    {
        final SampleModel sm = createCompatibleSampleModel(w, h);

        return Raster.createWritableRaster(sm, sm.createDataBuffer(), null);
    }

    /**
     * Create a writable raster from specified data and size.<br>
     */
    public WritableRaster createWritableRaster(Object[] data, int w, int h)
    {
        final SampleModel sm = createCompatibleSampleModel(w, h);

        switch (dataType)
        {
            case UBYTE:
            case BYTE:
                return Raster.createWritableRaster(sm, new DataBufferByte((byte[][]) data, w * h), null);
            case SHORT:
                return Raster.createWritableRaster(sm, new DataBufferShort((short[][]) data, w * h), null);
            case USHORT:
                return Raster.createWritableRaster(sm, new DataBufferUShort((short[][]) data, w * h), null);
            case UINT:
            case INT:
                return Raster.createWritableRaster(sm, new DataBufferInt((int[][]) data, w * h), null);
            case FLOAT:
                return Raster.createWritableRaster(sm, new DataBufferFloat((float[][]) data, w * h), null);
            case DOUBLE:
                return Raster.createWritableRaster(sm, new DataBufferDouble((double[][]) data, w * h), null);
            default:
                throw new IllegalArgumentException(
                        "IcyColorModel.createWritableRaster(..) error : unsupported data type : " + dataType);
        }
    }

    /**
     * Set bounds from specified {@link IcyColorModel}
     */
    public void setBounds(IcyColorModel source)
    {
        beginUpdate();
        try
        {
            for (int i = 0; i < numComponents; i++)
            {
                final Scaler srcNormalScaler = source.getNormalScalers()[i];
                final Scaler dstNormalScaler = normalScalers[i];
                final Scaler srcColorMapScaler = source.getColormapScalers()[i];
                final Scaler dstColorMapScaler = colormapScalers[i];

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

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

    /**
     * @deprecated Use {@link #setBounds(IcyColorModel)} instead.
     */
    @Deprecated
    public void copyBounds(IcyColorModel source)
    {
        setBounds(source);
    }

    /**
     * Return the toRGB colormap of specified RGB component
     */
    public IcyColorMap getColorMap(int component)
    {
        return getIcyColorSpace().getColorMap(component);
    }

    /**
     * @deprecated Use {@link #getColorMap(int)} instead (different case).
     */
    @Deprecated
    public IcyColorMap getColormap(int component)
    {
        return getColorMap(component);
    }

    /**
     * Set the toRGB colormaps from a compatible colorModel.
     * 
     * @param source
     *        source ColorModel to copy colormap from
     */
    public void setColorMaps(ColorModel source)
    {
        getIcyColorSpace().setColorMaps(source);
    }

    /**
     * @deprecated Use {@link #setColorMaps(ColorModel)} instead (different case).
     */
    @Deprecated
    public void setColormaps(ColorModel source)
    {
        setColorMaps(source);
    }

    /**
     * @deprecated Use {@link #setColorMaps(ColorModel)} instead.
     */
    @Deprecated
    public void copyColormap(ColorModel source)
    {
        setColorMaps(source);
    }

    /**
     * Set the toRGB colormap of specified component (actually copy the content).
     * 
     * @param component
     *        component we want to set the colormap
     * @param map
     *        source colormap to copy
     * @param setAlpha
     *        also set the alpha information
     */
    public void setColorMap(int component, IcyColorMap map, boolean setAlpha)
    {
        getIcyColorSpace().setColorMap(component, map, setAlpha);
    }

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

    /**
     * @see java.awt.image.ColorModel#getAlpha(int)
     */
    @Override
    public int getAlpha(int pixel)
    {
        throw new IllegalArgumentException("Argument type not supported for this color model");
    }

    /**
     * @see java.awt.image.ColorModel#getBlue(int)
     */
    @Override
    public int getBlue(int pixel)
    {
        throw new IllegalArgumentException("Argument type not supported for this color model");
    }

    /**
     * @see java.awt.image.ColorModel#getGreen(int)
     */
    @Override
    public int getGreen(int pixel)
    {
        throw new IllegalArgumentException("Argument type not supported for this color model");
    }

    /**
     * @see java.awt.image.ColorModel#getRed(int)
     */
    @Override
    public int getRed(int pixel)
    {
        throw new IllegalArgumentException("Argument type not supported for this color model");
    }

    @Override
    public abstract int getRGB(Object inData);

    public abstract int getRGB(Object pixel, LUT lut);

    /**
     * 
     */
    @Override
    public int getBlue(Object pixel)
    {
        return getRGB(pixel) & 0xFF;
    }

    /**
     * 
     */
    @Override
    public int getGreen(Object pixel)
    {
        return (getRGB(pixel) >> 8) & 0xFF;
    }

    /**
     * 
     */
    @Override
    public int getRed(Object pixel)
    {
        return (getRGB(pixel) >> 16) & 0xFF;
    }

    /**
     * 
     */
    @Override
    public int getAlpha(Object pixel)
    {
        return (getRGB(pixel) >> 24) & 0xFF;
    }

    /**
     * @see java.awt.image.ColorModel#getComponents(int, int[], int)
     */
    @Override
    public int[] getComponents(int pixel, int[] components, int offset)
    {
        throw new IllegalArgumentException("Not supported in this ColorModel");
    }

    /**
     * @see java.awt.image.ColorModel#getComponents(Object, int[], int)
     */
    @Override
    public abstract int[] getComponents(Object pixel, int[] components, int offset);

    /**
     * @see java.awt.image.ColorModel#getNormalizedComponents(Object, float[], int)
     */
    @Override
    public abstract float[] getNormalizedComponents(Object pixel, float[] normComponents, int normOffset);

    /**
     * @see java.awt.image.ColorModel#getNormalizedComponents(int[], int, float[], int)
     */
    @Override
    public float[] getNormalizedComponents(int[] components, int offset, float[] normComponents, int normOffset)
    {
        if ((components.length - offset) < numComponents)
            throw new IllegalArgumentException("Incorrect number of components.  Expecting " + numComponents);

        final float[] result = Array1DUtil.allocIfNull(normComponents, numComponents + normOffset);

        for (int i = 0; i < numComponents; i++)
            result[normOffset + i] = (float) normalScalers[i].scale(components[offset + i]);

        return result;
    }

    /**
     * @see java.awt.image.ColorModel#getUnnormalizedComponents(float[], int, int[], int)
     */
    @Override
    public int[] getUnnormalizedComponents(float[] normComponents, int normOffset, int[] components, int offset)
    {
        if ((normComponents.length - normOffset) < numComponents)
            throw new IllegalArgumentException("Incorrect number of components.  Expecting " + numComponents);

        final int[] result = Array1DUtil.allocIfNull(components, numComponents + offset);

        for (int i = 0; i < numComponents; i++)
            result[offset + i] = (int) normalScalers[i].unscale(normComponents[normOffset + i]);

        return result;
    }

    /**
     * @see java.awt.image.ColorModel#getDataElement(int[], int)
     */
    @Override
    public int getDataElement(int[] components, int offset)
    {
        throw new IllegalArgumentException("Not supported in this ColorModel");
    }

    /**
     * @see java.awt.image.ColorModel#getDataElement(float[], int)
     */
    @Override
    public int getDataElement(float[] normComponents, int normOffset)
    {
        throw new IllegalArgumentException("Not supported in this ColorModel");
    }

    /**
     * @see java.awt.image.ColorModel#getDataElements(int[], int, Object)
     */
    @Override
    public abstract Object getDataElements(int[] components, int offset, Object obj);

    /**
     * @see java.awt.image.ColorModel#getDataElements(int, Object)
     */
    @Override
    public Object getDataElements(int rgb, Object pixel)
    {
        return getDataElements(getIcyColorSpace().fromRGB(rgb), 0, pixel);
    }

    /**
     * @see java.awt.image.ColorModel#getDataElements(float[], int, Object)
     */
    @Override
    public abstract Object getDataElements(float[] normComponents, int normOffset, Object obj);

    /**
     * 
     */
    @Override
    public ColorModel coerceData(WritableRaster raster, boolean isAlphaPremultiplied)
    {
        // nothing to do
        return this;
    }

    /**
     * Scale input value for colormap indexing
     */
    protected double colormapScale(int component, double value)
    {
        return colormapScalers[component].scale(value);
    }

    /**
     * Tests if the specified <code>Object</code> is an instance of <code>ColorModel</code> and if
     * it equals this <code>ColorModel</code>.
     * 
     * @param obj
     *        the <code>Object</code> to test for equality
     * @return <code>true</code> if the specified <code>Object</code> is an instance of <code>ColorModel</code> and
     *         equals this <code>ColorModel</code>; <code>false</code> otherwise.
     */
    @Override
    public boolean equals(Object obj)
    {
        if (obj instanceof IcyColorModel)
            return isCompatible((IcyColorModel) obj);

        return false;
    }

    /**
     * 
     */
    public boolean isCompatible(IcyColorModel cm)
    {
        return (getNumComponents() == cm.getNumComponents()) && (getDataType_() == cm.getDataType_());
    }

    /**
     * 
     */
    @Override
    public boolean isCompatibleRaster(Raster raster)
    {
        final SampleModel sm = raster.getSampleModel();
        final int[] bits = getComponentSize();

        if (sm instanceof ComponentSampleModel)
        {
            if (sm.getNumBands() != numComponents)
                return false;

            for (int i = 0; i < bits.length; i++)
                if (sm.getSampleSize(i) < bits[i])
                    return false;

            return (raster.getTransferType() == transferType);
        }

        return false;
    }

    /**
     * 
     */
    @Override
    public boolean isCompatibleSampleModel(SampleModel sm)
    {
        // Must have the same number of components
        if (numComponents != sm.getNumBands())
            return false;
        if (sm.getTransferType() != transferType)
            return false;

        return true;
    }

    /**
     * @return the IcyColorSpace
     */
    public IcyColorSpace getIcyColorSpace()
    {
        return (IcyColorSpace) getColorSpace();
    }

    /**
     * Change the colorspace of the color model.<br/>
     * <b>You should never use this method directly (internal use only)</b>
     */
    public void setColorSpace(IcyColorSpace colorSpace)
    {
        final IcyColorSpace cs = getIcyColorSpace();

        if (cs != colorSpace)
        {
            try
            {
                final Field csField = ReflectionUtil.getField(ColorModel.class, "colorSpace", true);
                // set new colorSpace value
                csField.set(this, colorSpace);

                cs.removeListener(this);
                colorSpace.addListener(this);
            }
            catch (Exception e)
            {
                System.err.println("Warning: Couldn't change colorspace of IcyColorModel...");
            }
        }

        // do not notify about the change here as this method is only called internally
    }

    /**
     * @return the normalScalers
     */
    public Scaler[] getNormalScalers()
    {
        return normalScalers;
    }

    /**
     * @return the colormapScalers
     */
    public Scaler[] getColormapScalers()
    {
        return colormapScalers;
    }

    /**
     * Returns the number of components in this <code>ColorModel</code>.<br>
     * Note that alpha is embedded so we always have NumColorComponent = NumComponent
     * 
     * @return the number of components in this <code>ColorModel</code>
     */
    @Override
    public int getNumComponents()
    {
        return numComponents;
    }

    /**
     * @deprecated use {@link #getDataType_()} instead
     */
    @Deprecated
    public int getDataType()
    {
        return TypeUtil.dataBufferTypeToDataType(transferType);
    }

    /**
     * Return data type for this colormodel
     * 
     * @see DataType
     */
    public DataType getDataType_()
    {
        return dataType;
    }

    /**
     * return default component bounds for this colormodel
     */
    public double[] getDefaultComponentBounds()
    {
        return dataType.getDefaultBounds();
    }

    /**
     * Get component absolute minimum value
     */
    public double getComponentAbsMinValue(int component)
    {
        // use the normal scaler
        return normalScalers[component].getAbsLeftIn();
    }

    /**
     * Get component absolute maximum value
     */
    public double getComponentAbsMaxValue(int component)
    {
        // use the normal scaler
        return normalScalers[component].getAbsRightIn();
    }

    /**
     * Get component absolute bounds (min and max values)
     */
    public double[] getComponentAbsBounds(int component)
    {
        final double[] result = new double[2];

        result[0] = getComponentAbsMinValue(component);
        result[1] = getComponentAbsMaxValue(component);

        return result;
    }

    /**
     * Get component user minimum value
     */
    public double getComponentUserMinValue(int component)
    {
        // use the normal scaler
        return normalScalers[component].getLeftIn();
    }

    /**
     * Get user component user maximum value
     */
    public double getComponentUserMaxValue(int component)
    {
        // use the normal scaler
        return normalScalers[component].getRightIn();
    }

    /**
     * Get component user bounds (min and max values)
     */
    public double[] getComponentUserBounds(int component)
    {
        final double[] result = new double[2];

        result[0] = getComponentUserMinValue(component);
        result[1] = getComponentUserMaxValue(component);

        return result;
    }

    /**
     * Set component absolute minimum value
     */
    public void setComponentAbsMinValue(int component, double min)
    {
        // update both scalers
        normalScalers[component].setAbsLeftIn(min);
        colormapScalers[component].setAbsLeftIn(min);
    }

    /**
     * Set component absolute maximum value
     */
    public void setComponentAbsMaxValue(int component, double max)
    {
        // update both scalers
        normalScalers[component].setAbsRightIn(max);
        colormapScalers[component].setAbsRightIn(max);
    }

    /**
     * Set component absolute bounds (min and max values)
     */
    public void setComponentAbsBounds(int component, double[] bounds)
    {
        setComponentAbsBounds(component, bounds[0], bounds[1]);
    }

    /**
     * Set component absolute bounds (min and max values)
     */
    public void setComponentAbsBounds(int component, double min, double max)
    {
        // update both scalers
        normalScalers[component].setAbsLeftRightIn(min, max);
        colormapScalers[component].setAbsLeftRightIn(min, max);
    }

    /**
     * Set component user minimum value
     */
    public void setComponentUserMinValue(int component, double min)
    {
        // update both scalers
        normalScalers[component].setLeftIn(min);
        colormapScalers[component].setLeftIn(min);
    }

    /**
     * Set component user maximum value
     */
    public void setComponentUserMaxValue(int component, double max)
    {
        // update both scalers
        normalScalers[component].setRightIn(max);
        colormapScalers[component].setRightIn(max);
    }

    /**
     * Set component user bounds (min and max values)
     */
    public void setComponentUserBounds(int component, double[] bounds)
    {
        setComponentUserBounds(component, bounds[0], bounds[1]);
    }

    /**
     * Set component user bounds (min and max values)
     */
    public void setComponentUserBounds(int component, double min, double max)
    {
        // update both scalers
        normalScalers[component].setLeftRightIn(min, max);
        colormapScalers[component].setLeftRightIn(min, max);
    }

    /**
     * Set components absolute bounds (min and max values)
     */
    public void setComponentsAbsBounds(double[][] bounds)
    {
        final int numComponents = getNumComponents();

        if (bounds.length != numComponents)
            throw new IllegalArgumentException("bounds.length != ColorModel.numComponents");

        for (int component = 0; component < numComponents; component++)
            setComponentAbsBounds(component, bounds[component]);
    }

    /**
     * Set components user bounds (min and max values)
     */
    public void setComponentsUserBounds(double[][] bounds)
    {
        final int numComponents = getNumComponents();

        if (bounds.length != numComponents)
            throw new IllegalArgumentException("bounds.length != ColorModel.numComponents");

        for (int component = 0; component < numComponents; component++)
            setComponentUserBounds(component, bounds[component]);
    }

    /**
     * Return true if colorModel is float data type
     */
    public boolean isFloatDataType()
    {
        return dataType.isFloat();
    }

    /**
     * Return true if colorModel data type is signed
     */
    public boolean isSignedDataType()
    {
        return dataType.isSigned();
    }

    /**
     * Return true if color maps associated to this {@link IcyColorModel} are all linear map.
     * 
     * @see IcyColorMap#isLinear()
     */
    public boolean hasLinearColormaps()
    {
        for (int c = 0; c < numComponents; c++)
            if (!getColorMap(c).isLinear())
                return false;

        return true;
    }

    /**
     * Returns the <code>String</code> representation of the contents of this <code>ColorModel</code>object.
     * 
     * @return a <code>String</code> representing the contents of this <code>ColorModel</code> object.
     */
    @Override
    public String toString()
    {
        return new String("ColorModel: dataType = " + dataType + " numComponents = " + numComponents
                + " color space = " + getColorSpace());
    }

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

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

    /**
     * fire event
     * 
     * @param e
     */
    public void fireEvent(IcyColorModelEvent e)
    {
        for (IcyColorModelListener listener : new ArrayList<IcyColorModelListener>(listeners))
            listener.colorModelChanged(e);
    }

    /**
     * process on colormodel change
     */
    @Override
    public void onChanged(CollapsibleEvent compare)
    {
        final IcyColorModelEvent event = (IcyColorModelEvent) compare;

        // notify listener we have changed
        fireEvent(event);
    }

    @Override
    public void scalerChanged(ScalerEvent e)
    {
        // only listening colormapScalers
        final int ind = Scaler.indexOf(colormapScalers, e.getScaler());

        // handle changed via updater object
        if (ind != -1)
            updater.changed(new IcyColorModelEvent(this, IcyColorModelEventType.SCALER_CHANGED, ind));
    }

    @Override
    public void colorSpaceChanged(IcyColorSpaceEvent e)
    {
        // handle changed via updater object
        updater.changed(new IcyColorModelEvent(this, IcyColorModelEventType.COLORMAP_CHANGED, e.getComponent()));
    }

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

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

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