/*******************************************************************************
 * 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
 *     Dirk Fauth <dirk.fauth@googlemail.com> - Bug 447259, 446275, 447394, 446276
 *     Vincent Lorenzo <vincent.lorenzo@cea.fr> - Bug 478622
 ******************************************************************************/
package org.eclipse.nebula.widgets.nattable.selection;

import java.io.Serializable;
import java.util.ArrayList;
import java.util.Collection;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.locks.ReadWriteLock;
import java.util.concurrent.locks.ReentrantReadWriteLock;
import java.util.stream.IntStream;

import org.eclipse.nebula.widgets.nattable.coordinate.Range;
import org.eclipse.nebula.widgets.nattable.data.IRowDataProvider;
import org.eclipse.nebula.widgets.nattable.data.IRowIdAccessor;
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.selection.event.RowSelectionEvent;
import org.eclipse.swt.graphics.Rectangle;

public class RowSelectionModel<R> implements IRowSelectionModel<R> {

    protected final SelectionLayer selectionLayer;
    protected final IRowDataProvider<R> rowDataProvider;
    protected final IRowIdAccessor<R> rowIdAccessor;
    private boolean multipleSelectionAllowed;

    protected Map<Serializable, R> selectedRows;
    /**
     * *live* reference to last range parameter used in addSelection(range)
     */
    protected Rectangle lastSelectedRange;
    protected Set<Serializable> lastSelectedRowIds;
    protected final ReadWriteLock selectionsLock;

    public RowSelectionModel(SelectionLayer selectionLayer,
            IRowDataProvider<R> rowDataProvider, IRowIdAccessor<R> rowIdAccessor) {
        this(selectionLayer, rowDataProvider, rowIdAccessor, true);
    }

    public RowSelectionModel(SelectionLayer selectionLayer,
            IRowDataProvider<R> rowDataProvider,
            IRowIdAccessor<R> rowIdAccessor, boolean multipleSelectionAllowed) {
        this.selectionLayer = selectionLayer;
        this.rowDataProvider = rowDataProvider;
        this.rowIdAccessor = rowIdAccessor;
        this.multipleSelectionAllowed = multipleSelectionAllowed;

        this.selectedRows = new HashMap<>();
        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) {
        this.selectionsLock.writeLock().lock();

        try {
            if (!this.multipleSelectionAllowed) {
                this.selectedRows.clear();
            }

            R rowObject = getRowObjectByPosition(rowPosition);
            if (rowObject != null) {
                Serializable rowId = this.rowIdAccessor.getRowId(rowObject);
                this.selectedRows.put(rowId, rowObject);
            }
        } finally {
            this.selectionsLock.writeLock().unlock();
        }
    }

    @Override
    public void addSelection(Rectangle range) {
        this.selectionsLock.writeLock().lock();

        try {
            if (!this.multipleSelectionAllowed) {
                // as no multiple selection is allowed, ensure that only one row
                // will be selected
                this.selectedRows.clear();
                range.height = 1;
            }

            HashMap<Serializable, R> rowsToSelect = new HashMap<>();

            int maxY = Math.min(range.y + range.height, this.selectionLayer.getRowCount());
            for (int rowPosition = range.y; rowPosition < maxY; rowPosition++) {
                R rowObject = getRowObjectByPosition(rowPosition);
                if (rowObject != null) {
                    Serializable rowId = this.rowIdAccessor.getRowId(rowObject);
                    rowsToSelect.put(rowId, rowObject);
                }
            }

            this.selectedRows.putAll(rowsToSelect);

            if (range.equals(this.lastSelectedRange)) {
                this.lastSelectedRowIds = rowsToSelect.keySet();
            } else {
                this.lastSelectedRowIds = null;
            }

            this.lastSelectedRange = range;
        } finally {
            this.selectionsLock.writeLock().unlock();
        }
    }

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

    @Override
    public void clearSelection(int columnPosition, int rowPosition) {
        this.selectionsLock.writeLock().lock();

        try {
            Serializable rowId = getRowIdByPosition(rowPosition);
            this.selectedRows.remove(rowId);
        } finally {
            this.selectionsLock.writeLock().unlock();
        }
    }

