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

import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
import java.util.List;
import java.util.stream.Collectors;

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.group.model.IRowGroup;
import org.eclipse.nebula.widgets.nattable.group.model.IRowGroupModel;
import org.eclipse.nebula.widgets.nattable.group.performance.GroupModel;
import org.eclipse.nebula.widgets.nattable.group.performance.GroupModel.Group;
import org.eclipse.nebula.widgets.nattable.group.performance.RowGroupHeaderLayer;
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.selection.SelectionLayer.MoveDirectionEnum;

/**
 * The utility methods in this class bridge the divide between the world of row
 * indexes and positions and row objects used the model.
 *
 * @author Stefan Bolton
 * @author Matt Biggs
 *
 */
public final class RowGroupUtils {

    private RowGroupUtils() {
        // private default constructor for helper class
    }

    public static <T> IRowGroup<T> getRowGroupForRowIndex(final IRowGroupModel<T> model, final int rowIndex) {
        final T row = model.getRowFromIndexCache(rowIndex);
        return model.getRowGroupForRow(row);
    }

    public static <T> IRowGroup<T> getOwnRowGroupForRowIndex(final IRowGroupModel<T> model, final int rowIndex) {
        final T row = model.getRowFromIndexCache(rowIndex);
        IRowGroup<T> rowGroup = model.getRowGroupForRow(row);

        // If this is a sub-group row, then rowGroup will currently point to the
        // parent group.
        // We need to find the real, sub-group this row belongs to.
        if (rowGroup != null && !rowGroup.getOwnMemberRows(true).contains(row)) {
            rowGroup = rowGroup.getRowGroupForRow(row);
        }

        return rowGroup;
    }

    public static <T> boolean isPartOfAGroup(final IRowGroupModel<T> model, final int rowIndex) {
        final T row = model.getRowFromIndexCache(rowIndex);

        if (row != null) {
            return (model.getRowGroupForRow(row) != null);
        }

        return false;
    }

    public static <T> boolean isInTheSameGroup(final int fromRowIndex, final int toRowIndex, final IRowGroupModel<T> model) {
        final T fromRow = model.getRowFromIndexCache(fromRowIndex);
        final T toRow = model.getRowFromIndexCache(toRowIndex);

        IRowGroup<T> rowGroupFrom = getTopMostParentGroup(model.getRowGroupForRow(fromRow));
        IRowGroup<T> rowGroupTo = getTopMostParentGroup(model.getRowGroupForRow(toRow));

        return rowGroupFrom != null
                && rowGroupTo != null
                && rowGroupFrom.equals(rowGroupTo);
    }

    /**
     *
     * @param model
     *            The {@link IRowGroupModel} the given {@link IRowGroup} belongs
     *            to.
     * @param group
     *            The {@link IRowGroup} to check.
     * @return <code>true</code> if the given row group or one of its parent
     *         groups is collapsed.
     */
    public static <T> boolean isCollapsed(final IRowGroupModel<T> model, final IRowGroup<T> group) {
        return group == null
                || group.isCollapsed()
                || isAnyParentCollapsed(group);
    }

    /**
     *
     * @param group
     *            The {@link IRowGroup} to check.
     * @return <code>true</code> if any of the groups parent groups is
     *         collapsed.
     */
    public static <T> boolean isAnyParentCollapsed(IRowGroup<T> group) {
        boolean collapsed = false;

        if (group != null) {
            IRowGroup<T> topMostGroup = getTopMostParentGroup(group);

            // Walk up the group hierarchy until we find a collapsed group.
            while (!collapsed && group != topMostGroup) {
                group = group.getParentGroup();

                if (group == null) {
                    break;
                }

                collapsed = group.isCollapsed();
            }
        }

        return collapsed;
    }

    /**
     *
     * @param model
     *            The {@link IRowGroupModel} to check.
     * @param bodyRowIndex
     *            The index of a row whose row group should be inspected.
     * @return The number of rows in the row group to which the given
     *         bodyRowIndex belongs to.
     */
    public static <T> int sizeOfGroup(final IRowGroupModel<T> model, final int bodyRowIndex) {
        IRowGroup<T> group = getRowGroupForRowIndex(model, bodyRowIndex);

        if (group != null) {
            return getTopMostParentGroup(group).getMemberRows(true).size();
        } else {
            return 0;
        }
    }

