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

import icy.common.listener.AcceptListener;
import icy.common.listener.weak.WeakListener;
import icy.gui.frame.IcyFrame;
import icy.gui.inspector.InspectorPanel;
import icy.gui.inspector.LayersPanel;
import icy.gui.inspector.RoisPanel;
import icy.gui.main.MainEvent.MainEventSourceType;
import icy.gui.main.MainEvent.MainEventType;
import icy.gui.menu.ApplicationMenu;
import icy.gui.menu.ToolRibbonTask;
import icy.gui.viewer.Viewer;
import icy.gui.viewer.ViewerAdapter;
import icy.gui.viewer.ViewerEvent;
import icy.gui.viewer.ViewerListener;
import icy.image.IcyBufferedImage;
import icy.image.lut.LUT;
import icy.imagej.ImageJWrapper;
import icy.main.Icy;
import icy.painter.Overlay;
import icy.painter.OverlayWrapper;
import icy.painter.Painter;
import icy.plugin.abstract_.Plugin;
import icy.preferences.IcyPreferences;
import icy.preferences.XMLPreferences;
import icy.roi.ROI;
import icy.search.SearchEngine;
import icy.sequence.Sequence;
import icy.sequence.SequenceAdapter;
import icy.sequence.SequenceEvent;
import icy.sequence.SequenceListener;
import icy.swimmingPool.SwimmingPool;
import icy.system.thread.ThreadUtil;
import icy.undo.IcyUndoManager;
import icy.util.StringUtil;

import java.awt.Window;
import java.awt.event.WindowAdapter;
import java.awt.event.WindowEvent;
import java.beans.PropertyVetoException;
import java.lang.ref.WeakReference;
import java.util.ArrayList;
import java.util.HashSet;
import java.util.List;

import javax.swing.JFrame;
import javax.swing.JInternalFrame;
import javax.swing.JLayeredPane;
import javax.swing.event.EventListenerList;

/**
 * MainInterfaceGui
 * 
 * @author Fabrice de Chaumont & Stephane
 */
public class MainInterfaceGui implements MainInterface
{
    private class WeakAcceptListener extends WeakListener<AcceptListener> implements AcceptListener
    {
        public WeakAcceptListener(AcceptListener listener)
        {
            super(listener);
        }

        @Override
        public void removeListener(Object source)
        {
            internalRemoveCanExitListener(this);
        }

        @Override
        public boolean accept(Object source)
        {
            final AcceptListener listener = getListener();

            if (listener != null)
                return listener.accept(source);

            return true;
        }
    }

    // private final UpdateEventHandler updater;

    private final EventListenerList listeners;

    // use specific list for faster listeners retrieve
    private final List<MainListener> mainListeners;
    private final List<GlobalViewerListener> globalViewerListeners;
    private final List<GlobalSequenceListener> globalSequenceListeners;
    private final List<GlobalROIListener> globalROIListeners;
    private final List<GlobalOverlayListener> globalOverlayListeners;

    /**
     * used to generate focused sequence & viewer events
     */
    private final ViewerListener activeViewerListener;
    private final SequenceListener sequenceListener;

    private final List<Viewer> viewers;
    private final List<Sequence> sequences;
    private final List<WeakReference<Plugin>> activePlugins;

    private final SwimmingPool swimmingPool;
    private final TaskFrameManager taskFrameManager;

    MainFrame mainFrame;

    Viewer previousActiveViewer;
    Viewer activeViewer;
    Sequence activeSequence;

    /**
     * Take care that MainInterface constructor do not call the {@link Icy#getMainInterface()} method.<br>
     * We use a separate {@link #init()} for that purpose.
     */
    public MainInterfaceGui()
    {
        listeners = new EventListenerList();
        mainListeners = new ArrayList<MainListener>();
        globalViewerListeners = new ArrayList<GlobalViewerListener>();
        globalSequenceListeners = new ArrayList<GlobalSequenceListener>();
        globalROIListeners = new ArrayList<GlobalROIListener>();
        globalOverlayListeners = new ArrayList<GlobalOverlayListener>();

        viewers = new ArrayList<Viewer>();
        sequences = new ArrayList<Sequence>();
        activePlugins = new ArrayList<WeakReference<Plugin>>();
        swimmingPool = new SwimmingPool();
        taskFrameManager = new TaskFrameManager();

        // active viewer listener
        activeViewerListener = new ViewerAdapter()
        {
            @Override
            public void viewerChanged(ViewerEvent event)
            {
                activeViewerChanged(event);
            }
        };

        // global sequence listener
        sequenceListener = new SequenceAdapter()
        {
            @Override
            public void sequenceChanged(SequenceEvent event)
            {
                MainInterfaceGui.this.sequenceChanged(event);
            }
        };

        mainFrame = null;

        previousActiveViewer = null;
        activeViewer = null;
        activeSequence = null;
    }

    @Override
    public void init()
    {
        // build main frame
        mainFrame = new MainFrame();
        mainFrame.init();
        mainFrame.addWindowListener(new WindowAdapter()
        {
            @Override
            public void windowClosing(WindowEvent e)
            {
                // exit application
                Icy.exit(false);
            }
        });

        taskFrameManager.init();
    }

