/*******************************************************************************
 * Copyright (c) 2013, 2020 Dirk Fauth and others.
 *
 * This program and the accompanying materials are made
 * available under the terms of the Eclipse Public License 2.0
 * which is available at https://www.eclipse.org/legal/epl-2.0/
 *
 * SPDX-License-Identifier: EPL-2.0
 *
 * Contributors:
 *    Dirk Fauth <dirk.fauth@googlemail.com> - initial API and implementation
 *******************************************************************************/
package org.eclipse.nebula.widgets.nattable.extension.glazedlists.filterrow;

import java.util.ArrayList;
import java.util.Collection;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.concurrent.atomic.AtomicBoolean;

import org.eclipse.nebula.widgets.nattable.data.IColumnAccessor;
import org.eclipse.nebula.widgets.nattable.filterrow.combobox.FilterRowComboBoxDataProvider;
import org.eclipse.nebula.widgets.nattable.filterrow.combobox.FilterRowComboUpdateEvent;
import org.eclipse.nebula.widgets.nattable.layer.ILayer;
import org.eclipse.nebula.widgets.nattable.layer.event.CellVisualChangeEvent;
import org.eclipse.nebula.widgets.nattable.layer.event.ILayerEvent;
import org.eclipse.nebula.widgets.nattable.util.Scheduler;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import ca.odell.glazedlists.EventList;
import ca.odell.glazedlists.event.ListEvent;
import ca.odell.glazedlists.event.ListEventListener;

/**
 * Special implementation of FilterRowComboBoxDataProvider that performs
 * FilterRowComboUpdateEvents if the underlying list is changed.
 * <p>
 * This implementation is necessary for a special case. If a filter is applied
 * and a new row is added to the data model, the FilterList won't show the new
 * row because the current applied filter is not aware of the new values. This
 * is because of the inverse filter logic in then Excel like filter row. As the
 * FilterList doesn't show the new value, there is no ListEvent fired further,
 * so the FilterRowComboBoxDataProvider is not informed about the structural
 * change.
 * <p>
 * This implementation solves this issue by listening to the wrapped source
 * EventList of the FilterList instead of the NatTable IStructuralChangeEvent.
 */
