/*******************************************************************************
 * Copyright (c) 2012, 2020 Original authors 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:
 *     Original authors and others - initial API and implementation
 ******************************************************************************/
package org.eclipse.nebula.widgets.nattable.filterrow;

import java.lang.reflect.InvocationTargetException;
import java.util.Collection;
import java.util.HashMap;
import java.util.Iterator;
import java.util.Map;
import java.util.Properties;

import org.eclipse.nebula.widgets.nattable.config.CellConfigAttributes;
import org.eclipse.nebula.widgets.nattable.config.IConfigRegistry;
import org.eclipse.nebula.widgets.nattable.data.IDataProvider;
import org.eclipse.nebula.widgets.nattable.data.convert.IDisplayConverter;
import org.eclipse.nebula.widgets.nattable.filterrow.event.FilterAppliedEvent;
import org.eclipse.nebula.widgets.nattable.layer.ILayer;
import org.eclipse.nebula.widgets.nattable.persistence.IPersistable;
import org.eclipse.nebula.widgets.nattable.style.DisplayMode;
import org.eclipse.nebula.widgets.nattable.util.ObjectUtils;
import org.eclipse.nebula.widgets.nattable.util.PersistenceUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

/**
 * Data provider for the filter row
 * <ul>
 * <li>Stores filter strings</li>
 * <li>Applies them to the ca.odell.glazedlists.matchers.MatcherEditor on the
 * ca.odell.glazedlists.FilterList</li>
 * </ul>
 */
public class FilterRowDataProvider<T> implements IDataProvider, IPersistable {

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

    /**
     * Replacement for the pipe character | that is used for persistence. If
     * regular expressions are used for filtering, the pipe character can be
     * used in the regular expression to specify alternations. As the
     * persistence mechanism in NatTable uses the pipe character for separation
     * of values, the persistence breaks for such cases. By replacing the pipe
     * in the regular expression with some silly uncommon value specified here,
     * we ensure to be able to also persist pipes in the regular expressions, as
     * well as being backwards compatible with already saved filter row states.
     */
    public static final String PIPE_REPLACEMENT = "°~°"; //$NON-NLS-1$

    /**
     * The prefix String that will be used to mark that the following filter
     * value in the persisted state is a collection.
     */
    public static final String FILTER_COLLECTION_PREFIX = "°coll("; //$NON-NLS-1$

    /**
     * The {@link IFilterStrategy} to which the set filter value should be
     * applied.
     */
    private final IFilterStrategy<T> filterStrategy;
    /**
     * The column header layer where this {@link IDataProvider} is used for
     * filtering. Needed for retrieval of column indexes and firing according
     * filter events.
     */
    private final ILayer columnHeaderLayer;
    /**
     * The {@link IDataProvider} of the column header. This is necessary to
     * retrieve the real column count of the column header and not a transformed
     * one. (e.g. hiding a column would change the column count in the column
     * header but not in the column header {@link IDataProvider}).
     */
    private final IDataProvider columnHeaderDataProvider;

    /**
     * The {@link IConfigRegistry} needed to retrieve the
     * {@link IDisplayConverter} for converting the values on state save/load
     * operations.
     */
    private final IConfigRegistry configRegistry;

    /**
     * Contains the filter objects mapped to the column index. Basically the
     * data storage for the set filters in the filter row so they are visible to
     * the user who entered them.
     */
    private Map<Integer, Object> filterIndexToObjectMap = new HashMap<>();

    /**
     *
     * @param filterStrategy
     *            The {@link IFilterStrategy} to which the set filter value
     *            should be applied.
     * @param columnHeaderLayer
     *            The column header layer where this {@link IDataProvider} is
     *            used for filtering needed for retrieval of column indexes and
     *            firing according filter events..
     * @param columnHeaderDataProvider
     *            The {@link IDataProvider} of the column header needed to
     *            retrieve the real column count of the column header and not a
     *            transformed one.
     * @param configRegistry
     *            The {@link IConfigRegistry} needed to retrieve the
     *            {@link IDisplayConverter} for converting the values on state
     *            save/load operations.
     */
    public FilterRowDataProvider(
            IFilterStrategy<T> filterStrategy,
            ILayer columnHeaderLayer,
            IDataProvider columnHeaderDataProvider,
            IConfigRegistry configRegistry) {
        this.filterStrategy = filterStrategy;
        this.columnHeaderLayer = columnHeaderLayer;
        this.columnHeaderDataProvider = columnHeaderDataProvider;
        this.configRegistry = configRegistry;
    }