    @Override
    public boolean isHeadLess()
    {
        // we are not head less with this interface
        return false;
    }

    @Override
    public void addSequence(Sequence sequence)
    {
        if (sequence != null)
        {
            final Sequence seq = sequence;

            // thread safe
            ThreadUtil.invokeLater(new Runnable()
            {
                @Override
                public void run()
                {
                    new Viewer(seq);
                }
            });
        }
    }

    @Override
    public ArrayList<JFrame> getExternalFrames()
    {
        final ArrayList<JFrame> result = new ArrayList<JFrame>();
        final Window[] windows = Window.getWindows();

        for (Window w : windows)
            if (w instanceof JFrame)
                result.add((JFrame) w);

        return result;
    }

    @Override
    public ArrayList<JInternalFrame> getInternalFrames()
    {
        if (mainFrame == null)
            return new ArrayList<JInternalFrame>();

        return mainFrame.getInternalFrames();

    }

    /**
     * @return the preferences
     */
    @Override
    public XMLPreferences getPreferences()
    {
        return IcyPreferences.applicationRoot();
    }

    @Override
    public InspectorPanel getInspector()
    {
        if (mainFrame == null)
            return null;

        return mainFrame.getInspector();
    }

    @Override
    public RoisPanel getRoisPanel()
    {
        if (mainFrame == null)
            return null;

        final InspectorPanel inspector = mainFrame.getInspector();
        if (inspector == null)
            return null;

        return inspector.getRoisPanel();
    }

    @Override
    public LayersPanel getLayersPanel()
    {
        if (mainFrame == null)
            return null;

        final InspectorPanel inspector = mainFrame.getInspector();
        if (inspector == null)
            return null;

        return inspector.getLayersPanel();
    }

    @Override
    public ArrayList<Plugin> getActivePlugins()
    {
        final ArrayList<Plugin> result = new ArrayList<Plugin>();

        synchronized (activePlugins)
        {
            for (WeakReference<Plugin> ref : activePlugins)
            {
                final Plugin plugin = ref.get();

                if (plugin != null)
                    result.add(plugin);
            }
        }

        return result;
    }

    @Override
    public LUT getActiveLUT()
    {
        if (activeViewer != null)
            return activeViewer.getLut();

        return null;
    }

    @Override
    public Viewer getActiveViewer()
    {
        return activeViewer;
    }

    @Override
    public Sequence getActiveSequence()
    {
        return activeSequence;
    }

    @Override
    public IcyBufferedImage getActiveImage()
    {
        if (activeViewer != null)
            return activeViewer.getCurrentImage();

        return null;
    }

    @Override
    @Deprecated
    public Viewer getFocusedViewer()
    {
        return getActiveViewer();
    }

    @Override
    @Deprecated
    public Sequence getFocusedSequence()
    {
        return getActiveSequence();
    }

    @Override
    @Deprecated
    public IcyBufferedImage getFocusedImage()
    {
        return getActiveImage();
    }

    @Override
    public IcyUndoManager getUndoManager()
    {
        if (activeSequence != null)
            return activeSequence.getUndoManager();

        return null;
    }

    @Override
    public boolean undo()
    {
        if (activeSequence != null)
            return activeSequence.undo();

        return false;
    }

    @Override
    public boolean redo()
    {
        if (activeSequence != null)
            return activeSequence.redo();

        return false;
    }

    @Override
    public ArrayList<Viewer> getViewers()
    {
        synchronized (viewers)
        {
            return new ArrayList<Viewer>(viewers);
        }
    }

    @Override
    public synchronized void setActiveViewer(Viewer viewer)
    {
        if (activeViewer == viewer)
            return;

        // got a previously active viewer ?
        if (activeViewer != null)
        {
            // remove active viewer listener
            activeViewer.removeListener(activeViewerListener);

            // force previous viewer internal frame to release focus
            try
            {
                activeViewer.getInternalFrame().setSelected(false);
            }
            catch (PropertyVetoException e)
            {
                // ignore
            }
        }

        previousActiveViewer = activeViewer;
        activeViewer = viewer;

        // add active viewer listener
        if (activeViewer != null)
            activeViewer.addListener(activeViewerListener);

        // activation changed
        viewerActivationChanged(previousActiveViewer, activeViewer);
    }

    @Override
    @Deprecated
    public synchronized void setFocusedViewer(Viewer viewer)
    {
        setActiveViewer(viewer);
    }

    @Override
    public synchronized void addToDesktopPane(JInternalFrame internalFrame)
    {
        getDesktopPane().add(internalFrame, JLayeredPane.DEFAULT_LAYER);
    }

    @Override
    public IcyDesktopPane getDesktopPane()
    {
        if (mainFrame == null)
            return null;

        return mainFrame.getDesktopPane();
    }

    @Override
    public ApplicationMenu getApplicationMenu()
    {
        if (mainFrame == null)
            return null;

        return mainFrame.getApplicationMenu();
    }

    @Override
    public TaskFrameManager getTaskWindowManager()
    {
        return taskFrameManager;
    }

    private WeakReference<Plugin> getPluginReference(Plugin plugin)
    {
        synchronized (activePlugins)
        {
            for (WeakReference<Plugin> ref : activePlugins)
                if (ref.get() == plugin)
                    return ref;
        }

        return null;
    }

