/*****************************************************************************
 * Copyright (c) 2015, 2020 CEA LIST.
 *
 * 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
 *      Vincent Lorenzo <vincent.lorenzo@cea.fr> - Bug 486624
 *****************************************************************************/
package org.eclipse.nebula.widgets.nattable.fillhandle.command;

import java.math.BigDecimal;
import java.math.BigInteger;
import java.util.Calendar;
import java.util.Date;

import org.eclipse.nebula.widgets.nattable.command.ILayerCommandHandler;
import org.eclipse.nebula.widgets.nattable.config.IConfigRegistry;
import org.eclipse.nebula.widgets.nattable.coordinate.PositionCoordinate;
import org.eclipse.nebula.widgets.nattable.copy.InternalCellClipboard;
import org.eclipse.nebula.widgets.nattable.edit.command.EditUtils;
import org.eclipse.nebula.widgets.nattable.edit.command.UpdateDataCommand;
import org.eclipse.nebula.widgets.nattable.fillhandle.config.FillHandleConfigAttributes;
import org.eclipse.nebula.widgets.nattable.layer.ILayer;
import org.eclipse.nebula.widgets.nattable.layer.cell.ILayerCell;
import org.eclipse.nebula.widgets.nattable.selection.SelectionLayer;
import org.eclipse.nebula.widgets.nattable.selection.SelectionLayer.MoveDirectionEnum;
import org.eclipse.nebula.widgets.nattable.style.DisplayMode;
import org.eclipse.swt.graphics.Rectangle;

/**
 * Command handler for the {@link FillHandlePasteCommand}.
 *
 * @see FillHandlePasteCommand
 *
 * @since 1.4
 */
public class FillHandlePasteCommandHandler implements ILayerCommandHandler<FillHandlePasteCommand> {

    protected SelectionLayer selectionLayer;
    protected InternalCellClipboard clipboard;

    /**
     * Creates a {@link FillHandlePasteCommandHandler}
     *
     * @param selectionLayer
     *            The {@link SelectionLayer} needed to determine the fill handle
     *            region and perform the update command.
     * @param clipboard
     *            The internal clipboard that carries the cells for the copy
     *            &amp; paste operation triggered by using the fill handle.
     */
    public FillHandlePasteCommandHandler(
            SelectionLayer selectionLayer,
            InternalCellClipboard clipboard) {

        this.selectionLayer = selectionLayer;
        this.clipboard = clipboard;
    }

    @Override
    public boolean doCommand(ILayer targetLayer, FillHandlePasteCommand command) {
        if (this.clipboard.getCopiedCells() != null) {
            int pasteColumn = -1;
            int pasteRow = -1;
            int pasteWidth = this.clipboard.getCopiedCells().length;
            int pasteHeight = this.clipboard.getCopiedCells()[0].length;
            Rectangle handleRegion = this.selectionLayer.getFillHandleRegion();
            if (handleRegion != null) {
                pasteColumn = handleRegion.x;
                pasteRow = handleRegion.y;
                pasteWidth = handleRegion.width;
                pasteHeight = handleRegion.height;
            } else {
                PositionCoordinate coord = this.selectionLayer.getSelectionAnchor();
                pasteColumn = coord.getColumnPosition();
                pasteRow = coord.getRowPosition();
            }

            int pasteStartColumn = pasteColumn;

            int rowStartAdjustment = 0;
            if (command.direction == MoveDirectionEnum.UP) {
                rowStartAdjustment = pasteHeight % this.clipboard.getCopiedCells().length;
            }

            int columnStartAdjustment = 0;
            if (command.direction == MoveDirectionEnum.LEFT) {
                columnStartAdjustment = pasteWidth % this.clipboard.getCopiedCells()[0].length;
            }

            for (int i = 0; i < pasteHeight; i++) {
                ILayerCell[] cells = this.clipboard.getCopiedCells()[(i + rowStartAdjustment) % this.clipboard.getCopiedCells().length];
                for (int j = 0; j < pasteWidth; j++) {
                    ILayerCell cell = cells[(j + columnStartAdjustment) % this.clipboard.getCopiedCells()[0].length];

                    Object cellValue = getPasteValue(cell, command, pasteColumn, pasteRow);

                    if (EditUtils.isCellEditable(
                            new PositionCoordinate(this.selectionLayer,
                                    pasteColumn,
                                    pasteRow),
                            command.configRegistry)) {
                        this.selectionLayer.doCommand(new UpdateDataCommand(this.selectionLayer, pasteColumn, pasteRow, cellValue));
                    }

                    pasteColumn++;

                    if (pasteColumn >= this.selectionLayer.getColumnCount()) {
                        break;
                    }
                }

                pasteRow++;
                pasteColumn = pasteStartColumn;
            }
        }
        return true;
    }

