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

import java.util.ArrayList;
import java.util.Collection;
import java.util.List;

import org.eclipse.collections.api.list.primitive.MutableIntList;
import org.eclipse.collections.impl.factory.primitive.IntLists;
import org.eclipse.nebula.widgets.nattable.coordinate.PositionUtil;
import org.eclipse.nebula.widgets.nattable.coordinate.Range;
import org.eclipse.nebula.widgets.nattable.layer.ILayer;
import org.eclipse.nebula.widgets.nattable.layer.event.ColumnStructuralChangeEvent;
import org.eclipse.nebula.widgets.nattable.layer.event.StructuralDiff;
import org.eclipse.nebula.widgets.nattable.layer.event.StructuralDiff.DiffTypeEnum;
import org.eclipse.nebula.widgets.nattable.util.ArrayUtil;

/**
 * Event indicating that one or multiple columns are moved to a new position.
 */
public class ColumnReorderEvent extends ColumnStructuralChangeEvent {

    private ILayer beforeLayer;

    private Collection<Range> beforeFromColumnPositionRanges;
    private MutableIntList beforeFromColumnIndexes;

    private int beforeToColumnPosition;
    private int beforeToColumnIndex;

    private boolean reorderToLeftEdge;

    /**
     *
     * @param layer
     *            The layer to which the column positions match.
     * @param beforeFromColumnPosition
     *            The column position that was reordered, before the reorder
     *            operation was performed.
     * @param beforeFromColumnIndex
     *            The index of the reordered position.
     * @param beforeToColumnPosition
     *            The position of the column to which the reorder operation was
     *            performed, before the reorder operation was performed
     * @param beforeToColumnIndex
     *            The index of the column to which the reorder operation was
     *            performed.
     * @param reorderToLeftEdge
     *            whether the reorder operation was performed to the left or the
     *            right edge.
     *
     * @since 1.6
     */
    public ColumnReorderEvent(ILayer layer,
            int beforeFromColumnPosition,
            int beforeFromColumnIndex,
            int beforeToColumnPosition,
            int beforeToColumnIndex,
            boolean reorderToLeftEdge) {
        this(layer,
                new int[] { beforeFromColumnPosition },
                new int[] { beforeFromColumnIndex },
                beforeToColumnPosition,
                beforeToColumnIndex,
                reorderToLeftEdge);
    }

    /**
     *
     * @param layer
     *            The layer to which the column positions match.
     * @param beforeFromColumnPositions
     *            The column positions that were reordered, before the reorder
     *            operation was performed.
     * @param beforeFromColumnIndexes
     *            The indexes of the reordered positions.
     * @param beforeToColumnPosition
     *            The position of the column to which the reorder operation was
     *            performed, before the reorder operation was performed
     * @param beforeToColumnIndex
     *            The index of the column to which the reorder operation was
     *            performed.
     * @param reorderToLeftEdge
     *            whether the reorder operation was performed to the left or the
     *            right edge.
     *
     * @since 1.6
     */
    public ColumnReorderEvent(ILayer layer,
            List<Integer> beforeFromColumnPositions,
            List<Integer> beforeFromColumnIndexes,
            int beforeToColumnPosition,
            int beforeToColumnIndex,
            boolean reorderToLeftEdge) {

        this(layer,
                beforeFromColumnPositions.stream().mapToInt(Integer::intValue).toArray(),
                beforeFromColumnIndexes.stream().mapToInt(Integer::intValue).toArray(),
                beforeToColumnPosition,
                beforeToColumnIndex,
                reorderToLeftEdge);
    }

