/*
 * 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.gui.component.math;

import icy.gui.component.BorderedPanel;
import icy.math.ArrayMath;
import icy.math.Histogram;
import icy.math.MathUtil;
import icy.type.collection.array.Array1DUtil;

import java.awt.Color;
import java.awt.Dimension;
import java.awt.Graphics;
import java.awt.Graphics2D;
import java.util.EventListener;

import javax.swing.BorderFactory;

/**
 * Histogram component.
 * 
 * @author Stephane
 */
public class HistogramPanel extends BorderedPanel
{
    public static interface HistogramPanelListener extends EventListener
    {
        /**
         * histogram need to be refreshed (send values for recalculation)
         */
        public void histogramNeedRefresh(HistogramPanel source);
    }

    /**
     * 
     */
    private static final long serialVersionUID = -3932807727576675217L;

    protected static final int BORDER_WIDTH = 2;
    protected static final int BORDER_HEIGHT = 2;
    protected static final int MIN_SIZE = 16;

    /**
     * internal histogram
     */
    Histogram histogram;
    /**
     * histogram data cache
     */
    private double[] histogramData;

    /**
     * histogram properties
     */
    double minValue;
    double maxValue;
    boolean integer;

    /**
     * display properties
     */
    boolean logScaling;
    boolean useLAFColors;
    Color color;
    Color backgroundColor;

    /**
     * internals
     */
    boolean updating;

    /**
     * Create a new histogram panel for the specified value range.<br>
     * By default it uses a Logarithm representation (modifiable via {@link #setLogScaling(boolean)}
     * 
     * @param minValue
     * @param maxValue
     * @param integer
     */
    public HistogramPanel(double minValue, double maxValue, boolean integer)
    {
        super();

        setBorder(BorderFactory.createEmptyBorder(BORDER_HEIGHT, BORDER_WIDTH, BORDER_HEIGHT, BORDER_WIDTH));

        setMinimumSize(new Dimension(100, 100));
        setMaximumSize(new Dimension(Short.MAX_VALUE, Short.MAX_VALUE));

        histogram = new Histogram(0d, 1d, 1, true);
        histogramData = new double[0];

        this.minValue = minValue;
        this.maxValue = maxValue;
        this.integer = integer;

        logScaling = true;
        useLAFColors = true;
        // default drawing color
        color = Color.white;
        backgroundColor = Color.darkGray;

        buildHistogram(minValue, maxValue, integer);

        updating = false;
    }

    /**
     * Returns true when histogram is being calculated.
     */
    public boolean isUpdating()
    {
        return updating;
    }

    /**
     * Call this method to inform you start histogram computation (allow the panel to display
     * "computing" message).</br>
     * You need to call {@link #done()} when computation is done.
     * 
     * @see #done()
     */
    public void reset()
    {
        histogram.reset();
        // start histogram calculation
        updating = true;
    }

    /**
     * @deprecated Use <code>getHistogram.addValue(double)</code> instead.
     */
    @Deprecated
    public void addValue(double value)
    {
        histogram.addValue(value);
    }

    /**
     * @deprecated Use <code>getHistogram.addValue(Object, boolean signed)</code> instead.
     */
    @Deprecated
    public void addValues(Object array, boolean signed)
    {
        histogram.addValues(array, signed);
    }

    /**
     * @deprecated Use <code>getHistogram.addValue(byte[])</code> instead.
     */
    @Deprecated
    public void addValues(byte[] array, boolean signed)
    {
        histogram.addValues(array, signed);
    }

    /**
     * @deprecated Use <code>getHistogram.addValue(short[])</code> instead.
     */
    @Deprecated
    public void addValues(short[] array, boolean signed)
    {
        histogram.addValues(array, signed);
    }

    /**
     * @deprecated Use <code>getHistogram.addValue(int[])</code> instead.
     */
    @Deprecated
    public void addValues(int[] array, boolean signed)
    {
        histogram.addValues(array, signed);
    }

    /**
     * @deprecated Use <code>getHistogram.addValue(long[])</code> instead.
     */
    @Deprecated
    public void addValues(long[] array, boolean signed)
    {
        histogram.addValues(array, signed);
    }

    /**
     * @deprecated Use <code>getHistogram.addValue(float[])</code> instead.
     */
    @Deprecated
    public void addValues(float[] array)
    {
        histogram.addValues(array);
    }

    /**
     * @deprecated Use <code>getHistogram.addValue(double[])</code> instead.
     */
    @Deprecated
    public void addValues(double[] array)
    {
        histogram.addValues(array);
    }

    /**
     * Returns the adjusted size (linear / log normalized) of the specified bin.
     * 
     * @see #getBinSize(int)
     */
    public double getAdjustedBinSize(int index)
    {
        // cache
        final double[] data = histogramData;

        if ((index >= 0) && (index < data.length))
            return data[index];

        return 0d;
    }

