IndexGeneratorHelper.java

/*
 * The coLAB project
 * Copyright (C) 2021-2023 AlbaSim, MEI, HEIG-VD, HES-SO
 *
 * Licensed under the MIT License
 */
package ch.colabproject.colab.api.controller.document;

import ch.colabproject.colab.api.model.WithIndex;
import ch.colabproject.colab.generator.model.exceptions.HttpErrorMessage;
import ch.colabproject.colab.generator.model.exceptions.MessageI18nKey;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Comparator;
import java.util.List;
import javax.ejb.LocalBean;
import javax.ejb.Stateless;

/**
 * Deal with assigning index in a collection. The index is a field of the object and not the
 * built-in index of a list.
 * <p>
 * The indexes are progressive (from small to big) but the size of the room between two neighboring
 * indexes is not minimal nor has to be regular. In other words two neighboring indexes do not need
 * to be two just-following numbers.
 * <p>
 * The purpose is to be able to easily add an object between others without having to adjust other
 * indexes. In order to do that, we set a room between two neighboring indexes.
 *
 * @author sandra
 * @author maxence
 *
 * @param <T> Type of the items
 */
@Stateless
@LocalBean
public class IndexGeneratorHelper<T extends WithIndex> {

    /**
     * The default value of the smallest index that is suitable
     */
    private final int DEFAULT_MIN_INDEX = 0;

    /**
     * The default value of the biggest index that is suitable
     */
    private final int DEFAULT_MAX_INDEX = Integer.MAX_VALUE;

    /**
     * Default room between two neighboring indexes
     */
    private final int DEFAULT_INDEX_INC = 1000;

    /**
     * Value of the smallest index that is suitable
     */
    private final int minIndex;

    /**
     * Value of the biggest index that is suitable
     */
    private final int maxIndex;

    /**
     * Initial room between two neighboring indexes
     */
    private final int indexIncrement;

    /**
     * Index for the first item in the list
     */
    private final int soloIndex;

    /**
     * Default constructor
     */
    public IndexGeneratorHelper() {
        minIndex = DEFAULT_MIN_INDEX;
        maxIndex = DEFAULT_MAX_INDEX;
        indexIncrement = DEFAULT_INDEX_INC;

        soloIndex = minIndex + indexIncrement;
    }

    /**
     * Constructor with parameters
     *
     * @param minIndex       The value of the smallest index that is suitable
     * @param maxIndex       The value of the biggest index that is suitable
     * @param indexIncrement The initial room between two neighboring indexes
     */
    public IndexGeneratorHelper(int minIndex, int maxIndex, int indexIncrement) {
        this.minIndex = minIndex;
        this.maxIndex = maxIndex;
        this.indexIncrement = indexIncrement;

        soloIndex = minIndex + indexIncrement;
    }

    // *********************************************************************************************
    //
    // *********************************************************************************************

    /**
     * Move (or set) the given item at the beginning of the collection.
     * <p>
     * That means setting an index to the item so that it is the first when sorting by index.
     *
     * @param item       the item waiting for an index to be set
     * @param collection the collection of items
     *
     * @throws HttpErrorMessage if the collection is too big
     */
    public void moveItemToBeginning(T item, Collection<T> collection) {
        if (collection.isEmpty()) {
            item.setIndex(soloIndex);
        } else {
            List<T> sortedList = sortCollection(collection);

            T baseItem = sortedList.get(0);

            int justTooLow = minIndex - 1;

            if (!hasEnoughSpaceBetween(justTooLow, baseItem.getIndex())) {
                reorderIndexes(sortedList);
            }

            int wantedIndex = computeInBetweenIndex(justTooLow, baseItem.getIndex());

            item.setIndex(wantedIndex);
        }
    }

    /**
     * Move the given item one step ahead from where it is now.
     *
     * @param item       the item waiting for a new index
     * @param collection the collection of items
     *
     * @throws HttpErrorMessage if the item is not in the collection
     */
    public void moveOneStepAhead(T item, Collection<T> collection) {
        if (!collection.contains(item)) {
            throw HttpErrorMessage.dataError(MessageI18nKey.DATA_INTEGRITY_FAILURE);
        }

        List<T> sortedList = sortCollection(collection);

        T item1 = getItemBefore(item, sortedList);

        if (item1 == null) {
            // already at the beginning
            return;
        }

        int itemIndex = item.getIndex();
        item.setIndex(item1.getIndex());
        item1.setIndex(itemIndex);
    }