    /**
     *
     * @param layer
     *            The layer to which the column positions match.
     * @param beforeFromColumnPositions
     *            The column positions that were reordered, before the reorder
     *            operation was performed.
     * @param beforeFromColumnIndexes
     *            The indexes of the reordered positions.
     * @param beforeToColumnPosition
     *            The position of the column to which the reorder operation was
     *            performed, before the reorder operation was performed
     * @param beforeToColumnIndex
     *            The index of the column to which the reorder operation was
     *            performed.
     * @param reorderToLeftEdge
     *            whether the reorder operation was performed to the left or the
     *            right edge.
     *
     * @since 2.0
     */
    public ColumnReorderEvent(ILayer layer,
            int[] beforeFromColumnPositions,
            int[] beforeFromColumnIndexes,
            int beforeToColumnPosition,
            int beforeToColumnIndex,
            boolean reorderToLeftEdge) {
        super(layer);
        this.beforeLayer = layer;
        this.beforeFromColumnPositionRanges = PositionUtil.getRanges(beforeFromColumnPositions);
        this.beforeFromColumnIndexes = IntLists.mutable.of(beforeFromColumnIndexes);
        this.beforeToColumnPosition = beforeToColumnPosition;
        this.beforeToColumnIndex = beforeToColumnIndex;
        this.reorderToLeftEdge = reorderToLeftEdge;

        MutableIntList allColumnPositions = IntLists.mutable.of(beforeFromColumnPositions);
        allColumnPositions.add(beforeToColumnPosition);
        setColumnPositionRanges(PositionUtil.getRanges(allColumnPositions.toSortedArray()));
    }

    /**
     * Constructor for internal use to clone this event.
     *
     * @param event
     *            The event out of which the new one should be created
     */
    public ColumnReorderEvent(ColumnReorderEvent event) {
        super(event);
        this.beforeLayer = event.beforeLayer;
        this.beforeFromColumnPositionRanges = new ArrayList<>(event.beforeFromColumnPositionRanges);
        this.beforeFromColumnIndexes = IntLists.mutable.ofAll(event.beforeFromColumnIndexes);
        this.beforeToColumnPosition = event.beforeToColumnPosition;
        this.beforeToColumnIndex = event.beforeToColumnIndex;
        this.reorderToLeftEdge = event.reorderToLeftEdge;
    }

    /**
     *
     * @return The indexes of the reordered columns.
     * @since 1.6
     */
    public Collection<Integer> getBeforeFromColumnIndexes() {
        return ArrayUtil.asIntegerList(this.beforeFromColumnIndexes.toSortedArray());
    }

    /**
     *
     * @return The indexes of the reordered columns.
     * @since 2.0
     */
    public int[] getBeforeFromColumnIndexesArray() {
        return this.beforeFromColumnIndexes.toSortedArray();
    }

    /**
     *
     * @return The position ranges of the reordered columns.
     */
    public Collection<Range> getBeforeFromColumnPositionRanges() {
        return this.beforeFromColumnPositionRanges;
    }

    /**
     *
     * @return The position of the column to which the reorder operation was
     *         performed.
     */
    public int getBeforeToColumnPosition() {
        return this.beforeToColumnPosition;
    }

    /**
     *
     * @return The index of the column to which the reorder operation was
     *         performed.
     * @since 1.6
     */
    public int getBeforeToColumnIndex() {
        return this.beforeToColumnIndex;
    }

    /**
     * Setter for the beforeToColumnIndex that needs to be called used in case a
     * reorder operation was performed to a hidden column.
     *
     * @param beforeIndex
     *            The index of the column to which the reorder operation was
     *            performed.
     * @since 1.6
     */
    public void setBeforeToColumnIndex(int beforeIndex) {
        this.beforeToColumnIndex = beforeIndex;
    }

    /**
     *
     * @return <code>true</code> if the columns were reordered to the left edge
     *         of the toColumnPosition, <code>false</code> if the reorder
     *         operation was performed on the right edge (e.g. at the end of a
     *         table)
     */
    public boolean isReorderToLeftEdge() {
        return this.reorderToLeftEdge;
    }