    /**
     * Returns the size of the specified bin (number of element in the bin)
     * 
     * @see icy.math.Histogram#getBinSize(int)
     */
    public int getBinSize(int index)
    {
        return histogram.getBinSize(index);
    }

    /**
     * @see icy.math.Histogram#getBinNumber()
     */
    public int getBinNumber()
    {
        return histogram.getBinNumber();
    }

    /**
     * @see icy.math.Histogram#getBinWidth()
     */
    public double getBinWidth()
    {
        return histogram.getBinWidth();
    }

    /**
     * @see icy.math.Histogram#getBins()
     */
    public int[] getBins()
    {
        return histogram.getBins();
    }

    /**
     * Invoke this method when the histogram calculation has been completed to refresh data cache.
     */
    public void done()
    {
        refreshDataCache();

        // end histogram calculation
        updating = false;
    }

    /**
     * Returns the minimum allowed value of the histogram.
     */
    public double getMinValue()
    {
        return histogram.getMinValue();
    }

    /**
     * Returns the maximum allowed value of the histogram.
     */
    public double getMaxValue()
    {
        return histogram.getMaxValue();
    }

    /**
     * Returns true if the input value are integer values only.<br>
     * This is used to adapt the bin number of histogram..
     */
    public boolean isIntegerType()
    {
        return histogram.isIntegerType();
    }

    /**
     * Returns true if histogram is displayed with LOG scaling
     */
    public boolean getLogScaling()
    {
        return logScaling;
    }

    /**
     * Returns true if histogram use LAF color scheme.
     * 
     * @see #getColor()
     * @see #getBackgroundColor()
     */
    public boolean getUseLAFColors()
    {
        return useLAFColors;
    }

    /**
     * Returns the drawing color
     */
    public Color getColor()
    {
        return color;
    }

    /**
     * Returns the background color
     */
    public Color getBackgroundColor()
    {
        return color;
    }

    /**
     * Get histogram object
     */
    public Histogram getHistogram()
    {
        return histogram;
    }

    /**
     * Get computed histogram data
     */
    public double[] getHistogramData()
    {
        return histogramData;
    }

    /**
     * Set minimum, maximum and integer values at once
     */
    public void setMinMaxIntValues(double min, double max, boolean intType)
    {
        // test with cached value first
        if ((minValue != min) || (maxValue != max) || (integer != intType))
            buildHistogram(min, max, intType);
        // then test with uncached value (histo being updated)
        else if ((histogram.getMinValue() != min) || (histogram.getMaxValue() != max)
                || (histogram.isIntegerType() != intType))
            buildHistogram(min, max, intType);
    }

    /**
     * Set to true to display histogram with LOG scaling (else it uses linear scaling).
     */
    public void setLogScaling(boolean value)
    {
        if (logScaling != value)
        {
            logScaling = value;
            refreshDataCache();
        }
    }

    /**
     * Set to true to use LAF color scheme.
     * 
     * @see #setColor(Color)
     * @see #setBackgroundColor(Color)
     */
    public void setUseLAFColors(boolean value)
    {
        if (useLAFColors != value)
        {
            useLAFColors = value;
            repaint();
        }
    }

    /**
     * Set the drawing color
     */
    public void setColor(Color value)
    {
        if (!color.equals(value))
        {
            color = value;
            if (!useLAFColors)
                repaint();
        }
    }

    /**
     * Set the background color
     */
    public void setBackgroundColor(Color value)
    {
        if (!backgroundColor.equals(value))
        {
            backgroundColor = value;
            if (!useLAFColors)
                repaint();
        }
    }

    protected void checkHisto()
    {
        // create temporary histogram
        final Histogram newHisto = new Histogram(histogram.getMinValue(), histogram.getMaxValue(), Math.max(
                getClientWidth(), MIN_SIZE), histogram.isIntegerType());

        // histogram properties changed ?
        if (!hasSameProperties(newHisto))
        {
            // set new histogram
            histogram = newHisto;
            // notify listeners so they can fill it
            fireHistogramNeedRefresh();
        }
    }

    protected void buildHistogram(double min, double max, boolean intType)
    {
        // create temporary histogram
        final Histogram newHisto = new Histogram(min, max, Math.max(getClientWidth(), MIN_SIZE), intType);

        // histogram properties changed ?
        if (!hasSameProperties(newHisto))
        {
            // set new histogram
            histogram = newHisto;
            // notify listeners so they can fill it
            fireHistogramNeedRefresh();
        }
    }

