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

import icy.action.CanvasActions;
import icy.action.CanvasActions.ToggleLayersAction;
import icy.action.ViewerActions;
import icy.action.WindowActions;
import icy.canvas.IcyCanvas;
import icy.canvas.IcyCanvas2D;
import icy.canvas.IcyCanvasEvent;
import icy.canvas.IcyCanvasListener;
import icy.common.MenuCallback;
import icy.common.listener.ProgressListener;
import icy.gui.component.button.IcyButton;
import icy.gui.component.button.IcyToggleButton;
import icy.gui.component.renderer.LabelComboBoxRenderer;
import icy.gui.dialog.ConfirmDialog;
import icy.gui.dialog.MessageDialog;
import icy.gui.dialog.SaverDialog;
import icy.gui.frame.IcyFrame;
import icy.gui.frame.IcyFrameAdapter;
import icy.gui.frame.IcyFrameEvent;
import icy.gui.lut.LUTViewer;
import icy.gui.lut.abstract_.IcyLutViewer;
import icy.gui.plugin.PluginComboBoxRenderer;
import icy.gui.util.ComponentUtil;
import icy.gui.viewer.ViewerEvent.ViewerEventType;
import icy.image.IcyBufferedImage;
import icy.image.lut.LUT;
import icy.main.Icy;
import icy.plugin.PluginLoader;
import icy.plugin.PluginLoader.PluginLoaderEvent;
import icy.plugin.PluginLoader.PluginLoaderListener;
import icy.plugin.interface_.PluginCanvas;
import icy.preferences.GeneralPreferences;
import icy.sequence.DimensionId;
import icy.sequence.Sequence;
import icy.sequence.SequenceEvent;
import icy.sequence.SequenceListener;
import icy.system.IcyExceptionHandler;
import icy.system.IcyHandledException;
import icy.system.thread.ThreadUtil;
import icy.util.GraphicsUtil;
import icy.util.Random;
import icy.util.StringUtil;

import java.awt.AlphaComposite;
import java.awt.BorderLayout;
import java.awt.Color;
import java.awt.Dimension;
import java.awt.Font;
import java.awt.Graphics;
import java.awt.Graphics2D;
import java.awt.KeyboardFocusManager;
import java.awt.RenderingHints;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.awt.event.KeyEvent;
import java.awt.event.KeyListener;
import java.awt.geom.Rectangle2D;
import java.awt.image.BufferedImage;
import java.beans.PropertyChangeEvent;
import java.util.ArrayList;

import javax.swing.ActionMap;
import javax.swing.BorderFactory;
import javax.swing.Box;
import javax.swing.InputMap;
import javax.swing.JComboBox;
import javax.swing.JComponent;
import javax.swing.JLabel;
import javax.swing.JMenu;
import javax.swing.JMenuItem;
import javax.swing.JOptionPane;
import javax.swing.JPanel;
import javax.swing.JToolBar;
import javax.swing.WindowConstants;
import javax.swing.event.EventListenerList;

/**
 * Viewer send an event if the IcyCanvas change.
 * 
 * @author Fabrice de Chaumont & Stephane
 */
public class Viewer extends IcyFrame implements KeyListener, SequenceListener, IcyCanvasListener, PluginLoaderListener
{
    private class ViewerMainPanel extends JPanel
    {
        public ViewerMainPanel()
        {
            super();
        }

        public void drawTextCenter(Graphics2D g, String text, float alpha)
        {
            final Rectangle2D rect = GraphicsUtil.getStringBounds(g, text);
            final int w = (int) rect.getWidth();
            final int h = (int) rect.getHeight();
            final int x = (getWidth() - (w + 8 + 2)) / 2;
            final int y = (getHeight() - (h + 8 + 2)) / 2;

            g.setColor(Color.gray);
            g.setComposite(AlphaComposite.getInstance(AlphaComposite.SRC_OVER, alpha));
            g.fillRoundRect(x, y, w + 8, h + 8, 8, 8);

            g.setColor(Color.white);
            g.drawString(text, x + 4, y + 2 + h);
        }