    @Override
    public Collection<StructuralDiff> getColumnDiffs() {
        Collection<StructuralDiff> columnDiffs = new ArrayList<>();

        Collection<Range> beforeFromColumnPositionRanges = getBeforeFromColumnPositionRanges();

        final int beforeToColumnPosition = (this.reorderToLeftEdge)
                ? this.beforeToColumnPosition
                : (this.beforeToColumnPosition + 1);
        int afterAddColumnPosition = beforeToColumnPosition;
        for (Range beforeFromColumnPositionRange : beforeFromColumnPositionRanges) {
            if (beforeFromColumnPositionRange.start < beforeToColumnPosition) {
                afterAddColumnPosition -=
                        Math.min(beforeFromColumnPositionRange.end, beforeToColumnPosition) - beforeFromColumnPositionRange.start;
            } else {
                break;
            }
        }
        int cumulativeAddSize = 0;
        for (Range beforeFromColumnPositionRange : beforeFromColumnPositionRanges) {
            cumulativeAddSize += beforeFromColumnPositionRange.size();
        }

        int offset = 0;
        for (Range beforeFromColumnPositionRange : beforeFromColumnPositionRanges) {
            int afterDeleteColumnPosition = beforeFromColumnPositionRange.start - offset;
            if (afterAddColumnPosition < afterDeleteColumnPosition) {
                afterDeleteColumnPosition += cumulativeAddSize;
            }
            columnDiffs.add(new StructuralDiff(
                    DiffTypeEnum.DELETE,
                    beforeFromColumnPositionRange,
                    new Range(afterDeleteColumnPosition, afterDeleteColumnPosition)));
            offset += beforeFromColumnPositionRange.size();
        }
        Range beforeAddRange = new Range(beforeToColumnPosition, beforeToColumnPosition);
        offset = 0;
        for (Range beforeFromColumnPositionRange : beforeFromColumnPositionRanges) {
            int size = beforeFromColumnPositionRange.size();
            columnDiffs.add(new StructuralDiff(
                    DiffTypeEnum.ADD,
                    beforeAddRange,
                    new Range(afterAddColumnPosition + offset, afterAddColumnPosition + offset + size)));
            offset += size;
        }

        return columnDiffs;
    }

    /**
     * This method is intended to be used for position conversion of the before
     * positions. The locally stored before positions are the positions of the
     * reordered columns positions <b>before</b> the reorder is applied. Needed
     * in case the conversion needs to be done before other states in the given
     * layer are updated, and therefore the local before position cannot be
     * determined anymore.
     *
     * @param layer
     *            The layer that performed the conversion of the before
     *            positions.
     * @param fromColumnPositionRanges
     *            The converted column position ranges that were reordered
     *            before the reorder happened.
     * @param toColumnPosition
     *            The converted column position to which a reorder happened
     *            before the reordering was performed.
     *
     * @since 1.6
     */
    public void setConvertedBeforePositions(ILayer layer, Collection<Range> fromColumnPositionRanges, int toColumnPosition) {
        this.beforeLayer = layer;
        this.beforeFromColumnPositionRanges = fromColumnPositionRanges;
        this.beforeToColumnPosition = toColumnPosition;
    }

    @Override
    public boolean convertToLocal(ILayer targetLayer) {
        // if the conversion was done before, e.g. by an
        // AbstractColumnHideShowLayer, we don't need to perform the conversion
        // again
        if (this.beforeLayer != targetLayer) {
            this.beforeFromColumnPositionRanges =
                    targetLayer.underlyingToLocalColumnPositions(getLayer(), this.beforeFromColumnPositionRanges);
            this.beforeToColumnPosition =
                    targetLayer.underlyingToLocalColumnPosition(getLayer(), this.beforeToColumnPosition);
            this.beforeLayer = targetLayer;
        }

        if (this.beforeToColumnPosition >= 0) {
            return super.convertToLocal(targetLayer);
        } else {
            return false;
        }
    }

    @Override
    public ColumnReorderEvent cloneEvent() {
        return new ColumnReorderEvent(this);
    }

}
