/*
 * $Id: DefaultSelectionMapper.java,v 1.2 2008/07/12 11:37:03 dbarashev Exp $
 *
 * Copyright 2006 Sun Microsystems, Inc., 4150 Network Circle,
 * Santa Clara, California 95054, U.S.A. All rights reserved.
 *
 * This library is free software; you can redistribute it and/or
 * modify it under the terms of the GNU Lesser General Public
 * License as published by the Free Software Foundation; either
 * version 2.1 of the License, or (at your option) any later version.
 * 
 * This library 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
 * Lesser General Public License for more details.
 * 
 * You should have received a copy of the GNU Lesser General Public
 * License along with this library; if not, write to the Free Software
 * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA  02110-1301  USA
 *
 */
package org.jdesktop.swingx.decorator;



import javax.swing.*;
import javax.swing.event.ListSelectionListener;
import javax.swing.event.ListSelectionEvent;

/**
 * Responsible for keeping track of selection in model coordinates.<p>
 *
 * updates view selection on pipeline change.
 * updates model selection on view selection change.
 *
 * @author Jeanette Winzenburg
 */
public class DefaultSelectionMapper implements SelectionMapper {

    /** selection in view coordinates. */
    private ListSelectionModel viewSelection;

    /** selection in model coordinates. */
    protected final DefaultListSelectionModel modelSelection = new DefaultListSelectionModel();

    /** mapping pipeline. */
    private FilterPipeline pipeline;

    /** listener to view selection. */
    private ListSelectionListener viewSelectionListener;

    /**
     * Whether selection mapping is enabled. If true, we're currently
     * observing the view selection, using that to keep the model selection
     * up-to-date.
     */
    private boolean enabled = false;

    /** listener to mapping pipeline. */
    private PipelineListener pipelineListener;

    /**
     * PRE: selection != null;
     *
     * @param pipeline
     * @param selection
     */
    public DefaultSelectionMapper(FilterPipeline pipeline, ListSelectionModel selection) {
        setViewSelectionModel(selection);
        setEnabled(true);
        setFilters(pipeline);
    }

    /** {@inheritDoc} */
    public void setViewSelectionModel(ListSelectionModel viewSelectionModel) {
        if(viewSelectionModel == null) throw new IllegalArgumentException();

        boolean wasEnabled = isEnabled();
        setEnabled(false);
        try {
            clearModelSelection();
            this.viewSelection = viewSelectionModel;
            mapTowardsModel();
        } finally {
            setEnabled(wasEnabled);
        }
    }

    /** {@inheritDoc} */
    public ListSelectionModel getViewSelectionModel() {
        return viewSelection;
    }

    public void setFilters(FilterPipeline pipeline) {
        FilterPipeline old = this.pipeline;
        if (old != null) {
            old.removePipelineListener(pipelineListener);
        }
        this.pipeline = pipeline;
        if (pipeline != null) {
            pipeline.addPipelineListener(getPipelineListener());
        }
        mapTowardsView();
    }


    /**
     * Populate view selection from model selection. This is used to keep the
     * view's logical selection in sync whenever the model changes due to
     * filtering or sorting.
     */
    protected void mapTowardsView() {
        if(!enabled) return;

        setEnabled(false);
        try {
            clearViewSelection();

            int[] selected = getSelectedRows(modelSelection);
            for (int i = 0; i < selected.length; i++) {
              int index = convertToView(selected[i]);
              // index might be -1, but then addSelectionInterval ignores it.
              viewSelection.addSelectionInterval(index, index);
            }
            int lead = modelSelection.getLeadSelectionIndex();
            // TODO: PENDING: JW - this is a quick hack for spurious AIOB - need to enquire why
            // they happen in the first place
            if (lead >= 0) {
                lead = convertToView(lead);
            }
            if (viewSelection instanceof DefaultListSelectionModel) {
                ((DefaultListSelectionModel) viewSelection).moveLeadSelectionIndex(lead);
            } else {
                // PENDING: not tested, don't have a non-DefaultXX handy
//                viewSelection.removeSelectionInterval(lead, lead);
//                viewSelection.addSelectionInterval(lead, lead);
            }
        } finally {
            setEnabled(true);
        }
    }

    /** {@inheritDoc} */
    public void setEnabled(boolean enabled) {
        if(enabled == this.enabled) return;
        this.enabled = enabled;

        if (enabled) {
            viewSelection.setValueIsAdjusting(false);
            viewSelection.addListSelectionListener(getViewSelectionListener());
        } else {
            viewSelection.removeListSelectionListener(viewSelectionListener);
            viewSelection.setValueIsAdjusting(true);
        }
    }

    /** {@inheritDoc} */
    public boolean isEnabled() {
        return enabled;
    }

