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

import icy.gui.component.math.HistogramPanel;
import icy.gui.component.math.HistogramPanel.HistogramPanelListener;
import icy.gui.viewer.Viewer;
import icy.gui.viewer.ViewerEvent;
import icy.gui.viewer.ViewerEvent.ViewerEventType;
import icy.gui.viewer.ViewerListener;
import icy.image.lut.LUT.LUTChannel;
import icy.image.lut.LUT.LUTChannelEvent;
import icy.image.lut.LUT.LUTChannelEvent.LUTChannelEventType;
import icy.image.lut.LUT.LUTChannelListener;
import icy.math.Histogram;
import icy.math.MathUtil;
import icy.math.Scaler;
import icy.sequence.Sequence;
import icy.sequence.SequenceEvent;
import icy.sequence.SequenceEvent.SequenceEventSourceType;
import icy.sequence.SequenceListener;
import icy.system.thread.ThreadUtil;
import icy.type.DataType;
import icy.type.collection.array.Array1DUtil;
import icy.util.ColorUtil;
import icy.util.EventUtil;
import icy.util.GraphicsUtil;
import icy.util.StringUtil;

import java.awt.BorderLayout;
import java.awt.Color;
import java.awt.Cursor;
import java.awt.Dimension;
import java.awt.Font;
import java.awt.Graphics;
import java.awt.Graphics2D;
import java.awt.Point;
import java.awt.Rectangle;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.awt.event.MouseEvent;
import java.awt.event.MouseListener;
import java.awt.event.MouseMotionListener;
import java.awt.event.MouseWheelEvent;
import java.awt.event.MouseWheelListener;
import java.awt.geom.Point2D;
import java.lang.reflect.Array;
import java.util.EventListener;

import javax.swing.JMenuItem;
import javax.swing.JPanel;
import javax.swing.JPopupMenu;
import javax.swing.event.EventListenerList;

/**
 * @author stephane
 */
public class ScalerViewer extends JPanel implements SequenceListener, LUTChannelListener, ViewerListener
{
    protected static enum actionType
    {
        NULL, MODIFY_LOWBOUND, MODIFY_HIGHBOUND, MODIFY_MIDDLE
    }

    public static interface ScalerPositionListener extends EventListener
    {
        public void positionChanged(double index, int value, double normalizedValue);
    }

