/*******************************************************************************
 * Copyright (c) 2014, 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 462367
 ******************************************************************************/
package org.eclipse.nebula.widgets.nattable.extension.glazedlists.groupBy;

import java.text.MessageFormat;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

import org.eclipse.nebula.widgets.nattable.config.CellConfigAttributes;
import org.eclipse.nebula.widgets.nattable.config.ConfigRegistry;
import org.eclipse.nebula.widgets.nattable.config.IConfigRegistry;
import org.eclipse.nebula.widgets.nattable.data.convert.ContextualDisplayConverter;
import org.eclipse.nebula.widgets.nattable.data.convert.DefaultDisplayConverter;
import org.eclipse.nebula.widgets.nattable.data.convert.IDisplayConverter;
import org.eclipse.nebula.widgets.nattable.layer.LabelStack;
import org.eclipse.nebula.widgets.nattable.layer.cell.ILayerCell;
import org.eclipse.nebula.widgets.nattable.style.DisplayMode;
import org.eclipse.nebula.widgets.nattable.summaryrow.ISummaryProvider;
import org.eclipse.nebula.widgets.nattable.tree.TreeLayer;

/**
 * {@link IDisplayConverter} that is used for conversion of
 * {@link GroupByObject}s.
 * <p>
 * Should be registered for the label {@link GroupByDataLayer#GROUP_BY_OBJECT}.
 * </p>
 * <p>
 * It only returns a converted value in case of the tree column or a groupBy
 * summary is configured. For this it searches for an underlying
 * {@link IDisplayConverter} based on the GroupBy value or the summary column.
 * </p>
 *
 * @param <T>
 *            the type of the underlying data in the {@link GroupByDataLayer}
 *
 * @see GroupByDataLayer
 * @see GroupByDataLayerConfiguration
 */
public class GroupByDisplayConverter<T> extends ContextualDisplayConverter {

    private Object defaultSummaryValue = ISummaryProvider.DEFAULT_SUMMARY_VALUE;

    protected final Map<Integer, IDisplayConverter> wrappedConverters = new HashMap<>();
    protected final Map<Integer, IDisplayConverter> converterCache = new HashMap<>();

    protected final GroupByDataLayer<T> groupByDataLayer;

    /**
     *
     * @param groupByDataLayer
     *            The {@link GroupByDataLayer} necessary to perform index based
     *            or content based operations.
     */
    public GroupByDisplayConverter(GroupByDataLayer<T> groupByDataLayer) {
        this.groupByDataLayer = groupByDataLayer;
    }

    /**
     *
     * @param groupByDataLayer
     *            The {@link GroupByDataLayer} necessary to perform index based
     *            or content based operations.
     * @param defaultSummaryValue
     *            The value that will be shown in case the summary value is not
     *            calculated yet.
     */
    public GroupByDisplayConverter(GroupByDataLayer<T> groupByDataLayer, Object defaultSummaryValue) {
        this.groupByDataLayer = groupByDataLayer;
        this.defaultSummaryValue = defaultSummaryValue;
    }

    @Override
    public Object canonicalToDisplayValue(ILayerCell cell, IConfigRegistry configRegistry, Object canonicalValue) {
        // if the cell is a tree column cell return the data value
        if (cell.getConfigLabels().hasLabel(TreeLayer.TREE_COLUMN_CELL)) {
            Object displayValue = getDisplayValue(cell, configRegistry, canonicalValue);

            // handle child count pattern
            if (canonicalValue instanceof GroupByObject) {
                GroupByObject groupByObject = (GroupByObject) canonicalValue;

                String childCountPattern = configRegistry.getConfigAttribute(
                        GroupByConfigAttributes.GROUP_BY_CHILD_COUNT_PATTERN,
                        DisplayMode.NORMAL,
                        cell.getConfigLabels());

                if (childCountPattern != null && childCountPattern.length() > 0) {
                    List<T> children = this.groupByDataLayer.getItemsInGroup(groupByObject);
                    int directChildCount = this.groupByDataLayer.getTreeRowModel().getDirectChildren(cell.getRowIndex()).size();
                    displayValue = String.valueOf(displayValue)
                            + " " //$NON-NLS-1$
                            + MessageFormat.format(childCountPattern, children.size(), directChildCount);
                }
            }

            return displayValue;
        } else if (cell.getConfigLabels().hasLabel(GroupByDataLayer.GROUP_BY_SUMMARY)) {
            if (canonicalValue == null) {
                return this.defaultSummaryValue;
            }
            return getDisplayValue(cell, configRegistry, canonicalValue);
        } else {
            return ""; //$NON-NLS-1$
        }
    }