    /**
     * Returns the map that contains the filter objects mapped to the column
     * index. It is the data storage for the inserted filters into the filter
     * row by the user.
     * <p>
     * Note: Usually it is not intended to modify this Map directly. You should
     * rather call <code>setDataValue(int, int, Object)</code> or
     * <code>clearAllFilters()</code> to modify this Map to ensure consistency
     * in other framework code. It is made visible because there might be code
     * that needs to modify the Map without index transformations or firing
     * events.
     *
     * @return Map that contains the filter objects mapped to the column index.
     */
    public Map<Integer, Object> getFilterIndexToObjectMap() {
        return this.filterIndexToObjectMap;
    }

    /**
     * Set the map that contains the filter objects mapped to the column index
     * to be the data storage for the inserted filters into the filter row by
     * the user.
     * <p>
     * Note: Usually it is not intended to set this Map from the outside as it
     * is created in the constructor. But there might be use cases where you
     * e.g. need to connect filter rows to each other. In this case it might be
     * useful to override the local Map with the one form another
     * FilterRowDataProvider. This is not a typical use case, therefore you
     * should use this method carefully!
     *
     * @param filterIndexToObjectMap
     *            Map that contains the filter objects mapped to the column
     *            index.
     */
    public void setFilterIndexToObjectMap(Map<Integer, Object> filterIndexToObjectMap) {
        this.filterIndexToObjectMap = filterIndexToObjectMap;
    }

    @Override
    public int getColumnCount() {
        return this.columnHeaderDataProvider.getColumnCount();
    }

    @Override
    public Object getDataValue(int columnIndex, int rowIndex) {
        return this.filterIndexToObjectMap.get(columnIndex);
    }

    @Override
    public int getRowCount() {
        return 1;
    }

    @Override
    public void setDataValue(int columnIndex, int rowIndex, Object newValue) {
        if (ObjectUtils.isNotNull(newValue)) {
            this.filterIndexToObjectMap.put(columnIndex, newValue);
        } else {
            this.filterIndexToObjectMap.remove(columnIndex);
        }

        this.filterStrategy.applyFilter(this.filterIndexToObjectMap);

        this.columnHeaderLayer.fireLayerEvent(new FilterAppliedEvent(this.columnHeaderLayer));
    }

    // Load/save state

    @Override
    public void saveState(String prefix, Properties properties) {
        Map<Integer, String> filterTextByIndex = new HashMap<>();
        for (Integer columnIndex : this.filterIndexToObjectMap.keySet()) {
            final IDisplayConverter converter = this.configRegistry.getConfigAttribute(
                    CellConfigAttributes.DISPLAY_CONVERTER,
                    DisplayMode.NORMAL,
                    FilterRowDataLayer.FILTER_ROW_COLUMN_LABEL_PREFIX + columnIndex);

            String filterText = getFilterStringRepresentation(this.filterIndexToObjectMap.get(columnIndex), converter);
            filterText = filterText.replace("|", PIPE_REPLACEMENT); //$NON-NLS-1$
            filterTextByIndex.put(columnIndex, filterText);
        }

        String string = PersistenceUtils.mapAsString(filterTextByIndex);

        if (!ObjectUtils.isEmpty(string)) {
            properties.put(prefix + FilterRowDataLayer.PERSISTENCE_KEY_FILTER_ROW_TOKENS, string);
        }
    }

    @Override
    public void loadState(String prefix, Properties properties) {
        this.filterIndexToObjectMap.clear();

        try {
            Object property = properties.get(prefix + FilterRowDataLayer.PERSISTENCE_KEY_FILTER_ROW_TOKENS);
            Map<Integer, String> filterTextByIndex = PersistenceUtils.parseString(property);
            for (Integer columnIndex : filterTextByIndex.keySet()) {
                final IDisplayConverter converter = this.configRegistry.getConfigAttribute(
                        CellConfigAttributes.DISPLAY_CONVERTER,
                        DisplayMode.NORMAL,
                        FilterRowDataLayer.FILTER_ROW_COLUMN_LABEL_PREFIX + columnIndex);

                String filterText = filterTextByIndex.get(columnIndex);
                filterText = filterText.replace(PIPE_REPLACEMENT, "|"); //$NON-NLS-1$
                this.filterIndexToObjectMap.put(columnIndex, getFilterFromString(filterText, converter));
            }
        } catch (Exception e) {
            LOG.error("Error while restoring filter row text!", e); //$NON-NLS-1$
        }

        this.filterStrategy.applyFilter(this.filterIndexToObjectMap);

        this.columnHeaderLayer.fireLayerEvent(new FilterAppliedEvent(this.columnHeaderLayer));
    }