    public class ScalerHistogramPanel extends HistogramPanel implements MouseListener, MouseMotionListener,
            MouseWheelListener
    {
        /**
         * 
         */
        private static final long serialVersionUID = -7020904979961676368L;

        /**
         * internals
         */
        private actionType action;
        private final Point2D positionInfo;
        private boolean mouseOnLeft;

        public ScalerHistogramPanel(Scaler s)
        {
            super(s.getAbsLeftIn(), s.getAbsRightIn(), s.isIntegerData());

            action = actionType.NULL;
            positionInfo = new Point2D.Double();
            mouseOnLeft = false;

            // we want to display our own background
            // setOpaque(false);
            // dimension (don't change it or you will regret !)
            setMinimumSize(new Dimension(100, 100));
            setPreferredSize(new Dimension(240, 100));

            // add listeners
            addMouseListener(this);
            addMouseMotionListener(this);
            addMouseWheelListener(this);
        }

        /**
         * update mouse cursor
         */
        private void updateCursor(Point pos)
        {
            final int cursor;

            if (action != actionType.NULL)
                cursor = Cursor.W_RESIZE_CURSOR;
            else if (isOverX(pos, getLowBoundPos()) || isOverX(pos, getHighBoundPos()) || isOverX(pos, getMiddlePos()))
                cursor = Cursor.HAND_CURSOR;
            else
                cursor = Cursor.DEFAULT_CURSOR;

            // only if different
            if (getCursor().getType() != cursor)
                setCursor(Cursor.getPredefinedCursor(cursor));
        }

        private void setPositionInfo(double index, int value, double normalizedValue)
        {
            if ((positionInfo.getX() != index) || (positionInfo.getY() != value))
            {
                positionInfo.setLocation(index, normalizedValue);
                scalerPositionChanged(index, value, normalizedValue);
                repaint();
            }
        }

        /**
         * Check if Point p is over area (u, *)
         * 
         * @param p
         *        point
         * @param x
         *        area position
         * @return boolean
         */
        private boolean isOverX(Point p, int u)
        {
            return isOver(p.x, p.y, u, -1, ISOVER_DEFAULT_MARGIN);
        }

        /**
         * Check if (x, y) is over area (u, v)
         * 
         * @param x
         * @param y
         *        pointer
         * @param u
         * @param v
         *        area position
         * @param margin
         *        allowed margin
         * @return boolean
         */
        private boolean isOver(int x, int y, int u, int v, int margin)
        {
            final boolean x_ok;
            final boolean y_ok;

            x_ok = (u == -1) || ((x >= (u - margin)) && (x <= (u + margin)));
            y_ok = (v == -1) || ((y >= (v - margin)) && (y <= (v + margin)));

            return x_ok && y_ok;
        }

        public int getLowBoundPos()
        {
            return dataToPixel(getLowBound());
        }

        public int getHighBoundPos()
        {
            return dataToPixel(getHighBound());
        }

        public int getMiddlePos()
        {
            return (getHighBoundPos() + getLowBoundPos()) / 2;
        }

        private void setLowBoundPos(int pos)
        {
            setLowBound(pixelToData(pos));
        }

        private void setHighBoundPos(int pos)
        {
            setHighBound(pixelToData(pos));
        }

        @Override
        protected void paintComponent(Graphics g)
        {
            updateHisto();

            super.paintComponent(g);

            final Graphics2D g2 = (Graphics2D) g.create();
            try
            {
                // display mouse position infos
                if (positionInfo.getX() != -1)
                {
                    final int x = dataToPixel(positionInfo.getX());
                    final int hRange = getClientHeight() - 1;
                    final int bottom = hRange + getClientY();
                    final int y = bottom - (int) (positionInfo.getY() * hRange);

                    g2.setColor(ColorUtil.xor(getForeground()));
                    g2.drawLine(x, bottom, x, y);
                }

                paintBounds(g2);

                if (!StringUtil.isEmpty(message))
                {
                    // string display
                    g2.setFont(new Font(Font.SANS_SERIF, Font.BOLD, 12));

                    final Rectangle hintBounds = GraphicsUtil.getHintBounds(g2, message, 10, 4);

                    if (mouseOnLeft)
                        GraphicsUtil.drawHint(g2, message, getWidth() - (10 + hintBounds.width), 4, getForeground(),
                                getBackground());
                    else
                        GraphicsUtil.drawHint(g2, message, 10, 4, getForeground(), getBackground());
                }
            }
            finally
            {
                g2.dispose();
            }
        }

        /**
         * draw bounds
         */
        private void paintBounds(Graphics2D g)
        {
            final int h = getClientHeight() - 1;
            final int y = getClientY();
            final int lowBound = getLowBoundPos();
            final int highBound = getHighBoundPos();
            final int middle = getMiddlePos();

            g.setColor(ColorUtil.mix(Color.blue, Color.white, false));
            g.drawRect(lowBound - 2, y, 2, h);
            g.setColor(Color.blue);
            g.fillRect(lowBound - 1, y + 1, 1, h - 1);
            g.setColor(ColorUtil.mix(Color.red, Color.white, false));
            g.drawRect(highBound - 1, y, 2, h);
            g.setColor(Color.red);
            g.fillRect(highBound, y + 1, 1, h - 1);
            g.setColor(ColorUtil.mix(Color.green, Color.white, false));
            g.drawRect(middle - 1, y + 10, 1, h - 10);
            g.setColor(Color.green);
            g.fillRect(middle, y + 11, 0, h - 11);
        }

        @Override
        public void mouseClicked(MouseEvent e)
        {
            if (e.isConsumed())
                return;

            if (e.getClickCount() == 2)
            {
                showRangeSettingDialog();
                e.consume();
            }
        }

        @Override
        public void mouseEntered(MouseEvent e)
        {
            updateCursor(e.getPoint());
        }

        @Override
        public void mouseExited(MouseEvent e)
        {
            if (getCursor().getType() != Cursor.getDefaultCursor().getType())
                setCursor(Cursor.getDefaultCursor());

            // hide message
            setMessage("");
            setPositionInfo(-1, -1, -1);
        }

        @Override
        public void mousePressed(MouseEvent e)
        {
            if (e.isConsumed())
                return;

            final Point pos = e.getPoint();

            if (EventUtil.isLeftMouseButton(e))
            {
                if (isOverX(pos, getLowBoundPos()))
                    action = actionType.MODIFY_LOWBOUND;
                else if (isOverX(pos, getHighBoundPos()))
                    action = actionType.MODIFY_HIGHBOUND;
                else if (isOverX(pos, getMiddlePos()))
                    action = actionType.MODIFY_MIDDLE;

                // show message
                if (action != actionType.NULL)
                {
                    if (EventUtil.isShiftDown(e))
                        setMessage("GLOBAL MOVE");
                    else
                        setMessage("Maintain 'Shift' for global move");
                }

                updateCursor(e.getPoint());
                e.consume();
            }
            else if (EventUtil.isRightMouseButton(e))
            {
                showSettingPopup(pos);
                e.consume();
            }
        }

        @Override
        public void mouseReleased(MouseEvent e)
        {
            if (EventUtil.isLeftMouseButton(e))
            {
                action = actionType.NULL;

                updateCursor(e.getPoint());

                setMessage("");
            }
        }

        @Override
        public void mouseDragged(MouseEvent e)
        {
            if (e.isConsumed())
                return;

            final Point pos = e.getPoint();
            final boolean shift = EventUtil.isShiftDown(e);

            mouseOnLeft = pos.x < (getWidth() / 2);

            switch (action)
            {
                case MODIFY_LOWBOUND:
                    setLowBoundPos(pos.x);
                    // also modify others bounds
                    if (shift)
                    {
                        final double newLowBound = getLowBound();
                        for (LUTChannel lc : lutChannel.getLut().getLutChannels())
                            lc.setMin(newLowBound);
                    }
                    e.consume();
                    break;

                case MODIFY_HIGHBOUND:
                    setHighBoundPos(pos.x);
                    // also modify others bounds
                    if (shift)
                    {
                        final double newHighBound = getHighBound();
                        for (LUTChannel lc : lutChannel.getLut().getLutChannels())
                            lc.setMax(newHighBound);
                    }
                    e.consume();
                    break;

                case MODIFY_MIDDLE:
                    final double width = (getHighBound() - getLowBound()) / 2d;
                    final double value = pixelToData(pos.x);
                    final double min = value - width;
                    final double max = value + width;

                    // global change
                    if (shift)
                    {
                        for (LUTChannel lc : lutChannel.getLut().getLutChannels())
                        {
                            if ((min >= lc.getMinBound()) && (max <= lc.getMaxBound()))
                            {
                                lc.setMin(min);
                                lc.setMax(max);
                            }
                        }
                    }
                    else
                    {
                        if ((min >= lutChannel.getMinBound()) && (max <= lutChannel.getMaxBound()))
                        {
                            setLowBound(min);
                            setHighBound(max);
                        }
                    }
                    e.consume();
                    break;
            }

            // message
            if (action != actionType.NULL)
            {
                if (shift)
                    setMessage("GLOBAL MOVE");
                else
                    setMessage("Maintain 'Shift' for global move");
            }

            if (getBinNumber() > 0)
            {
                final int bin = pixelToBin(pos.x);
                double index = pixelToData(pos.x);
                final int value = getBinSize(bin);

                // use integer index with integer data type
                if (isIntegerType())
                    index = Math.floor(index);

                if (action == actionType.NULL)
                {
                    final String valueText = "value : " + MathUtil.roundSignificant(index, 5, true);
                    final String pixelText = "pixel number : " + value;

                    setMessage(valueText + "\n" + pixelText);
                    // setToolTipText("<html>" + valueText + "<br>" + pixelText);
                }

                setPositionInfo(index, value, getAdjustedBinSize(bin));
            }
        }

        @Override
        public void mouseMoved(MouseEvent e)
        {
            final Point pos = e.getPoint();

            mouseOnLeft = pos.x < (getWidth() / 2);

            updateCursor(e.getPoint());

            if (getBinNumber() > 0)
            {
                final int bin = pixelToBin(pos.x);
                double index = pixelToData(pos.x);
                final int value = getBinSize(bin);

                // use integer index with integer data type
                if (isIntegerType())
                    index = Math.round(index);

                final String valueText = "value : " + MathUtil.roundSignificant(index, 5, true);
                final String pixelText = "pixel number : " + value;

                setMessage(valueText + "\n" + pixelText);
                // setToolTipText("<html>" + valueText + "<br>" + pixelText);

                setPositionInfo(index, value, getAdjustedBinSize(bin));
            }
        }

        @Override
        public void mouseWheelMoved(MouseWheelEvent e)
        {

        }
    }

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

