/**
 * 
 */
package icy.image.colormap;

import icy.file.xml.XMLPersistent;
import icy.math.Interpolator;
import icy.util.XMLUtil;

import java.awt.Point;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.List;

import org.w3c.dom.Element;
import org.w3c.dom.Node;

/**
 * @author Stephane
 */
public class IcyColorMapComponent implements XMLPersistent
{
    static final String ID_INDEX = "index";
    static final String ID_VALUE = "value";

    public class ControlPoint implements Comparable<ControlPoint>, XMLPersistent
    {
        int index;
        int value;
        private final boolean fixed;

        /**
         * @param index
         * @param value
         */
        public ControlPoint(int index, int value, boolean fixed)
        {
            super();

            this.index = index;
            this.value = value;
            this.fixed = fixed;
        }

        /**
         * @param index
         * @param value
         */
        public ControlPoint(int index, int value)
        {
            this(index, value, false);
        }

        /**
         * @return the fixed flag
         */
        public boolean isFixed()
        {
            return fixed;
        }

        /**
         * @return the index
         */
        public int getIndex()
        {
            return index;
        }

        /**
         * @param index
         *        the index to set
         */
        public void setIndex(int index)
        {
            if ((!fixed) && (this.index != index))
            {
                this.index = index;

                changed();
            }
        }

        /**
         * @return the value
         */
        public int getValue()
        {
            return value;
        }

        /**
         * @param value
         *        the value to set
         */
        public void setValue(int value)
        {
            if (this.value != value)
            {
                this.value = value;

                changed();
            }
        }

        /**
         * Set control point position
         * 
         * @param p
         *        point
         */
        public void setPosition(Point p)
        {
            setPosition(p.x, p.y);
        }

        /**
         * Get control point position
         * 
         * @return point position
         */
        public Point getPosition()
        {
            return new Point(index, value);
        }

        /**
         * Set control point position
         * 
         * @param index
         * @param value
         */
        public void setPosition(int index, int value)
        {
            if (((!fixed) && (this.index != index)) || (this.value != value))
            {
                if (!fixed)
                    this.index = index;
                this.value = value;

                changed();
            }
        }

        /**
         * remove the control point
         */
        public void remove()
        {
            if (!fixed)
                removeControlPoint(this);
        }

        /**
         * put here process on changed event
         */
        protected void onChanged()
        {
            // nothing for now

        }

        /**
         * changed event
         */
        protected void changed()
        {
            // common process on change
            onChanged();
            // inform colormap that control point has changed
            controlPointChanged(this);
        }

        @Override
        public int compareTo(ControlPoint o)
        {
            if (index < o.getIndex())
                return -1;
            else if (index > o.getIndex())
                return 1;
            else
                return 0;
        }

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

            final int ind = XMLUtil.getElementIntValue(node, ID_INDEX, 0);
            final int val = XMLUtil.getElementIntValue(node, ID_VALUE, 0);

            setPosition(ind, val);

            return true;
        }

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

            XMLUtil.setElementIntValue(node, ID_INDEX, getIndex());
            XMLUtil.setElementIntValue(node, ID_VALUE, getValue());