    @Override
    public void clearSelection(Rectangle removedSelection) {
        this.selectionsLock.writeLock().lock();

        try {
            int maxY = Math.min(
                    removedSelection.y + removedSelection.height,
                    this.selectionLayer.getRowCount());
            for (int rowPosition = removedSelection.y; rowPosition < maxY; rowPosition++) {
                clearSelection(0, rowPosition);
            }
        } finally {
            this.selectionsLock.writeLock().unlock();
        }
    }

    @Override
    public void clearSelection(R rowObject) {
        this.selectionsLock.writeLock().lock();

        try {
            this.selectedRows.remove(this.rowIdAccessor.getRowId(rowObject));
        } finally {
            this.selectionsLock.writeLock().unlock();
        }
    }

    @Override
    public boolean isEmpty() {
        this.selectionsLock.readLock().lock();

        try {
            return this.selectedRows.isEmpty();
        } finally {
            this.selectionsLock.readLock().unlock();
        }
    }

    @Override
    public List<Rectangle> getSelections() {
        ArrayList<Rectangle> selectionRectangles = new ArrayList<>(this.selectedRows.size());

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

        try {
            int width = this.selectionLayer.getColumnCount();
            for (Serializable rowId : this.selectedRows.keySet()) {
                int rowPosition = getRowPositionById(rowId);
                selectionRectangles.add(new Rectangle(0, rowPosition, width, 1));
            }
        } finally {
            this.selectionsLock.readLock().unlock();
        }

        return selectionRectangles;
    }

    // Cell features

    @Override
    public boolean isCellPositionSelected(int columnPosition, int rowPosition) {
        ILayerCell cell = this.selectionLayer.getCellByPosition(columnPosition, rowPosition);
        if (cell != null) {
            int cellOriginRowPosition = cell.getOriginRowPosition();
            for (int testRowPosition = cellOriginRowPosition; testRowPosition < cellOriginRowPosition + cell.getRowSpan(); testRowPosition++) {
                if (isRowPositionSelected(testRowPosition)) {
                    return true;
                }
            }
        }
        return false;
    }

    // Column features

    @Override
    public int[] getSelectedColumnPositions() {
        if (!isEmpty()) {
            this.selectionsLock.readLock().lock();

            int columnCount;

            try {
                columnCount = this.selectionLayer.getColumnCount();
            } finally {
                this.selectionsLock.readLock().unlock();
            }

            int[] columns = new int[columnCount];
            for (int i = 0; i < columnCount; i++) {
                columns[i] = i;
            }
            return columns;
        }
        return new int[] {};
    }

    @Override
    public boolean isColumnPositionSelected(int columnPosition) {
        this.selectionsLock.readLock().lock();

        try {
            return !this.selectedRows.isEmpty();
        } finally {
            this.selectionsLock.readLock().unlock();
        }
    }

    @Override
    public int[] getFullySelectedColumnPositions(int fullySelectedColumnRowCount) {
        this.selectionsLock.readLock().lock();

        try {
            if (isColumnPositionFullySelected(0, fullySelectedColumnRowCount)) {
                return getSelectedColumnPositions();
            }
        } finally {
            this.selectionsLock.readLock().unlock();
        }

        return new int[] {};
    }

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