    /**
     * Returns the value from the given cell that should be pasted to the given
     * position.
     *
     * @param cell
     *            The cell that is copied.
     * @param command
     *            The command that contains the necessary information for the
     *            paste operation.
     * @param toColumn
     *            The column position of the cell to which the value should be
     *            pasted.
     * @param toRow
     *            The row position of the cell to which the value should be
     *            pasted.
     * @return The value that should be set to the given position.
     */
    protected Object getPasteValue(ILayerCell cell, FillHandlePasteCommand command, int toColumn, int toRow) {
        if (cell != null) {
            switch (command.operation) {
                case COPY:
                    return cell.getDataValue();
                case SERIES:
                    Object diff = 0;
                    if (command.direction == MoveDirectionEnum.LEFT || command.direction == MoveDirectionEnum.RIGHT) {
                        diff = calculateVerticalDiff(cell, toColumn, command.configRegistry);
                    } else if (command.direction == MoveDirectionEnum.UP || command.direction == MoveDirectionEnum.DOWN) {
                        diff = calculateHorizontalDiff(cell, toRow, command.configRegistry);
                    }
                    Object value = cell.getDataValue();
                    // if we can not determine a common diff value we perform a
                    // copy
                    if (diff != null) {
                        if (value instanceof Byte) {
                            return (byte) (((Byte) value).byteValue() + (Byte) diff);
                        } else if (value instanceof Short) {
                            return (short) (((Short) value).shortValue() + (Short) diff);
                        } else if (value instanceof Integer) {
                            return (Integer) value + (Integer) diff;
                        } else if (value instanceof Long) {
                            return (Long) value + (Long) diff;
                        } else if (value instanceof Float) {
                            return (Float) value + (Float) diff;
                        } else if (value instanceof Double) {
                            return (Double) value + (Double) diff;
                        } else if (value instanceof BigInteger) {
                            return ((BigInteger) value).add((BigInteger) diff);
                        } else if (value instanceof BigDecimal) {
                            return ((BigDecimal) value).add((BigDecimal) diff);
                        } else if (value instanceof Date) {
                            Calendar cal = Calendar.getInstance();
                            cal.setTime((Date) value);
                            cal.add(getIncrementDateField(cell, command.configRegistry), (Integer) diff);
                            return cal.getTime();
                        }
                    }
                    // if the value is neither a number nor a date simply return
                    // the value as we can't calculate a series for other data
                    // types
                    return value;
                default:
                    return cell.getDataValue();
            }
        }
        return null;
    }

