/*******************************************************************************
 * 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.hideshow;

import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.HashSet;

import org.eclipse.collections.api.map.primitive.IntIntMap;
import org.eclipse.collections.api.map.primitive.MutableIntIntMap;
import org.eclipse.collections.impl.factory.primitive.IntIntMaps;
import org.eclipse.nebula.widgets.nattable.coordinate.PositionUtil;
import org.eclipse.nebula.widgets.nattable.coordinate.Range;
import org.eclipse.nebula.widgets.nattable.layer.AbstractLayerTransform;
import org.eclipse.nebula.widgets.nattable.layer.ILayer;
import org.eclipse.nebula.widgets.nattable.layer.IUniqueIndexLayer;
import org.eclipse.nebula.widgets.nattable.layer.LayerUtil;
import org.eclipse.nebula.widgets.nattable.layer.cell.ILayerCell;
import org.eclipse.nebula.widgets.nattable.layer.cell.SpanningLayerCell;
import org.eclipse.nebula.widgets.nattable.layer.event.ILayerEvent;
import org.eclipse.nebula.widgets.nattable.layer.event.IStructuralChangeEvent;
import org.eclipse.nebula.widgets.nattable.layer.event.VisualRefreshEvent;
import org.eclipse.nebula.widgets.nattable.reorder.event.ColumnReorderEvent;

/**
 * Abstract implementation for column hide/show operations.
 */
public abstract class AbstractColumnHideShowLayer extends AbstractLayerTransform implements IUniqueIndexLayer {

    private MutableIntIntMap cachedVisibleColumnIndexPositionMapping;
    private MutableIntIntMap cachedVisibleColumnPositionIndexMapping;
    private MutableIntIntMap cachedHiddenColumnIndexPositionMapping;
    private MutableIntIntMap startXCache = IntIntMaps.mutable.empty();

    /**
     * Constructor.
     *
     * @param underlyingLayer
     *            The underlying layer.
     */
    public AbstractColumnHideShowLayer(IUniqueIndexLayer underlyingLayer) {
        super(underlyingLayer);
    }

    /**
     * @return The underlying layer.
     * @since 2.0
     */
    @Override
    protected IUniqueIndexLayer getUnderlyingLayer() {
        return (IUniqueIndexLayer) super.getUnderlyingLayer();
    }

    @Override
    public void handleLayerEvent(ILayerEvent event) {
        if (hasHiddenColumns() && event instanceof ColumnReorderEvent) {
            // we need to convert the before positions in the event BEFORE the
            // local states are changed, otherwise we are not able to convert
            // the before positions as the changed layer states would return
            // incorrect values
            ColumnReorderEvent reorderEvent = (ColumnReorderEvent) event;

            int[] fromPositions = reorderEvent.getBeforeFromColumnIndexes().stream()
                    .mapToInt(Integer::intValue)
                    .map(this::getColumnPositionByIndex)
                    .toArray();
            Collection<Range> fromRanges = PositionUtil.getRanges(fromPositions);

            int pos = -1;
            if (!isColumnIndexHidden(reorderEvent.getBeforeToColumnIndex())) {
                pos = getColumnPositionByIndex(reorderEvent.getBeforeToColumnIndex());
            } else {
                int i = 1;
                while (pos < 0) {
                    int next = reorderEvent.getBeforeToColumnPosition() + i;
                    if (next >= this.underlyingLayer.getColumnCount()) {
                        break;
                    }
                    pos = underlyingToLocalColumnPosition(this.underlyingLayer, next);
                    i++;
                }
                if (pos >= 0) {
                    reorderEvent.setBeforeToColumnIndex(getColumnIndexByPosition(pos));
                }
            }
            if (pos >= 0) {
                reorderEvent.setConvertedBeforePositions(this, fromRanges, pos);
            }
        }

        if (event instanceof IStructuralChangeEvent) {
            IStructuralChangeEvent structuralChangeEvent = (IStructuralChangeEvent) event;
            if (structuralChangeEvent.isHorizontalStructureChanged()) {
                invalidateCache();
            }
        } else if (event instanceof VisualRefreshEvent) {
            // visual change, e.g. font change, the startXCache needs to be
            // cleared in order to re-render correctly
            this.startXCache = IntIntMaps.mutable.empty();
        }
        super.handleLayerEvent(event);
    }