    @Deprecated
    @Override
    public void registerExternalFrame(JFrame frame)
    {

    }

    @Deprecated
    @Override
    public void unRegisterExternalFrame(JFrame frame)
    {

    }

    @Override
    public synchronized void registerPlugin(Plugin plugin)
    {
        synchronized (activePlugins)
        {
            activePlugins.add(new WeakReference<Plugin>(plugin));
        }

        // plugin opened
        pluginStarted(plugin);
    }

    @Override
    public synchronized void unRegisterPlugin(Plugin plugin)
    {
        final WeakReference<Plugin> ref = getPluginReference(plugin);

        synchronized (activePlugins)
        {
            // reference found
            if (ref != null)
                activePlugins.remove(ref);
        }

        // plugin closed
        pluginEnded(plugin);
    }

    @Override
    public synchronized void registerViewer(Viewer viewer)
    {
        if (viewer == null)
            return;

        // viewer opened
        viewerOpened(viewer);
    }

    @Override
    public synchronized void unRegisterViewer(Viewer viewer)
    {
        if (viewer == null)
            return;

        // viewer closed
        viewerClosed(viewer);

        // no more opened viewer ?
        if (viewers.isEmpty())
            // set focus to null
            setActiveViewer(null);
        else
        {
            final IcyFrame frame = IcyFrame.findIcyFrame(getDesktopPane().getSelectedFrame());

            if (frame instanceof Viewer)
                ((Viewer) frame).requestFocus();
            else
            {
                // it was the active viewer ?
                if (activeViewer == viewer)
                {
                    // restore focus to previous active
                    if (previousActiveViewer != null)
                    {
                        setActiveViewer(previousActiveViewer);
                        // no more previous active now
                        previousActiveViewer = null;
                    }
                    else
                        // or just focus another one
                        setActiveViewer(viewers.get(viewers.size() - 1));
                }
            }
        }
    }

    @Override
    @Deprecated
    public MainFrame getFrame()
    {
        return getMainFrame();
    }

    @Override
    public MainFrame getMainFrame()
    {
        return mainFrame;
    }

    @Override
    public SearchEngine getSearchEngine()
    {
        return mainFrame.getSearchBar().getSearchEngine();
    }

    @Override
    public void closeSequence(Sequence sequence)
    {
        // use copy as this actually modify viewers list
        for (Viewer v : getViewers())
            if (v.getSequence() == sequence)
                v.close();
    }

    @Deprecated
    @Override
    public void closeViewersOfSequence(Sequence sequence)
    {
        closeSequence(sequence);
    }

    @Override
    public synchronized void closeAllViewers()
    {
        // use copy as this actually modify viewers list
        for (Viewer viewer : getViewers())
            viewer.close();
    }

    @Override
    public Viewer getFirstViewerContaining(ROI roi)
    {
        return getFirstViewer(getFirstSequenceContaining(roi));
    }

    @Deprecated
    @Override
    public Viewer getFirstViewerContaining(Painter painter)
    {
        return getFirstViewer(getFirstSequenceContaining(painter));
    }

    @Override
    public Viewer getFirstViewerContaining(Overlay overlay)
    {
        return getFirstViewer(getFirstSequenceContaining(overlay));
    }

    @Override
    public Viewer getFirstViewer(Sequence sequence)
    {
        if (sequence != null)
        {
            for (Viewer viewer : getViewers())
                if (viewer.getSequence() == sequence)
                    return viewer;
        }

        return null;
    }

    @Override
    public ArrayList<Viewer> getViewers(Sequence sequence)
    {
        final ArrayList<Viewer> result = new ArrayList<Viewer>();

        for (Viewer v : getViewers())
            if (v.getSequence() == sequence)
                result.add(v);

        return result;
    }

    @Override
    public boolean isUniqueViewer(Viewer viewer)
    {
        final List<Viewer> viewers = getViewers(viewer.getSequence());

        return (viewers.size() == 1) && (viewers.get(0) == viewer);
    }

    @Override
    public ArrayList<Sequence> getSequences()
    {
        synchronized (sequences)
        {
            return new ArrayList<Sequence>(sequences);
        }
    }

    @Override
    public ArrayList<Sequence> getSequences(String name)
    {
        final ArrayList<Sequence> result = new ArrayList<Sequence>();

        synchronized (viewers)
        {
            for (Viewer viewer : viewers)
            {
                final Sequence sequence = viewer.getSequence();

                // matching name and no duplicate
                if (!result.contains(sequence) && StringUtil.equals(name, sequence.getName()))
                    result.add(sequence);
            }
        }

        return result;
    }

    @Override
    public boolean isOpened(Sequence sequence)
    {
        return getSequences().contains(sequence);
    }

    @Deprecated
    @Override
    public Sequence getFirstSequencesContaining(ROI roi)
    {
        return getFirstSequenceContaining(roi);
    }

    @Override
    public Sequence getFirstSequenceContaining(ROI roi)
    {
        for (Sequence seq : getSequences())
            if (seq.contains(roi))
                return seq;

        return null;
    }

