/*
 * #%L
 * gitools-ui-platform
 * %%
 * Copyright (C) 2013 Universitat Pompeu Fabra - Biomedical Genomics group
 * %%
 * This program 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.
 * 
 * This program 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 this program.  If not, see
 * <http://www.gnu.org/licenses/gpl-3.0.html>.
 * #L%
 */
package org.gitools.ui.platform.imageviewer;

import javax.swing.*;
import javax.swing.event.ChangeEvent;
import javax.swing.event.ChangeListener;
import java.util.ArrayList;
import java.util.List;
import java.util.WeakHashMap;

/**
 * Manages the synchronization of a collection of viewers.
 * <h2>Implementation notes</h2>
 * This class is necessary mainly due to the complicated operations that are performed for some property changes.
 * When e.g. a resize strategy is changed, that triggers a change in the component's size, leading to an update to the
 * scroll bars. But after that, the viewer performs rescrolling to try to restore the original image center. These
 * multiple scroll operations do not work very well together when two synchrnoized viewers have images of different sizes.
 * <p/>
 * This scenario is managed in the following way. While the resize strategy property is synchronized, all scroll
 * synchronizations are disabled, so size changes do not rewrite the scroll positions of other viewers. Rescrolling is
 * only allowed for the viewer in which the property change was initiated, and after it finished rescrolling, this
 * synchronizer matches the scroll positions of the other viewers.
 * <p/>
 * All this cannot be implemented using property change listeners, because then by the time we'd learn that the
 * resize strategy has changed, a bunch of scrolls would have happened, and it is entirely possible that the rescroll
 * operation of the originating viewer is overwritten by a subsequent scroll synchronization from one of the other viewers.
 * <p/>
 * So the viewer needs a reference the the synchronizer. It announces to the synchronizer that a complex operation
 * begins, and asks permission to perform rescrolling
 * ({@link #resizeStrategyChangedCanIRescroll(ImageViewer)}). The synchronizer uses this
 * mechanism to make note of the viewer in which the change originated, storing it in the variable {@link #leader}, and using
 * it to deny rescrolling to all the other viewers, and to disable scroll synchronization. Then, when the originator finishes the rescroll, it
 * notifies the synchronizer that the operation is complete using the {@link #doneRescrolling(ImageViewer)} function. At this point the
 * synchronizer adjusts the scroll panes of the other viewers to match the originator.
 *
 * @author Kazó Csaba
 */
class Synchronizer {
    private final WeakHashMap<ImageViewer, Void> viewers = new WeakHashMap<>(4);

    /*
     * If there is currently a synchronization update taking place, the viewer which initiated the change is stored
     * in this variable.
     */
    private ImageViewer leader = null;

    private final ChangeListener scrollChangeListener = new ChangeListener() {
        boolean adjusting = false;

        @Override
        public void stateChanged(ChangeEvent e) {
            if (leader != null) {
                // ignore every scrolling event during synchronization, it will all be adjusted after the final rescroll
                return;
            }
            if (adjusting) {
                // also ignore changes that our adjustments cause
                return;
            }
            ImageViewer source = null;
            for (ImageViewer viewer : viewers.keySet()) {
                if (viewer.getScrollPane().getHorizontalScrollBar().getModel() == e.getSource() || viewer.getScrollPane().getVerticalScrollBar().getModel() == e.getSource()) {
                    source = viewer;
                    break;
                }
            }
            if (source == null) throw new AssertionError("Couldn't find the source of the scroll bar change event");
            adjusting = true;
            for (ImageViewer viewer : viewers.keySet()) {
                updateScroll(viewer, source);
            }
            adjusting = false;
        }
    };

    public Synchronizer(ImageViewer viewer) {
        viewers.put(viewer, null);
        viewer.getScrollPane().getHorizontalScrollBar().getModel().addChangeListener(scrollChangeListener);
        viewer.getScrollPane().getVerticalScrollBar().getModel().addChangeListener(scrollChangeListener);
    }