            return true;
        }

        @Override
        public boolean equals(Object obj)
        {
            if (obj instanceof ControlPoint)
            {
                final ControlPoint pt = (ControlPoint) obj;
                return (index == pt.index) && (value == pt.value);
            }

            return super.equals(obj);
        }

        @Override
        public int hashCode()
        {
            return index ^ value;
        }
    }

    private static final String ID_RAWDATA = "rawdata";
    private static final String ID_POINT = "point";

    /**
     * parent colormap
     */
    private final IcyColorMap colormap;
    /**
     * list of control point
     */
    protected final ArrayList<ControlPoint> controlPoints;
    /**
     * we use short to store byte to avoid "sign problem"
     */
    public final short[] map;
    /**
     * normalized maps
     */
    public final float[] mapf;

    /**
     * internals
     */
    private int updateCnt;
    private boolean controlPointsChangedPending;
    private boolean mapDataChangedPending;
    private boolean mapFDataChangedPending;
    private boolean rawData;

    public IcyColorMapComponent(IcyColorMap colorMap, short initValue)
    {
        super();

        controlPoints = new ArrayList<ControlPoint>();

        this.colormap = colorMap;

        // allocate map
        map = new short[IcyColorMap.SIZE];
        mapf = new float[IcyColorMap.SIZE];

        // default
        Arrays.fill(map, initValue);
        updateFloatMapFromIntMap();

        // add fixed control point to index 0
        controlPoints.add(new ControlPoint(0, map[0], true));
        // add fixed control point to index IcyColorMap.MAX_INDEX
        controlPoints.add(new ControlPoint(IcyColorMap.MAX_INDEX, map[IcyColorMap.MAX_INDEX], true));

        updateCnt = 0;
        controlPointsChangedPending = false;
        mapDataChangedPending = false;
        mapFDataChangedPending = false;
        rawData = false;
    }

    public IcyColorMapComponent(IcyColorMap colorMap)
    {
        this(colorMap, (short) 0);
    }

    public int getControlPointCount()
    {
        return controlPoints.size();
    }

    public ArrayList<ControlPoint> getControlPoints()
    {
        return controlPoints;
    }

    /**
     * get the control point
     */
    public ControlPoint getControlPoint(int index)
    {
        return controlPoints.get(index);
    }

    /**
     * get the control point at specified index (return null if not found)
     */
    public ControlPoint getControlPointWithIndex(int index, boolean create)
    {
        // TODO: search can be optimized as the list is sorted on index value
        for (ControlPoint cp : controlPoints)
            if (cp.getIndex() == index)
                return cp;

        if (create)
        {
            final ControlPoint result = new ControlPoint(index, 0, (index == 0) || (index == IcyColorMap.MAX_INDEX));
            // add to list
            controlPoints.add(result);
            // and return
            return result;
        }

        return null;
    }

    /**
     * Return true if there is a control point at specified index
     */
    public boolean hasControlPointWithIndex(int index)
    {
        return getControlPointWithIndex(index, false) != null;
    }

    /**
     * Set a control point to specified index and value (normalized)
     */
    public ControlPoint setControlPoint(int index, float value)
    {
        return setControlPoint(index, (int) (value * IcyColorMap.MAX_LEVEL));
    }

    /**
     * Set a control point to specified index and value
     */
    public ControlPoint setControlPoint(int index, int value)
    {
        // flag to indicate we don't have raw data
        rawData = false;

        // search for an existing control point at this index
        ControlPoint controlPoint = getControlPointWithIndex(index, false);

        // not found ?
        if (controlPoint == null)
        {
            // create a new control point
            controlPoint = new ControlPoint(index, value);
            // and add it to the list
            controlPoints.add(controlPoint);
            // notify point added
            controlPointAdded(controlPoint);
        }
        else
        {
            // modify intensity of control point
            controlPoint.setValue(value);
        }

        return controlPoint;
    }

    /**
     * Remove the specified control point
     * 
     * @param controlPoint
     */
    public void removeControlPoint(ControlPoint controlPoint)
    {
        if (controlPoints.remove(controlPoint))
            controlPointRemoved(controlPoint);
    }

    /**
     * Remove all control point
     */
    public void removeAllControlPoint()
    {
        if (controlPoints.size() <= 2)
            return;

        beginUpdate();
        try
        {
            // more than the 2 fixed controls point ?
            while (controlPoints.size() > 2)
                removeControlPoint(controlPoints.get(1));
        }
        finally
        {
            endUpdate();
        }
    }

    /**
     * Copy data from specified source colormap band
     */
    public void copyFrom(IcyColorMapComponent source)
    {
        // copy the rawData property
        rawData = source.rawData;

        // we remove all controls points (even fixed ones)
        controlPoints.clear();

        for (ControlPoint cp : source.controlPoints)
            controlPoints.add(new ControlPoint(cp.getIndex(), cp.getValue(), cp.isFixed()));

        // only the 2 fixed controls point ?
        if (controlPoints.size() <= 2)
        {
            // directly copy table data
            System.arraycopy(source.map, 0, map, 0, IcyColorMap.SIZE);
            // notify we changed table data
            mapDataChanged();
        }
        else
            // notify we modified control point
            controlPointsChanged();
    }

    /**
     * Copy data from specified byte array
     */
    public void copyFrom(byte[] src)
    {
        // we remove all controls points (even fixed ones)
        controlPoints.clear();

        final double srcOffsetStep = src.length / IcyColorMap.SIZE;
        double srcOffset = 0;

        // directly copy table data
        for (int dstOffset = 0; dstOffset < IcyColorMap.SIZE; dstOffset++)
        {
            map[dstOffset] = (short) (src[(int) srcOffset] & 0xFF);
            srcOffset += srcOffsetStep;
        }

        // take it as this is a raw map
        rawData = true;

        // notify we changed table data
        mapDataChanged();
    }

    /**
     * Copy data from specified short array.<br>
     * 
     * @param src
     *        data short array
     * @param shift
     *        shift factor if value need to be shifted (8 if data are short formatted)
     */
    public void copyFrom(short[] src, int shift)
    {
        final byte[] byteMap = new byte[src.length];

        // transform short map to byte map
        for (int i = 0; i < src.length; i++)
            byteMap[i] = (byte) (src[i] >> shift);

        // copy
        copyFrom(byteMap);
    }

    /**
     * Returns colormap content as an array of byte (length = IcyColorMap.SIZE).
     */
    public byte[] asByteArray()
    {
        final byte[] result = new byte[IcyColorMap.SIZE];

        for (int i = 0; i < result.length; i++)
            result[i] = (byte) getValue(i);

        return result;
    }

    /**
     * Return value for specified index
     */
    public short getValue(int index)
    {
        return map[index];
    }

    /**
     * @deprecated Use {@link #getValue(int)} instead.
     */
    @Deprecated
    public short getIntensity(int index)
    {
        return getValue(index);
    }

    /**
     * Set direct intensity value to specified index
     */
    public void setValue(int index, int value)
    {
        // flag to indicate we have raw data
        rawData = true;

        if (map[index] != value)
        {
            // clear control point as we are manually setting map value
            removeAllControlPoint();

            // set value
            map[index] = (short) value;

            // notify change
            mapDataChanged();
        }
    }

    /**
     * Set direct intensity (normalized) value to specified index
     */
    public void setNormalizedValue(int index, float value)
    {
        // flag to indicate we have raw data
        rawData = true;

        if (mapf[index] != value)
        {
            // clear control point as we are manually setting map value
            removeAllControlPoint();

            // set value
            mapf[index] = value;

            // notify change
            mapFDataChanged();
        }
    }

    /**
     * Return true is the color map band is all set to a fixed value.
     */
    public boolean isAllSame()
    {
        final short value = map[0];

        for (int i = 1; i < IcyColorMap.SIZE; i++)
            if (map[i] != value)
                return false;

        return true;
    }

    /**
     * Return true is the color map band is all set to zero.
     */
    public boolean isAllZero()
    {
        for (short value : map)
            if (value != 0)
                return false;

        return true;
    }

    /**
     * Return true is the color map band is all set to one.
     */
    public boolean isAllOne()
    {
        for (short value : map)
            if (value != IcyColorMap.MAX_LEVEL)
                return false;

        return true;
    }

    /**
     * Return true is the color map band is a linear one.<br>
     * Linear map are used to display plain gray or plain color image.<br>
     * Non linear map means you may have an indexed color image or
     * you want to enhance contrast/color in display.
     */
    public boolean isLinear()
    {
        float lastdiff = mapf[1] - mapf[0];

        for (int i = 2; i < IcyColorMap.SIZE; i++)
        {
            final float diff = mapf[i] - mapf[i - 1];

            if ((diff == 0) || (lastdiff == 0))
                continue;

            // difference changed ?
            if ((diff != lastdiff) && (Math.abs(diff / (diff - lastdiff)) < 1000f))
                return false;

            lastdiff = diff;
        }

        return true;
    }

    /**
     * update float map from int map
     */
    private void updateFloatMapFromIntMap()
    {
        for (int i = 0; i < IcyColorMap.SIZE; i++)
            mapf[i] = (float) map[i] / IcyColorMap.MAX_LEVEL;
    }

    /**
     * update float map from int map
     */
    private void updateIntMapFromFloatMap()
    {
        for (int i = 0; i < IcyColorMap.SIZE; i++)
            map[i] = (short) (mapf[i] * IcyColorMap.MAX_LEVEL);
    }

    /**
     * update fixed controls points with map data
     */
    private void updateFixedCP()
    {
        // internal update (no event wanted)
        getControlPointWithIndex(0, true).value = map[0];
        getControlPointWithIndex(IcyColorMap.MAX_INDEX, true).value = map[IcyColorMap.MAX_INDEX];
    }

    /**
     * Called when a control point has been modified
     * 
     * @param controlPoint
     *        modified control point
     */
    public void controlPointChanged(ControlPoint controlPoint)
    {
        controlPointsChanged();
    }

    /**
     * Called when a control point has been added
     * 
     * @param controlPoint
     *        added control point
     */
    public void controlPointAdded(ControlPoint controlPoint)
    {
        controlPointsChanged();
    }

    /**
     * Called when a control point has been removed
     * 
     * @param controlPoint
     *        removed control point
     */
    public void controlPointRemoved(ControlPoint controlPoint)
    {
        controlPointsChanged();
    }

    /**
     * common process on Control Point list change
     */
    public void onControlPointsChanged()
    {
        // sort the list
        Collections.sort(controlPoints);

        final List<Point> points = new ArrayList<Point>();

        // get position only
        for (ControlPoint point : controlPoints)
            points.add(point.getPosition());

        // get linear interpolation values
        final double[] values = Interpolator.doYLinearInterpolation(points, 1);

        // directly modify the colormap table data
        for (int i = 0; i < IcyColorMap.SIZE; i++)
            map[i] = (short) Math.round(values[i]);

        mapDataChanged();
    }

    /**
     * common process on map (int) data change
     */
    public void onMapDataChanged()
    {
        // update float map from the modified int map
        updateFloatMapFromIntMap();
        // update fixed controls points
        updateFixedCP();

        // take it as this is a raw map
        if (rawData)
            rawData = !isLinear();

        // manually set a changed event as we directly modified the colormap
        colormap.changed();
    }

    /**
     * common process on map (float) data change
     */
    public void onMapFDataChanged()
    {
        // update int map from the modified float map
        updateIntMapFromFloatMap();
        // udpate fixed controls points
        updateFixedCP();
        // manually set a changed event as we directly modified the colormap
        colormap.changed();
    }

    /**
     * called when the controller modified Control Point list
     */
    public void controlPointsChanged()
    {
        if (isUpdating())
        {
            controlPointsChangedPending = true;
            // map will be modified anyway
            mapDataChangedPending = false;
            mapFDataChangedPending = false;
        }
        else
            onControlPointsChanged();
    }

    /**
     * called when the controller directly modified the map (int) data
     */
    public void mapDataChanged()
    {
        if (isUpdating())
        {
            mapDataChangedPending = true;
            // to keep the changed made to map (int)
            mapFDataChangedPending = false;
            controlPointsChangedPending = false;
        }
        else
            onMapDataChanged();
    }

    /**
     * called when the controller directly modified the map (float) data
     */
    public void mapFDataChanged()
    {
        if (isUpdating())
        {
            mapFDataChangedPending = true;
            // to keep the changed made to map (float)
            mapDataChangedPending = false;
            controlPointsChangedPending = false;
        }
        else
            onMapFDataChanged();
    }

    public void beginUpdate()
    {
        updateCnt++;
    }

    public void endUpdate()
    {
        updateCnt--;
        if (updateCnt <= 0)
        {
            // process pending tasks
            if (controlPointsChangedPending)
            {
                onControlPointsChanged();
                controlPointsChangedPending = false;
            }
            else if (mapDataChangedPending)
            {
                onMapDataChanged();
                mapDataChangedPending = false;
            }
            else if (mapFDataChangedPending)
            {
                onMapFDataChanged();
                mapFDataChangedPending = false;
            }
        }
    }

    public boolean isUpdating()
    {
        return updateCnt > 0;
    }

    /**
     * returns true when the LUT is specified by raw data (for example GIF files),
     * false when the LUT is specified by control points.
     */
    public boolean isRawData()
    {
        return rawData;
    }

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

        rawData = XMLUtil.getAttributeBooleanValue((Element) node, ID_RAWDATA, false);

        final List<Node> nodesPoint = XMLUtil.getChildren(node, ID_POINT);

        beginUpdate();
        try
        {
            if (rawData)
            {
                int ind = 0;
                if (nodesPoint.size() == 0)
                {
                    final byte[] data = XMLUtil.getElementBytesValue(node, ID_VALUE, new byte[] {});

                    // an error occurred while retrieved XML data
                    if (data == null)
                        return false;

                    copyFrom(data);
                }
                else
                {
                    // backward compatibility
                    for (Node nodePoint : nodesPoint)
                    {
                        final int val = XMLUtil.getElementIntValue(nodePoint, ID_VALUE, 0);

                        setValue(ind, val);
                        ind++;
                    }
                }
            }
            else
            {
                removeAllControlPoint();
                for (Node nodePoint : nodesPoint)
                {
                    final int ind = XMLUtil.getElementIntValue(nodePoint, ID_INDEX, 0);
                    final int val = XMLUtil.getElementIntValue(nodePoint, ID_VALUE, 0);

                    setControlPoint(ind, val);
                }
            }
        }
        finally
        {
            endUpdate();
        }

        return true;
    }

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

        XMLUtil.setAttributeBooleanValue((Element) node, ID_RAWDATA, rawData);
        XMLUtil.removeChildren(node, ID_POINT);

        boolean result = true;

        if (rawData)
        {
            XMLUtil.removeChildren(node, ID_VALUE);
            XMLUtil.setElementBytesValue(node, ID_VALUE, asByteArray());
        }
        else
        {
            for (int ind = 0; ind < controlPoints.size(); ind++)
            {
                final ControlPoint cp = controlPoints.get(ind);
                final Node nodePoint = XMLUtil.addElement(node, ID_POINT);

                result = result && cp.saveToXML(nodePoint);
            }
        }

        return result;
    }

    @Override
    public boolean equals(Object obj)
    {
        if (obj instanceof IcyColorMapComponent)
            // just compare the map content (we don't care about control point here)
            return Arrays.equals(map, ((IcyColorMapComponent) obj).map);

        return super.equals(obj);
    }

    @Override
    public int hashCode()
    {
        return map.hashCode();
    }
}