    /**
     *
     * @param rowGroup
     *            The {@link IRowGroup} to check.
     * @return the top-most parent group of the given group or the group
     *         specified if it has no parents.
     */
    public static <T> IRowGroup<T> getTopMostParentGroup(final IRowGroup<T> rowGroup) {
        return (rowGroup == null
                ? null
                : (rowGroup.getParentGroup() == null)
                        ? rowGroup
                        : getTopMostParentGroup(rowGroup.getParentGroup()));
    }

    /**
     *
     * @param model
     *            The {@link IRowGroupModel} to check.
     * @param bodyRowIndex
     *            The index of a row whose row group should be inspected.
     * @return <code>true</code> if <code>bodyRowIndex</code> is contained in
     *         the list of static rows of the row group this index belongs to
     */
    public static <T> boolean isStaticRow(final IRowGroupModel<T> model, final int bodyRowIndex) {
        final T row = model.getRowFromIndexCache(bodyRowIndex);

        if (row != null) {
            IRowGroup<T> group = model.getRowGroupForRow(row);

            if (group != null) {
                return group.getStaticMemberRows().contains(row);
            }
        }

        return false;
    }

    public static boolean isRowIndexHiddenInUnderLyingLayer(int rowIndex, ILayer layer, IUniqueIndexLayer underlyingLayer) {
        return underlyingLayer.getRowPositionByIndex(rowIndex) == -1;
    }

    /**
     * Helper method to get the row positions for a specified layer
     *
     * If a row is currently invisible (-1) it will not be returned within the
     * collection
     *
     * @param layer
     *            The layer for which the position transformation should be
     *            performed.
     * @param bodyRowIndexes
     *            The row indexes for which the positions are requested.
     * @return Unmodifiable list of the row positions for the given layer
     */
    public static List<Integer> getRowPositionsInGroup(IUniqueIndexLayer layer, Collection<Integer> bodyRowIndexes) {
        return Collections.unmodifiableList(bodyRowIndexes.stream()
                .map(layer::getRowPositionByIndex)
                .filter(pos -> pos != -1)
                .sorted()
                .collect(Collectors.toList()));
    }

    /**
     *
     * @param model
     *            The {@link IRowGroupModel} to check.
     * @param rowIndex
     *            The index of a row whose row group should be inspected.
     * @return Unmodifiable list of row indexes and static row indexes in the
     *         same group as this index
     */
    public static <T> List<Integer> getRowIndexesInGroup(IRowGroupModel<T> model, int rowIndex) {
        final IRowGroup<T> group = getRowGroupForRowIndex(model, rowIndex);
        return getRowIndexesInGroup(model, group, true);
    }

    /**
     * Return the row indexes of the rows that belong to a group.
     *
     * @param <T>
     *            The type of the objects in the backing data.
     * @param model
     *            The {@link IRowGroupModel} to check.
     * @param group
     *            The {@link IRowGroup} to check.
     * @param includeStatic
     *            <code>true</code> if static rows should be included,
     *            <code>false</code> if not.
     * @return The row indexes of the rows that belong to the given group.
     */
    public static <T> List<Integer> getRowIndexesInGroup(IRowGroupModel<T> model, IRowGroup<T> group, boolean includeStatic) {
        return Collections.unmodifiableList(group.getMemberRows(includeStatic).stream()
                .map(model::getIndexFromRowCache)
                .sorted()
                .collect(Collectors.toList()));
    }

    /**
     * Helper method to get the row positions for a specified layer
     *
     * If a row is currently invisible (-1) it will not be returned within the
     * collection
     *
     * @param layer
     *            The layer for which the position transformation should be
     *            performed.
     * @param bodyRowIndexes
     *            The row indexes for which the positions are requested.
     * @return The row positions for the given layer.
     * @since 2.0
     */
    public static int[] getRowPositionsInGroup(IUniqueIndexLayer layer, int... bodyRowIndexes) {
        return Arrays.stream(bodyRowIndexes)
                .map(layer::getRowPositionByIndex)
                .filter(pos -> pos != -1)
                .sorted()
                .toArray();
    }