        try {
            int selectedRowCount = this.selectedRows.size();

            if (selectedRowCount == 0) {
                return false;
            }

            return selectedRowCount == fullySelectedColumnRowCount;
        } finally {
            this.selectionsLock.readLock().unlock();
        }
    }

    // Row features

    @Override
    public List<R> getSelectedRowObjects() {
        ArrayList<R> rowObjects = new ArrayList<>(this.selectedRows.size());

        this.selectionsLock.readLock().lock();
        try {
            rowObjects.addAll(this.selectedRows.values());
        } finally {
            this.selectionsLock.readLock().unlock();
        }

        return rowObjects;
    }

    @Override
    public int getSelectedRowCount() {
        this.selectionsLock.readLock().lock();

        try {
            return this.selectedRows.size();
        } finally {
            this.selectionsLock.readLock().unlock();
        }
    }

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

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

        try {
            for (Serializable rowId : this.selectedRows.keySet()) {
                int rowPosition = getRowPositionById(rowId);
                selectedRowRanges.add(new Range(rowPosition, rowPosition + 1));
            }
        } finally {
            this.selectionsLock.readLock().unlock();
        }

        return selectedRowRanges;
    }

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

        try {
            Serializable rowId = getRowIdByPosition(rowPosition);
            return this.selectedRows.containsKey(rowId);
        } finally {
            this.selectionsLock.readLock().unlock();
        }
    }

    @Override
    public int[] getFullySelectedRowPositions(int rowWidth) {
        this.selectionsLock.readLock().lock();

        try {
            int selectedRowCount = this.selectedRows.size();
            int[] selectedRowPositions = new int[selectedRowCount];
            int i = 0;
            for (Serializable rowId : this.selectedRows.keySet()) {
                selectedRowPositions[i] = getRowPositionById(rowId);
                i++;
            }
            return selectedRowPositions;
        } finally {
            this.selectionsLock.readLock().unlock();
        }
    }

    @Override
    public boolean isRowPositionFullySelected(int rowPosition, int rowWidth) {
        return isRowPositionSelected(rowPosition);
    }

    private Serializable getRowIdByPosition(int rowPosition) {
        R rowObject = getRowObjectByPosition(rowPosition);
        if (rowObject != null) {
            return this.rowIdAccessor.getRowId(rowObject);
        }
        return null;
    }

    private R getRowObjectByPosition(int rowPosition) {
        this.selectionsLock.readLock().lock();

        try {
            int rowIndex = this.selectionLayer.getRowIndexByPosition(rowPosition);
            if (rowIndex >= 0) {
                try {
                    return this.rowDataProvider.getRowObject(rowIndex);
                } catch (Exception e) {
                    // row index is invalid for the data provider
                }
            }
        } finally {
            this.selectionsLock.readLock().unlock();
        }

        return null;
    }

    private int getRowPositionById(Serializable rowId) {
        this.selectionsLock.readLock().lock();

        try {
            R rowObject = this.selectedRows.get(rowId);
            int rowIndex = this.rowDataProvider.indexOfRowObject(rowObject);
            if (rowIndex == -1) {
                return -1;
            }
            return this.selectionLayer.getRowPositionByIndex(rowIndex);
        } finally {
            this.selectionsLock.readLock().unlock();
        }
    }

    @Override
    public void handleLayerEvent(IStructuralChangeEvent event) {
        // handling for deleting rows
        if (event.isVerticalStructureChanged()) {
            // the change is already done and we don't know about indexes, so we
            // need to check if the selected objects still exist
            ArrayList<Serializable> keysToRemove = new ArrayList<>();
            for (Map.Entry<Serializable, R> entry : this.selectedRows.entrySet()) {
                int rowIndex = this.rowDataProvider.indexOfRowObject(entry.getValue());
                if (rowIndex == -1) {
                    keysToRemove.add(entry.getKey());
                }
            }

            this.selectionsLock.readLock().lock();
            try {
                for (Serializable toRemove : keysToRemove) {
                    this.selectedRows.remove(toRemove);
                }
            } finally {
                this.selectionsLock.readLock().unlock();
            }

            // fire row selection event
            // since we are not able to identify the row position of the deleted
            // selection we use all rows in the event to indicate the selection
            // change for all deleted rows
            if (!keysToRemove.isEmpty()) {
                Collection<StructuralDiff> diffs = event.getRowDiffs();

                int[] rowPositions = null;
                if (diffs != null) {
                    rowPositions = diffs.stream()
                            .filter(diff -> diff.getDiffType() != null && diff.getDiffType().equals(DiffTypeEnum.DELETE))
                            .map(StructuralDiff::getBeforePositionRange)
                            .flatMapToInt(range -> IntStream.range(range.start, range.end))
                            .toArray();
                } else {
                    rowPositions = new int[0];
                }

                // if there is no diff in the event we assume everything has
                // changed, in such a case we are not able to fire an
                // appropriate event the layer stack upwards since it will be
                // stopped while converting it to the target layer
                // for the RowSelectionProvider this is sufficient because it
                // registers itself as a listener to the SelectionLayer and
                // therefore gets informed about the selection change
                this.selectionLayer.fireLayerEvent(
                        new RowSelectionEvent(this.selectionLayer, rowPositions, -1, false, false));
            }
        }
    }

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