/*******************************************************************************
 * 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
 *     Roman Flueckiger <roman.flueckiger@mac.com> - Bug 450334
 *     Dirk Fauth <dirk.fauth@googlemail.com> - Bug 446276, 446275
 ******************************************************************************/
package org.eclipse.nebula.widgets.nattable.selection;

import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.HashSet;
import java.util.LinkedList;
import java.util.List;
import java.util.Set;
import java.util.TreeSet;
import java.util.concurrent.locks.ReadWriteLock;
import java.util.concurrent.locks.ReentrantReadWriteLock;

import org.eclipse.nebula.widgets.nattable.coordinate.PositionCoordinate;
import org.eclipse.nebula.widgets.nattable.coordinate.Range;
import org.eclipse.nebula.widgets.nattable.layer.cell.ILayerCell;
import org.eclipse.nebula.widgets.nattable.layer.event.IStructuralChangeEvent;
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;
import org.eclipse.nebula.widgets.nattable.util.ObjectUtils;
import org.eclipse.swt.graphics.Rectangle;

/**
 * Tracks the selections made in the table. All selections are tracked in terms
 * of Rectangles.
 *
 * For example if the table has 10 rows and column 2 is selected, the Rectangle
 * tracked is (0, 2, 10, 1)
 *
 * Coordinates are in <i>Selection Layer positions</i>
 *
 * @see SelectionLayer
 */
public class SelectionModel implements ISelectionModel {

    protected final SelectionLayer selectionLayer;
    private boolean multipleSelectionAllowed;

    private final List<Rectangle> selections;
    private final ReadWriteLock selectionsLock;

    private boolean clearSelectionOnChange = true;

    public SelectionModel(SelectionLayer selectionLayer) {
        this(selectionLayer, true);
    }

    public SelectionModel(SelectionLayer selectionLayer, boolean multipleSelectionAllowed) {
        this.selectionLayer = selectionLayer;
        this.multipleSelectionAllowed = multipleSelectionAllowed;

        this.selections = new LinkedList<>();
        this.selectionsLock = new ReentrantReadWriteLock();
    }

    @Override
    public boolean isMultipleSelectionAllowed() {
        return this.multipleSelectionAllowed;
    }

    @Override
    public void setMultipleSelectionAllowed(boolean multipleSelectionAllowed) {
        this.multipleSelectionAllowed = multipleSelectionAllowed;
    }

    @Override
    public void addSelection(int columnPosition, int rowPosition) {
        addSelectionIntoList(new Rectangle(columnPosition, rowPosition, 1, 1));
    }

    @Override
    public void addSelection(final Rectangle range) {
        if (range != null) {
            addSelectionIntoList(range);
        }
    }

    private void addSelectionIntoList(Rectangle selection) {
        this.selectionsLock.writeLock().lock();
        try {
            if (this.multipleSelectionAllowed) {
                ArrayList<Rectangle> itemsToRemove = null;
                for (Rectangle r : this.selections) {
                    if (selection.intersects(r)) {
                        if (r.equals(selection)) {
                            break;
                        }

                        Rectangle intersection = selection.intersection(r);
                        if (intersection.equals(r)) {
                            // r is a subset of intersection
                            if (itemsToRemove == null) {
                                itemsToRemove = new ArrayList<>();
                            }

                            itemsToRemove.add(r);
                        } else if (intersection.equals(selection)) {
                            // selection is a subset of r
                            break;
                        }
                    }
                }

                if (itemsToRemove != null) {
                    this.selections.removeAll(itemsToRemove);
                }
            } else {
                this.selections.clear();
                // as no multiple selection is allowed, ensure that only one
                // column and one row will be selected
                selection.height = 1;
                selection.width = 1;
            }

            this.selections.add(selection);
        } finally {
            this.selectionsLock.writeLock().unlock();
        }

    }

    @Override
    public void clearSelection() {
        this.selectionsLock.writeLock().lock();
        try {
            this.selections.clear();
        } finally {
            this.selectionsLock.writeLock().unlock();
        }
    }

    @Override
    public void clearSelection(int columnPosition, int rowPosition) {
        clearSelection(new Rectangle(columnPosition, rowPosition, 1, 1));
    }