    /**
     *
     * @param model
     *            The {@link IRowGroupModel} to check.
     * @param rowIndex
     *            The index of a row whose row group should be inspected.
     * @return The row indexes and static row indexes in the same group as this
     *         index.
     * @since 2.0
     */
    public static <T> int[] getRowIndexesInGroupAsArray(IRowGroupModel<T> model, int rowIndex) {
        final IRowGroup<T> group = getRowGroupForRowIndex(model, rowIndex);
        return getRowIndexesInGroupAsArray(model, group, true);
    }

    /**
     * Return the row indexes of the rows that belong to a group.
     *
     * @param <T>
     *            The type of the objects in the backing data.
     * @param model
     *            The {@link IRowGroupModel} to check.
     * @param group
     *            The {@link IRowGroup} to check.
     * @param includeStatic
     *            <code>true</code> if static rows should be included,
     *            <code>false</code> if not.
     * @return The row indexes of the rows that belong to the given group.
     * @since 2.0
     */
    public static <T> int[] getRowIndexesInGroupAsArray(IRowGroupModel<T> model, IRowGroup<T> group, boolean includeStatic) {
        return group.getMemberRows(includeStatic).stream()
                .mapToInt(model::getIndexFromRowCache)
                .sorted()
                .toArray();
    }

    public static <T> String getRowGroupNameForIndex(IRowGroupModel<T> model, int bodyRowIndex) {
        IRowGroup<T> group = getRowGroupForRowIndex(model, bodyRowIndex);
        if (group != null) {
            return group.getGroupName();
        }
        return null;
    }

    /**
     * Checks if the two given row positions on the given layer belong to the
     * same group at the given level on the given {@link RowGroupHeaderLayer}.
     *
     * @param layer
     *            The {@link RowGroupHeaderLayer} which is needed to perform the
     *            check against.
     * @param level
     *            The grouping level to check.
     * @param fromPosition
     *            The row position to check based on the position layer of the
     *            given {@link RowGroupHeaderLayer}.
     * @param toPosition
     *            The row position to check based on the position layer of the
     *            given {@link RowGroupHeaderLayer}.
     * @return <code>true</code> if both given positions belong to the same
     *         group, <code>false</code> if not.
     *
     * @since 1.6
     */
    public static boolean isInTheSameGroup(RowGroupHeaderLayer layer, int level, int fromPosition, int toPosition) {
        Group fromGroup = layer.getGroupModel(level).getGroupByPosition(fromPosition);
        Group toGroup = layer.getGroupModel(level).getGroupByPosition(toPosition);

        return fromGroup != null && toGroup != null && fromGroup == toGroup;
    }

    /**
     * Checks if the row positions at the given y coordinates belong to the same
     * group or not.
     *
     * @param natLayer
     *            The layer to which the given positions match. Typically the
     *            NatTable itself.
     * @param startY
     *            The y coordinate of the row that should be checked. Typically
     *            the drag start y coordinate.
     * @param endY
     *            The y coordinate of the row that should be checked. Typically
     *            the drag end y coordinate.
     * @param layer
     *            The {@link RowGroupHeaderLayer} which is needed to perform the
     *            check against.
     * @param level
     *            The grouping level to check.
     * @return <code>true</code> if there is a row group boundary between startY
     *         and endY, <code>false</code> if both positions are in the same
     *         group.
     *
     * @since 1.6
     */
    public static boolean isBetweenTwoGroups(ILayer natLayer, int startY, int endY, RowGroupHeaderLayer layer, int level) {
        int natFromPosition = natLayer.getRowPositionByY(startY);
        int natToPosition = natLayer.getRowPositionByY(endY);

        // convert grid position to layer position
        int fromPosition = LayerUtil.convertRowPosition(natLayer, natFromPosition, layer.getPositionLayer());
        int toPosition = LayerUtil.convertRowPosition(natLayer, natToPosition, layer.getPositionLayer());

        boolean result = !RowGroupUtils.isInTheSameGroup(
                layer,
                level,
                fromPosition,
                toPosition);

        // special check for reordering to position 0 left of an unbreakable
        // group
        if (!result && fromPosition == toPosition && natFromPosition < natToPosition) {
            result = true;
        }

        return result;
    }