    protected Number calculateHorizontalDiff(ILayerCell cell, int toRow, IConfigRegistry configRegistry) {
        Class<?> type = cell.getDataValue() != null ? cell.getDataValue().getClass() : null;

        ILayerCell[][] cells = this.clipboard.getCopiedCells();
        int rowDiff = getRowDiff(cell, toRow);
        if (cells.length == 1) {
            return getCastValue(rowDiff, type);
        } else if (type != null) {
            int columnArrayIndex = cell.getColumnIndex() - this.clipboard.getCopiedCells()[0][0].getColumnIndex();
            if (type == Byte.class) {
                Byte diff = calculateByteDiff(cells[1][columnArrayIndex], cells[0][columnArrayIndex]);
                if (diff == null) {
                    return null;
                }
                Byte temp = diff;
                for (int i = 1; i < cells.length; i++) {
                    temp = calculateByteDiff(cells[i][columnArrayIndex], cells[i - 1][columnArrayIndex]);
                    if (temp == null || !temp.equals(diff)) {
                        return null;
                    }
                }
                return (byte) (diff * rowDiff);
            } else if (type == Short.class) {
                Short diff = calculateShortDiff(cells[1][columnArrayIndex], cells[0][columnArrayIndex]);
                if (diff == null) {
                    return null;
                }
                Short temp = diff;
                for (int i = 1; i < cells.length; i++) {
                    temp = calculateShortDiff(cells[i][columnArrayIndex], cells[i - 1][columnArrayIndex]);
                    if (temp == null || !temp.equals(diff)) {
                        return null;
                    }
                }
                return (short) (diff * rowDiff);
            } else if (type == Integer.class) {
                Integer diff = calculateIntDiff(cells[1][columnArrayIndex], cells[0][columnArrayIndex]);
                if (diff == null) {
                    return null;
                }
                Integer temp = diff;
                for (int i = 1; i < cells.length; i++) {
                    temp = calculateIntDiff(cells[i][columnArrayIndex], cells[i - 1][columnArrayIndex]);
                    if (temp == null || !temp.equals(diff)) {
                        return null;
                    }
                }
                return diff * rowDiff;
            } else if (type == Long.class) {
                Long diff = calculateLongDiff(cells[1][columnArrayIndex], cells[0][columnArrayIndex]);
                if (diff == null) {
                    return null;
                }
                Long temp = diff;
                for (int i = 1; i < cells.length; i++) {
                    temp = calculateLongDiff(cells[i][columnArrayIndex], cells[i - 1][columnArrayIndex]);
                    if (temp == null || !temp.equals(diff)) {
                        return null;
                    }
                }
                return diff * rowDiff;
            } else if (type == Float.class) {
                Float diff = calculateFloatDiff(cells[1][columnArrayIndex], cells[0][columnArrayIndex]);
                if (diff == null) {
                    return null;
                }
                Float temp = diff;
                for (int i = 1; i < cells.length; i++) {
                    temp = calculateFloatDiff(cells[i][columnArrayIndex], cells[i - 1][columnArrayIndex]);
                    if (temp == null || !temp.equals(diff)) {
                        return null;
                    }
                }
                return BigDecimal.valueOf(diff).multiply(BigDecimal.valueOf(rowDiff)).floatValue();
            } else if (type == Double.class) {
                Double diff = calculateDoubleDiff(cells[1][columnArrayIndex], cells[0][columnArrayIndex]);
                if (diff == null) {
                    return null;
                }
                Double temp = diff;
                for (int i = 1; i < cells.length; i++) {
                    temp = calculateDoubleDiff(cells[i][columnArrayIndex], cells[i - 1][columnArrayIndex]);
                    if (temp == null || !temp.equals(diff)) {
                        return null;
                    }
                }
                return BigDecimal.valueOf(diff).multiply(BigDecimal.valueOf(rowDiff)).doubleValue();
            } else if (type == BigInteger.class) {
                BigInteger diff = calculateBigIntegerDiff(cells[1][columnArrayIndex], cells[0][columnArrayIndex]);
                if (diff == null) {
                    return null;
                }
                BigInteger temp = diff;
                for (int i = 1; i < cells.length; i++) {
                    temp = calculateBigIntegerDiff(cells[i][columnArrayIndex], cells[i - 1][columnArrayIndex]);
                    if (temp == null || !temp.equals(diff)) {
                        return null;
                    }
                }
                return diff.multiply(BigInteger.valueOf(rowDiff));
            } else if (type == BigDecimal.class) {
                BigDecimal diff = calculateBigDecimalDiff(cells[1][columnArrayIndex], cells[0][columnArrayIndex]);
                if (diff == null) {
                    return null;
                }
                BigDecimal temp = diff;
                for (int i = 1; i < cells.length; i++) {
                    temp = calculateBigDecimalDiff(cells[i][columnArrayIndex], cells[i - 1][columnArrayIndex]);
                    if (temp == null || !temp.equals(diff)) {
                        return null;
                    }
                }
                return diff.multiply(BigDecimal.valueOf(rowDiff));
            } else if (type == Date.class) {
                Integer diff = calculateDateDiff(cells[1][columnArrayIndex], cells[0][columnArrayIndex], configRegistry);
                if (diff == null) {
                    return null;
                }
                Integer temp = diff;
                for (int i = 1; i < cells.length; i++) {
                    temp = calculateDateDiff(cells[i][columnArrayIndex], cells[i - 1][columnArrayIndex], configRegistry);
                    if (temp == null || !temp.equals(diff)) {
                        return null;
                    }
                }
                return diff * rowDiff;
            }
        }
        return null;
    }