    /**
     * Move (or set) the given item before the baseItem.
     *
     * @param item       the item waiting for a new index
     * @param baseItem   the item of reference
     * @param collection the collection of items
     *
     * @throws HttpErrorMessage if the baseItem is not in the collection or if the collection is too
     *                          big
     */
    public void moveItemBefore(T item, T baseItem, Collection<T> collection) {
        if (!collection.contains(baseItem)) {
            throw HttpErrorMessage.dataError(MessageI18nKey.DATA_INTEGRITY_FAILURE);
        }

        List<T> sortedList = sortCollection(collection);

        T item1 = baseItem;
        T item2 = getItemBefore(baseItem, sortedList);

        if (item2 == null) {
            // set before item1 which is the first item
            moveItemToBeginning(item, collection);

        } else {
            // between two items
            if (!hasEnoughSpaceBetween(item1.getIndex(), item2.getIndex())) {
                reorderIndexes(sortedList);
            }

            int wantedIndex = computeInBetweenIndex(item1.getIndex(), item2.getIndex());

            item.setIndex(wantedIndex);
        }
    }

    /**
     * Move an item one step behind from where it is now.
     *
     * @param item       the item waiting for a new index
     * @param collection the collection of items
     *
     * @throws HttpErrorMessage if the item is not in the collection
     */
    public void moveOneStepBehind(T item, Collection<T> collection) {
        if (!collection.contains(item)) {
            throw HttpErrorMessage.dataError(MessageI18nKey.DATA_INTEGRITY_FAILURE);
        }

        List<T> sortedList = sortCollection(collection);

        T item1 = getItemAfter(item, sortedList);

        if (item1 == null) {
            // already at the end
            return;
        }

        int itemIndex = item.getIndex();
        item.setIndex(item1.getIndex());
        item1.setIndex(itemIndex);
    }

    /**
     * Move (or set) the given item after the baseItem.
     *
     * @param item       the item waiting for a new index
     * @param baseItem   the item of reference
     * @param collection the collection of items
     *
     * @throws HttpErrorMessage if the baseItem is not in the collection or if the collection is too
     *                          big
     */
    public void moveItemAfter(T item, T baseItem, Collection<T> collection) {
        if (!collection.contains(baseItem)) {
            throw HttpErrorMessage.dataError(MessageI18nKey.DATA_INTEGRITY_FAILURE);
        }

        List<T> sortedList = sortCollection(collection);

        T item1 = baseItem;
        T item2 = getItemAfter(baseItem, sortedList);

        if (item2 == null) {
            // set after item1 which is the last item
            moveItemToEnd(item, collection);

        } else {
            if (!hasEnoughSpaceBetween(item1.getIndex(), item2.getIndex())) {
                reorderIndexes(sortedList);
            }

            int wantedIndex = computeInBetweenIndex(item1.getIndex(), item2.getIndex());

            item.setIndex(wantedIndex);
        }
    }

    /**
     * Move (or set) the given item at the end of the collection.
     * <p>
     * That means setting an index to the item so that it is the last when sorting by index.
     *
     * @param item       the item waiting for an index to be set
     * @param collection the collection of items
     *
     * @throws HttpErrorMessage if the collection is too big
     */
    public void moveItemToEnd(T item, Collection<T> collection) {
        if (collection.isEmpty()) {
            item.setIndex(soloIndex);
        } else {
            List<T> sortedList = sortCollection(collection);

            T baseItem = sortedList.get(sortedList.size() - 1);

            int justTooBigIndex = maxIndex + 1;

            if (!hasEnoughSpaceBetween(baseItem.getIndex(), justTooBigIndex)) {
                reorderIndexes(sortedList);
            }

            int wantedIndex;
            if ((justTooBigIndex - baseItem.getIndex()) > indexIncrement) {
                wantedIndex = baseItem.getIndex() + indexIncrement;
            } else {
                wantedIndex = computeInBetweenIndex(baseItem.getIndex(), justTooBigIndex);
            }

            item.setIndex(wantedIndex);
        }
    }

    // *********************************************************************************************
    //
    // *********************************************************************************************