    private void updateScroll(ImageViewer viewer, ImageViewer reference) {
        if (reference == viewer) return;
        /*
         * Note that this method may be called during a resize, before the viewport has had a chance to reshape itself
		 * so we cannot rely on the view rectangle.
		 */
        viewer.getScrollPane().getHorizontalScrollBar().getModel().setValue(reference.getScrollPane().getHorizontalScrollBar().getModel().getValue());
        viewer.getScrollPane().getVerticalScrollBar().getModel().setValue(reference.getScrollPane().getVerticalScrollBar().getModel().getValue());
    }

    public void add(ImageViewer viewer) {
        if (viewer.getSynchronizer() == this) return;
        ImageViewer referenceViewer = viewers.keySet().iterator().next();

        List<ImageViewer> otherViewers = new ArrayList<>(viewer.getSynchronizer().viewers.keySet());
        for (ImageViewer otherViewer : otherViewers) {
            otherViewer.getSynchronizer().remove(otherViewer);
            otherViewer.setSynchronizer(this);
            viewers.put(otherViewer, null);
            otherViewer.setStatusBarVisible(referenceViewer.isStatusBarVisible());
            otherViewer.setResizeStrategy(referenceViewer.getResizeStrategy());
            otherViewer.setZoomFactor(referenceViewer.getZoomFactor());
            otherViewer.setPixelatedZoom(referenceViewer.isPixelatedZoom());
            otherViewer.setInterpolationType(referenceViewer.getInterpolationType());

            updateScroll(otherViewer, referenceViewer);
            otherViewer.getScrollPane().getHorizontalScrollBar().getModel().addChangeListener(scrollChangeListener);
            otherViewer.getScrollPane().getVerticalScrollBar().getModel().addChangeListener(scrollChangeListener);
        }
    }

    public void remove(ImageViewer viewer) {
        viewers.remove(viewer);
        viewer.getScrollPane().getHorizontalScrollBar().getModel().removeChangeListener(scrollChangeListener);
        viewer.getScrollPane().getVerticalScrollBar().getModel().removeChangeListener(scrollChangeListener);
    }


    boolean resizeStrategyChangedCanIRescroll(ImageViewer source) {
        if (leader != null) {
            // leader is leading an adjustment operation; wait for it to rescroll, and then adjust everything else
            return false;
        }
        leader = source;
        for (ImageViewer viewer : viewers.keySet())
            viewer.setResizeStrategy(source.getResizeStrategy());
        return true;
    }

    boolean zoomFactorChangedCanIRescroll(ImageViewer source) {
        if (leader != null) {
            // leader is leading an adjustment operation; wait for it to rescroll, and then adjust everything else
            return false;
        }
        leader = source;
        for (ImageViewer viewer : viewers.keySet())
            viewer.setZoomFactor(source.getZoomFactor());
        return true;
    }

    void doneRescrolling(ImageViewer source) {
        if (leader != source) throw new AssertionError();
        for (ImageViewer otherViewer : viewers.keySet()) {
            if (otherViewer != leader) {
                ((JComponent) otherViewer.getScrollPane().getViewport().getView()).scrollRectToVisible(leader.getScrollPane().getViewport().getViewRect());
                updateScroll(otherViewer, leader);
            }
        }
        leader = null;
    }

    void interpolationTypeChanged(ImageViewer source) {
        for (ImageViewer viewer : viewers.keySet())
            viewer.setInterpolationType(source.getInterpolationType());
    }

    void statusBarVisibilityChanged(ImageViewer source) {
        for (ImageViewer viewer : viewers.keySet())
            viewer.setStatusBarVisible(source.isStatusBarVisible());
    }

    void pixelatedZoomChanged(ImageViewer source) {
        for (ImageViewer viewer : viewers.keySet())
            viewer.setPixelatedZoom(source.isPixelatedZoom());
    }
}