    protected Number calculateVerticalDiff(ILayerCell cell, int toColumn, IConfigRegistry configRegistry) {
        Class<?> type = cell.getDataValue() != null ? cell.getDataValue().getClass() : null;

        ILayerCell[][] cells = this.clipboard.getCopiedCells();
        int columnDiff = getColumnDiff(cell, toColumn);
        int rowArrayIndex = cell.getRowIndex() - this.clipboard.getCopiedCells()[0][0].getRowIndex();
        if (cells[rowArrayIndex].length == 1) {
            return getCastValue(columnDiff, type);
        } else if (type != null) {
            if (type == Byte.class) {
                Byte diff = calculateByteDiff(cells[rowArrayIndex][1], cells[rowArrayIndex][0]);
                if (diff == null) {
                    return null;
                }
                Byte temp = diff;
                for (int i = 1; i < cells.length; i++) {
                    temp = calculateByteDiff(cells[rowArrayIndex][i], cells[rowArrayIndex][i - 1]);
                    if (temp == null || !temp.equals(diff)) {
                        return null;
                    }
                }
                return (byte) (diff * columnDiff);
            } else if (type == Short.class) {
                Short diff = calculateShortDiff(cells[rowArrayIndex][1], cells[rowArrayIndex][0]);
                if (diff == null) {
                    return null;
                }
                Short temp = diff;
                for (int i = 1; i < cells.length; i++) {
                    temp = calculateShortDiff(cells[rowArrayIndex][i], cells[rowArrayIndex][i - 1]);
                    if (temp == null || !temp.equals(diff)) {
                        return null;
                    }
                }
                return (short) (diff * columnDiff);
            } else if (type == Integer.class) {
                Integer diff = calculateIntDiff(cells[rowArrayIndex][1], cells[rowArrayIndex][0]);
                if (diff == null) {
                    return null;
                }
                Integer temp = diff;
                for (int i = 1; i < cells.length; i++) {
                    temp = calculateIntDiff(cells[rowArrayIndex][i], cells[rowArrayIndex][i - 1]);
                    if (temp == null || !temp.equals(diff)) {
                        return null;
                    }
                }
                return diff * columnDiff;
            } else if (type == Long.class) {
                Long diff = calculateLongDiff(cells[rowArrayIndex][1], cells[rowArrayIndex][0]);
                if (diff == null) {
                    return null;
                }
                Long temp = diff;
                for (int i = 1; i < cells.length; i++) {
                    temp = calculateLongDiff(cells[rowArrayIndex][i], cells[rowArrayIndex][i - 1]);
                    if (temp == null || !temp.equals(diff)) {
                        return null;
                    }
                }
                return diff * columnDiff;
            } else if (type == Float.class) {
                Float diff = calculateFloatDiff(cells[rowArrayIndex][1], cells[rowArrayIndex][0]);
                if (diff == null) {
                    return null;
                }
                Float temp = diff;
                for (int i = 1; i < cells.length; i++) {
                    temp = calculateFloatDiff(cells[rowArrayIndex][i], cells[rowArrayIndex][i - 1]);
                    if (temp == null || !temp.equals(diff)) {
                        return null;
                    }
                }
                return BigDecimal.valueOf(diff).multiply(BigDecimal.valueOf(columnDiff)).floatValue();
            } else if (type == Double.class) {
                Double diff = calculateDoubleDiff(cells[rowArrayIndex][1], cells[rowArrayIndex][0]);
                if (diff == null) {
                    return null;
                }
                Double temp = diff;
                for (int i = 1; i < cells.length; i++) {
                    temp = calculateDoubleDiff(cells[rowArrayIndex][i], cells[rowArrayIndex][i - 1]);
                    if (temp == null || !temp.equals(diff)) {
                        return null;
                    }
                }
                return BigDecimal.valueOf(diff).multiply(BigDecimal.valueOf(columnDiff)).doubleValue();
            } else if (type == BigInteger.class) {
                BigInteger diff = calculateBigIntegerDiff(cells[rowArrayIndex][1], cells[rowArrayIndex][0]);
                if (diff == null) {
                    return null;
                }
                BigInteger temp = diff;
                for (int i = 1; i < cells.length; i++) {
                    temp = calculateBigIntegerDiff(cells[rowArrayIndex][i], cells[rowArrayIndex][i - 1]);
                    if (temp == null || !temp.equals(diff)) {
                        return null;
                    }
                }
                return diff.multiply(BigInteger.valueOf(columnDiff));
            } else if (type == BigDecimal.class) {
                BigDecimal diff = calculateBigDecimalDiff(cells[rowArrayIndex][1], cells[rowArrayIndex][0]);
                if (diff == null) {
                    return null;
                }
                BigDecimal temp = diff;
                for (int i = 1; i < cells.length; i++) {
                    temp = calculateBigDecimalDiff(cells[rowArrayIndex][i], cells[rowArrayIndex][i - 1]);
                    if (temp == null || !temp.equals(diff)) {
                        return null;
                    }
                }
                return diff.multiply(BigDecimal.valueOf(columnDiff));
            } else if (type == Date.class) {
                Integer diff = calculateDateDiff(cells[rowArrayIndex][1], cells[rowArrayIndex][0], configRegistry);
                if (diff == null) {
                    return null;
                }
                Integer temp = diff;
                for (int i = 1; i < cells.length; i++) {
                    temp = calculateDateDiff(cells[rowArrayIndex][i], cells[rowArrayIndex][i - 1], configRegistry);
                    if (temp == null || !temp.equals(diff)) {
                        return null;
                    }
                }
                return diff * columnDiff;
            }
        }
        return null;
    }