    /**
     * Reorder the list so that each neighboring items have items separated by
     * {@link #indexIncrement}.
     * <p>
     * The first item also has a room of {@link #indexIncrement} before it.
     *
     * @param sortedCollection The sorted collection to index again
     *
     * @throws HttpErrorMessage if the collection is too big to be reorder
     */
    private void reorderIndexes(List<T> sortedCollection) {
        if (!canBeReordered(sortedCollection)) {
            throw HttpErrorMessage.dataError(MessageI18nKey.DATA_INTEGRITY_FAILURE);
        }

        int index = minIndex + indexIncrement;

        for (WithIndex obj : sortedCollection) {
            obj.setIndex(index);
            index += indexIncrement;
        }
    }

    /**
     * Compute if the collection size fits into items indexes separated by {@link #indexIncrement}
     * between {@link #minIndex} and {@link #maxIndex}
     *
     * @param collection
     *
     * @return True if the collection size fits into
     */
    private boolean canBeReordered(Collection<T> collection) {
        return Math.floor(((float) (maxIndex - minIndex)) / indexIncrement) - 1 >= collection
            .size();
    }

    // *********************************************************************************************
    //
    // *********************************************************************************************

    /**
     * Sort the collection by the items index field.
     *
     * @param collection the collection of items to sort
     *
     * @return a list of sorted items by index
     */
    private List<T> sortCollection(Collection<T> collection) {
        List<T> sortedList = new ArrayList<>(collection);

        sortedList.sort(Comparator.comparingInt(obj -> obj.getIndex()));

        // TODO sandra ask Maxence if the old way was more suitable
//      sortedList.sort((a, b) -> {
//      if (a != null) {
//          if (b != null) {
//              return Math.max(a.getIndex(), b.getIndex());
//          } else {
//              // a is not null, b is null
//              return -1;
//          }
//      } else if (b != null) {
//          // a is null, not b
//          return 1;
//      }
//      //both are null
//      return 0;

        return sortedList;
    }

    // *********************************************************************************************
    //
    // *********************************************************************************************

    /**
     * Determine if there is enough space between two indexes so that we can insert something new.
     *
     * @param indexA an index
     * @param indexB another index
     *
     * @return True if there is at least 1 room left between the two indexes
     */
    private boolean hasEnoughSpaceBetween(int indexA, int indexB) {
        return Math.abs(indexB - indexA) > 1;
    }

    /**
     * Determine the new index which will take place between the two given indexes.
     *
     * @param indexA an index
     * @param indexB another index
     *
     * @return An index that fits between the two given indexes
     */
    private int computeInBetweenIndex(int indexA, int indexB) {
        int baseIndex = indexA;

        int delta = indexB - baseIndex;
        int increment = (int) Math.floor(delta / 2.0);

        return baseIndex + increment;
    }

    /**
     * Fetch the item which is just before the given baseItem in the sortedList.
     *
     * @param baseItem   the reference item
     * @param sortedList the sorted list of items
     *
     * @return the item just before baseItem
     *
     * @throws HttpErrorMessage if the sorted list does not contain the baseItem
     */
    private T getItemBefore(T baseItem, List<T> sortedList) {
        if (!sortedList.contains(baseItem)) {
            throw HttpErrorMessage.dataError(MessageI18nKey.DATA_INTEGRITY_FAILURE);
        }

        int baseIndexInList = sortedList.indexOf(baseItem);

        if (baseIndexInList > 0) {
            return sortedList.get(baseIndexInList - 1);
        } else {
            // nothing before baseItem
            return null;
        }
    }

    /**
     * Fetch the item which is just after the given baseItem in the sortedList.
     *
     * @param baseItem   the reference item
     * @param sortedList the sorted list of items
     *
     * @return the item just after baseItem
     *
     * @throws HttpErrorMessage if the sorted list does not contain the baseItem
     */
    private T getItemAfter(T baseItem, List<T> sortedList) {
        if (!sortedList.contains(baseItem)) {
            throw HttpErrorMessage.dataError(MessageI18nKey.DATA_INTEGRITY_FAILURE);
        }

        int baseIndexInList = sortedList.indexOf(baseItem);

        if (baseIndexInList < sortedList.size() - 1) {
            return sortedList.get(baseIndexInList + 1);
        } else {
            // nothing after baseItem
            return null;
        }
    }
}