    @Override
    public void clearSelection(Rectangle removedSelection) {

        List<Rectangle> removedItems = new LinkedList<>();
        List<Rectangle> addedItems = new LinkedList<>();

        this.selectionsLock.readLock().lock();

        try {
            for (Rectangle r : this.selections) {
                if (r.intersects(removedSelection)) {
                    Rectangle intersection = removedSelection.intersection(r);
                    removedItems.add(r);

                    Rectangle topSelection = getTopSelection(intersection, r);
                    if (topSelection != null) {
                        addedItems.add(topSelection);
                    }

                    Rectangle rightSelection = getRightSelection(intersection,
                            r);
                    if (rightSelection != null)
                        addedItems.add(rightSelection);

                    Rectangle leftSelection = getLeftSelection(intersection, r);
                    if (leftSelection != null)
                        addedItems.add(leftSelection);

                    Rectangle bottomSelection = getBottomSelection(
                            intersection, r);
                    if (bottomSelection != null)
                        addedItems.add(bottomSelection);
                }
            }
        } finally {
            this.selectionsLock.readLock().unlock();
        }

        if (!removedItems.isEmpty()) {
            this.selectionsLock.writeLock().lock();
            try {
                this.selections.removeAll(removedItems);
            } finally {
                this.selectionsLock.writeLock().unlock();
            }

            removedItems.clear();
        }

        if (!addedItems.isEmpty()) {
            this.selectionsLock.writeLock().lock();
            try {
                this.selections.addAll(addedItems);
            } finally {
                this.selectionsLock.writeLock().unlock();
            }

            addedItems.clear();
        }

    }

    @Override
    public boolean isEmpty() {
        this.selectionsLock.readLock().lock();
        try {
            return this.selections.isEmpty();
        } finally {
            this.selectionsLock.readLock().unlock();
        }
    }

    @Override
    public List<Rectangle> getSelections() {
        return this.selections;
    }

    // Cell features

    @Override
    public boolean isCellPositionSelected(int columnPosition, int rowPosition) {
        this.selectionsLock.readLock().lock();

        try {
            ILayerCell cell = this.selectionLayer.getCellByPosition(columnPosition, rowPosition);
            if (cell != null) {
                Rectangle cellRectangle = new Rectangle(
                        cell.getOriginColumnPosition(),
                        cell.getOriginRowPosition(),
                        cell.getColumnSpan(),
                        cell.getRowSpan());

                for (Rectangle selectionRectangle : this.selections) {
                    if (selectionRectangle.intersects(cellRectangle))
                        return true;
                }
            }
        } finally {
            this.selectionsLock.readLock().unlock();
        }

        return false;
    }

    // Column features

    @Override
    public int[] getSelectedColumnPositions() {
        TreeSet<Integer> selectedColumns = new TreeSet<>();

        this.selectionsLock.readLock().lock();

        int columnCount = this.selectionLayer.getColumnCount();
        try {
            for (Rectangle r : this.selections) {
                int startColumn = r.x;
                if (startColumn < columnCount) {
                    int numColumns = (r.x + r.width <= columnCount) ? r.width : columnCount - r.x;

                    // Change from row < startRow to row < startRow+numRows
                    for (int column = startColumn; column < startColumn + numColumns; column++) {
                        selectedColumns.add(Integer.valueOf(column));
                    }
                }
            }
        } finally {
            this.selectionsLock.readLock().unlock();
        }

        // Convert to array
        return ObjectUtils.asIntArray(selectedColumns);
    }

    /**
     * @since 1.5
     */
    protected Set<Range> internalGetSelectedColumnPositions() {
        Set<Range> selectedColumnsRange = new HashSet<>();

        this.selectionsLock.readLock().lock();

        int columnCount = this.selectionLayer.getColumnCount();
        try {
            for (Rectangle r : this.selections) {
                if (r.x < columnCount) {
                    int width = (r.x + r.width <= columnCount) ? r.width : columnCount - r.x;
                    selectedColumnsRange.add(new Range(r.x, r.x + width));
                }
            }
        } finally {
            this.selectionsLock.readLock().unlock();
        }

        ArrayList<Range> ranges = new ArrayList<>(selectedColumnsRange);
        Range.sortByStart(ranges);
        List<Range> uniqueRanges = new ArrayList<>(ranges.size());

        // Adjust for overlaps - between consecutive selections
        for (int i = 0; i < ranges.size(); i++) {
            if (i > 0) {
                Range previousRange = ranges.get(i - 1);
                Range currentRange = ranges.get(i);
                if (previousRange.overlap(currentRange)
                        || (previousRange.end == currentRange.start)) {
                    int largerRangeEnd = (previousRange.end > currentRange.end) ? previousRange.end : currentRange.end;
                    uniqueRanges.get(uniqueRanges.size() - 1).end = largerRangeEnd;
                    ranges.get(i).end = largerRangeEnd;
                } else {
                    uniqueRanges.add(ranges.get(i));
                }
            } else {
                uniqueRanges.add(ranges.get(i));
            }
        }
        return new HashSet<>(uniqueRanges);
    }