    // Horizontal features

    // Columns

    @Override
    public int getColumnCount() {
        if (!hasHiddenColumns()) {
            return super.getColumnCount();
        }
        return getCachedVisibleColumnIndexPositionMapping().size();
    }

    @Override
    public int getColumnIndexByPosition(int columnPosition) {
        if (columnPosition < 0 || columnPosition >= getColumnCount()) {
            return -1;
        }

        if (!hasHiddenColumns()) {
            return super.getColumnIndexByPosition(columnPosition);
        }

        return getCachedVisibleColumnPositionIndexMapping().getIfAbsent(columnPosition, -1);
    }

    @Override
    public int getColumnPositionByIndex(int columnIndex) {
        if (!hasHiddenColumns()) {
            return getUnderlyingLayer().getColumnPositionByIndex(columnIndex);
        }

        return getCachedVisibleColumnIndexPositionMapping().getIfAbsent(columnIndex, -1);
    }

    /**
     * Get the local column positions for the given column indexes.
     *
     * @param columnIndexes
     *            The column indexes for which the local column positions are
     *            requested.
     * @return The local column positions for the given column indexes.
     */
    public Collection<Integer> getColumnPositionsByIndexes(Collection<Integer> columnIndexes) {
        Collection<Integer> columnPositions = new HashSet<>();
        for (int columnIndex : columnIndexes) {
            columnPositions.add(getColumnPositionByIndex(columnIndex));
        }
        return columnPositions;
    }

    /**
     * Get the local column positions for the given column indexes.
     *
     * @param columnIndexes
     *            The column indexes for which the local column positions are
     *            requested.
     * @return The local column positions for the given column indexes.
     *
     * @since 2.0
     */
    public int[] getColumnPositionsByIndexes(int... columnIndexes) {
        return (columnIndexes != null && columnIndexes.length > 0)
                ? Arrays.stream(columnIndexes).map(this::getColumnPositionByIndex).toArray()
                : new int[0];
    }

    @Override
    public int localToUnderlyingColumnPosition(int localColumnPosition) {
        if (localColumnPosition < 0 || localColumnPosition >= getColumnCount()) {
            return -1;
        }

        if (!hasHiddenColumns()) {
            return localColumnPosition;
        }

        int columnIndex = getColumnIndexByPosition(localColumnPosition);
        return getUnderlyingLayer().getColumnPositionByIndex(columnIndex);
    }

    @Override
    public int underlyingToLocalColumnPosition(ILayer sourceUnderlyingLayer, int underlyingColumnPosition) {
        if (!hasHiddenColumns()) {
            return underlyingColumnPosition;
        }

        int columnIndex = getUnderlyingLayer().getColumnIndexByPosition(underlyingColumnPosition);
        int columnPosition = getColumnPositionByIndex(columnIndex);
        if (columnPosition >= 0) {
            return columnPosition;
        } else {
            return getCachedHiddenColumnIndexPositionMapping().getIfAbsent(columnIndex, -1);
        }
    }

    @Override
    public Collection<Range> underlyingToLocalColumnPositions(
            ILayer sourceUnderlyingLayer, Collection<Range> underlyingColumnPositionRanges) {
        Collection<Range> localColumnPositionRanges = new ArrayList<>(underlyingColumnPositionRanges.size());

        for (Range underlyingColumnPositionRange : underlyingColumnPositionRanges) {
            int startColumnPosition = getAdjustedUnderlyingToLocalStartPosition(
                    sourceUnderlyingLayer,
                    underlyingColumnPositionRange.start,
                    underlyingColumnPositionRange.end);
            int endColumnPosition = getAdjustedUnderlyingToLocalEndPosition(
                    sourceUnderlyingLayer,
                    underlyingColumnPositionRange.end,
                    underlyingColumnPositionRange.start);

            // teichstaedt: fixes the problem that ranges where added even if
            // the corresponding startPosition weren't found in the underlying
            // layer. Without that fix a bunch of ranges of kind Range [-1, 180]
            // which causes strange behaviour in Freeze- and other Layers were
            // returned.
            if (startColumnPosition > -1) {
                localColumnPositionRanges.add(new Range(startColumnPosition, endColumnPosition));
            }
        }

        return localColumnPositionRanges;
    }