    /**
     * Calculate the row difference between the cell row index and the row index
     * of the cell to copy to.
     *
     * @param currentCell
     *            The current cell to manage.
     * @param toRow
     *            The row index of the cell to copy to.
     * @return The difference as integer.
     */
    protected int getRowDiff(final ILayerCell currentCell, final int toRow) {
        return toRow - currentCell.getRowIndex();
    }

    /**
     * Calculate the column difference between the cell column index and the
     * column index of the cell to copy to.
     *
     * @param currentCell
     *            The current cell to manage.
     * @param toColumn
     *            The column index of the cell to copy to.
     * @return The difference as integer.
     */
    protected int getColumnDiff(final ILayerCell currentCell, final int toColumn) {
        return toColumn - currentCell.getColumnIndex();
    }

    protected Number getCastValue(int diff, Class<?> type) {
        if (type != null) {
            if (type == Byte.class) {
                return (byte) diff;
            } else if (type == Short.class) {
                return (short) diff;
            } else if (type == Long.class) {
                return (long) diff;
            } else if (type == Float.class) {
                return (float) diff;
            } else if (type == Double.class) {
                return (double) diff;
            } else if (type == BigInteger.class) {
                return BigInteger.valueOf(diff);
            } else if (type == BigDecimal.class) {
                return BigDecimal.valueOf(diff);
            }
        }
        return diff;
    }

    protected Byte calculateByteDiff(ILayerCell c1, ILayerCell c2) {
        if (c1 == null || c2 == null) {
            return null;
        }
        return (c1.getDataValue() == null || c2.getDataValue() == null
                || !(c1.getDataValue() instanceof Byte) || !(c2.getDataValue() instanceof Byte))
                        ? null
                        : (byte) (((Byte) c1.getDataValue()) - ((Byte) c2.getDataValue()));
    }

    protected Short calculateShortDiff(ILayerCell c1, ILayerCell c2) {
        if (c1 == null || c2 == null) {
            return null;
        }
        return (c1.getDataValue() == null || c2.getDataValue() == null
                || !(c1.getDataValue() instanceof Short) || !(c2.getDataValue() instanceof Short))
                        ? null
                        : (short) (((Short) c1.getDataValue()) - ((Short) c2.getDataValue()));
    }

    protected Integer calculateIntDiff(ILayerCell c1, ILayerCell c2) {
        if (c1 == null || c2 == null) {
            return null;
        }
        return (c1.getDataValue() == null || c2.getDataValue() == null
                || !(c1.getDataValue() instanceof Integer) || !(c2.getDataValue() instanceof Integer))
                        ? null
                        : (Integer) (c1.getDataValue()) - (Integer) (c2.getDataValue());
    }