    @Override
    public boolean isColumnPositionSelected(int columnPosition) {
        this.selectionsLock.readLock().lock();
        try {
            for (Range columnRange : internalGetSelectedColumnPositions()) {
                if (columnRange.contains(columnPosition)) {
                    return true;
                }
            }
        } finally {
            this.selectionsLock.readLock().unlock();
        }

        return false;
    }

    @Override
    public int[] getFullySelectedColumnPositions(int columnHeight) {
        final int[] selectedColumns = getSelectedColumnPositions();
        int[] columnsToHide = new int[selectedColumns.length];
        int index = 0;
        for (int columnPosition : selectedColumns) {
            if (isColumnPositionFullySelected(columnPosition, columnHeight)) {
                columnsToHide[index++] = columnPosition;
            }
        }

        return index > 0 ? ArrayUtil.subarray(columnsToHide, 0, index) : new int[0];
    }

    /**
     * Are all cells in this column selected? Different selection rectangles
     * might aggregate to cover the entire column. We need to take into account
     * any overlapping selections or any selection rectangles contained within
     * each other.
     *
     * See the related tests for a better understanding.
     */
    @Override
    public boolean isColumnPositionFullySelected(int columnPosition, int columnHeight) {
        this.selectionsLock.readLock().lock();

        try {
            // Aggregate all rectangles in the column which are in the selection
            // model
            List<Rectangle> selectedRectanglesInColumn = new ArrayList<>(this.selections.size());

            // If X is same add up the height of the selected area
            for (Rectangle r : this.selections) {
                // Column is within the bounds of the selected rectangle
                if (columnPosition >= r.x && columnPosition < r.x + r.width) {
                    selectedRectanglesInColumn.add(new Rectangle(
                            columnPosition,
                            r.y,
                            1,
                            r.height));
                }
            }
            if (selectedRectanglesInColumn.isEmpty()) {
                return false;
            }
            sortByY(selectedRectanglesInColumn);
            Rectangle finalRectangle = new Rectangle(
                    columnPosition,
                    selectedRectanglesInColumn.get(0).y,
                    0,
                    0);

            // Ensure that selections in the column are contiguous and cover the
            // entire column
            for (int i = 0; i < selectedRectanglesInColumn.size(); i++) {
                Rectangle rectangle = selectedRectanglesInColumn.get(i);
                if (contains(finalRectangle, rectangle)) {
                    continue;
                }
                if (i > 0) {
                    Rectangle previousRect = selectedRectanglesInColumn
                            .get(i - 1);
                    if (rectangle.union(previousRect).height > (rectangle.height + previousRect.height)) {
                        // Rectangles not contiguous
                        return false;
                    }
                }
                // Union will resolve any overlapping area
                finalRectangle = finalRectangle.union(rectangle);
            }
            return finalRectangle.height >= columnHeight;
        } finally {
            this.selectionsLock.readLock().unlock();
        }
    }

    // Row features

    @Override
    public int getSelectedRowCount() {
        Set<Range> selectedRows = getSelectedRowPositions();
        int count = 0;
        for (Range range : selectedRows) {
            count += range.end - range.start;
        }
        return count;
    }

    @Override
    public Set<Range> getSelectedRowPositions() {
        Set<Range> selectedRowsRange = new HashSet<>();

        this.selectionsLock.readLock().lock();

        int rowCount = this.selectionLayer.getRowCount();
        try {
            for (Rectangle r : this.selections) {
                if (r.y < rowCount) {
                    int height = (r.y + r.height <= rowCount) ? r.height : rowCount - r.y;
                    selectedRowsRange.add(new Range(r.y, r.y + height));
                }
            }
        } finally {
            this.selectionsLock.readLock().unlock();
        }

        ArrayList<Range> ranges = new ArrayList<>(selectedRowsRange);
        Range.sortByStart(ranges);
        List<Range> uniqueRanges = new ArrayList<>(ranges.size());

        // Adjust for overlaps - between consecutive selections
        for (int i = 0; i < ranges.size(); i++) {
            if (i > 0) {
                Range previousRange = ranges.get(i - 1);
                Range currentRange = ranges.get(i);
                if (previousRange.overlap(currentRange)
                        || (previousRange.end == currentRange.start)) {
                    int largerRangeEnd = (previousRange.end > currentRange.end) ? previousRange.end : currentRange.end;
                    uniqueRanges.get(uniqueRanges.size() - 1).end = largerRangeEnd;
                    ranges.get(i).end = largerRangeEnd;
                } else {
                    uniqueRanges.add(ranges.get(i));
                }
            } else {
                uniqueRanges.add(ranges.get(i));
            }
        }
        return new HashSet<>(uniqueRanges);
    }