    @Override
    public Object displayToCanonicalValue(ILayerCell cell, IConfigRegistry configRegistry, Object displayValue) {
        // this method is called by editing features
        // since the default implementation doesn't support editing of
        // GroupByValues, this method simply returns the display value
        return displayValue;
    }

    /**
     * Searches for the {@link IDisplayConverter} to use for the given
     * {@link ILayerCell} and converts the given canonical value to the display
     * value using the found converter.
     *
     * @param cell
     *            The {@link ILayerCell} for which the display value is asked.
     * @param configRegistry
     *            The {@link ConfigRegistry} necessary to retrieve the
     *            {@link IDisplayConverter}.
     * @param canonicalValue
     *            The canonical value to convert.
     * @return The canonical value converted to the display value.
     */
    protected Object getDisplayValue(ILayerCell cell, IConfigRegistry configRegistry, Object canonicalValue) {
        IDisplayConverter converter = null;
        Object canonical = canonicalValue;

        if (this.wrappedConverters.containsKey(cell.getColumnIndex())) {
            converter = this.wrappedConverters.get(cell.getColumnIndex());
        } else if (canonicalValue instanceof GroupByObject) {
            GroupByObject groupByObject = (GroupByObject) canonicalValue;
            int lastGroupingIndex = -1;
            for (Map.Entry<Integer, Object> groupEntry : groupByObject.getDescriptor().entrySet()) {
                lastGroupingIndex = groupEntry.getKey();
            }

            if (lastGroupingIndex >= 0) {
                canonical = ((GroupByObject) canonicalValue).getValue();

                // check if we already have a converter for that index in the
                // cache
                if (this.converterCache.containsKey(lastGroupingIndex)) {
                    converter = this.converterCache.get(lastGroupingIndex);
                } else {
                    int rowPosition = cell.getRowPosition() + 1;
                    LabelStack stackBelow = this.groupByDataLayer.getConfigLabelsByPosition(lastGroupingIndex, rowPosition);
                    while (stackBelow.hasLabel(GroupByDataLayer.GROUP_BY_OBJECT)) {
                        stackBelow = this.groupByDataLayer.getConfigLabelsByPosition(lastGroupingIndex, ++rowPosition);
                    }

                    converter = configRegistry.getConfigAttribute(
                            CellConfigAttributes.DISPLAY_CONVERTER,
                            DisplayMode.NORMAL,
                            stackBelow);

                    // this way we are caching the found converters to avoid
                    // performance issues on searching for the correct one
                    // Note:
                    // Doing this avoids the possibility to change the converter
                    // at runtime, which is a rather uncommon scenario.
                    // In case the exchanging the converter at runtime is
                    // necessary you need to unregister any cached converter in
                    // this converter additionally
                    if (!this.converterCache.containsKey(lastGroupingIndex)) {
                        this.converterCache.put(lastGroupingIndex, converter);
                    }
                }
            }
        } else {
            // create a copy of the label stack to avoid finding this
            // converter again
            // Note: this displayConverter needs to be registered for the
            // GroupByDataLayer.GROUP_BY_OBJECT label
            List<String> labels = new ArrayList<>(cell.getConfigLabels());
            labels.remove(GroupByDataLayer.GROUP_BY_OBJECT);
            converter = configRegistry.getConfigAttribute(
                    CellConfigAttributes.DISPLAY_CONVERTER,
                    DisplayMode.NORMAL,
                    labels);

            if (converter == this) {
                // we found ourself again, so let's skip this
                converter = null;
            }
        }

        if (converter == null) {
            converter = new DefaultDisplayConverter();
        }

        return converter.canonicalToDisplayValue(cell, configRegistry, canonical);
    }

    /**
     * Registers a given {@link IDisplayConverter} for a column index. Only
     * necessary to override the searching for a {@link IDisplayConverter} that
     * is registered for the column of the grouping.
     *
     * @param columnIndex
     *            The column index for which the groupBy display converter
     *            should be overriden.
     * @param converter
     *            The {@link IDisplayConverter} that should be used for
     *            converting the groupBy value of the given index.
     */
    public void registerUnderlyingDisplayConverter(int columnIndex, IDisplayConverter converter) {
        this.wrappedConverters.put(columnIndex, converter);
    }

    /**
     * Unregister the {@link IDisplayConverter} that is registered for the given
     * column index.
     *
     * @param columnIndex
     *            The index for which the registered {@link IDisplayConverter}
     *            should be unregistered.
     */
    public void unregisterUnderlyingDisplayConverter(int columnIndex) {
        this.wrappedConverters.remove(columnIndex);
    }

    /**
     * Clear the internal converter cache. Needed in case of dynamical converter
     * registry updates.
     */
    public void clearConverterCache() {
        this.converterCache.clear();
    }
}
