/*******************************************************************************
 * Copyright (c) 2018, 2020 Dirk Fauth.
 *
 * 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:
 *     Dirk Fauth <dirk.fauth@googlemail.com> - initial API and implementation
 ******************************************************************************/
package org.eclipse.nebula.widgets.nattable.datachange;

import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;

import org.eclipse.nebula.widgets.nattable.coordinate.PositionUtil;
import org.eclipse.nebula.widgets.nattable.coordinate.Range;
import org.eclipse.nebula.widgets.nattable.datachange.event.KeyRowInsertEvent;
import org.eclipse.nebula.widgets.nattable.layer.event.ILayerEventHandler;
import org.eclipse.nebula.widgets.nattable.layer.event.IStructuralChangeEvent;
import org.eclipse.nebula.widgets.nattable.layer.event.RowInsertEvent;
import org.eclipse.nebula.widgets.nattable.layer.event.StructuralDiff;
import org.eclipse.nebula.widgets.nattable.layer.event.StructuralDiff.DiffTypeEnum;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

/**
 * {@link DataChangeHandler} to track row insert operations. Registers as
 * {@link ILayerEventHandler} for the {@link RowInsertEvent}. It is intended to
 * be used with a configuration that directly updates the backing data.
 * Temporary data storage is not supported. It therefore is able to perform
 * discard operations and will do nothing on save.
 *
 * @since 1.6
 */
public class RowInsertDataChangeHandler extends AbstractDataChangeHandler<RowInsertDataChange> implements ILayerEventHandler<RowInsertEvent> {

    private static final Logger LOG = LoggerFactory.getLogger(RowInsertDataChangeHandler.class);

    /**
     *
     * @param layer
     *            The {@link DataChangeLayer} this handler should be assigned
     *            to.
     * @param keyHandler
     *            The {@link CellKeyHandler} that is used to store dataChanges
     *            for a specific key.
     */
    public RowInsertDataChangeHandler(DataChangeLayer layer, CellKeyHandler<?> keyHandler) {
        super(layer, keyHandler, new ConcurrentHashMap<>());
    }

    @Override
    public void handleStructuralChange(IStructuralChangeEvent structuralChangeEvent) {
        if (structuralChangeEvent.isVerticalStructureChanged()
                && structuralChangeEvent.getRowDiffs() != null) {

            if (this.keyHandler.updateOnVerticalStructuralChange()) {
                Collection<StructuralDiff> structuralDiffs = structuralChangeEvent.getRowDiffs();
                handleRowDelete(structuralDiffs);
                handleRowInsert(structuralDiffs);
            }
        }
    }

    /**
     * Will check for events that indicate that rows have been deleted. In that
     * case the cached dataChanges need to be updated because the index of the
     * rows might have changed. E.g. cell with row at index 3 is changed in the
     * given layer, deleting row at index 1 will cause the row at index 3 to be
     * moved to index 2. Without transforming the index regarding the delete
     * event, the wrong cell at the incorrect row would be shown as changed.
     *
     * @param rowDiffs
     *            The collection of {@link StructuralDiff}s to handle.
     */
    @SuppressWarnings("unchecked")
    private void handleRowDelete(Collection<StructuralDiff> rowDiffs) {
        // for correct calculation the diffs need to be processed from lowest
        // position to highest
        List<StructuralDiff> diffs = new ArrayList<>(rowDiffs);
        Collections.sort(diffs, (o1, o2) -> o1.getBeforePositionRange().start - o2.getBeforePositionRange().start);

        List<Integer> toRemove = new ArrayList<>();
        for (StructuralDiff rowDiff : diffs) {
            if (rowDiff.getDiffType() != null
                    && rowDiff.getDiffType().equals(DiffTypeEnum.DELETE)) {
                Range beforePositionRange = rowDiff.getBeforePositionRange();
                for (int i = beforePositionRange.start; i < beforePositionRange.end; i++) {
                    int index = i;
                    if (index >= 0) {
                        toRemove.add(index);
                    }
                }
            }
        }

        if (!toRemove.isEmpty()) {
            // modify row indexes regarding the deleted rows
            Map<Object, RowInsertDataChange> modifiedRows = new HashMap<>();
            for (Map.Entry<Object, RowInsertDataChange> entry : this.dataChanges.entrySet()) {
                int rowIndex = this.keyHandler.getRowIndex(entry.getKey());
                if (!toRemove.contains(rowIndex)) {
                    // check number of removed indexes that are lower than the
                    // current one
                    int deletedBefore = 0;
                    for (Integer removed : toRemove) {
                        if (removed < rowIndex) {
                            deletedBefore++;
                        }
                    }
                    int modRow = rowIndex - deletedBefore;
                    if (modRow >= 0) {
                        Object oldKey = entry.getKey();
                        Object updatedKey = this.keyHandler.getKeyWithRowUpdate(oldKey, modRow);
                        entry.getValue().updateKey(updatedKey);
                        modifiedRows.put(updatedKey, entry.getValue());
                    }
                }
            }

            this.dataChanges.clear();
            this.dataChanges.putAll(modifiedRows);
        }
    }