    /**
     * Checks if the edge of a row position is the top-most or the bottom most
     * row on any level of a row group.
     *
     * @param rowGroupHeaderLayer
     *            The {@link RowGroupHeaderLayer} to handle the checks.
     * @param toPosition
     *            The position to check. Needs to be related to the
     *            positionLayer.
     * @param reorderToTopEdge
     *            <code>true</code> if the check should be performed to the top
     *            edge or the bottom edge of the toPosition.
     * @param moveDirection
     *            The direction in which the reordering is performed.
     * @return <code>true</code> if the destination would be between two groups,
     *         <code>false</code> if the destination would be inside a group.
     *
     * @since 1.6
     */
    public static boolean isBetweenTwoGroups(
            RowGroupHeaderLayer rowGroupHeaderLayer, int toPosition, boolean reorderToTopEdge, MoveDirectionEnum moveDirection) {

        if ((toPosition == 0 && reorderToTopEdge)
                || (toPosition == (rowGroupHeaderLayer.getPositionLayer().getRowCount() - 1) && !reorderToTopEdge)) {
            // start or end of the table
            return true;
        }

        int toPositionToCheck = toPosition;
        if (reorderToTopEdge
                && MoveDirectionEnum.DOWN == moveDirection) {
            toPositionToCheck--;
        }

        boolean valid = true;
        for (int level = 0; level < rowGroupHeaderLayer.getLevelCount(); level++) {
            if (MoveDirectionEnum.DOWN == moveDirection) {
                valid = !isInTheSameGroup(rowGroupHeaderLayer, level, toPosition, toPositionToCheck);
            } else {
                valid = !isInTheSameGroup(rowGroupHeaderLayer, level, toPosition, toPositionToCheck + (reorderToTopEdge ? -1 : 1));
            }

            if (!valid) {
                break;
            }
        }

        return valid;
    }

    /**
     * Checks if the edge of a row position for a specific grouping level is the
     * top-most or the bottom most row of a row group.
     *
     * @param rowGroupHeaderLayer
     *            The {@link RowGroupHeaderLayer} to handle the checks.
     * @param level
     *            The grouping level on which the check should be performed.
     * @param toPosition
     *            The position to check. Needs to be related to the
     *            positionLayer.
     * @param reorderToTopEdge
     *            <code>true</code> if the check should be performed to the top
     *            edge or the bottom edge of the toPosition.
     * @param moveDirection
     *            The direction in which the reordering is performed.
     * @return <code>true</code> if the destination would be between two groups,
     *         <code>false</code> if the destination would be inside a group.
     *
     * @since 1.6
     */
    public static boolean isBetweenTwoGroups(
            RowGroupHeaderLayer rowGroupHeaderLayer, int level, int toPosition, boolean reorderToTopEdge, MoveDirectionEnum moveDirection) {

        if ((toPosition == 0 && reorderToTopEdge)
                || (toPosition == (rowGroupHeaderLayer.getPositionLayer().getRowCount() - 1) && !reorderToTopEdge)) {
            // start or end of the table
            return true;
        }

        int toPositionToCheck = toPosition;
        if (MoveDirectionEnum.DOWN == moveDirection) {
            if (reorderToTopEdge) {
                toPositionToCheck--;
            } else {
                toPositionToCheck++;
            }
        }

        boolean valid = true;
        if (MoveDirectionEnum.DOWN == moveDirection) {
            valid = !isInTheSameGroup(rowGroupHeaderLayer, level, toPosition, toPositionToCheck);
        } else {
            valid = !isInTheSameGroup(rowGroupHeaderLayer, level, toPosition, toPositionToCheck + (reorderToTopEdge ? -1 : 1));
        }

        if (valid && level > 0) {
            // check on deeper levels
            valid = isBetweenTwoGroups(rowGroupHeaderLayer, level - 1, toPosition, reorderToTopEdge, moveDirection);
        }

        return valid;
    }

