/*******************************************************************************
 * Copyright (c) 2014, 2020 Jonas Hugo, Markus Wahl.
 *
 * 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:
 *     Jonas Hugo <Jonas.Hugo@jeppesen.com>,
 *       Markus Wahl <Markus.Wahl@jeppesen.com> - initial API and implementation
 *     Dirk Fauth <dirk.fauth@googlemail.com> - Bug 447396
 *     Dirk Fauth <dirk.fauth@googlemail.com> - made inner classes static for better generic handling
 ******************************************************************************/
package org.eclipse.nebula.widgets.nattable.selection.preserve;

import java.io.Serializable;
import java.util.ArrayList;
import java.util.Collection;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Map;

import org.eclipse.collections.api.map.primitive.MutableIntObjectMap;
import org.eclipse.collections.api.set.primitive.MutableIntSet;
import org.eclipse.collections.api.tuple.primitive.IntObjectPair;
import org.eclipse.collections.impl.factory.primitive.IntObjectMaps;
import org.eclipse.collections.impl.factory.primitive.IntSets;

/**
 * The selected cells of columns and rows
 *
 * @param <T>
 *            the type of object underlying each row
 */
class Selections<T> {

    /**
     * A map for looking up rows given their row IDs
     */
    private HashMap<Serializable, Row<T>> selectedRows = new HashMap<>();

    /**
     * A map for looking up columns given their column positions
     */
    private MutableIntObjectMap<Column> selectedColumns = IntObjectMaps.mutable.empty();

    /**
     * Select the cell at the intersection of the specified row and column.
     *
     * @param rowId
     * @param rowObject
     *            row object with the row rowId
     * @param columnPosition
     */
    void select(Serializable rowId, T rowObject, int columnPosition) {
        Row<T> row = retrieveRow(rowId, rowObject);
        row.addItem(columnPosition);

        Column column = retrieveColumn(columnPosition);
        column.addItem(rowId);
    }

    /**
     * Removes the selection of the cell at the intersection of the specified
     * row and column.
     *
     * @param rowId
     * @param columnPosition
     */
    void deselect(Serializable rowId, int columnPosition) {
        Row<T> row = getSelectedColumns(rowId);
        if (row != null) {
            row.removeItem(columnPosition);
            if (!row.hasSelection()) {
                this.selectedRows.remove(rowId);
            }
        }

        Column column = getSelectedRows(columnPosition);
        if (column != null) {
            column.removeItem(rowId);
            if (!column.hasSelection()) {
                this.selectedColumns.remove(columnPosition);
            }
        }
    }

    /**
     * Removes the selection of all cells for the specified row id.
     *
     * @param rowId
     */
    void deselectRow(Serializable rowId) {
        Row<T> row = getSelectedColumns(rowId);
        if (row != null) {
            row.clearItems();
            this.selectedRows.remove(rowId);
        }

        MutableIntSet toRemove = IntSets.mutable.empty();
        for (IntObjectPair<Column> pair : this.selectedColumns.keyValuesView()) {
            Column column = pair.getTwo();
            column.removeItem(rowId);
            if (!column.hasSelection()) {
                toRemove.add(pair.getOne());
            }
        }

        toRemove.forEach(this.selectedColumns::remove);
    }

    /**
     * Removes the selection of all cells for the specified column.
     *
     * @param columnPosition
     */
    void deselectColumn(int columnPosition) {
        Column column = getSelectedRows(columnPosition);
        if (column != null) {
            column.clearItems();
            this.selectedColumns.remove(columnPosition);
        }

        HashSet<Serializable> toRemove = new HashSet<>();
        for (Map.Entry<Serializable, Row<T>> entry : this.selectedRows.entrySet()) {
            entry.getValue().removeItem(columnPosition);
            if (!entry.getValue().hasSelection()) {
                toRemove.add(entry.getKey());
            }
        }

        for (Serializable key : toRemove) {
            this.selectedRows.remove(key);
        }
    }

    void updateColumnsForRemoval(int columnPosition) {
        // find maximum selected column
        int maxColumn = this.selectedColumns.keySet().maxIfEmpty(0);

        for (int i = columnPosition + 1; i <= maxColumn; i++) {
            Column column = this.selectedColumns.get(i);
            if (column != null) {
                this.selectedColumns.put(i - 1, new Column(i - 1, column.getItems()));
                this.selectedColumns.remove(i);

                // also update the row references
                for (Row<T> row : this.selectedRows.values()) {
                    HashSet<Integer> toRemove = new HashSet<>();
                    HashSet<Integer> toAdd = new HashSet<>();
                    for (Integer col : row.getItems()) {
                        if (col <= i) {
                            toRemove.add(i);
                            toAdd.add(i - 1);
                        }
                    }
                    row.getItems().removeAll(toRemove);
                    row.getItems().addAll(toAdd);
                }
            }
        }
    }