    private int getAdjustedUnderlyingToLocalStartPosition(
            ILayer sourceUnderlyingLayer,
            int startUnderlyingPosition,
            int endUnderlyingPosition) {

        int localStartColumnPosition =
                underlyingToLocalColumnPosition(
                        sourceUnderlyingLayer,
                        startUnderlyingPosition);
        int offset = 0;
        while (localStartColumnPosition < 0
                && (startUnderlyingPosition + offset < endUnderlyingPosition)) {
            localStartColumnPosition =
                    underlyingToLocalColumnPosition(
                            sourceUnderlyingLayer,
                            startUnderlyingPosition + offset++);
        }
        return localStartColumnPosition;
    }

    private int getAdjustedUnderlyingToLocalEndPosition(
            ILayer sourceUnderlyingLayer,
            int endUnderlyingPosition,
            int startUnderlyingPosition) {

        int localEndColumnPosition =
                underlyingToLocalColumnPosition(
                        sourceUnderlyingLayer,
                        endUnderlyingPosition - 1);
        int offset = 0;
        while (localEndColumnPosition < 0
                && (endUnderlyingPosition - offset > startUnderlyingPosition)) {
            localEndColumnPosition =
                    underlyingToLocalColumnPosition(
                            sourceUnderlyingLayer,
                            endUnderlyingPosition - offset++);
        }
        return localEndColumnPosition + 1;
    }

    // Width

    @Override
    public int getWidth() {
        if (getColumnCount() == 0) {
            return 0;
        }

        int lastColumnPosition = getColumnCount() - 1;
        return getStartXOfColumnPosition(lastColumnPosition) + getColumnWidthByPosition(lastColumnPosition);
    }

    // X

    @Override
    public int getColumnPositionByX(int x) {
        return LayerUtil.getColumnPositionByX(this, x);
    }

    @Override
    public int getStartXOfColumnPosition(int localColumnPosition) {
        int cachedStartX = this.startXCache.getIfAbsent(localColumnPosition, -1);
        if (cachedStartX != -1) {
            return cachedStartX;
        }

        IUniqueIndexLayer underlyingLayer = getUnderlyingLayer();
        int underlyingPosition = localToUnderlyingColumnPosition(localColumnPosition);
        if (underlyingPosition < 0) {
            return -1;
        }
        int underlyingStartX = underlyingLayer.getStartXOfColumnPosition(underlyingPosition);
        if (underlyingStartX < 0) {
            return -1;
        }

        for (int hiddenIndex : getHiddenColumnIndexesArray()) {
            int hiddenPosition = underlyingLayer.getColumnPositionByIndex(hiddenIndex);
            // if the hidden position is -1, it is hidden in the underlying
            // layer therefore the underlying layer should handle the
            // positioning
            if (hiddenPosition >= 0 && hiddenPosition <= underlyingPosition) {
                underlyingStartX -= underlyingLayer.getColumnWidthByPosition(hiddenPosition);
            }
        }

        this.startXCache.put(localColumnPosition, underlyingStartX);
        return underlyingStartX;
    }

    // Vertical features

    // Rows

    @Override
    public int getRowPositionByIndex(int rowIndex) {
        return getUnderlyingLayer().getRowPositionByIndex(rowIndex);
    }

    // Hide/show

    /**
     * Will check if the column at the specified index is hidden or not.
     *
     * @param columnIndex
     *            The column index of the column whose visibility state should
     *            be checked.
     * @return <code>true</code> if the column at the specified index is hidden,
     *         <code>false</code> if it is visible.
     */
    public abstract boolean isColumnIndexHidden(int columnIndex);

    /**
     * Will collect and return all indexes of the columns that are hidden in
     * this layer.
     * <p>
     * <b>Note:</b> It is not intended that it also collects the column indexes
     * of underlying layers. This would cause issues on calculating positions,
     * as every layer is responsible for those calculations itself.
     * </p>
     * <p>
     * Since 2.0 it is recommended to use {@link #getHiddenColumnIndexesArray()}
     * to avoid unnecessary autoboxing operations.
     * </p>
     *
     * @return Collection of all column indexes that are hidden in this layer.
     */
    public abstract Collection<Integer> getHiddenColumnIndexes();