    /**
     * Will check for events that indicate that rows are added. In that case the
     * cached dataChanges need to be updated because the index of the rows might
     * have changed. E.g. Row with index 3 is hidden in the given layer, adding
     * a row at index 1 will cause the row at index 3 to be moved to index 4.
     * Without transforming the index regarding the add event, the wrong row
     * would be hidden.
     *
     * @param rowDiffs
     *            The collection of {@link StructuralDiff}s to handle.
     */
    @SuppressWarnings("unchecked")
    private void handleRowInsert(Collection<StructuralDiff> rowDiffs) {
        // for correct calculation the diffs need to be processed from highest
        // position to lowest
        List<StructuralDiff> diffs = new ArrayList<>(rowDiffs);
        Collections.sort(diffs, (o1, o2) -> o2.getBeforePositionRange().start - o1.getBeforePositionRange().start);

        for (StructuralDiff rowDiff : diffs) {
            if (rowDiff.getDiffType() != null
                    && rowDiff.getDiffType().equals(DiffTypeEnum.ADD)) {
                Range beforePositionRange = rowDiff.getBeforePositionRange();
                // modify row indexes regarding the inserted rows
                Map<Object, RowInsertDataChange> modifiedRows = new HashMap<>();
                for (Map.Entry<Object, RowInsertDataChange> entry : this.dataChanges.entrySet()) {
                    int rowIndex = this.keyHandler.getRowIndex(entry.getKey());

                    int modRow = -1;
                    if (rowIndex >= beforePositionRange.start) {
                        modRow = rowIndex + 1;
                    } else {
                        modRow = rowIndex;
                    }

                    Object updatedKey = this.keyHandler.getKeyWithRowUpdate(entry.getKey(), modRow);
                    entry.getValue().updateKey(updatedKey);
                    modifiedRows.put(updatedKey, entry.getValue());
                }

                this.dataChanges.clear();
                this.dataChanges.putAll(modifiedRows);
            }
        }
    }

    @Override
    public boolean isColumnDirty(int columnPosition) {
        return !this.dataChanges.isEmpty();
    }

    @Override
    public boolean isRowDirty(int rowPosition) {
        // we do not care about the column position,
        // so we use -1 for key generation
        Object key = this.keyHandler.getKey(-1, rowPosition);
        if (key != null) {
            return this.dataChanges.containsKey(key);
        }
        return false;
    }

    @Override
    public boolean isCellDirty(int columnPosition, int rowPosition) {
        return isRowDirty(rowPosition);
    }

    @Override
    public void handleLayerEvent(RowInsertEvent event) {
        if (this.handleDataUpdate) {
            synchronized (this.dataChanges) {
                if (event instanceof KeyRowInsertEvent) {
                    KeyRowInsertEvent e = (KeyRowInsertEvent) event;
                    List<Object> keys = new ArrayList<>(e.getKeys());
                    Collections.reverse(keys);
                    for (Object key : keys) {
                        // store the change locally
                        this.dataChanges.put(key, new RowInsertDataChange(key, e.getKeyHandler()));

                        // store the change in the DataChangeLayer
                        this.layer.addDataChange(new RowInsertDataChange(key, e.getKeyHandler()));
                    }
                } else {
                    // we need to ensure that the data changes are in correct
                    // order to ensure that deleting them again delete in the
                    // correct places when discarding backwards
                    int[] positions = PositionUtil.getPositions(event.getRowPositionRanges());
                    for (int i : positions) {
                        Object key = this.keyHandler.getKey(-1, i);
                        if (key != null) {
                            // store the change locally
                            this.dataChanges.put(key, new RowInsertDataChange(key, this.keyHandler));

                            // store the change in the DataChangeLayer
                            this.layer.addDataChange(new RowInsertDataChange(key, this.keyHandler));
                        } else {
                            LOG.warn("key was null for position {}", i); //$NON-NLS-1$
                        }
                    }
                }
            }
        }
    }

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

}