    void updateColumnsForAddition(int columnPosition) {
        // find maximum selected column
        int maxColumn = this.selectedColumns.keySet().maxIfEmpty(0);

        for (int i = maxColumn; i >= columnPosition; i--) {
            Column column = this.selectedColumns.get(i);
            if (column != null) {
                this.selectedColumns.put(i + 1, new Column(i + 1, column.getItems()));
                this.selectedColumns.remove(i);

                // also update the row references
                for (Row<T> row : this.selectedRows.values()) {
                    HashSet<Integer> toRemove = new HashSet<>();
                    HashSet<Integer> toAdd = new HashSet<>();
                    for (Integer col : row.getItems()) {
                        if (col >= i) {
                            toRemove.add(i);
                            toAdd.add(i + 1);
                        }
                    }
                    row.getItems().removeAll(toRemove);
                    row.getItems().addAll(toAdd);
                }
            }
        }
    }

    /**
     * Removes all cell selections.
     */
    void clear() {
        this.selectedRows.clear();
        this.selectedColumns.clear();
    }

    /**
     * Retrieves all rows that have selected cells.
     *
     * @return all rows that have selected cells
     */
    Collection<Row<T>> getRows() {
        return this.selectedRows.values();
    }

    /**
     * Retrieves the column positions of all columns with selected cells. The
     * positions are naturally sorted.
     *
     * @return all columns positions with selected cells
     */
    int[] getColumnPositions() {
        return this.selectedColumns.keySet().toSortedArray();
    }

    /**
     * Retrieves the selected rows of a column
     *
     * @param columnPosition
     *            column for retrieving selected rows
     * @return selected rows of columnPosition, or null if no selected rows in
     *         that column
     */
    Column getSelectedRows(int columnPosition) {
        return this.selectedColumns.get(columnPosition);
    }

    /**
     * Retrieves the selected columns of a row
     *
     * @param rowId
     *            row ID for retrieving selected columns
     * @return selected columns of rowId, or null if no selected columns in that
     *         row
     */
    Row<T> getSelectedColumns(Serializable rowId) {
        return this.selectedRows.get(rowId);
    }

    /**
     * Retrieves all selected cell positions expressed in row object and column
     * position. The size of the collection is zero when there are no selected
     * cells.
     *
     * @return all selected cell positions
     */
    Collection<CellPosition<T>> getSelections() {
        ArrayList<CellPosition<T>> selectedCells = new ArrayList<>();
        for (Row<T> row : this.selectedRows.values()) {
            for (int columnPosition : row.getItems()) {
                CellPosition<T> cell = new CellPosition<>(row.getRowObject(), columnPosition);
                selectedCells.add(cell);
            }
        }
        return selectedCells;
    }

    /**
     * Determines whether a cell is selected
     *
     * @param rowId
     *            row ID of the inspected cell
     * @param columnPosition
     *            column position of the inspected cell
     * @return whether the specified cell is selected
     */
    boolean isSelected(Serializable rowId, int columnPosition) {
        if (isRowSelected(rowId)) {
            return getSelectedColumns(rowId).contains(columnPosition);
        } else {
            return false;
        }
    }

    /**
     * Determines whether a row contains a selected cell
     *
     * @param rowId
     *            row ID to inspect
     * @return whether the specified row contains a selected cell
     */
    boolean isRowSelected(Serializable rowId) {
        return this.selectedRows.containsKey(rowId);
    }

    /**
     * Determines whether there are selected cells
     *
     * @return whether there are selected cells
     */
    boolean isEmpty() {
        return this.selectedRows.isEmpty();
    }

    /**
     * Retrieves a collection of selected columns for a row. The row is
     * specified by row ID, but row object is needed when the row does not
     * already have any other selected cells.
     *
     * @param rowId
     *            row to retrieve columns for
     * @param rowObject
     *            row object with the row rowId
     * @return a collection of selected columns for the row
     */
    private Row<T> retrieveRow(Serializable rowId, T rowObject) {
        Row<T> row = getSelectedColumns(rowId);
        if (row == null) {
            row = new Row<>(rowId, rowObject);
            this.selectedRows.put(rowId, row);
        }
        return row;
    }

    /**
     * Retrieves a collection of selected rows for a column.
     *
     * @param columnPosition
     *            column to retrieve columns for
     * @return a collection of selected rows for a column
     */
    private Column retrieveColumn(int columnPosition) {
        Column column = getSelectedRows(columnPosition);
        if (column == null) {
            column = new Column(columnPosition);
            this.selectedColumns.put(columnPosition, column);
        }
        return column;
    }