        @Override
        public void paint(Graphics g)
        {
            // currently modifying canvas ?
            super.paint(g);

            // display a message
            if (settingCanvas)
            {
                final Graphics2D g2 = (Graphics2D) g.create();

                g2.setFont(new Font("Arial", Font.BOLD, 16));
                g2.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON);
                drawTextCenter(g2, "Loading canvas...", 0.8f);
                g2.dispose();
            }
        }
    }

    /**
     * associated LUT
     */
    private LUT lut;
    /**
     * associated canvas
     */
    private IcyCanvas canvas;
    /**
     * associated sequence
     */
    Sequence sequence;

    /***/
    private final EventListenerList listeners = new EventListenerList();

    /**
     * GUI
     */
    private JToolBar toolBar;
    private ViewerMainPanel mainPanel;
    private LUTViewer lutViewer;

    JComboBox canvasComboBox;
    JComboBox lockComboBox;
    IcyToggleButton layersEnabledButton;
    IcyButton screenShotButton;
    IcyButton screenShotAlternateButton;
    IcyButton duplicateButton;
    IcyButton switchStateButton;

    /**
     * internals
     */
    boolean initialized;
    private final Runnable lutUpdater;
    boolean settingCanvas;

    public Viewer(Sequence sequence, boolean visible)
    {
        super("Viewer", true, true, true, true);

        if (sequence == null)
            throw new IllegalArgumentException("Can't open a null sequence.");

        this.sequence = sequence;

        // default
        canvas = null;
        lut = null;
        lutViewer = null;
        initialized = false;
        settingCanvas = false;

        lutUpdater = new Runnable()
        {
            @Override
            public void run()
            {
                // don't need to update that too much
                ThreadUtil.sleep(1);

                ThreadUtil.invokeNow(new Runnable()
                {
                    @Override
                    public void run()
                    {
                        final LUT lut = getLut();

                        // closed --> ignore
                        if (lut != null)
                        {
                            // refresh LUT viewer
                            setLutViewer(new LUTViewer(Viewer.this, lut));
                            // notify
                            fireViewerChanged(ViewerEventType.LUT_CHANGED);
                        }
                    }
                });
            }
        };

        mainPanel = new ViewerMainPanel();
        mainPanel.setLayout(new BorderLayout());

        // set menu directly in system menu so we don't need a extra MenuBar
        setSystemMenuCallback(new MenuCallback()
        {
            @Override
            public JMenu getMenu()
            {
                return Viewer.this.getMenu();
            }
        });

        // build tool bar
        buildToolBar();

        // create a new compatible LUT
        final LUT lut = sequence.createCompatibleLUT();
        // restore user LUT if needed
        if (sequence.hasUserLUT())
        {
            final LUT userLut = sequence.getUserLUT();

            // restore colormaps (without alpha)
            lut.setColorMaps(userLut, false);
            // then restore scalers
            lut.setScalers(userLut);
        }

        // set lut (this modify lutPanel)
        setLut(lut);
        // set default canvas to first available canvas plugin (Canvas2D should be first)
        setCanvas(IcyCanvas.getCanvasPluginNames().get(0));

        setLayout(new BorderLayout());

        add(toolBar, BorderLayout.NORTH);
        add(mainPanel, BorderLayout.CENTER);

        // setting frame
        refreshViewerTitle();
        setFocusable(true);
        // set position depending window mode
        setLocationInternal(20 + Random.nextInt(100), 20 + Random.nextInt(60));
        setLocationExternal(100 + Random.nextInt(200), 100 + Random.nextInt(150));
        setSize(640, 480);

        // initial position in sequence
        if (sequence.isEmpty())
            setPositionZ(0);
        else
            setPositionZ(((sequence.getSizeZ() + 1) / 2) - 1);

        addFrameListener(new IcyFrameAdapter()
        {
            @Override
            public void icyFrameOpened(IcyFrameEvent e)
            {
                if (!initialized)
                {
                    if ((Viewer.this.sequence != null) && !Viewer.this.sequence.isEmpty())
                    {
                        adjustViewerToImageSize();
                        initialized = true;
                    }
                }
            }

            @Override
            public void icyFrameActivated(IcyFrameEvent e)
            {
                Icy.getMainInterface().setActiveViewer(Viewer.this);
            }

            @Override
            public void icyFrameExternalized(IcyFrameEvent e)
            {
                refreshToolBar();
            }

            @Override
            public void icyFrameInternalized(IcyFrameEvent e)
            {
                refreshToolBar();
            }

            @Override
            public void icyFrameClosing(IcyFrameEvent e)
            {
                onClosing();
            }
        });

        addKeyListener(this);
        sequence.addListener(this);
        PluginLoader.addListener(this);

        // do this when viewer is initialized
        Icy.getMainInterface().registerViewer(this);
        // automatically add it to the desktop pane
        addToDesktopPane();

        if (visible)
        {
            setVisible(true);
            requestFocus();
        }
        else
            setVisible(false);

        // can be done after setVisible
        buildActionMap();
    }

    public Viewer(Sequence sequence)
    {
        this(sequence, true);
    }

    void buildActionMap()
    {
        // global input map
        buildActionMap(getInputMap(JComponent.WHEN_FOCUSED), getActionMap());
    }

    protected void buildActionMap(InputMap imap, ActionMap amap)
    {
        imap.put(WindowActions.gridTileAction.getKeyStroke(), WindowActions.gridTileAction.getName());
        imap.put(WindowActions.horizontalTileAction.getKeyStroke(), WindowActions.horizontalTileAction.getName());
        imap.put(WindowActions.verticalTileAction.getKeyStroke(), WindowActions.verticalTileAction.getName());
        imap.put(CanvasActions.globalDisableSyncAction.getKeyStroke(), CanvasActions.globalDisableSyncAction.getName());
        imap.put(CanvasActions.globalSyncGroup1Action.getKeyStroke(), CanvasActions.globalSyncGroup1Action.getName());
        imap.put(CanvasActions.globalSyncGroup2Action.getKeyStroke(), CanvasActions.globalSyncGroup2Action.getName());
        imap.put(CanvasActions.globalSyncGroup3Action.getKeyStroke(), CanvasActions.globalSyncGroup3Action.getName());
        imap.put(CanvasActions.globalSyncGroup4Action.getKeyStroke(), CanvasActions.globalSyncGroup4Action.getName());

        amap.put(WindowActions.gridTileAction.getName(), WindowActions.gridTileAction);
        amap.put(WindowActions.horizontalTileAction.getName(), WindowActions.horizontalTileAction);
        amap.put(WindowActions.verticalTileAction.getName(), WindowActions.verticalTileAction);
        amap.put(CanvasActions.globalDisableSyncAction.getName(), CanvasActions.globalDisableSyncAction);
        amap.put(CanvasActions.globalSyncGroup1Action.getName(), CanvasActions.globalSyncGroup1Action);
        amap.put(CanvasActions.globalSyncGroup2Action.getName(), CanvasActions.globalSyncGroup2Action);
        amap.put(CanvasActions.globalSyncGroup3Action.getName(), CanvasActions.globalSyncGroup3Action);
        amap.put(CanvasActions.globalSyncGroup4Action.getName(), CanvasActions.globalSyncGroup4Action);
    }

    /**
     * Called when user want to close viewer
     */
    protected void onClosing()
    {
        // this sequence was not saved
        if ((sequence != null) && (sequence.getFilename() == null))
        {
            // save new sequence enabled ?
            if (GeneralPreferences.getSaveNewSequence())
            {
                final int res = ConfirmDialog.confirmEx("Save sequence", "Do you want to save '" + sequence.getName()
                        + "' before closing it ?", ConfirmDialog.YES_NO_CANCEL_OPTION);

                switch (res)
                {
                    case JOptionPane.YES_OPTION:
                        // save the image
                        new SaverDialog(sequence);

                    case JOptionPane.NO_OPTION:
                        setDefaultCloseOperation(WindowConstants.DISPOSE_ON_CLOSE);
                        break;

                    case JOptionPane.CANCEL_OPTION:
                    default:
                        setDefaultCloseOperation(WindowConstants.DO_NOTHING_ON_CLOSE);
                        break;
                }

                return;
            }
        }

        // just close it
        setDefaultCloseOperation(WindowConstants.DISPOSE_ON_CLOSE);
    }

    /**
     * Called when viewer is closed.<br>
     * Release as much references we can here because of the
     * <a href="http://bugs.sun.com/bugdatabase/view_bug.do?bug_id=4759312">
     * JInternalFrame bug</a>.
     */
    @Override
    public void onClosed()
    {
        // notify close
        fireViewerClosed();

        // remove listeners
        sequence.removeListener(this);
        if (canvas != null)
            canvas.removeCanvasListener(this);
        PluginLoader.removeListener(this);

        icy.main.Icy.getMainInterface().unRegisterViewer(this);

        // AWT JDesktopPane keep reference on last closed JInternalFrame
        // it's good to free as much reference we can here
        if (canvas != null)
            canvas.shutDown();
        if (lutViewer != null)
            lutViewer.dispose();
        if (mainPanel != null)
            mainPanel.removeAll();
        if (toolBar != null)
            toolBar.removeAll();

        // remove all listeners for this viewer
        ViewerListener[] vls = listeners.getListeners(ViewerListener.class);
        for (ViewerListener vl : vls)
            listeners.remove(ViewerListener.class, vl);

        lutViewer = null;
        mainPanel = null;

        canvas = null;
        sequence = null;
        lut = null;
        toolBar = null;
        canvasComboBox = null;
        lockComboBox = null;
        duplicateButton = null;
        layersEnabledButton = null;
        screenShotAlternateButton = null;
        screenShotButton = null;
        switchStateButton = null;

        super.onClosed();
    }

    void adjustViewerToImageSize()
    {
        if (canvas instanceof IcyCanvas2D)
        {
            final IcyCanvas2D cnv = (IcyCanvas2D) canvas;

            final int ix = cnv.getImageSizeX();
            final int iy = cnv.getImageSizeY();

            if ((ix > 0) && (iy > 0))
            {
                // find scale factor to fit image in a 640x540 sized window
                // and limit zoom to 100%
                final double scale = Math.min(Math.min(640d / ix, 540d / iy), 1d);

                cnv.setScaleX(scale);
                cnv.setScaleY(scale);

                // this actually resize viewer as canvas size depend from it
                cnv.fitCanvasToImage();
            }
        }

        // minimum size to start : 400, 240
        final Dimension size = new Dimension(Math.max(getWidth(), 400), Math.max(getHeight(), 240));
        // minimum size global : 200, 140
        final Dimension minSize = new Dimension(200, 140);

        // adjust size of both frames
        setSizeExternal(size);
        setSizeInternal(size);
        setMinimumSizeInternal(minSize);
        setMinimumSizeExternal(minSize);
    }

    /**
     * Rebuild and return viewer menu
     */
    JMenu getMenu()
    {
        final JMenu result = getDefaultSystemMenu();

        final JMenuItem overlayItem = new JMenuItem(CanvasActions.toggleLayersAction);
        if ((canvas != null) && canvas.isLayersVisible())
            overlayItem.setText("Hide layers");
        else
            overlayItem.setText("Show layers");
        final JMenuItem duplicateItem = new JMenuItem(ViewerActions.duplicateAction);

        // set menu
        result.insert(overlayItem, 0);
        result.insertSeparator(1);
        result.insert(duplicateItem, 2);

        return result;
    }

    protected void buildLockCombo()
    {
        final ArrayList<JLabel> labels = new ArrayList<JLabel>();

        // get sync action labels
        labels.add(CanvasActions.disableSyncAction.getLabelComponent(true, false));
        labels.add(CanvasActions.syncGroup1Action.getLabelComponent(true, false));
        labels.add(CanvasActions.syncGroup2Action.getLabelComponent(true, false));
        labels.add(CanvasActions.syncGroup3Action.getLabelComponent(true, false));
        labels.add(CanvasActions.syncGroup4Action.getLabelComponent(true, false));

        // build comboBox with lock id
        lockComboBox = new JComboBox(labels.toArray());
        // set specific renderer
        lockComboBox.setRenderer(new LabelComboBoxRenderer(lockComboBox));
        // limit size
        ComponentUtil.setFixedWidth(lockComboBox, 48);
        lockComboBox.setToolTipText("Select synchronisation group");
        // don't want focusable here
        lockComboBox.setFocusable(false);
        // needed because of VTK
        lockComboBox.setLightWeightPopupEnabled(false);

        // action on canvas change
        lockComboBox.addActionListener(new ActionListener()
        {
            @Override
            public void actionPerformed(ActionEvent e)
            {
                // adjust lock id
                setViewSyncId(lockComboBox.getSelectedIndex());
            }
        });
    }

    void buildCanvasCombo()
    {
        // build comboBox with canvas plugins
        canvasComboBox = new JComboBox(IcyCanvas.getCanvasPluginNames().toArray());
        // specific renderer
        canvasComboBox.setRenderer(new PluginComboBoxRenderer(canvasComboBox, false));
        // limit size
        ComponentUtil.setFixedWidth(canvasComboBox, 48);
        canvasComboBox.setToolTipText("Select canvas type");
        // don't want focusable here
        canvasComboBox.setFocusable(false);
        // needed because of VTK
        canvasComboBox.setLightWeightPopupEnabled(false);

        // action on canvas change
        canvasComboBox.addActionListener(new ActionListener()
        {
            @Override
            public void actionPerformed(ActionEvent e)
            {
                // set selected canvas
                setCanvas((String) canvasComboBox.getSelectedItem());
            }
        });
    }

    /**
     * build the toolBar
     */
    protected void buildToolBar()
    {
        // build combo box
        buildLockCombo();
        buildCanvasCombo();

        // build buttons
        layersEnabledButton = new IcyToggleButton(new ToggleLayersAction(true));
        layersEnabledButton.setHideActionText(true);
        layersEnabledButton.setFocusable(false);
        layersEnabledButton.setSelected(true);
        screenShotButton = new IcyButton(CanvasActions.screenShotAction);
        screenShotButton.setFocusable(false);
        screenShotButton.setHideActionText(true);
        screenShotAlternateButton = new IcyButton(CanvasActions.screenShotAlternateAction);
        screenShotAlternateButton.setFocusable(false);
        screenShotAlternateButton.setHideActionText(true);
        duplicateButton = new IcyButton(ViewerActions.duplicateAction);
        duplicateButton.setFocusable(false);
        duplicateButton.setHideActionText(true);
        // duplicateButton.setToolTipText("Duplicate view (no data duplication)");
        switchStateButton = new IcyButton(getSwitchStateAction());
        switchStateButton.setFocusable(false);
        switchStateButton.setHideActionText(true);

        // and build the toolbar
        toolBar = new JToolBar();
        toolBar.setFloatable(false);
        // so we don't have any border
        toolBar.setBorder(BorderFactory.createEmptyBorder(1, 0, 1, 0));
        ComponentUtil.setPreferredHeight(toolBar, 26);

        updateToolbarComponents();
    }

    void updateToolbarComponents()
    {
        if (toolBar != null)
        {
            toolBar.removeAll();

            toolBar.add(lockComboBox);
            toolBar.addSeparator();
            toolBar.add(canvasComboBox);
            toolBar.addSeparator();
            toolBar.add(layersEnabledButton);
            if (canvas != null)
                canvas.customizeToolbar(toolBar);
            toolBar.add(Box.createHorizontalGlue());
            toolBar.addSeparator();
            toolBar.add(screenShotButton);
            toolBar.add(screenShotAlternateButton);
            toolBar.addSeparator();
            toolBar.add(duplicateButton);
            toolBar.add(switchStateButton);
        }
    }

    void refreshLockCombo()
    {
        final int syncId = getViewSyncId();

        lockComboBox.setEnabled(isSynchronizedViewSupported());
        lockComboBox.setSelectedIndex(syncId);

        switch (syncId)
        {
            case 0:
                lockComboBox.setBackground(Color.gray);
                lockComboBox.setToolTipText("Synchronization disabled");
                break;

            case 1:
                lockComboBox.setBackground(Color.green);
                lockComboBox.setToolTipText("Full synchronization group 1 (view and Z/T position)");
                break;

            case 2:
                lockComboBox.setBackground(Color.yellow);
                lockComboBox.setToolTipText("Full synchronization group 2 (view and Z/T position)");
                break;

            case 3:
                lockComboBox.setBackground(Color.blue);
                lockComboBox.setToolTipText("View synchronization group (view synched but not Z/T position)");
                break;

            case 4:
                lockComboBox.setBackground(Color.red);
                lockComboBox.setToolTipText("Slice synchronization group (Z/T position synched but not view)");
                break;
        }
    }

    void refreshCanvasCombo()
    {
        if (canvas != null)
        {
            // get plugin class name for this canvas
            final String pluginName = IcyCanvas.getPluginClassName(canvas.getClass().getName());

            if (pluginName != null)
            {
                // align canvas combo to plugin name
                if (!canvasComboBox.getSelectedItem().equals(pluginName))
                    canvasComboBox.setSelectedItem(pluginName);
            }
        }
    }

    void refreshToolBar()
    {
        // FIXME : switchStateButton stay selected after action

        final boolean layersVisible = (canvas != null) ? canvas.isLayersVisible() : false;

        layersEnabledButton.setSelected(layersVisible);
        if (layersVisible)
            layersEnabledButton.setToolTipText("Hide layers");
        else
            layersEnabledButton.setToolTipText("Show layers");

        // refresh combos
        refreshLockCombo();
        refreshCanvasCombo();
    }

    /**
     * @return the sequence
     */
    public Sequence getSequence()
    {
        return sequence;
    }

    /**
     * Set the specified LUT for the viewer.
     */
    public void setLut(LUT value)
    {
        if ((lut != value) && sequence.isLutCompatible(value))
        {
            // set new lut & notify change
            lut = value;
            lutChanged();
        }
    }

    /**
     * Returns the viewer LUT
     */
    public LUT getLut()
    {
        // have to test this as we release sequence reference on closed
        if (sequence == null)
            return lut;

        // sequence can be asynchronously modified so we have to test change on Getter
        if ((lut == null) || !sequence.isLutCompatible(lut))
        {
            // sequence type has changed, we need to recreate a compatible LUT
            final LUT newLut = sequence.createCompatibleLUT();

            // keep the color map of previous LUT if they have the same number of channels
            if ((lut != null) && (lut.getNumChannel() == newLut.getNumChannel()))
                newLut.getColorSpace().setColorMaps(lut.getColorSpace(), true);

            // set the new lut
            setLut(newLut);
        }

        return lut;
    }

    /**
     * Set the specified canvas for the viewer (from the {@link PluginCanvas} class name).
     * 
     * @see IcyCanvas#getCanvasPluginNames()
     */
    public void setCanvas(String pluginClassName)
    {
        // not the same canvas ?
        if ((canvas == null) || !canvas.getClass().getName().equals(IcyCanvas.getCanvasClassName(pluginClassName)))
        {
            try
            {
                IcyCanvas newCanvas;

                settingCanvas = true;
                // show loading message
                mainPanel.paintImmediately(mainPanel.getBounds());
                try
                {
                    // try to create the new canvas
                    newCanvas = IcyCanvas.create(pluginClassName, this);
                }
                catch (Throwable e)
                {
                    if (e instanceof IcyHandledException)
                    {
                        // just ignore
                    }
                    else if (e instanceof UnsupportedOperationException)
                    {
                        MessageDialog.showDialog(e.getLocalizedMessage(), MessageDialog.ERROR_MESSAGE);
                    }
                    else if (e instanceof Exception)
                    {
                        IcyExceptionHandler.handleException(new ClassNotFoundException("Cannot find '"
                                + pluginClassName + "' class --> cannot create the canvas.", e), true);
                    }
                    else
                        IcyExceptionHandler.handleException(e, true);

                    // create a new instance of current canvas
                    newCanvas = IcyCanvas.create(canvas.getClass().getName(), this);
                }
                finally
                {
                    settingCanvas = false;
                }

                final int saveX;
                final int saveY;
                final int saveZ;
                final int saveT;
                final int saveC;

                // save properties and shutdown previous canvas
                if (canvas != null)
                {
                    // save position
                    saveX = canvas.getPositionX();
                    saveY = canvas.getPositionY();
                    saveZ = canvas.getPositionZ();
                    saveT = canvas.getPositionT();
                    saveC = canvas.getPositionC();

                    canvas.removePropertyChangeListener(IcyCanvas.PROPERTY_LAYERS_VISIBLE, this);
                    canvas.removeCanvasListener(this);
                    // --> this actually can do some restore operation (as the palette) after creation of the new canvas
                    canvas.shutDown();
                    // remove from mainPanel
                    mainPanel.remove(canvas);
                }
                else
                    saveX = saveY = saveZ = saveT = saveC = -1;

                // prepare new canvas
                newCanvas.addCanvasListener(this);
                newCanvas.addPropertyChangeListener(IcyCanvas.PROPERTY_LAYERS_VISIBLE, this);
                // add to mainPanel
                mainPanel.add(newCanvas, BorderLayout.CENTER);

                // restore position
                if (saveX != -1)
                    newCanvas.setPositionX(saveX);
                if (saveY != -1)
                    newCanvas.setPositionY(saveY);
                if (saveZ != -1)
                    newCanvas.setPositionZ(saveZ);
                if (saveT != -1)
                    newCanvas.setPositionT(saveT);
                if (saveC != -1)
                    newCanvas.setPositionC(saveC);

                // canvas set :)
                canvas = newCanvas;
            }
            catch (Throwable e)
            {
                IcyExceptionHandler.handleException(e, true);
            }

            mainPanel.revalidate();

            // refresh viewer menu (so overlay checkbox is correctly set)
            updateSystemMenu();
            updateToolbarComponents();
            refreshToolBar();

            // fix the OSX lost keyboard focus on canvas change in detached mode.
            KeyboardFocusManager.getCurrentKeyboardFocusManager().upFocusCycle(getCanvas());

            // notify canvas changed to listener
            fireViewerChanged(ViewerEventType.CANVAS_CHANGED);
        }
    }

    /**
     * @deprecated Use {@link #setCanvas(String)} instead.
     */
    @Deprecated
    public void setCanvas(IcyCanvas value)
    {
        if (canvas == value)
            return;

        final int saveX;
        final int saveY;
        final int saveZ;
        final int saveT;
        final int saveC;

        if (canvas != null)
        {
            // save position
            saveX = canvas.getPositionX();
            saveY = canvas.getPositionY();
            saveZ = canvas.getPositionZ();
            saveT = canvas.getPositionT();
            saveC = canvas.getPositionC();

            canvas.removePropertyChangeListener(IcyCanvas.PROPERTY_LAYERS_VISIBLE, this);
            canvas.removeCanvasListener(this);
            canvas.shutDown();
            // remove from mainPanel
            mainPanel.remove(canvas);
        }
        else
            saveX = saveY = saveZ = saveT = saveC = -1;

        // set new canvas
        canvas = value;

        if (canvas != null)
        {
            canvas.addCanvasListener(this);
            canvas.addPropertyChangeListener(IcyCanvas.PROPERTY_LAYERS_VISIBLE, this);
            // add to mainPanel
            mainPanel.add(canvas, BorderLayout.CENTER);

            // restore position
            if (saveX != -1)
                canvas.setPositionX(saveX);
            if (saveY != -1)
                canvas.setPositionY(saveY);
            if (saveZ != -1)
                canvas.setPositionZ(saveZ);
            if (saveT != -1)
                canvas.setPositionT(saveT);
            if (saveC != -1)
                canvas.setPositionC(saveC);
        }

        mainPanel.revalidate();

        // refresh viewer menu (so overlay checkbox is correctly set)
        updateSystemMenu();
        updateToolbarComponents();
        refreshToolBar();

        // fix the OSX lost keyboard focus on canvas change in detached mode.
        KeyboardFocusManager.getCurrentKeyboardFocusManager().upFocusCycle(getCanvas());

        // notify canvas changed to listener
        fireViewerChanged(ViewerEventType.CANVAS_CHANGED);
    }

    /**
     * Returns true if the viewer initialization (correct image resizing) is completed.
     */
    public boolean isInitialized()
    {
        return initialized;
    }

    /**
     * Return the viewer Canvas object
     */
    public IcyCanvas getCanvas()
    {
        return canvas;
    }

    /**
     * Return the viewer Canvas panel
     */
    public JPanel getCanvasPanel()
    {
        if (canvas != null)
            return canvas.getPanel();

        return null;
    }

    /**
     * Return the viewer Lut panel
     */
    public LUTViewer getLutViewer()
    {
        return lutViewer;
    }

    /**
     * Set the {@link LUTViewer} for this viewer.
     */
    public void setLutViewer(LUTViewer value)
    {
        if (lutViewer != value)
        {
            if (lutViewer != null)
                lutViewer.dispose();
            lutViewer = value;
        }
    }

    /**
     * @deprecated Use {@link #getLutViewer()} instead
     */
    @Deprecated
    public IcyLutViewer getLutPanel()
    {
        return getLutViewer();
    }

    /**
     * @deprecated Use {@link #setLutViewer(LUTViewer)} instead.
     */
    @Deprecated
    public void setLutPanel(IcyLutViewer lutViewer)
    {
        setLutViewer((LUTViewer) lutViewer);
    }

    /**
     * Return the viewer ToolBar object
     */
    public JToolBar getToolBar()
    {
        return toolBar;
    }

    /**
     * @return current T (-1 if all selected/displayed)
     */
    public int getPositionT()
    {
        if (canvas != null)
            return canvas.getPositionT();

        return 0;
    }

    /**
     * Set the current T position (for multi frame sequence).
     */
    public void setPositionT(int t)
    {
        if (canvas != null)
            canvas.setPositionT(t);
    }

    /**
     * @return current Z (-1 if all selected/displayed)
     */
    public int getPositionZ()
    {
        if (canvas != null)
            return canvas.getPositionZ();

        return 0;
    }

    /**
     * Set the current Z position (for stack sequence).
     */
    public void setPositionZ(int z)
    {
        if (canvas != null)
            canvas.setPositionZ(z);
    }

    /**
     * @return current C (-1 if all selected/displayed)
     */
    public int getPositionC()
    {
        if (canvas != null)
            return canvas.getPositionC();

        return 0;
    }

    /**
     * Set the current C (channel) position (multi channel sequence)
     */
    public void setPositionC(int c)
    {
        if (canvas != null)
            canvas.setPositionC(c);
    }

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

    /**
     * @deprecated Use {@link #setPositionT(int)} instead.
     */
    @Deprecated
    public void setT(int t)
    {
        setPositionT(t);
    }

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

    /**
     * @deprecated Use {@link #setPositionZ(int)} instead.
     */
    @Deprecated
    public void setZ(int z)
    {
        setPositionZ(z);
    }

    /**
     * @deprecated Use {@link #getPositionZ()} instead.
     */
    @Deprecated
    public int getC()
    {
        return getPositionC();
    }

    /**
     * @deprecated Use {@link #setPositionZ(int)} instead.
     */
    @Deprecated
    public void setC(int c)
    {
        setPositionC(c);
    }

    /**
     * Get maximum T value
     */
    public int getMaxT()
    {
        if (canvas != null)
            return canvas.getMaxPositionT();

        return 0;
    }

    /**
     * Get maximum Z value
     */
    public int getMaxZ()
    {
        if (canvas != null)
            return canvas.getMaxPositionZ();

        return 0;
    }

    /**
     * Get maximum C value
     */
    public int getMaxC()
    {
        if (canvas != null)
            return canvas.getMaxPositionC();

        return 0;
    }

    /**
     * return true if current canvas's viewer does support synchronized view
     */
    public boolean isSynchronizedViewSupported()
    {
        if (canvas != null)
            return canvas.isSynchronizationSupported();

        return false;
    }

    /**
     * @return the viewSyncId
     */
    public int getViewSyncId()
    {
        if (canvas != null)
            return canvas.getSyncId();

        return -1;
    }

    /**
     * Set the view synchronization group id (0 means unsynchronized).
     * 
     * @param id
     *        the view synchronization id to set
     * @see IcyCanvas#setSyncId(int)
     */
    public boolean setViewSyncId(int id)
    {
        if (canvas != null)
            return canvas.setSyncId(id);

        return false;
    }

    /**
     * Return true if this viewer has its view synchronized
     */
    public boolean isViewSynchronized()
    {
        if (canvas != null)
            return canvas.isSynchronized();

        return false;
    }

    /**
     * Delegation for {@link IcyCanvas#getImage(int, int, int)}
     */
    public IcyBufferedImage getImage(int t, int z, int c)
    {
        if (canvas != null)
            return canvas.getImage(t, z, c);

        return null;
    }

    /**
     * @deprecated Use {@link #getImage(int, int, int)} with C = -1 instead.
     */
    @Deprecated
    public IcyBufferedImage getImage(int t, int z)
    {
        return getImage(t, z, -1);
    }

    /**
     * Get the current image
     * 
     * @return current image
     */
    public IcyBufferedImage getCurrentImage()
    {
        if (canvas != null)
            return canvas.getCurrentImage();

        return null;
    }

    /**
     * Return the number of "selected" samples
     */
    public int getNumSelectedSamples()
    {
        if (canvas != null)
            return canvas.getNumSelectedSamples();

        return 0;
    }

    /**
     * @see icy.canvas.IcyCanvas#getRenderedImage(int, int, int, boolean)
     */
    public BufferedImage getRenderedImage(int t, int z, int c, boolean canvasView)
    {
        if (canvas == null)
            return null;

        return canvas.getRenderedImage(t, z, c, canvasView);
    }

    /**
     * @see icy.canvas.IcyCanvas#getRenderedSequence(boolean, icy.common.listener.ProgressListener)
     */
    public Sequence getRenderedSequence(boolean canvasView, ProgressListener progressListener)
    {
        if (canvas == null)
            return null;

        return canvas.getRenderedSequence(canvasView, progressListener);
    }

    /**
     * Returns the T navigation panel.
     */
    protected TNavigationPanel getTNavigationPanel()
    {
        if (canvas == null)
            return null;

        return canvas.getTNavigationPanel();
    }

    /**
     * Returns the frame rate (given in frame per second) for play command.
     */
    public int getFrameRate()
    {
        final TNavigationPanel tNav = getTNavigationPanel();

        if (tNav != null)
            return tNav.getFrameRate();

        return 0;
    }

    /**
     * Sets the frame rate (given in frame per second) for play command.
     */
    public void setFrameRate(int fps)
    {
        final TNavigationPanel tNav = getTNavigationPanel();

        if (tNav != null)
            tNav.setFrameRate(fps);
    }

    /**
     * Returns true if <code>repeat</code> is enabled for play command.
     */
    public boolean isRepeat()
    {
        final TNavigationPanel tNav = getTNavigationPanel();

        if (tNav != null)
            return tNav.isRepeat();

        return false;
    }

    /**
     * Set <code>repeat</code> mode for play command.
     */
    public void setRepeat(boolean value)
    {
        final TNavigationPanel tNav = getTNavigationPanel();

        if (tNav != null)
            tNav.setRepeat(value);
    }

    /**
     * Returns true if currently playing.
     */
    public boolean isPlaying()
    {
        final TNavigationPanel tNav = getTNavigationPanel();

        if (tNav != null)
            return tNav.isPlaying();

        return false;
    }

    /**
     * Start sequence play.
     * 
     * @see #stopPlay()
     * @see #setRepeat(boolean)
     */
    public void startPlay()
    {
        final TNavigationPanel tNav = getTNavigationPanel();

        if (tNav != null)
            tNav.startPlay();
    }

    /**
     * Stop sequence play.
     * 
     * @see #startPlay()
     */
    public void stopPlay()
    {
        final TNavigationPanel tNav = getTNavigationPanel();

        if (tNav != null)
            tNav.stopPlay();
    }

    /**
     * Return true if only this viewer is currently displaying its attached sequence
     */
    public boolean isUnique()
    {
        return Icy.getMainInterface().isUniqueViewer(this);
    }

    protected void lutChanged()
    {
        // can be called from external thread, replace it in AWT dispatch thread
        ThreadUtil.bgRunSingle(lutUpdater);
    }

    protected void positionChanged(DimensionId dim)
    {
        fireViewerChanged(ViewerEventType.POSITION_CHANGED, dim);
    }

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

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

    void fireViewerChanged(ViewerEventType eventType, DimensionId dim)
    {
        final ViewerEvent event = new ViewerEvent(this, eventType, dim);

        for (ViewerListener viewerListener : listeners.getListeners(ViewerListener.class))
            viewerListener.viewerChanged(event);
    }

    void fireViewerChanged(ViewerEventType event)
    {
        fireViewerChanged(event, DimensionId.NULL);
    }

    protected void fireViewerClosed()
    {
        for (ViewerListener viewerListener : listeners.getListeners(ViewerListener.class))
            viewerListener.viewerClosed(this);
    }

    @Override
    public void keyPressed(KeyEvent e)
    {
        // forward to canvas
        if ((canvas != null) && (!e.isConsumed()))
            canvas.keyPressed(e);
    }

    @Override
    public void keyReleased(KeyEvent e)
    {
        // forward to canvas
        if ((canvas != null) && (!e.isConsumed()))
            canvas.keyReleased(e);
    }

    @Override
    public void keyTyped(KeyEvent e)
    {
        // forward to canvas
        if ((canvas != null) && (!e.isConsumed()))
            canvas.keyTyped(e);
    }

    /**
     * Change the frame's title.
     */
    protected void refreshViewerTitle()
    {
        // have to test this as we release sequence reference on closed
        if (sequence != null)
            setTitle(sequence.getName());
    }

    @Override
    public void sequenceChanged(SequenceEvent event)
    {
        switch (event.getSourceType())
        {
            case SEQUENCE_META:
                final String meta = (String) event.getSource();

                if (StringUtil.isEmpty(meta) || StringUtil.equals(meta, Sequence.ID_NAME))
                    refreshViewerTitle();
                break;

            case SEQUENCE_DATA:

                break;

            case SEQUENCE_TYPE:
                // might need initialization
                if (!initialized && (sequence != null) && !sequence.isEmpty())
                {
                    adjustViewerToImageSize();
                    initialized = true;
                }

                // we update LUT on type change directly on getLut() method

                // // try to keep current LUT if possible
                if (!sequence.isLutCompatible(lut))
                    // need to update the lut according to the colormodel change
                    setLut(sequence.createCompatibleLUT());
                break;

            case SEQUENCE_COLORMAP:

                break;

            case SEQUENCE_COMPONENTBOUNDS:
                // refresh lut scalers from sequence default lut
                final LUT sequenceLut = sequence.getDefaultLUT();

                if (!sequenceLut.isCompatible(lut) || (lutViewer == null))
                    lut.setScalers(sequenceLut);
                break;
        }
    }

    @Override
    public void sequenceClosed(Sequence sequence)
    {

    }

    @Override
    public void canvasChanged(IcyCanvasEvent event)
    {
        switch (event.getType())
        {
            case POSITION_CHANGED:
                // common process on position change
                positionChanged(event.getDim());
                break;

            case SYNC_CHANGED:
                ThreadUtil.invokeLater(new Runnable()
                {
                    @Override
                    public void run()
                    {
                        refreshLockCombo();
                    }
                });
                break;
        }
    }

    @Override
    public void propertyChange(PropertyChangeEvent evt)
    {
        super.propertyChange(evt);

        // Canvas property "layer visible" changed ?
        if (StringUtil.equals(evt.getPropertyName(), IcyCanvas.PROPERTY_LAYERS_VISIBLE))
        {
            refreshToolBar();
            updateSystemMenu();
        }
    }

    @Override
    public void pluginLoaderChanged(PluginLoaderEvent e)
    {
        ThreadUtil.invokeLater(new Runnable()
        {
            @Override
            public void run()
            {
                // refresh available canvas
                buildCanvasCombo();
                // and refresh components
                refreshCanvasCombo();
                updateToolbarComponents();
            }
        });
    }
}