    /**
     * Return true if specified histogram has same bounds and number of bin than current one
     */
    protected boolean hasSameProperties(Histogram h)
    {
        return (histogram.getBinNumber() == h.getBinNumber()) && (histogram.getMinValue() == h.getMinValue())
                && (histogram.getMaxValue() == h.getMaxValue()) && (histogram.isIntegerType() == h.isIntegerType());
    }

    /**
     * update histogram data cache
     */
    protected void refreshDataCache()
    {
        // get histogram data
        final double[] newHistogramData = Array1DUtil.intArrayToDoubleArray(histogram.getBins(), false);

        // we want all values to >= 1
        final double min = ArrayMath.min(newHistogramData);
        MathUtil.add(newHistogramData, min + 1f);
        // log
        if (logScaling)
            MathUtil.log(newHistogramData);
        // normalize data
        MathUtil.normalize(newHistogramData);

        // get new data cache and apply min, max, integer type
        histogramData = newHistogramData;
        minValue = getMinValue();
        maxValue = getMaxValue();
        integer = isIntegerType();

        // request repaint
        repaint();
    }

    /**
     * Returns the ratio to convert a data value to corresponding pixel X position
     */
    protected double getDataToPixelRatio()
    {
        final double pixelRange = Math.max(getClientWidth() - 1, 32);
        final double dataRange = maxValue - minValue;

        if (dataRange != 0d)
            return pixelRange / dataRange;

        return 0d;
    }

    /**
     * Returns the ratio to convert a pixel X position to corresponding data value
     */
    protected double getPixelToDataRatio()
    {
        final double pixelRange = Math.max(getClientWidth() - 1, 32);
        final double dataRange = maxValue - minValue;

        if (pixelRange != 0d)
            return dataRange / pixelRange;

        return 0d;
    }

    /**
     * Returns the ratio to convert a pixel X position to corresponding histo bin
     */
    protected double getPixelToHistoRatio()
    {
        final double histogramRange = histogramData.length - 1;
        final double pixelRange = Math.max(getClientWidth() - 1, 32);

        if (pixelRange != 0d)
            return histogramRange / pixelRange;

        return 0d;
    }

    /**
     * Convert a data value to the corresponding pixel position
     */
    public int dataToPixel(double value)
    {
        return (int) Math.round(((value - minValue) * getDataToPixelRatio())) + getClientX();
    }

    /**
     * Convert a pixel position to corresponding data value
     */
    public double pixelToData(int value)
    {
        final double data = ((value - getClientX()) * getPixelToDataRatio()) + minValue;
        return Math.min(Math.max(data, minValue), maxValue);
    }

    /**
     * Convert a pixel position to corresponding bin index
     */
    public int pixelToBin(int value)
    {
        final int index = (int) Math.round((value - getClientX()) * getPixelToHistoRatio());
        return Math.min(Math.max(index, 0), histogramData.length - 1);
    }

    /**
     * Notify all listeners that histogram need to be recomputed
     */
    protected void fireHistogramNeedRefresh()
    {
        for (HistogramPanelListener l : listenerList.getListeners(HistogramPanelListener.class))
            l.histogramNeedRefresh(this);
    }

    public void addListener(HistogramPanelListener l)
    {
        listenerList.add(HistogramPanelListener.class, l);
    }

    public void removeListener(HistogramPanelListener l)
    {
        listenerList.remove(HistogramPanelListener.class, l);
    }

    @Override
    protected void paintComponent(Graphics g)
    {
        final Color fc;
        final Color bc;

        if (useLAFColors)
        {
            fc = getForeground();
            bc = getBackground();
        }
        else
        {
            fc = color;
            bc = backgroundColor;
        }

        final Graphics2D g2 = (Graphics2D) g.create();

        g2.setColor(fc);
        g2.setBackground(bc);

        // background color
        if (isOpaque())
            g2.clearRect(0, 0, getWidth(), getHeight());

        // data cache
        final double ratio = getPixelToHistoRatio();
        final double[] data = histogramData;

        // not yet computed
        if (data.length != 0)
        {
            final int histoRange = data.length - 1;
            final int hRange = getClientHeight() - 1;
            final int bottom = getClientY() + hRange;
            final int l = getClientX();
            final int r = l + getClientWidth();

            for (int i = l; i < r; i++)
            {
                int index = (int) Math.round((i - l) * ratio);

                if (index < 0)
                    index = 0;
                else if (index > histoRange)
                    index = histoRange;

                g2.drawLine(i, bottom, i, bottom - (int) Math.round(data[index] * hRange));
            }
        }

        if ((data.length == 0) || updating)
        {
            final int x = (getWidth() / 2) - 60;
            final int y = (getHeight() / 2) - 20;

            g2.drawString("computing...", x, y);
        }

        g2.dispose();

        // just check for histogram properties change
        checkHisto();
    }
}