    @Deprecated
    @Override
    public Sequence getFirstSequencesContaining(Painter painter)
    {
        return getFirstSequenceContaining(painter);
    }

    @Deprecated
    @Override
    public Sequence getFirstSequenceContaining(Painter painter)
    {
        for (Sequence seq : getSequences())
            if (seq.contains(painter))
                return seq;

        return null;
    }

    @Override
    public Sequence getFirstSequenceContaining(Overlay overlay)
    {
        for (Sequence seq : getSequences())
            if (seq.contains(overlay))
                return seq;

        return null;
    }

    @Override
    public ArrayList<Sequence> getSequencesContaining(ROI roi)
    {
        final ArrayList<Sequence> result = getSequences();

        for (int i = result.size() - 1; i >= 0; i--)
            if (!result.get(i).contains(roi))
                result.remove(i);

        return result;
    }

    @Override
    @Deprecated
    public ArrayList<Sequence> getSequencesContaining(Painter painter)
    {
        final ArrayList<Sequence> result = getSequences();

        for (int i = result.size() - 1; i >= 0; i--)
            if (!result.get(i).contains(painter))
                result.remove(i);

        return result;
    }

    @Override
    public List<Sequence> getSequencesContaining(Overlay overlay)
    {
        final ArrayList<Sequence> result = getSequences();

        for (int i = result.size() - 1; i >= 0; i--)
            if (!result.get(i).contains(overlay))
                result.remove(i);

        return result;
    }

    @Override
    public ArrayList<ROI> getROIs()
    {
        // HashSet is better suited for add elements
        final HashSet<ROI> result = new HashSet<ROI>();

        for (Sequence seq : getSequences())
            for (ROI roi : seq.getROISet())
                result.add(roi);

        // TODO: add ROI from swimming pool ?

        return new ArrayList<ROI>(result);
    }

    @Override
    @Deprecated
    public ROI getROI(Painter painter)
    {
        if (painter instanceof Overlay)
            return getROI((Overlay) painter);

        return null;
    }

    @Override
    public ROI getROI(Overlay overlay)
    {
        final List<ROI> rois = getROIs();

        for (ROI roi : rois)
            if (roi.getOverlay() == overlay)
                return roi;

        return null;
    }

    @Override
    @Deprecated
    public ArrayList<Painter> getPainters()
    {
        // HashSet better suited for add element
        final HashSet<Painter> result = new HashSet<Painter>();

        for (Sequence seq : getSequences())
            result.addAll(seq.getPainterSet());

        // TODO: add Painter from swimming pool ?

        return new ArrayList<Painter>();
    }

    @Override
    public List<Overlay> getOverlays()
    {
        // HashSet better suited for add element
        final HashSet<Overlay> result = new HashSet<Overlay>();

        for (Sequence seq : getSequences())
            result.addAll(seq.getOverlaySet());

        // TODO: add Overlay from swimming pool ?

        return new ArrayList<Overlay>();
    }

    @Override
    public SwimmingPool getSwimmingPool()
    {
        return swimmingPool;
    }

    @Override
    public ImageJWrapper getImageJ()
    {
        if (mainFrame == null)
            return null;

        return mainFrame.getMainRibbon().getImageJ();
    }

    @Override
    public String getSelectedTool()
    {
        if (mainFrame == null)
            return null;

        return mainFrame.getMainRibbon().getToolRibbon().getSelected();
    }

    @Override
    public void setSelectedTool(String command)
    {
        if (mainFrame != null)
            mainFrame.getMainRibbon().getToolRibbon().setSelected(command);
    }

    @Override
    public ToolRibbonTask getToolRibbon()
    {
        if (mainFrame == null)
            return null;

        return mainFrame.getMainRibbon().getToolRibbon();
    }

    @Override
    public boolean isAlwaysOnTop()
    {
        if (mainFrame == null)
            return false;

        return mainFrame.isAlwaysOnTop();
    }

    @Override
    public void setAlwaysOnTop(boolean value)
    {
        if (mainFrame != null)
            mainFrame.setAlwaysOnTop(value);
    }

    @Override
    public boolean isDetachedMode()
    {
        if (mainFrame == null)
            return false;

        return mainFrame.isDetachedMode();
    }

    @Override
    public void setDetachedMode(boolean value)
    {
        if (mainFrame != null)
            mainFrame.setDetachedMode(value);
    }

    @Override
    @Deprecated
    public synchronized void addListener(MainListener listener)
    {
        if (listener != null)
            mainListeners.add(listener);
    }

    @Override
    @Deprecated
    public synchronized void removeListener(MainListener listener)
    {
        mainListeners.remove(listener);
    }

    @Override
    public synchronized void addGlobalViewerListener(GlobalViewerListener listener)
    {
        if (listener != null)
            globalViewerListeners.add(listener);
    }

    @Override
    public synchronized void removeGlobalViewerListener(GlobalViewerListener listener)
    {
        globalViewerListeners.remove(listener);
    }

    @Override
    public synchronized void addGlobalSequenceListener(GlobalSequenceListener listener)
    {
        if (listener != null)
            globalSequenceListeners.add(listener);
    }

    @Override
    public synchronized void removeGlobalSequenceListener(GlobalSequenceListener listener)
    {
        globalSequenceListeners.remove(listener);
    }