    /**
     * A collection of selected columns for a row. Row is a Line<Serializable,
     * Integer> where <Serializable> denotes the row ID of the row and <Integer>
     * denotes the type of the selected columns (column positions).
     */
    static class Row<R> extends Line<Serializable, Integer> {
        /**
         * The underlying row object
         */
        private final R rowObject;

        /**
         * Creates a row with the specified row
         *
         * @param rowId
         *            ID of the row
         * @param rowObject
         *            underlying row object
         */
        Row(Serializable rowId, R rowObject) {
            super(rowId);
            this.rowObject = rowObject;
        }

        /**
         * Retrieves the underlying row object
         *
         * @return the underlying row object
         */
        R getRowObject() {
            return this.rowObject;
        }
    }

    /**
     * A collection of selected rows for a column. Column is a Line<Integer,
     * Serializable> where <Integer> denotes the column position of the column
     * and <Serializable> denotes the type of the selected rows (row ID).
     */
    static class Column extends Line<Integer, Serializable> {
        /**
         * Creates a column with the specified column
         *
         * @param columnPosition
         *            position of the column
         */
        Column(Integer columnPosition) {
            super(columnPosition);
        }

        /**
         * Creates a column with the specified column and the given content.
         * Needed on selection update.
         *
         * @param columnPosition
         *            position of the column
         * @param content
         *            the content to set initially
         */
        Column(Integer columnPosition, Collection<Serializable> content) {
            super(columnPosition, content);
        }
    }

    /**
     * A collection of selected items in a line.
     *
     * @param <I>
     *            the type of the line identifier
     * @param <S>
     *            the type of the selected items
     */
    static class Line<I, S> {
        /**
         * Identifying the line
         */
        private final I lineId;

        /**
         * The selected items
         */
        private HashSet<S> content = new HashSet<>();

        /**
         * Creates a line with the specified ID
         *
         * @param lineId
         *            identifying the line
         */
        Line(I lineId) {
            this.lineId = lineId;
        }

        /**
         * Creates a line with the specified ID and the given content. Needed on
         * selection update.
         *
         * @param lineId
         *            identifying the line
         * @param content
         *            the content to set initially
         */
        Line(I lineId, Collection<S> content) {
            this.lineId = lineId;
            this.content.addAll(content);
        }

        /**
         * Retrieves the selected items of the line
         *
         * @return the selected items
         */
        Collection<S> getItems() {
            return this.content;
        }

        /**
         * Adds an item to the selected items
         *
         * @param item
         *            item to add
         */
        void addItem(S item) {
            this.content.add(item);
        }

        /**
         * Removes an item from the selected items
         *
         * @param item
         *            item to remove
         */
        void removeItem(S item) {
            this.content.remove(item);
        }

        /**
         * Clears the selected items.
         */
        void clearItems() {
            this.content.clear();
        }

        /**
         * Determines whether a certain item is selected in the line
         *
         * @param item
         *            item to look for
         * @return whether the item is selected in the line
         */
        boolean contains(S item) {
            return this.content.contains(item);
        }

        /**
         * Determines whether the line has any selections
         *
         * @return whether the line has any selections
         */
        boolean hasSelection() {
            return !this.content.isEmpty();
        }

        /**
         * Retrieves the line identifier
         *
         * @return the line identifier
         */
        I getId() {
            return this.lineId;
        }
    }

    /**
     * Position of a cell expressed in row object and column position
     *
     * @param <T>
     *            the type of row object
     */
    static class CellPosition<T> {
        @Override
        public int hashCode() {
            final int prime = 31;
            int result = 1;
            result = prime * result + this.columnPosition;
            result = prime * result
                    + ((this.rowObject == null) ? 0 : this.rowObject.hashCode());
            return result;
        }

        @Override
        public boolean equals(Object obj) {
            if (this == obj)
                return true;
            if (obj == null)
                return false;
            if (getClass() != obj.getClass())
                return false;
            @SuppressWarnings("unchecked")
            CellPosition<T> other = (CellPosition<T>) obj;
            if (this.columnPosition != other.columnPosition)
                return false;
            if (this.rowObject == null) {
                if (other.rowObject != null)
                    return false;
            } else if (!this.rowObject.equals(other.rowObject))
                return false;
            return true;
        }

        /**
         * Column position
         */
        private final int columnPosition;

        /**
         * Row object
         */
        private final T rowObject;

        /**
         * Creates a cell position
         *
         * @param rowObject
         *            row object
         * @param columnPosition
         *            column position
         */
        CellPosition(T rowObject, int columnPosition) {
            this.rowObject = rowObject;
            this.columnPosition = columnPosition;
        }

        /**
         * Retrieves the row object
         *
         * @return the row object
         */
        T getRowObject() {
            return this.rowObject;
        }

        /**
         * Retrieves the column position
         *
         * @return the column position
         */
        Integer getColumnPosition() {
            return this.columnPosition;
        }
    }

}