    @Override
    public boolean isRowPositionSelected(int rowPosition) {
        this.selectionsLock.readLock().lock();

        try {
            for (Range rowRange : getSelectedRowPositions()) {
                if (rowRange.contains(rowPosition)) {
                    return true;
                }
            }
        } finally {
            this.selectionsLock.readLock().unlock();
        }

        return false;
    }

    @Override
    public int[] getFullySelectedRowPositions(int rowWidth) {
        final Set<Range> selectedRows = getSelectedRowPositions();
        int[] fullySelectedRows = new int[getSelectedRowCount()];
        int index = 0;

        for (Range rowRange : selectedRows) {
            for (int i = rowRange.start; i < rowRange.end; i++) {
                if (isRowPositionFullySelected(i, rowWidth)) {
                    fullySelectedRows[index++] = i;
                }
            }
        }

        return index > 0 ? ArrayUtil.subarray(fullySelectedRows, 0, index) : new int[0];
    }

    @Override
    public boolean isRowPositionFullySelected(int rowPosition, int rowWidth) {
        this.selectionsLock.readLock().lock();

        try {
            // Aggregate all rectangles in the row which are in the selection
            // model
            List<Rectangle> selectedRectanglesInRow = new ArrayList<>(this.selections.size());

            // If X is same add up the width of the selected area
            for (Rectangle r : this.selections) {
                // Row is within the bounds of the selected rectangle
                if (rowPosition >= r.y && rowPosition < r.y + r.height) {
                    selectedRectanglesInRow.add(new Rectangle(
                            r.x,
                            rowPosition,
                            r.width,
                            1));
                }
            }
            if (selectedRectanglesInRow.isEmpty()) {
                return false;
            }
            sortByX(selectedRectanglesInRow);
            Rectangle finalRectangle = new Rectangle(
                    selectedRectanglesInRow.get(0).x,
                    rowPosition,
                    0,
                    0);

            // Ensure that selections in the row are contiguous and cover the
            // entire row
            for (int i = 0; i < selectedRectanglesInRow.size(); i++) {
                Rectangle rectangle = selectedRectanglesInRow.get(i);
                if (contains(finalRectangle, rectangle)) {
                    continue;
                }
                if (i > 0) {
                    Rectangle previousRect = selectedRectanglesInRow.get(i - 1);
                    if (rectangle.union(previousRect).width > (rectangle.width + previousRect.width)) {
                        // Rectangles not contiguous
                        return false;
                    }
                }
                // Union will resolve any overlapping area
                finalRectangle = finalRectangle.union(rectangle);
            }
            return finalRectangle.width >= rowWidth;
        } finally {
            this.selectionsLock.readLock().unlock();
        }
    }

    protected boolean contains(Rectangle containerRectangle, Rectangle rectangle) {
        Rectangle union = containerRectangle.union(rectangle);
        return union.equals(containerRectangle);
    }

    protected void sortByX(List<Rectangle> selectionRectanglesInRow) {
        Collections.sort(selectionRectanglesInRow, (rectangle1, rectangle2) -> Integer.valueOf(rectangle1.x).compareTo(Integer.valueOf(rectangle2.x)));
    }

    protected void sortByY(List<Rectangle> selectionRectanglesInColumn) {
        Collections.sort(selectionRectanglesInColumn,
                (rectangle1, rectangle2) -> Integer.valueOf(rectangle1.y).compareTo(Integer.valueOf(rectangle2.y)));
    }

    private Rectangle getLeftSelection(Rectangle intersection, Rectangle selection) {
        if (intersection.x > selection.x) {
            return new Rectangle(
                    selection.x,
                    selection.y,
                    intersection.x - selection.x,
                    selection.height);
        }

        return null;
    }

    private Rectangle getRightSelection(Rectangle intersection, Rectangle selection) {
        int newX = intersection.x + intersection.width;

        if (newX < selection.x + selection.width) {
            return new Rectangle(
                    newX,
                    selection.y,
                    selection.x + selection.width - newX,
                    selection.height);
        }

        return null;
    }

    private Rectangle getTopSelection(Rectangle intersection, Rectangle selection) {
        if (intersection.y > selection.y) {
            return new Rectangle(
                    selection.x,
                    selection.y,
                    selection.width,
                    intersection.y - selection.y);
        }
        return null;
    }