    @Override
    public synchronized void addGlobalROIListener(GlobalROIListener listener)
    {
        if (listener != null)
            globalROIListeners.add(listener);
    }

    @Override
    public synchronized void removeGlobalROIListener(GlobalROIListener listener)
    {
        globalROIListeners.remove(listener);
    }

    @Override
    public synchronized void addGlobalOverlayListener(GlobalOverlayListener listener)
    {
        if (listener != null)
            globalOverlayListeners.add(listener);
    }

    @Override
    public synchronized void removeGlobalOverlayListener(GlobalOverlayListener listener)
    {
        globalOverlayListeners.remove(listener);
    }

    @Override
    public synchronized void addGlobalPluginListener(GlobalPluginListener listener)
    {
        if (listener != null)
            listeners.add(GlobalPluginListener.class, listener);
    }

    @Override
    public synchronized void removeGlobalPluginListener(GlobalPluginListener listener)
    {
        listeners.remove(GlobalPluginListener.class, listener);
    }

    @Override
    public synchronized void addCanExitListener(AcceptListener listener)
    {
        if (listener != null)
            listeners.add(WeakAcceptListener.class, new WeakAcceptListener(listener));
    }

    @Override
    public synchronized void removeCanExitListener(AcceptListener listener)
    {
        // we use weak reference so we have to find base listener...
        for (WeakAcceptListener l : listeners.getListeners(WeakAcceptListener.class))
            if (listener == l.getListener())
                internalRemoveCanExitListener(l);
    }

    public synchronized void internalRemoveCanExitListener(WeakAcceptListener listener)
    {
        listeners.remove(WeakAcceptListener.class, listener);
    }

    @Deprecated
    @Override
    public synchronized void addFocusedViewerListener(FocusedViewerListener listener)
    {
        listeners.add(FocusedViewerListener.class, listener);
    }

    @Deprecated
    @Override
    public synchronized void removeFocusedViewerListener(FocusedViewerListener listener)
    {
        listeners.remove(FocusedViewerListener.class, listener);
    }

    @Deprecated
    @Override
    public synchronized void addFocusedSequenceListener(FocusedSequenceListener listener)
    {
        listeners.add(FocusedSequenceListener.class, listener);
    }

    @Deprecated
    @Override
    public synchronized void removeFocusedSequenceListener(FocusedSequenceListener listener)
    {
        listeners.remove(FocusedSequenceListener.class, listener);
    }

    @Override
    public synchronized void addActiveViewerListener(ActiveViewerListener listener)
    {
        listeners.add(ActiveViewerListener.class, listener);
    }

    @Override
    public synchronized void removeActiveViewerListener(ActiveViewerListener listener)
    {
        listeners.remove(ActiveViewerListener.class, listener);
    }

    @Override
    public synchronized void addActiveSequenceListener(ActiveSequenceListener listener)
    {
        listeners.add(ActiveSequenceListener.class, listener);
    }

    @Override
    public synchronized void removeActiveSequenceListener(ActiveSequenceListener listener)
    {
        listeners.remove(ActiveSequenceListener.class, listener);
    }

    /**
     * fire plugin opened event
     */
    @SuppressWarnings("deprecation")
    private void firePluginStartedEvent(Plugin plugin)
    {
        for (GlobalPluginListener listener : listeners.getListeners(GlobalPluginListener.class))
            listener.pluginStarted(plugin);

        // backward compatibility
        final MainEvent event = new MainEvent(MainEventSourceType.PLUGIN, MainEventType.OPENED, plugin);
        for (MainListener listener : new ArrayList<MainListener>(mainListeners))
            listener.pluginOpened(event);
    }

    /**
     * fire plugin closed event
     */
    @SuppressWarnings("deprecation")
    private void firePluginEndedEvent(Plugin plugin)
    {
        for (GlobalPluginListener listener : listeners.getListeners(GlobalPluginListener.class))
            listener.pluginEnded(plugin);

        // backward compatibility
        final MainEvent event = new MainEvent(MainEventSourceType.PLUGIN, MainEventType.CLOSED, plugin);
        for (MainListener listener : new ArrayList<MainListener>(mainListeners))
            listener.pluginClosed(event);
    }

    /**
     * fire viewer opened event
     */
    @SuppressWarnings("deprecation")
    private void fireViewerOpenedEvent(Viewer viewer)
    {
        for (GlobalViewerListener listener : new ArrayList<GlobalViewerListener>(globalViewerListeners))
            listener.viewerOpened(viewer);

        // backward compatibility
        final MainEvent event = new MainEvent(MainEventSourceType.VIEWER, MainEventType.OPENED, viewer);
        for (MainListener listener : new ArrayList<MainListener>(mainListeners))
            listener.viewerOpened(event);
    }

    /**
     * fire viewer close event
     */
    @SuppressWarnings("deprecation")
    private void fireViewerClosedEvent(Viewer viewer)
    {
        for (GlobalViewerListener listener : new ArrayList<GlobalViewerListener>(globalViewerListeners))
            listener.viewerClosed(viewer);

        // backward compatibility
        final MainEvent event = new MainEvent(MainEventSourceType.VIEWER, MainEventType.CLOSED, viewer);
        for (MainListener listener : new ArrayList<MainListener>(mainListeners))
            listener.viewerClosed(event);
    }