    private static final int ISOVER_DEFAULT_MARGIN = 3;

    /**
     * associated viewer & lutChannel
     */
    Viewer viewer;
    LUTChannel lutChannel;
    /**
     * histogram
     */
    private ScalerHistogramPanel histogram;
    private boolean histoNeedRefresh;

    /**
     * listeners
     */
    private final EventListenerList scalerMapPositionListeners;

    /**
     * internals
     */
    private final Runnable histoUpdater;
    String message;
    private int retry;

    /**
     * 
     */
    public ScalerViewer(Viewer viewer, LUTChannel lutChannel)
    {
        super();

        this.viewer = viewer;
        this.lutChannel = lutChannel;

        message = "";
        retry = 0;
        scalerMapPositionListeners = new EventListenerList();
        histoUpdater = new Runnable()
        {
            @Override
            public void run()
            {
                try
                {
                    // refresh histogram
                    refreshHistoDataInternal();
                }
                catch (Exception e)
                {
                    // just ignore error, it's permitted here
                }
            }
        };

        histogram = new ScalerHistogramPanel(lutChannel.getScaler());
        // listen for need refresh event
        histogram.addListener(new HistogramPanelListener()
        {
            @Override
            public void histogramNeedRefresh(HistogramPanel source)
            {
                internalRequestHistoDataRefresh();
            }
        });
        histoNeedRefresh = false;

        setLayout(new BorderLayout());
        add(histogram, BorderLayout.CENTER);
        validate();

        // force first refresh
        internalRequestHistoDataRefresh();

        // add listeners
        final Sequence sequence = viewer.getSequence();

        if (sequence != null)
            sequence.addListener(this);
        viewer.addListener(this);
        lutChannel.addListener(this);
    }