    protected Long calculateLongDiff(ILayerCell c1, ILayerCell c2) {
        if (c1 == null || c2 == null) {
            return null;
        }
        return (c1.getDataValue() == null || c2.getDataValue() == null
                || !(c1.getDataValue() instanceof Long) || !(c2.getDataValue() instanceof Long))
                        ? null
                        : ((Long) c1.getDataValue()) - ((Long) c2.getDataValue());
    }

    protected Float calculateFloatDiff(ILayerCell c1, ILayerCell c2) {
        if (c1 == null || c2 == null) {
            return null;
        }
        if (c1.getDataValue() == null || c2.getDataValue() == null
                || !(c1.getDataValue() instanceof Float) || !(c2.getDataValue() instanceof Float)) {
            return null;
        }

        // Use BigDecimal for exact calculations because of floating point
        // arithmetic issues. Because of that we also use the String constructor
        // of BigDecimal.
        BigDecimal v1 = new BigDecimal(c1.getDataValue().toString());
        BigDecimal v2 = new BigDecimal(c2.getDataValue().toString());
        return v1.subtract(v2).floatValue();
    }

    protected Double calculateDoubleDiff(ILayerCell c1, ILayerCell c2) {
        if (c1 == null || c2 == null) {
            return null;
        }
        if (c1.getDataValue() == null || c2.getDataValue() == null
                || !(c1.getDataValue() instanceof Double) || !(c2.getDataValue() instanceof Double)) {
            return null;
        }

        // Use BigDecimal for exact calculations because of floating point
        // arithmetic issues. Because of that we also use the String constructor
        // of BigDecimal.
        BigDecimal v1 = new BigDecimal(c1.getDataValue().toString());
        BigDecimal v2 = new BigDecimal(c2.getDataValue().toString());
        return v1.subtract(v2).doubleValue();
    }

    protected BigInteger calculateBigIntegerDiff(ILayerCell c1, ILayerCell c2) {
        if (c1 == null || c2 == null) {
            return null;
        }
        return (c1.getDataValue() == null || c2.getDataValue() == null
                || !(c1.getDataValue() instanceof BigInteger) || !(c2.getDataValue() instanceof BigInteger))
                        ? null
                        : ((BigInteger) c1.getDataValue()).subtract((BigInteger) c2.getDataValue());
    }

    protected BigDecimal calculateBigDecimalDiff(ILayerCell c1, ILayerCell c2) {
        if (c1 == null || c2 == null) {
            return null;
        }
        return (c1.getDataValue() == null || c2.getDataValue() == null
                || !(c1.getDataValue() instanceof BigDecimal) || !(c2.getDataValue() instanceof BigDecimal))
                        ? null
                        : ((BigDecimal) c1.getDataValue()).subtract((BigDecimal) c2.getDataValue());
    }

    protected Integer calculateDateDiff(ILayerCell c1, ILayerCell c2, IConfigRegistry configRegistry) {
        if (c1 == null || c2 == null) {
            return null;
        }
        if (c1.getDataValue() == null || c2.getDataValue() == null
                || !(c1.getDataValue() instanceof Date) || !(c2.getDataValue() instanceof Date)) {
            return null;
        }

        int dateField = getIncrementDateField(c1, configRegistry);

        Calendar cal1 = Calendar.getInstance();
        cal1.setTime((Date) c1.getDataValue());
        int fieldValue1 = cal1.get(dateField);

        Calendar cal2 = Calendar.getInstance();
        cal2.setTime((Date) c2.getDataValue());
        int fieldValue2 = cal2.get(dateField);

        return fieldValue1 - fieldValue2;
    }

    protected int getIncrementDateField(ILayerCell cell, IConfigRegistry configRegistry) {
        Integer dateField = configRegistry.getConfigAttribute(
                FillHandleConfigAttributes.INCREMENT_DATE_FIELD,
                DisplayMode.NORMAL,
                cell.getConfigLabels());

        if (dateField == null) {
            dateField = Calendar.DATE;
        }

        return dateField;
    }

    @Override
    public Class<FillHandlePasteCommand> getCommandClass() {
        return FillHandlePasteCommand.class;
    }

}