    /**
     * fire viewer deactive event
     */
    private void fireViewerDeactivatedEvent(Viewer viewer)
    {
        for (ActiveViewerListener listener : listeners.getListeners(ActiveViewerListener.class))
            listener.viewerDeactivated(viewer);
    }

    /**
     * fire viewer active event
     */
    @SuppressWarnings("deprecation")
    private void fireViewerActivatedEvent(Viewer viewer)
    {
        for (ActiveViewerListener listener : listeners.getListeners(ActiveViewerListener.class))
            listener.viewerActivated(viewer);

        // backward compatibility
        final MainEvent event = new MainEvent(MainEventSourceType.VIEWER, MainEventType.FOCUSED, viewer);
        for (MainListener listener : new ArrayList<MainListener>(mainListeners))
            listener.viewerFocused(event);
        for (FocusedViewerListener listener : listeners.getListeners(FocusedViewerListener.class))
            listener.focusChanged(viewer);
    }

    /**
     * fire sequence opened event
     */
    @SuppressWarnings("deprecation")
    private void fireSequenceOpenedEvent(Sequence sequence)
    {
        for (GlobalSequenceListener listener : new ArrayList<GlobalSequenceListener>(globalSequenceListeners))
            listener.sequenceOpened(sequence);

        // backward compatibility
        final MainEvent event = new MainEvent(MainEventSourceType.SEQUENCE, MainEventType.OPENED, sequence);
        for (MainListener listener : new ArrayList<MainListener>(mainListeners))
            listener.sequenceOpened(event);
    }

    /**
     * fire sequence active event
     */
    @SuppressWarnings("deprecation")
    private void fireSequenceClosedEvent(Sequence sequence)
    {
        for (GlobalSequenceListener listener : new ArrayList<GlobalSequenceListener>(globalSequenceListeners))
            listener.sequenceClosed(sequence);

        // backward compatibility
        final MainEvent event = new MainEvent(MainEventSourceType.SEQUENCE, MainEventType.CLOSED, sequence);
        for (MainListener listener : new ArrayList<MainListener>(mainListeners))
            listener.sequenceClosed(event);
    }

    /**
     * fire sequence deactive event
     */
    private void fireSequenceDeactivatedEvent(Sequence sequence)
    {
        for (ActiveSequenceListener listener : listeners.getListeners(ActiveSequenceListener.class))
            listener.sequenceDeactivated(sequence);
    }

    /**
     * fire sequence active event
     */
    @SuppressWarnings("deprecation")
    private void fireSequenceActivatedEvent(Sequence sequence)
    {
        for (ActiveSequenceListener listener : listeners.getListeners(ActiveSequenceListener.class))
            listener.sequenceActivated(sequence);

        // backward compatibility
        final MainEvent event = new MainEvent(MainEventSourceType.SEQUENCE, MainEventType.FOCUSED, sequence);
        for (MainListener listener : new ArrayList<MainListener>(mainListeners))
            listener.sequenceFocused(event);
        for (FocusedSequenceListener listener : listeners.getListeners(FocusedSequenceListener.class))
            listener.focusChanged(sequence);
    }

    /**
     * fire ROI added event
     */
    @SuppressWarnings("deprecation")
    private void fireRoiAddedEvent(ROI roi)
    {
        for (GlobalROIListener listener : new ArrayList<GlobalROIListener>(globalROIListeners))
            listener.roiAdded(roi);

        // backward compatibility
        final MainEvent event = new MainEvent(MainEventSourceType.ROI, MainEventType.ADDED, roi);
        for (MainListener listener : new ArrayList<MainListener>(mainListeners))
            listener.roiAdded(event);
    }

    /**
     * fire ROI removed event
     */
    @SuppressWarnings("deprecation")
    private void fireRoiRemovedEvent(ROI roi)
    {
        for (GlobalROIListener listener : new ArrayList<GlobalROIListener>(globalROIListeners))
            listener.roiRemoved(roi);

        // backward compatibility
        final MainEvent event = new MainEvent(MainEventSourceType.ROI, MainEventType.REMOVED, roi);
        for (MainListener listener : new ArrayList<MainListener>(mainListeners))
            listener.roiRemoved(event);
    }

    /**
     * fire painter added event
     */
    @SuppressWarnings("deprecation")
    private void fireOverlayAddedEvent(Overlay overlay)
    {
        for (GlobalOverlayListener listener : new ArrayList<GlobalOverlayListener>(globalOverlayListeners))
            listener.overlayAdded(overlay);

        // backward compatibility
        final Painter painter;

        if (overlay instanceof OverlayWrapper)
            painter = ((OverlayWrapper) overlay).getPainter();
        else
            painter = overlay;

        final MainEvent event = new MainEvent(MainEventSourceType.PAINTER, MainEventType.ADDED, painter);
        for (MainListener listener : new ArrayList<MainListener>(mainListeners))
            listener.painterAdded(event);
    }