    /**
     * Checks if a reorder operation is valid by checking the unbreakable states
     * of the groups below the from and the to position.
     *
     * @param rowGroupHeaderLayer
     *            The {@link RowGroupHeaderLayer} to get the groups to check.
     * @param fromPosition
     *            The position from which a row should be reordered.
     * @param toPosition
     *            The position to which a row should be reordered.
     * @param reorderToTopEdge
     *            <code>true</code> if the reorder should be performed to the
     *            top edge of the toPosition.
     * @return <code>true</code> if the reorder operation would be valid,
     *         <code>false</code> if the either the source or the target belongs
     *         to an unbreakable group.
     *
     * @since 1.6
     */
    public static boolean isReorderValid(RowGroupHeaderLayer rowGroupHeaderLayer, int fromPosition, int toPosition, boolean reorderToTopEdge) {
        for (int level = 0; level < rowGroupHeaderLayer.getLevelCount(); level++) {
            if (!isReorderValid(rowGroupHeaderLayer, level, fromPosition, toPosition, reorderToTopEdge)) {
                return false;
            }
        }
        return true;
    }

    /**
     * Checks if a reorder operation is valid by checking the unbreakable states
     * of the groups below the from and the to position.
     *
     * @param rowGroupHeaderLayer
     *            The {@link RowGroupHeaderLayer} to get the groups to check.
     * @param level
     *            The grouping level that should be checked.
     * @param fromPosition
     *            The position from which a row should be reordered.
     * @param toPosition
     *            The position to which a row should be reordered.
     * @param reorderToTopEdge
     *            <code>true</code> if the reorder should be performed to the
     *            top edge of the toPosition.
     * @return <code>true</code> if the reorder operation would be valid,
     *         <code>false</code> if the either the source or the target belongs
     *         to an unbreakable group.
     *
     * @since 1.6
     */
    public static boolean isReorderValid(
            RowGroupHeaderLayer rowGroupHeaderLayer, int level, int fromPosition, int toPosition, boolean reorderToTopEdge) {

        MoveDirectionEnum moveDirection = PositionUtil.getVerticalMoveDirection(fromPosition, toPosition);

        int toPositionToCheck = toPosition;
        if (MoveDirectionEnum.DOWN == moveDirection && reorderToTopEdge
                || toPosition == rowGroupHeaderLayer.getPositionLayer().getRowCount() && !reorderToTopEdge) {
            toPositionToCheck--;
        }

        boolean fromUnbreakable = false;
        boolean valid = true;
        // check if the from position is unbreakable
        GroupModel model = rowGroupHeaderLayer.getGroupModel(level);
        Group group = model.getGroupByPosition(fromPosition);
        if (group != null && group.isUnbreakable() && group.getVisibleSpan() > 1) {
            fromUnbreakable = true;
            valid = isInTheSameGroup(rowGroupHeaderLayer, level, fromPosition, toPositionToCheck);
        }

        // if the from position is part of an unbreakable group, we already know
        // the result
        if (!fromUnbreakable) {
            if ((toPosition == 0 && reorderToTopEdge)
                    || (toPosition == (rowGroupHeaderLayer.getPositionLayer().getRowCount() - 1) && !reorderToTopEdge)) {
                // start or end of the table
                return true;
            }

            // check if the to position is unbreakable
            if (model.isPartOfAnUnbreakableGroup(toPositionToCheck)) {
                // check if the original toPosition is in another group to
                // see if we might reorder between groups
                if (MoveDirectionEnum.DOWN == moveDirection) {
                    valid = !isInTheSameGroup(rowGroupHeaderLayer, level, toPosition, toPositionToCheck);
                } else {
                    valid = !isInTheSameGroup(rowGroupHeaderLayer, level, toPosition, toPositionToCheck + (reorderToTopEdge ? -1 : 1));
                }
            }
        }

        return valid;
    }

    /**
     * Check if a complete group is reordered.
     *
     * @param fromGroup
     *            The group to check.
     * @param fromPositions
     *            The positions to check.
     * @return <code>true</code> if the fromPositions are all part of the given
     *         group.
     * @since 1.6
     */
    public static boolean isGroupReordered(Group fromGroup, int[] fromPositions) {
        int[] visiblePositions = fromGroup.getVisiblePositions();
        if (visiblePositions.length > fromPositions.length) {
            return false;
        } else if (visiblePositions.length < fromPositions.length) {
            MutableIntList from = IntLists.mutable.of(fromPositions);
            return from.containsAll(visiblePositions);
        } else {
            return Arrays.equals(visiblePositions, fromPositions);
        }
    }
}