    public void requestHistoDataRefresh()
    {
        internalRequestHistoDataRefresh();
    }

    private boolean isHistoVisible()
    {
        if (!isValid())
            return false;

        return getVisibleRect().intersects(histogram.getBounds());
    }

    void internalRequestHistoDataRefresh()
    {
        if (isHistoVisible())
            refreshHistoData();
        else
            histoNeedRefresh = true;
    }

    void updateHisto()
    {
        if (histoNeedRefresh)
        {
            refreshHistoData();
            histoNeedRefresh = false;
        }
    }

    private void refreshHistoData()
    {
        // send refresh operation
        ThreadUtil.bgRunSingle(histoUpdater);
    }

    // this method is called by processor, we don't mind about exception here
    void refreshHistoDataInternal()
    {
        final Histogram histo = histogram.getHistogram();
        final Sequence seq = viewer.getSequence();

        histogram.reset();
        try
        {
            if (seq != null)
            {
                final int maxZ;
                final int maxT;
                int t = viewer.getPositionT();
                int z = viewer.getPositionZ();

                if (t != -1)
                    maxT = t;
                else
                {
                    t = 0;
                    maxT = seq.getSizeT() - 1;
                }

                if (z != -1)
                    maxZ = z;
                else
                {
                    z = 0;
                    maxZ = seq.getSizeZ() - 1;
                }

                final int c = lutChannel.getChannel();

                for (; t <= maxT; t++)
                {
                    for (; z <= maxZ; z++)
                    {
                        final Object data = seq.getDataXY(t, z, c);

                        // need to test for empty sequence
                        if (data != null)
                        {
                            final DataType dataType = seq.getDataType_();
                            final int len = Array.getLength(data);

                            for (int i = 0; i < len; i++)
                            {
                                if ((i & 0xFFF) == 0)
                                {
                                    // need to be recalculated so don't waste time here...
                                    if (ThreadUtil.hasWaitingBgSingleTask(histoUpdater))
                                        return;
                                }

                                histo.addValue(Array1DUtil.getValue(data, i, dataType));
                            }
                        }
                    }
                }
            }
            
            retry = 0;
        }
        catch (Exception e)
        {
            // just redo it later
            if (retry++ < 3)
                refreshHistoData();
        }
        finally
        {
            // notify that histogram computation is done
            histogram.done();

            // histogram changed in the meantime --> recompute
            if (histo != histogram.getHistogram())
                refreshHistoData();
        }
    }

    /**
     * @return the histogram
     */
    public HistogramPanel getHistogram()
    {
        return histogram;
    }

    /**
     * @return the histoData
     */
    public double[] getHistoData()
    {
        return histogram.getHistogramData();
    }

    /**
     * @return the scaler
     */
    public Scaler getScaler()
    {
        return lutChannel.getScaler();
    }