    /**
     * fire painter removed event
     */
    @SuppressWarnings("deprecation")
    private void fireOverlayRemovedEvent(Overlay overlay)
    {
        for (GlobalOverlayListener listener : new ArrayList<GlobalOverlayListener>(globalOverlayListeners))
            listener.overlayRemoved(overlay);

        // backward compatibility
        final Painter painter;

        if (overlay instanceof OverlayWrapper)
            painter = ((OverlayWrapper) overlay).getPainter();
        else
            painter = overlay;

        final MainEvent event = new MainEvent(MainEventSourceType.PAINTER, MainEventType.REMOVED, painter);
        for (MainListener listener : new ArrayList<MainListener>(mainListeners))
            listener.painterRemoved(event);
    }

    /**
     * fire active viewer changed event
     */
    @SuppressWarnings("deprecation")
    private void fireActiveViewerChangedEvent(ViewerEvent event)
    {
        for (ActiveViewerListener listener : listeners.getListeners(ActiveViewerListener.class))
            listener.activeViewerChanged(event);

        // backward compatibility
        for (FocusedViewerListener listener : listeners.getListeners(FocusedViewerListener.class))
            listener.focusedViewerChanged(event);
    }

    /**
     * fire active sequence changed event
     */
    @SuppressWarnings("deprecation")
    private void fireActiveSequenceChangedEvent(SequenceEvent event)
    {
        for (ActiveSequenceListener listener : listeners.getListeners(ActiveSequenceListener.class))
            listener.activeSequenceChanged(event);

        // backward compatibility
        for (FocusedSequenceListener listener : listeners.getListeners(FocusedSequenceListener.class))
            listener.focusedSequenceChanged(event);
    }

    @Override
    public boolean canExitExternal()
    {
        for (AcceptListener listener : listeners.getListeners(WeakAcceptListener.class))
            if (!listener.accept(mainFrame))
                return false;

        return true;
    }

    @Deprecated
    @Override
    public void beginUpdate()
    {
        // updater.beginUpdate();
    }

    @Deprecated
    @Override
    public void endUpdate()
    {
        // updater.endUpdate();
    }

    @Deprecated
    @Override
    public boolean isUpdating()
    {
        return false;
        // return updater.isUpdating();
    }

    /**
     * called when a plugin is opened
     */
    private void pluginStarted(Plugin plugin)
    {
        firePluginStartedEvent(plugin);
    }

    /**
     * called when a plugin is closed
     */
    private void pluginEnded(Plugin plugin)
    {
        firePluginEndedEvent(plugin);
    }

    /**
     * called when a viewer is opened
     */
    private void viewerOpened(Viewer viewer)
    {
        final Sequence sequence = viewer.getSequence();
        boolean opened = true;

        synchronized (viewers)
        {
            // check if the sequence has just been opened
            if (sequence != null)
            {
                for (Viewer v : viewers)
                {
                    if (v.getSequence() == sequence)
                    {
                        opened = false;
                        break;
                    }
                }
            }

            // add viewer to the viewer list
            viewers.add(viewer);
        }

        // single viewer for this sequence ?
        if ((sequence != null) && opened)
            // send opened event
            sequenceOpened(sequence);

        // fire viewer open event (after sequence open)
        fireViewerOpenedEvent(viewer);
    }

    /**
     * called when viewer activation changed
     */
    private void viewerActivationChanged(Viewer oldActive, Viewer newActive)
    {
        final Sequence sequence;

        // new active viewer is not null ?
        if (newActive != null)
        {
            // remove focus on ImageJ image
            final ImageJWrapper ij = Icy.getMainInterface().getImageJ();
            if (ij != null)
                ij.setActiveImage(null);

            // get active sequence
            sequence = newActive.getSequence();
        }
        else
            sequence = null;

        final Sequence oldActiveSequence = activeSequence;

        // sequence active changed ?
        if (oldActiveSequence != sequence)
        {
            activeSequence = sequence;
            sequenceActivationChanged(oldActiveSequence, sequence);
        }

        // fire deactivated / activated events
        fireViewerDeactivatedEvent(oldActive);
        fireViewerActivatedEvent(newActive);
    }

    /**
     * called when the active viewer changed
     */
    void activeViewerChanged(ViewerEvent event)
    {
        // propagate event if it comes from the active viewer
        // FIXME: why we need to test that ? it should always be the active viewer ?
        if (event.getSource() == activeViewer)
            fireActiveViewerChangedEvent(event);
    }

    /**
     * called when a viewer is closed
     */
    private void viewerClosed(Viewer viewer)
    {
        // retrieve the viewer LUT before we release it
        final LUT lut = viewer.getLut();

        // remove active viewer listener
        if (viewer == activeViewer)
            viewer.removeListener(activeViewerListener);
        // remove viewer from the viewer list
        synchronized (viewers)
        {
            viewers.remove(viewer);
        }
        // fire viewer closed event (before sequence close)
        fireViewerClosedEvent(viewer);

        final Sequence sequence = viewer.getSequence();

        // check if a sequence has been closed
        if (sequence != null)
        {
            // if no viewer for this sequence
            if (getViewers(sequence).isEmpty())
                // sequence close
                sequenceClosed(sequence, lut);
        }
    }