    public void clearModelSelection() {
        if(modelSelection == null) return;
        // TODO: JW: need to reset anchor/lead?
        modelSelection.clearSelection();
        modelSelection.setAnchorSelectionIndex(-1);
        modelSelection.setLeadSelectionIndex(-1);
    }

    /**
     *
     */
    private void clearViewSelection() {
        // TODO: JW - hmm... clearSelection doesn't reset the lead/anchor. Why not?
        viewSelection.clearSelection();
        viewSelection.setAnchorSelectionIndex(-1);
        viewSelection.setLeadSelectionIndex(-1);
    }

    public void insertIndexInterval(int start, int length, boolean before) {
        modelSelection.insertIndexInterval(start, length, before);
    }

    public void removeIndexInterval(int start, int end) {
        modelSelection.removeIndexInterval(start, end);
    }

    /**
     * Populate view selection from model selection.
     */
    private void mapTowardsModel() {
        if(modelSelection == null) return;

        clearModelSelection();
        int[] selected = getSelectedRows(viewSelection);
        for (int i = 0; i < selected.length; i++) {
            int modelIndex = convertToModel(selected[i]);
            modelSelection.addSelectionInterval(modelIndex, modelIndex);
        }
        if (selected.length > 0) {
            // convert lead selection index to model coordinates
            modelSelection.moveLeadSelectionIndex(convertToModel(viewSelection.getLeadSelectionIndex()));
        }
    }

    private int convertToModel(int index) {
        // TODO: JW: check for valid index? must be < pipeline.getOutputSize()
        return (pipeline != null) && pipeline.isAssigned() ? pipeline.convertRowIndexToModel(index) : index;
    }

    private int convertToView(int index) {
        // TODO: JW: check for valid index? must be < pipeline.getInputSize()
        return (pipeline != null) && pipeline.isAssigned() ? pipeline.convertRowIndexToView(index) : index;
    }

    /**
     * Respond to a change in the view selection by updating the view selection.
     *
     * @param firstIndex the first view index that changed, inclusive
     * @param lastIndex the last view index that changed, inclusive
     */
    private void mapTowardsModel(int firstIndex, int lastIndex) {
        int safeFirstIndex = Math.max(0, firstIndex);
        // Fix for #855-swingx: JXList AIOOB on select after remove/add data items
        int safeLastIndex = getSafeLastIndex(lastIndex);
        for (int i = safeFirstIndex; i <= safeLastIndex; i++) {
            int modelIndex = convertToModel(i);
            if (viewSelection.isSelectedIndex(i)) {
                modelSelection.addSelectionInterval(modelIndex, modelIndex);
            } else {
                modelSelection.removeSelectionInterval(modelIndex, modelIndex);
            }
        }
        int lead = viewSelection.getLeadSelectionIndex();
        if (lead >= 0) {
            modelSelection.moveLeadSelectionIndex(convertToModel(lead));
        }

    }

    /**
     * @param lastIndex the view index to limit against the pipeline's output size
     * @return a valid view index (can be passed into convertToModel)
     */
    private int getSafeLastIndex(int lastIndex) {
        if ((pipeline == null) || !pipeline.isAssigned()) return lastIndex;
        // PENDING JW: negative? 
        return Math.min(lastIndex, pipeline.getOutputSize() - 1);
    }

    private int[] getSelectedRows(ListSelectionModel selection) {
        int iMin = selection.getMinSelectionIndex();
        int iMax = selection.getMaxSelectionIndex();

        if ((iMin == -1) || (iMax == -1)) {
            return new int[0];
        }

        int[] rvTmp = new int[1 + (iMax - iMin)];
        int n = 0;
        for (int i = iMin; i <= iMax; i++) {
            if (selection.isSelectedIndex(i)) {
                rvTmp[n++] = i;
            }
        }
        int[] rv = new int[n];
        System.arraycopy(rvTmp, 0, rv, 0, n);
        return rv;
    }

    /**
     * When the filter pipeline changes, update our view selection.
     */
    private PipelineListener getPipelineListener() {
        if (pipelineListener == null) {
            pipelineListener = new PipelineListener() {

                public void contentsChanged(PipelineEvent e) {
                    mapTowardsView();
                }

            };
        }
        return pipelineListener;
    }

    /**
     * When the view selection changes, update our model selection.
     */
    private ListSelectionListener getViewSelectionListener() {
        if (viewSelectionListener == null) {
            viewSelectionListener = new ListSelectionListener() {

                public void valueChanged(ListSelectionEvent e) {
                    if (e.getValueIsAdjusting()) return;
                    mapTowardsModel(e.getFirstIndex(), e.getLastIndex());
                }

            };
        }
        return viewSelectionListener;
    }

}
