/*******************************************************************************
 * 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
 *    Dirk Fauth <dirk.fauth@googlemail.com> - Bug 454503
 *******************************************************************************/
package org.eclipse.nebula.widgets.nattable.extension.glazedlists.filterrow;

import static org.eclipse.nebula.widgets.nattable.filterrow.FilterRowDataLayer.FILTER_ROW_COLUMN_LABEL_PREFIX;
import static org.eclipse.nebula.widgets.nattable.filterrow.config.FilterRowConfigAttributes.FILTER_DISPLAY_CONVERTER;
import static org.eclipse.nebula.widgets.nattable.style.DisplayMode.NORMAL;

import java.util.Collection;
import java.util.HashMap;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import java.util.regex.Pattern;

import org.eclipse.nebula.widgets.nattable.config.IConfigRegistry;
import org.eclipse.nebula.widgets.nattable.data.IColumnAccessor;
import org.eclipse.nebula.widgets.nattable.data.convert.IDisplayConverter;
import org.eclipse.nebula.widgets.nattable.edit.EditConstants;
import org.eclipse.nebula.widgets.nattable.filterrow.combobox.FilterRowComboBoxDataProvider;

import ca.odell.glazedlists.FilterList;
import ca.odell.glazedlists.GlazedLists;
import ca.odell.glazedlists.matchers.CompositeMatcherEditor;
import ca.odell.glazedlists.matchers.MatcherEditor;
import ca.odell.glazedlists.matchers.Matchers;

/**
 * Specialisation of the DefaultGlazedListsStaticFilterStrategy that is intended
 * to be used in combination with FilterRowComboBoxCellEditors that allows
 * filtering via multiselect comboboxes like in Excel. As it extends
 * DefaultGlazedListsStaticFilterStrategy it also supports static filters which
 * allows to integrate it with the GlazedListsRowHideShowLayer.
 * <p>
 * The special case in here is that if nothing is selected in the filter
 * combobox, then everything should be filtered.
 */
public class ComboBoxGlazedListsFilterStrategy<T> extends DefaultGlazedListsStaticFilterStrategy<T> {

    /**
     * The FilterRowComboBoxDataProvider needed to determine whether filters
     * should applied or not. If there are no values specified for filtering of
     * a column then everything should be filtered, if all possible values are
     * given as filter then no filter needs to be applied.
     */
    private FilterRowComboBoxDataProvider<T> comboBoxDataProvider;

    /**
     * A MatcherEditor that will never match anything.
     */
    private MatcherEditor<T> matchNone = GlazedLists.fixedMatcherEditor(Matchers.falseMatcher());

    /**
     *
     * @param comboBoxDataProvider
     *            The FilterRowComboBoxDataProvider needed to determine whether
     *            filters should applied or not. If there are no values
     *            specified for filtering of a column then everything should be
     *            filtered, if all possible values are given as filter then no
     *            filter needs to be applied.
     * @param filterList
     *            The CompositeMatcherEditor that is used for GlazedLists
     *            filtering
     * @param columnAccessor
     *            The IColumnAccessor needed to access the row data to perform
     *            filtering
     * @param configRegistry
     *            The IConfigRegistry to retrieve several configurations from
     */
    public ComboBoxGlazedListsFilterStrategy(
            FilterRowComboBoxDataProvider<T> comboBoxDataProvider,
            FilterList<T> filterList,
            IColumnAccessor<T> columnAccessor,
            IConfigRegistry configRegistry) {
        super(filterList, columnAccessor, configRegistry);
        this.comboBoxDataProvider = comboBoxDataProvider;
    }

    /**
     * @param comboBoxDataProvider
     *            The FilterRowComboBoxDataProvider needed to determine whether
     *            filters should applied or not. If there are no values
     *            specified for filtering of a column then everything should be
     *            filtered, if all possible values are given as filter then no
     *            filter needs to be applied.
     * @param filterList
     *            The FilterList that is used within the GlazedLists based
     *            NatTable for filtering.
     * @param matcherEditor
     *            The CompositeMatcherEditor that should be used by this
     *            DefaultGlazedListsStaticFilterStrategy.
     * @param columnAccessor
     *            The IColumnAccessor necessary to access the column data of the
     *            row objects in the FilterList.
     * @param configRegistry
     *            The IConfigRegistry necessary to retrieve filter specific
     *            configurations.
     */
    public ComboBoxGlazedListsFilterStrategy(
            FilterRowComboBoxDataProvider<T> comboBoxDataProvider,
            FilterList<T> filterList,
            CompositeMatcherEditor<T> matcherEditor,
            IColumnAccessor<T> columnAccessor,
            IConfigRegistry configRegistry) {
        super(filterList, matcherEditor, columnAccessor, configRegistry);
        this.comboBoxDataProvider = comboBoxDataProvider;
    }