    /**
     * Will collect and return all indexes of the columns that are hidden in
     * this layer.
     * <p>
     * <b>Note:</b> It is not intended that it also collects the column indexes
     * of underlying layers. This would cause issues on calculating positions,
     * as every layer is responsible for those calculations itself.
     * </p>
     *
     * @return All column indexes that are hidden in this layer.
     *
     * @since 2.0
     */
    public abstract int[] getHiddenColumnIndexesArray();

    /**
     * Check if this layer actively hides columns.
     *
     * @return <code>true</code> if columns are hidden by this layer,
     *         <code>false</code> if not.
     *
     * @since 2.0
     */
    public abstract boolean hasHiddenColumns();

    @Override
    public ILayerCell getCellByPosition(int columnPosition, int rowPosition) {
        ILayerCell cell = super.getCellByPosition(columnPosition, rowPosition);
        if (cell != null && cell.isSpannedCell()) {
            // the spanning needs to be updated to reflect the
            // hiding accordingly
            int underlyingColumnPosition = localToUnderlyingColumnPosition(columnPosition);
            int underlyingRowPosition = localToUnderlyingRowPosition(rowPosition);
            ILayerCell underlyingCell = this.underlyingLayer.getCellByPosition(underlyingColumnPosition, underlyingRowPosition);

            boolean columnSpanUpdated = false;
            int columnSpan = underlyingCell.getColumnSpan();
            for (int column = 0; column < underlyingCell.getColumnSpan(); column++) {
                int columnIndex = this.underlyingLayer.getColumnIndexByPosition(underlyingCell.getOriginColumnPosition() + column);
                if (isColumnIndexHidden(columnIndex)) {
                    columnSpan--;
                    columnSpanUpdated = true;
                }
            }

            if (columnSpanUpdated) {
                cell = new SpanningLayerCell(cell, columnSpan, cell.getRowSpan());
            }
        }
        return cell;
    }

    // Cache

    /**
     * Invalidate the cache to ensure that information is rebuild.
     */
    protected synchronized void invalidateCache() {
        this.cachedVisibleColumnIndexPositionMapping = null;
        this.cachedVisibleColumnPositionIndexMapping = null;
        this.cachedHiddenColumnIndexPositionMapping = null;
        this.startXCache = IntIntMaps.mutable.empty();
    }

    private synchronized IntIntMap getCachedVisibleColumnIndexPositionMapping() {
        if (this.cachedVisibleColumnIndexPositionMapping == null) {
            cacheVisibleColumnIndexes();
        }
        return this.cachedVisibleColumnIndexPositionMapping;
    }

    private synchronized IntIntMap getCachedVisibleColumnPositionIndexMapping() {
        if (this.cachedVisibleColumnPositionIndexMapping == null) {
            cacheVisibleColumnIndexes();
        }
        return this.cachedVisibleColumnPositionIndexMapping;
    }

    private synchronized IntIntMap getCachedHiddenColumnIndexPositionMapping() {
        if (this.cachedHiddenColumnIndexPositionMapping == null) {
            cacheVisibleColumnIndexes();
        }
        return this.cachedHiddenColumnIndexPositionMapping;
    }

    /**
     * Build up the column caches.
     *
     * @since 2.0
     */
    protected synchronized void cacheVisibleColumnIndexes() {
        this.cachedVisibleColumnIndexPositionMapping = IntIntMaps.mutable.empty();
        this.cachedVisibleColumnPositionIndexMapping = IntIntMaps.mutable.empty();
        this.cachedHiddenColumnIndexPositionMapping = IntIntMaps.mutable.empty();
        this.startXCache = IntIntMaps.mutable.empty();

        // only build up a cache if it is necessary
        if (hasHiddenColumns()) {
            ILayer underlyingLayer = getUnderlyingLayer();
            int columnPosition = 0;
            for (int parentColumnPosition = 0; parentColumnPosition < underlyingLayer.getColumnCount(); parentColumnPosition++) {
                int columnIndex = underlyingLayer.getColumnIndexByPosition(parentColumnPosition);

                if (!isColumnIndexHidden(columnIndex)) {
                    this.cachedVisibleColumnIndexPositionMapping.put(columnIndex, columnPosition);
                    this.cachedVisibleColumnPositionIndexMapping.put(columnPosition, columnIndex);
                    columnPosition++;
                } else {
                    this.cachedHiddenColumnIndexPositionMapping.put(columnIndex, columnPosition);
                }
            }
        }
    }

}