    /**
     * called when a sequence is opened
     */
    private void sequenceOpened(Sequence sequence)
    {
        // add to sequence list
        synchronized (sequences)
        {
            sequences.add(sequence);
        }
        // listen the sequence
        sequence.addListener(sequenceListener);
        // fire sequence opened event (before roi / overlay events)
        fireSequenceOpenedEvent(sequence);

        // check about ROI add event
        for (ROI roi : sequence.getROIs())
            checkRoiAdded(roi, false);
        // check about Overlay add event
        for (Overlay overlay : sequence.getOverlays())
            checkOverlayAdded(overlay, false);
    }

    /**
     * called when sequence activation changed
     */
    private void sequenceActivationChanged(Sequence oldActive, Sequence newActive)
    {
        // fire events
        fireSequenceDeactivatedEvent(oldActive);
        fireSequenceActivatedEvent(newActive);
    }

    /**
     * called when a sequence changed
     */
    void sequenceChanged(SequenceEvent event)
    {
        final Sequence sequence = event.getSequence();

        // handle event for active sequence only
        if (isOpened(sequence))
        {
            switch (event.getSourceType())
            {
                case SEQUENCE_ROI:
                    switch (event.getType())
                    {
                        case ADDED:
                            checkRoiAdded((ROI) event.getSource(), false);
                            break;

                        case REMOVED:
                            checkRoiRemoved((ROI) event.getSource(), false);
                            break;
                    }
                    break;

                case SEQUENCE_OVERLAY:
                    switch (event.getType())
                    {
                        case ADDED:
                            checkOverlayAdded((Overlay) event.getSource(), false);
                            break;

                        case REMOVED:
                            checkOverlayRemoved((Overlay) event.getSource(), false);
                            break;
                    }
                    break;
            }
        }

        // propagate event if it comes from the active sequence
        if (sequence == activeSequence)
            fireActiveSequenceChangedEvent(event);
    }

    /**
     * Called when a sequence is closed.
     * 
     * @param sequence
     *        the sequence which has been closed.
     * @param userLut
     *        the viewer's LUT used when sequence has been closed.
     */
    private void sequenceClosed(Sequence sequence, LUT userLut)
    {
        // check about Overlay remove event
        for (Overlay overlay : sequence.getOverlays())
            checkOverlayRemoved(overlay, true);
        // check about ROI remove event
        for (ROI roi : sequence.getROIs())
            checkRoiRemoved(roi, true);

        // remove from sequence listener
        sequence.removeListener(sequenceListener);
        // remove from the sequence list
        synchronized (sequences)
        {
            sequences.remove(sequence);
        }

        // set the user LUT in the sequence
        if (userLut != null)
            sequence.setUserLUT(userLut);
        // inform sequence is now closed
        sequence.closed();

        // fire sequence closed event (after roi / overlay events)
        fireSequenceClosedEvent(sequence);
    }

    private void checkRoiAdded(ROI roi, boolean checkBeforeAdd)
    {
        final List<Sequence> sequencesContainingRoi = getSequencesContaining(roi);

        // only 1 sequence contains (or will contain) this roi ?
        if (sequencesContainingRoi.size() == (checkBeforeAdd ? 0 : 1))
            // roi added
            roiAdded(roi);
    }

    private void checkRoiRemoved(ROI roi, boolean checkBeforeRemove)
    {
        final List<Sequence> sequencesContainingRoi = getSequencesContaining(roi);

        // no more sequence contains (or will contain) this roi ?
        if (sequencesContainingRoi.size() == (checkBeforeRemove ? 1 : 0))
            // roi removed
            roiRemoved(roi);
    }

    private void checkOverlayAdded(Overlay overlay, boolean checkBeforeAdd)
    {
        final List<Sequence> sequencesContainingOverlay = getSequencesContaining(overlay);

        // only 1 sequence contains (or will contain) this overlay ?
        if (sequencesContainingOverlay.size() == (checkBeforeAdd ? 0 : 1))
            // overlay added
            overlayAdded(overlay);
    }

    private void checkOverlayRemoved(Overlay overlay, boolean checkBeforeRemove)
    {
        final List<Sequence> sequencesContainingOverlay = getSequencesContaining(overlay);

        // no more sequence contains (or will contain) this overlay ?
        if (sequencesContainingOverlay.size() == (checkBeforeRemove ? 1 : 0))
            // overlay removed
            overlayRemoved(overlay);
    }

    /**
     * called when a roi is added for the first time in a sequence
     */
    private void roiAdded(ROI roi)
    {
        fireRoiAddedEvent(roi);
    }

    /**
     * called when a roi is removed from all sequence
     */
    private void roiRemoved(ROI roi)
    {
        fireRoiRemovedEvent(roi);
    }

    /**
     * called when an overlay is added for the first time in a sequence
     */
    private void overlayAdded(Overlay overlay)
    {
        fireOverlayAddedEvent(overlay);
    }

    /**
     * called when an overlay is removed from all sequence
     */
    private void overlayRemoved(Overlay overlay)
    {
        fireOverlayRemovedEvent(overlay);
    }

    @Override
    public void setGlobalViewSyncId(int id)
    {
        for (Viewer viewer : getViewers())
            viewer.setViewSyncId(id);
    }
}