/*******************************************************************************
 * Copyright (c) 1998, 2015 Oracle and/or its affiliates. All rights reserved.
 * This program and the accompanying materials are made available under the
 * terms of the Eclipse Public License v1.0 and Eclipse Distribution License v. 1.0
 * which accompanies this distribution.
 * The Eclipse Public License is available at http://www.eclipse.org/legal/epl-v10.html
 * and the Eclipse Distribution License is available at
 * http://www.eclipse.org/org/documents/edl-v10.php.
 *
 * Contributors:
 *     Oracle - initial API and implementation from Oracle TopLink
******************************************************************************/
package org.eclipse.persistence.tools.workbench.uitools.app.swing;

import javax.swing.AbstractListModel;
import javax.swing.event.ListDataListener;

import org.eclipse.persistence.tools.workbench.uitools.app.CollectionListValueModelAdapter;
import org.eclipse.persistence.tools.workbench.uitools.app.CollectionValueModel;
import org.eclipse.persistence.tools.workbench.uitools.app.ListValueModel;
import org.eclipse.persistence.tools.workbench.uitools.app.ValueModel;
import org.eclipse.persistence.tools.workbench.utility.events.ListChangeEvent;
import org.eclipse.persistence.tools.workbench.utility.events.ListChangeListener;
import org.eclipse.persistence.tools.workbench.utility.string.StringTools;


/**
 * This javax.swing.ListModel can be used to keep a ListDataListener
 * (e.g. a JList) in synch with a ListValueModel (or a CollectionValueModel).
 *
 * An instance of this ListModel *must* be supplied with a value model,
 * which is a ListValueModel on the bound list or a CollectionValueModel
 * on the bound collection. This is required - the list (or collection)
 * itself can be null, but the value model that holds it cannot.
 */
public class ListModelAdapter extends AbstractListModel {

    /** A value model on the underlying model list. */
    protected ListValueModel listHolder;

    /**
     * Cache the size of the list for "dramatic" changes.
     * @see #listChanged(ListChangeEvent)
     */
    protected int listSize;

    /** A listener that allows us to forward changes made to the underlying model list. */
    protected ListChangeListener listChangeListener;


    // ********** constructors **********

    /**
     * Default constructor - initialize stuff.
     */
    private ListModelAdapter() {
        super();
        this.initialize();
    }

    /**
     * Constructor - the list holder is required.
     */
    public ListModelAdapter(ListValueModel listHolder) {
        this();
        this.setModel(listHolder);
    }

    /**
     * Constructor - the collection holder is required.
     */
    public ListModelAdapter(CollectionValueModel collectionHolder) {
        this();
        this.setModel(collectionHolder);
    }


    // ********** initialization **********

    protected void initialize() {
        this.listSize = 0;
        this.listChangeListener = this.buildListChangeListener();
    }

    protected ListChangeListener buildListChangeListener() {
        return new ListChangeListener() {
            public void itemsAdded(ListChangeEvent e) {
                ListModelAdapter.this.itemsAdded(e);
            }
            public void itemsRemoved(ListChangeEvent e) {
                ListModelAdapter.this.itemsRemoved(e);
            }
            public void itemsReplaced(ListChangeEvent e) {
                ListModelAdapter.this.itemsReplaced(e);
            }
            public void listChanged(ListChangeEvent e) {
                ListModelAdapter.this.listChanged();
            }
            public String toString() {
                return "list listener";
            }
        };
    }


    // ********** ListModel implementation **********

    /**
     * @see javax.swing.ListModel#getSize()
     */
    public int getSize() {
        return this.listHolder.size();
    }

    /**
     * @see javax.swing.ListModel#getElementAt(int)
     */
    public Object getElementAt(int index) {
        return this.listHolder.getItem(index);
    }

    /**
     * Extend to start listening to the underlying model list if necessary.
     * @see javax.swing.ListModel#addListDataListener(javax.swing.event.ListDataListener)
     */
    public void addListDataListener(ListDataListener l) {
        if (this.hasNoListDataListeners()) {
            this.engageModel();
            this.listSize = this.listHolder.size();
        }
        super.addListDataListener(l);
    }

    /**
     * Extend to stop listening to the underlying model list if appropriate.
     * @see javax.swing.ListModel#removeListDataListener(javax.swing.event.ListDataListener)
     */
    public void removeListDataListener(ListDataListener l) {
        super.removeListDataListener(l);
        if (this.hasNoListDataListeners()) {
            this.disengageModel();
            this.listSize = 0;
        }
    }


    // ********** public API **********

    /**
     * Return the underlying list model.
     */
    public ListValueModel getModel() {
        return this.listHolder;
    }

    /**
     * Set the underlying list model.
     */
    public void setModel(ListValueModel listHolder) {
        if (listHolder == null) {
            throw new NullPointerException();
        }
        boolean hasListeners = this.hasListDataListeners();
        if (hasListeners) {
            this.disengageModel();
        }
        this.listHolder = listHolder;
        if (hasListeners) {
            this.engageModel();
            this.listChanged();
        }
    }

    /**
     * Set the underlying collection model.
     */
    public void setModel(CollectionValueModel collectionHolder) {
        this.setModel(new CollectionListValueModelAdapter(collectionHolder));
    }


    // ********** queries **********

    /**
     * Return whether this model has no listeners.
     */
    protected boolean hasNoListDataListeners() {
        return this.getListDataListeners().length == 0;
    }

    /**
     * Return whether this model has any listeners.
     */
    protected boolean hasListDataListeners() {
        return ! this.hasNoListDataListeners();
    }


    // ********** behavior **********

    protected void engageModel() {
        this.listHolder.addListChangeListener(ValueModel.VALUE, this.listChangeListener);
    }

    protected void disengageModel() {
        this.listHolder.removeListChangeListener(ValueModel.VALUE, this.listChangeListener);
    }



    // ********** list change support **********

    /**
     * Items were added to the underlying model list.
     * Notify listeners of the changes.
     */
    protected void itemsAdded(ListChangeEvent e) {
        int start = e.getIndex();
        int end = start + e.size() - 1;
        this.fireIntervalAdded(this, start, end);
        this.listSize += e.size();
    }

    /**
     * Items were removed from the underlying model list.
     * Notify listeners of the changes.
     */
    protected void itemsRemoved(ListChangeEvent e) {
        int start = e.getIndex();
        int end = start + e.size() - 1;
        this.fireIntervalRemoved(this, start, end);
        this.listSize -= e.size();
    }

    /**
     * Items were replaced in the underlying model list.
     * Notify listeners of the changes.
     */
    protected void itemsReplaced(ListChangeEvent e) {
        int start = e.getIndex();
        int end = start + e.size() - 1;
        this.fireContentsChanged(this, start, end);
    }

    /**
     * The underlying model list has changed "dramatically".
     * Notify listeners of the changes.
     */
    protected void listChanged() {
        if (this.listSize != 0) {
            this.fireIntervalRemoved(this, 0, this.listSize - 1);
        }
        this.listSize = this.listHolder.size();
        if (this.listSize != 0) {
            this.fireIntervalAdded(this, 0, this.listSize - 1);
        } else {
            // this can happen when the "context" of the list changes;
            // even though there are no items in the list,
            // listeners might want to know that the list has "changed"
            this.fireContentsChanged(this, 0, -1);
        }
    }


    // ********** Object overrides **********

    public String toString() {
        return StringTools.buildToStringFor(this, this.listHolder);
    }

}