    private Rectangle getBottomSelection(Rectangle intersection, Rectangle selection) {
        int newY = intersection.y + intersection.height;

        if (newY < selection.y + selection.height) {
            return new Rectangle(
                    selection.x,
                    newY,
                    selection.width,
                    selection.y + selection.height - newY);
        }

        return null;
    }

    // Object methods

    @Override
    public String toString() {
        this.selectionsLock.readLock().lock();

        try {
            return this.selections.toString();
        } finally {
            this.selectionsLock.readLock().unlock();
        }
    }

    @Override
    public void handleLayerEvent(IStructuralChangeEvent event) {
        if (this.clearSelectionOnChange) {
            if (event.isHorizontalStructureChanged()) {
                if (event.getColumnDiffs() == null) {
                    Collection<Rectangle> rectangles = event.getChangedPositionRectangles();
                    for (Rectangle rectangle : rectangles) {
                        Range changedRange = new Range(rectangle.y, rectangle.y + rectangle.height);
                        if (selectedColumnModified(changedRange)) {
                            this.selectionLayer.clear();
                            break;
                        }
                    }
                } else {
                    for (StructuralDiff diff : event.getColumnDiffs()) {
                        // DiffTypeEnum.CHANGE is used for resizing and
                        // shouldn't result in clearing the selection
                        if (diff.getDiffType() != DiffTypeEnum.CHANGE) {
                            if (selectedColumnModified(diff.getBeforePositionRange())) {
                                this.selectionLayer.clear();
                                break;
                            }
                        }
                    }
                }
            }

            if (event.isVerticalStructureChanged()) {
                // if there are no row diffs, it seems to be a complete refresh
                if (event.getRowDiffs() == null) {
                    Collection<Rectangle> rectangles = event.getChangedPositionRectangles();
                    for (Rectangle rectangle : rectangles) {
                        Range changedRange = new Range(rectangle.y, rectangle.y + rectangle.height);
                        if (selectedRowModified(changedRange)) {
                            this.selectionLayer.clear();
                            break;
                        }
                    }
                } else {
                    // there are row diffs so we try to determine the diffs to
                    // process
                    for (StructuralDiff diff : event.getRowDiffs()) {
                        // DiffTypeEnum.CHANGE is used for resizing and
                        // shouldn't result in clearing the selection
                        if (diff.getDiffType() != DiffTypeEnum.CHANGE) {
                            if (selectedRowModified(diff.getBeforePositionRange())) {
                                this.selectionLayer.clear();
                                break;
                            }
                        }
                    }
                }
            }
        } else {
            // keep the selection as is in case of changes
            // Note:
            // this is the the same code I posted in various forums as a
            // workaround for the cleaning of the selection on changes
            // search for PreserveSelectionStructuralChangeEventHandler to get
            // more information on this
            PositionCoordinate[] coords = this.selectionLayer.getSelectedCellPositions();
            for (PositionCoordinate coord : coords) {
                if (coord.getColumnPosition() >= this.selectionLayer.getColumnCount()
                        || coord.getRowPosition() >= this.selectionLayer.getRowCount()) {
                    // if the coordinates of the selected cells are outside the
                    // valid range remove the selection
                    this.selectionLayer.clearSelection(
                            coord.getColumnPosition(),
                            coord.getRowPosition());
                }
            }
        }
    }

    private boolean selectedRowModified(Range changedRange) {
        Set<Range> selectedRows = this.selectionLayer.getSelectedRowPositions();
        for (Range rowRange : selectedRows) {
            if (rowRange.overlap(changedRange)) {
                return true;
            }
        }

        // if the selection layer is empty, we should clear the selection also
        return (this.selectionLayer.getRowCount() == 0 && !this.isEmpty());
    }

    private boolean selectedColumnModified(Range changedRange) {
        for (int i = changedRange.start; i <= changedRange.end; i++) {
            if (isColumnPositionSelected(i)) {
                return true;
            }
        }

        return false;
    }

    /**
     *
     * @param clearSelectionOnChange
     *            <code>true</code> to simply clear the selection on structural
     *            changes, <code>false</code> to keep the valid selection
     *            (selection of cells that still exist)
     */
    public void setClearSelectionOnChange(boolean clearSelectionOnChange) {
        this.clearSelectionOnChange = clearSelectionOnChange;
    }

    @Override
    public Class<IStructuralChangeEvent> getLayerEventClass() {
        return IStructuralChangeEvent.class;
    }
}