    /**
     * This method is used to support saving of a filter collection, e.g. in the
     * context of the Excel like filter row. In such cases the filter value is
     * not a simple String but a Collection of filter values that need to be
     * converted to a String representation. As the state persistence is
     * encapsulated to be handled here, we need to take care of such states here
     * also.
     *
     * @param filterValue
     *            The filter value object that is used for filtering.
     * @param converter
     *            The converter that is used to convert the filter value, which
     *            is necessary to support filtering of custom types.
     * @return The String representation of the filter value.
     */
    private String getFilterStringRepresentation(Object filterValue, IDisplayConverter converter) {
        // in case the filter value is a collection of values, we need to create
        // a special string representation
        if (filterValue instanceof Collection) {
            String collectionSpec = FILTER_COLLECTION_PREFIX + filterValue.getClass().getName() + ")"; //$NON-NLS-1$
            StringBuilder builder = new StringBuilder(collectionSpec);
            builder.append("["); //$NON-NLS-1$
            Collection<?> filterCollection = (Collection<?>) filterValue;
            for (Iterator<?> iterator = filterCollection.iterator(); iterator.hasNext();) {
                Object filterObject = iterator.next();
                builder.append(converter.canonicalToDisplayValue(filterObject));
                if (iterator.hasNext()) {
                    builder.append(IPersistable.VALUE_SEPARATOR);
                }
            }

            builder.append("]"); //$NON-NLS-1$
            return builder.toString();
        }
        return (String) converter.canonicalToDisplayValue(filterValue);
    }

    /**
     * This method is used to support loading of a filter collection, e.g. in
     * the context of the Excel like filter row. In such cases the saved filter
     * value is not a simple String but represents a Collection of filter values
     * that need to be converted to the corresponding values. As the state
     * persistence is encapsulated to be handled here, we need to take care of
     * such states here also.
     *
     * @param filterText
     *            The String representation of the applied saved filter.
     * @param converter
     *            The converter that is used to convert the filter value, which
     *            is necessary to support filtering of custom types.
     * @return The filter value that will be used to apply a filter to the
     *         IFilterStrategy
     *
     * @throws InstantiationException
     * @throws IllegalAccessException
     * @throws ClassNotFoundException
     * @throws SecurityException
     * @throws NoSuchMethodException
     * @throws InvocationTargetException
     * @throws IllegalArgumentException
     */
    @SuppressWarnings({ "rawtypes", "unchecked" })
    private Object getFilterFromString(String filterText, IDisplayConverter converter)
            throws InstantiationException, IllegalAccessException, ClassNotFoundException, NoSuchMethodException, InvocationTargetException {

        if (filterText.startsWith(FILTER_COLLECTION_PREFIX)) {
            // the filter text represents a collection
            int indexEndCollSpec = filterText.indexOf(")"); //$NON-NLS-1$
            String collectionSpec = filterText.substring(filterText.indexOf("(") + 1, indexEndCollSpec); //$NON-NLS-1$
            Collection filterCollection = (Collection) Class.forName(collectionSpec).getDeclaredConstructor().newInstance();

            // also get rid of the collection marks
            filterText = filterText.substring(indexEndCollSpec + 2, filterText.length() - 1);
            String[] filterSplit = filterText.split(IPersistable.VALUE_SEPARATOR);
            for (String filterString : filterSplit) {
                filterCollection.add(converter.displayToCanonicalValue(filterString));
            }

            return filterCollection;
        }
        return converter.displayToCanonicalValue(filterText);
    }

    /**
     * Clear all filters that are currently applied.
     */
    public void clearAllFilters() {
        this.filterIndexToObjectMap.clear();
        this.filterStrategy.applyFilter(this.filterIndexToObjectMap);

        this.columnHeaderLayer.fireLayerEvent(new FilterAppliedEvent(this.columnHeaderLayer));
    }

}