    public double getLowBound()
    {
        return lutChannel.getMin();
    }

    public double getHighBound()
    {
        return lutChannel.getMax();
    }

    void setLowBound(double value)
    {
        lutChannel.setMin(value);
    }

    void setHighBound(double value)
    {
        lutChannel.setMax(value);
    }

    /**
     * tasks to do on scaler changes
     */
    public void onScalerChanged()
    {
        final Scaler s = getScaler();

        histogram.setMinMaxIntValues(s.getAbsLeftIn(), s.getAbsRightIn(), s.isIntegerData());

        // repaint component now as bounds may have changed
        repaint();
    }

    /**
     * process on sequence change
     */
    void onSequenceDataChanged()
    {
        final LUTViewer lutViewer = viewer.getLutViewer();

        // update histogram
        if ((lutViewer != null) && lutViewer.getAutoRefreshHistogram())
            requestHistoDataRefresh();
    }

    /**
     * process on position changed
     */
    private void onPositionChanged()
    {
        final LUTViewer lutViewer = viewer.getLutViewer();

        // update histogram
        if ((lutViewer != null) && lutViewer.getAutoRefreshHistogram())
            requestHistoDataRefresh();
    }

    /**
     * @return the message
     */
    public String getMessage()
    {
        return message;
    }

    /**
     * @param value
     *        the message to set
     */
    public void setMessage(String value)
    {
        if (!StringUtil.equals(message, value))
        {
            message = value;
            repaint();
        }
    }

    /**
     * Should be called when histogram scaling type changed
     */
    public void scaleTypeChanged(boolean log)
    {
        if (log)
            histogram.setLogScaling(true);
        else
            histogram.setLogScaling(false);
    }

    /**
     * show popup menu
     */
    protected void showSettingPopup(final Point pos)
    {
        // rebuild menu
        final JPopupMenu menu = new JPopupMenu("Actions");

        final JMenuItem refreshItem = new JMenuItem("Refresh now");
        refreshItem.addActionListener(new ActionListener()
        {
            @Override
            public void actionPerformed(ActionEvent e)
            {
                requestHistoDataRefresh();
            }
        });
        final JMenuItem setBoundsItem = new JMenuItem("Set range");
        setBoundsItem.addActionListener(new ActionListener()
        {
            @Override
            public void actionPerformed(ActionEvent e)
            {
                showRangeSettingDialog();
            }
        });

        menu.add(refreshItem);
        menu.add(setBoundsItem);

        menu.pack();
        menu.validate();

        // display menu
        menu.show(this, pos.x, pos.y);
    }

    void showRangeSettingDialog()
    {
        final ScalerBoundsSettingDialog boundsSettingDialog = new ScalerBoundsSettingDialog(lutChannel);

        boundsSettingDialog.pack();
        boundsSettingDialog.setLocationRelativeTo(this);
        boundsSettingDialog.setVisible(true);
    }

    /**
     * Add a listener
     * 
     * @param listener
     */
    public void addScalerPositionListener(ScalerPositionListener listener)
    {
        scalerMapPositionListeners.add(ScalerPositionListener.class, listener);
    }

    /**
     * Remove a listener
     * 
     * @param listener
     */
    public void removeScalerPositionListener(ScalerPositionListener listener)
    {
        scalerMapPositionListeners.remove(ScalerPositionListener.class, listener);
    }

    /**
     * mouse position on scaler info changed
     */
    public void scalerPositionChanged(double index, int value, double normalizedValue)
    {
        for (ScalerPositionListener listener : scalerMapPositionListeners.getListeners(ScalerPositionListener.class))
            listener.positionChanged(index, value, normalizedValue);
    }

    @Override
    public void lutChannelChanged(LUTChannelEvent event)
    {
        if (event.getType() == LUTChannelEventType.SCALER_CHANGED)
            onScalerChanged();
    }

    @Override
    public void viewerChanged(ViewerEvent event)
    {
        if (event.getType() == ViewerEventType.POSITION_CHANGED)
            onPositionChanged();
    }

    @Override
    public void viewerClosed(Viewer viewer)
    {
        viewer.removeListener(this);
    }

    @Override
    public void sequenceChanged(SequenceEvent sequenceEvent)
    {
        if (sequenceEvent.getSourceType() == SequenceEventSourceType.SEQUENCE_DATA)
            onSequenceDataChanged();
    }

    @Override
    public void sequenceClosed(Sequence sequence)
    {
        sequence.removeListener(this);
    }

}