public class GlazedListsFilterRowComboBoxDataProvider<T> extends
        FilterRowComboBoxDataProvider<T> implements ListEventListener<T> {

    private static final Logger LOG = LoggerFactory.getLogger(GlazedListsFilterRowComboBoxDataProvider.class);

    private static final Scheduler SCHEDULER = new Scheduler("GlazedListsFilterRowComboBoxDataProvider"); //$NON-NLS-1$

    private AtomicBoolean changeHandlingProcessing = new AtomicBoolean(false);

    private EventList<T> baseEventList;

    /**
     * @param bodyLayer
     *            A layer in the body region. Usually the DataLayer or a layer
     *            that is responsible for list event handling. Needed to
     *            register ourself as listener for data changes.
     * @param baseCollection
     *            The base collection used to collect the unique values from.
     *            This need to be a collection that is not filtered, otherwise
     *            after modifications the content of the filter row combo boxes
     *            will only contain the current visible (not filtered) elements.
     * @param columnAccessor
     *            The IColumnAccessor to be able to read the values out of the
     *            base collection objects.
     */
    public GlazedListsFilterRowComboBoxDataProvider(
            ILayer bodyLayer,
            Collection<T> baseCollection,
            IColumnAccessor<T> columnAccessor) {
        this(bodyLayer, baseCollection, columnAccessor, true);
    }

    /**
     * @param bodyLayer
     *            A layer in the body region. Usually the DataLayer or a layer
     *            that is responsible for list event handling. Needed to
     *            register ourself as listener for data changes.
     * @param baseCollection
     *            The base collection used to collect the unique values from.
     *            This need to be a collection that is not filtered, otherwise
     *            after modifications the content of the filter row combo boxes
     *            will only contain the current visible (not filtered) elements.
     * @param columnAccessor
     *            The IColumnAccessor to be able to read the values out of the
     *            base collection objects.
     * @param lazy
     *            <code>true</code> to configure this
     *            {@link FilterRowComboBoxDataProvider} should load the combobox
     *            values lazily, <code>false</code> to pre-build the value
     *            cache.
     * @since 1.4
     */
    public GlazedListsFilterRowComboBoxDataProvider(
            ILayer bodyLayer,
            Collection<T> baseCollection,
            IColumnAccessor<T> columnAccessor,
            boolean lazy) {
        super(bodyLayer, baseCollection, columnAccessor, lazy);

        if (baseCollection instanceof EventList) {
            this.baseEventList = ((EventList<T>) baseCollection);
            this.baseEventList.addListEventListener(this);
        } else {
            LOG.error("baseCollection is not of type EventList. List changes can not be tracked."); //$NON-NLS-1$
        }
    }

    @Override
    public void listChanged(ListEvent<T> listChanges) {
        if (!this.changeHandlingProcessing.getAndSet(true)) {
            // a new row was added or a row was deleted
            SCHEDULER.schedule(() -> {
                List<FilterRowComboUpdateEvent> updateEvents = new ArrayList<>();
                getValueCacheLock().writeLock().lock();
                try {
                    // remember the cache before updating
                    Map<Integer, List<?>> cacheBefore = new HashMap<>(getValueCache());

                    // perform a refresh of the whole cache
                    getValueCache().clear();

                    if (!GlazedListsFilterRowComboBoxDataProvider.this.lazyLoading) {
                        buildValueCache();
                    } else {
                        // to determine the diff for the update event
                        // the current values need to be collected,
                        // otherwise on clear() - addAll() a full reset
                        // will be triggered since there are no cached
                        // values
                        for (Map.Entry<Integer, List<?>> entry : cacheBefore.entrySet()) {
                            getValueCache().put(entry.getKey(),
                                    collectValues(entry.getKey()));
                        }
                    }

                    // fire events for every column that has cached data
                    for (Map.Entry<Integer, List<?>> entry : cacheBefore.entrySet()) {
                        updateEvents.add(buildUpdateEvent(
                                entry.getKey(),
                                entry.getValue(),
                                getValueCache().get(entry.getKey())));
                    }

                    GlazedListsFilterRowComboBoxDataProvider.this.changeHandlingProcessing.set(false);
                } finally {
                    getValueCacheLock().writeLock().unlock();
                }

                if (isUpdateEventsEnabled()) {
                    for (FilterRowComboUpdateEvent event : updateEvents) {
                        fireCacheUpdateEvent(event);
                    }
                }
            }, 100);
        }
    }

    @Override
    public void handleLayerEvent(final ILayerEvent event) {
        // we only need to perform event handling if caching is enabled
        if (this.cachingEnabled && event instanceof CellVisualChangeEvent) {
            SCHEDULER.schedule(() -> {
                // usually this is fired for data updates so we need to update
                // the value cache for the updated column
                getValueCacheLock().writeLock().lock();
                try {
                    int column = ((CellVisualChangeEvent) event).getColumnPosition();

                    List<?> cacheBefore = getValueCache().get(column);

                    // only update the cache in case a cache was build already
                    if (!GlazedListsFilterRowComboBoxDataProvider.this.lazyLoading
                            || cacheBefore != null) {
                        getValueCache().put(column, collectValues(column));
                    }

                    if (isUpdateEventsEnabled()) {
                        // get the diff and fire the event
                        fireCacheUpdateEvent(buildUpdateEvent(column, cacheBefore, getValueCache().get(column)));
                    }
                } finally {
                    getValueCacheLock().writeLock().unlock();
                }
            }, 0);
        }
    }

    @Override
    public void dispose() {
        super.dispose();
        SCHEDULER.shutdownNow();
    }

}