    @SuppressWarnings("rawtypes")
    @Override
    public void applyFilter(Map<Integer, Object> filterIndexToObjectMap) {
        if (filterIndexToObjectMap.isEmpty()) {
            this.filterLock.writeLock().lock();
            try {
                this.getMatcherEditor().getMatcherEditors().add(this.matchNone);
            } finally {
                this.filterLock.writeLock().unlock();
            }
            return;
        }

        // we need to create a new Map for applying a filter using the parent
        // class otherwise we would remove the previous added pre-selected
        // values
        Map<Integer, Object> newIndexToObjectMap = new HashMap<>();
        newIndexToObjectMap.putAll(filterIndexToObjectMap);

        // remove all complete selected
        for (Iterator<Map.Entry<Integer, Object>> it = newIndexToObjectMap.entrySet().iterator(); it.hasNext();) {
            Entry<Integer, Object> entry = it.next();
            Object filterObject = entry.getValue();
            if (EditConstants.SELECT_ALL_ITEMS_VALUE.equals(filterObject)) {
                it.remove();
            } else {
                List<?> dataProviderList = this.comboBoxDataProvider.getValues(entry.getKey(), 0);

                // selecting all is transported as String to support lazy
                // loading of combo box values
                Collection filterCollection = (filterObject instanceof Collection) ? (Collection) filterObject : null;
                if (filterCollection == null || filterCollection.isEmpty()) {
                    // for one column there are no items selected in the combo,
                    // therefore nothing matches
                    this.filterLock.writeLock().lock();
                    try {
                        this.getMatcherEditor().getMatcherEditors().add(this.matchNone);
                    } finally {
                        this.filterLock.writeLock().unlock();
                    }
                    return;
                } else if (filterCollectionsEqual(filterCollection, dataProviderList)) {
                    it.remove();
                }
            }

        }

        super.applyFilter(newIndexToObjectMap);
    }

    /**
     * {@inheritDoc}
     * <p>
     * This implementation is able to handle Collections and will generate a
     * regular expression containing all values in the Collection.
     */
    @SuppressWarnings("rawtypes")
    @Override
    protected String getStringFromColumnObject(final int columnIndex, final Object object) {
        final IDisplayConverter displayConverter = this.configRegistry.getConfigAttribute(
                FILTER_DISPLAY_CONVERTER,
                NORMAL,
                FILTER_ROW_COLUMN_LABEL_PREFIX + columnIndex);

        if (object instanceof Collection) {
            String result = ""; //$NON-NLS-1$
            Collection valueCollection = (Collection) object;
            for (Object value : valueCollection) {
                if (result.length() > 0) {
                    result += "|"; //$NON-NLS-1$
                }
                String convertedValue = displayConverter.canonicalToDisplayValue(value).toString();
                if (convertedValue.isEmpty()) {
                    // for an empty String add the regular expression for empty
                    // String
                    result += "^$"; //$NON-NLS-1$
                } else {
                    result += Pattern.quote(convertedValue);
                }
            }
            return "(" + result + ")"; //$NON-NLS-1$//$NON-NLS-2$
        }

        if (displayConverter != null) {
            Object result = displayConverter.canonicalToDisplayValue(object);
            if (result != null) {
                return result.toString();
            }
        }
        return ""; //$NON-NLS-1$
    }

    @SuppressWarnings("rawtypes")
    protected boolean filterCollectionsEqual(Collection filter1, Collection filter2) {
        if ((filter1 != null && filter2 != null)
                && filter1.size() == filter2.size()) {

            if (!filter1.equals(filter2)) {
                // as equality for collections take into account the order and
                // the elements we perform an additional check if the same items
                // regardless the order are contained in both lists
                for (Object f1 : filter1) {
                    if (!filter2.contains(f1)) {
                        return false;
                    }
                }
                // as lists can contain the same element twice, we also perform
                // a counter check
                for (Object f2 : filter2) {
                    if (!filter1.contains(f2)) {
                        return false;
                    }
                }
            }

            return true;
        }

        return false;
    }
}
