/*******************************************************************************
 * 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
 ******************************************************************************/
package org.eclipse.nebula.widgets.nattable.painter.layer;

import java.util.Collection;
import java.util.HashMap;
import java.util.Map;

import org.eclipse.collections.api.factory.Sets;
import org.eclipse.nebula.widgets.nattable.config.IConfigRegistry;
import org.eclipse.nebula.widgets.nattable.layer.ILayer;
import org.eclipse.nebula.widgets.nattable.layer.cell.ILayerCell;
import org.eclipse.nebula.widgets.nattable.painter.cell.ICellPainter;
import org.eclipse.swt.graphics.GC;
import org.eclipse.swt.graphics.Rectangle;

public class CellLayerPainter implements ILayerPainter {

    private ILayer natLayer;
    private Map<Integer, Integer> horizontalPositionToPixelMap;
    private Map<Integer, Integer> verticalPositionToPixelMap;

    private final boolean clipLeft;
    private final boolean clipTop;

    /**
     * Create a default CellLayerPainter with default clipping behaviour.
     */
    public CellLayerPainter() {
        this(false, false);
    }

    /**
     * Create a CellLayerPainter with specified clipping behaviour.
     *
     * @param clipLeft
     *            Configure the rendering behaviour when cells overlap. If set
     *            to <code>true</code> the left cell will be clipped, if set to
     *            <code>false</code> the right cell will be clipped. The default
     *            value is <code>false</code>.
     * @param clipTop
     *            Configure the rendering behaviour when cells overlap. If set
     *            to <code>true</code> the top cell will be clipped, if set to
     *            <code>false</code> the bottom cell will be clipped. The
     *            default value is <code>false</code>.
     */
    public CellLayerPainter(boolean clipLeft, boolean clipTop) {
        this.clipLeft = clipLeft;
        this.clipTop = clipTop;
    }

    @Override
    public void paintLayer(ILayer natLayer, GC gc, int xOffset, int yOffset,
            Rectangle pixelRectangle, IConfigRegistry configRegistry) {
        if (pixelRectangle.width <= 0 || pixelRectangle.height <= 0) {
            return;
        }

        this.natLayer = natLayer;
        Rectangle positionRectangle = getPositionRectangleFromPixelRectangle(natLayer, pixelRectangle);

        calculateDimensionInfo(positionRectangle);

        Collection<ILayerCell> spannedCells = Sets.mutable.empty();

        for (int rowPosition = positionRectangle.y; rowPosition < positionRectangle.y
                + positionRectangle.height; rowPosition++) {
            for (int columnPosition = positionRectangle.x; columnPosition < positionRectangle.x
                    + positionRectangle.width; columnPosition++) {
                if (columnPosition == -1 || rowPosition == -1) {
                    continue;
                }
                ILayerCell cell = natLayer.getCellByPosition(columnPosition, rowPosition);
                if (cell != null) {
                    if (cell.isSpannedCell()) {
                        spannedCells.add(cell);
                    } else {
                        paintCell(cell, gc, configRegistry);
                    }
                }
            }
        }

        for (ILayerCell cell : spannedCells) {
            paintCell(cell, gc, configRegistry);
        }
    }

    /**
     * Determines the rendering behavior when two cells overlap. If
     * <code>true</code>, the left cell will be clipped. If <code>false</code>,
     * the right cell will be clipped. Typically this value is changed in
     * conjunction with split viewports.
     *
     * @param position
     *            The column position for which the clipping behaviour is
     *            requested. By default for all columns the same clipping
     *            behaviour is used. Only for special cases like split viewports
     *            with one header, per position a different behaviour may be
     *            needed.
     * @return <code>true</code> if the left cell will be clipped,
     *         <code>false</code> if the right cell will be clipped.
     */
    protected boolean isClipLeft(int position) {
        return this.clipLeft;
    }

    /**
     * Determines the rendering behavior when two cells overlap. If
     * <code>true</code>, the top cell will be clipped. If <code>false</code>,
     * the bottom cell will be clipped. Typically this value is changed in
     * conjunction with split viewports.
     *
     * @param position
     *            The row position for which the clipping behaviour is
     *            requested. By default for all rows the same clipping behaviour
     *            is used. Only for special cases like split viewports with one
     *            header, per position a different behaviour may be needed.
     * @return <code>true</code> if the top cell will be clipped,
     *         <code>false</code> if the bottom cell will be clipped.
     */
    protected boolean isClipTop(int position) {
        return this.clipTop;
    }

    private void calculateDimensionInfo(Rectangle positionRectangle) {
        {
            this.horizontalPositionToPixelMap = new HashMap<>();
            final int startPosition = positionRectangle.x;
            final int endPosition = startPosition + positionRectangle.width;
            int previousEndX = (startPosition > 0)
                    ? this.natLayer.getStartXOfColumnPosition(startPosition - 1)
                            + this.natLayer.getColumnWidthByPosition(startPosition - 1)
                    : Integer.MIN_VALUE;
            for (int position = startPosition; position < endPosition; position++) {
                int startX = this.natLayer.getStartXOfColumnPosition(position);
                this.horizontalPositionToPixelMap.put(
                        position,
                        isClipLeft(position) ? startX : Math.max(startX, previousEndX));
                previousEndX = startX + this.natLayer.getColumnWidthByPosition(position);
            }
            if (endPosition < this.natLayer.getColumnCount()) {
                int startX = this.natLayer.getStartXOfColumnPosition(endPosition);
                this.horizontalPositionToPixelMap.put(endPosition, Math.max(startX, previousEndX));
            }
        }
        {
            this.verticalPositionToPixelMap = new HashMap<>();
            final int startPosition = positionRectangle.y;
            final int endPosition = startPosition + positionRectangle.height;
            int previousEndY = (startPosition > 0)
                    ? this.natLayer.getStartYOfRowPosition(startPosition - 1)
                            + this.natLayer.getRowHeightByPosition(startPosition - 1)
                    : Integer.MIN_VALUE;
            for (int position = startPosition; position < endPosition; position++) {
                int startY = this.natLayer.getStartYOfRowPosition(position);
                this.verticalPositionToPixelMap.put(
                        position,
                        isClipTop(position) ? startY : Math.max(startY, previousEndY));
                previousEndY = startY + this.natLayer.getRowHeightByPosition(position);
            }
            if (endPosition < this.natLayer.getRowCount()) {
                int startY = this.natLayer.getStartYOfRowPosition(endPosition);
                this.verticalPositionToPixelMap.put(endPosition, Math.max(startY, previousEndY));
            }
        }
    }

    @Override
    public Rectangle adjustCellBounds(int columnPosition, int rowPosition, Rectangle cellBounds) {
        return cellBounds;
    }

    protected Rectangle getPositionRectangleFromPixelRectangle(ILayer natLayer, Rectangle pixelRectangle) {
        int columnPositionOffset = natLayer.getColumnPositionByX(pixelRectangle.x);
        int rowPositionOffset = natLayer.getRowPositionByY(pixelRectangle.y);
        int numColumns = natLayer.getColumnPositionByX(
                Math.min(natLayer.getWidth(), pixelRectangle.x + pixelRectangle.width) - 1)
                - columnPositionOffset + 1;
        int numRows = natLayer.getRowPositionByY(
                Math.min(natLayer.getHeight(), pixelRectangle.y + pixelRectangle.height) - 1)
                - rowPositionOffset + 1;

        return new Rectangle(
                columnPositionOffset,
                rowPositionOffset,
                numColumns,
                numRows);
    }

    protected void paintCell(ILayerCell cell, GC gc, IConfigRegistry configRegistry) {
        ILayer layer = cell.getLayer();
        int columnPosition = cell.getColumnPosition();
        int rowPosition = cell.getRowPosition();
        ICellPainter cellPainter = layer.getCellPainter(columnPosition, rowPosition, cell, configRegistry);
        Rectangle adjustedCellBounds = layer
                .getLayerPainter()
                .adjustCellBounds(columnPosition, rowPosition, cell.getBounds());
        if (cellPainter != null) {
            Rectangle originalClipping = gc.getClipping();

            int startX = getStartXOfColumnPosition(columnPosition);
            int startY = getStartYOfRowPosition(rowPosition);

            int endX = getStartXOfColumnPosition(cell.getOriginColumnPosition() + cell.getColumnSpan());
            int endY = getStartYOfRowPosition(cell.getOriginRowPosition() + cell.getRowSpan());

            Rectangle cellClipBounds = originalClipping.intersection(
                    new Rectangle(startX, startY, endX - startX, endY - startY));
            gc.setClipping(cellClipBounds.intersection(adjustedCellBounds));

            cellPainter.paintCell(cell, gc, adjustedCellBounds, configRegistry);

            gc.setClipping(originalClipping);
        }
    }

    protected int getStartXOfColumnPosition(final int columnPosition) {
        if (columnPosition < this.natLayer.getColumnCount()) {
            Integer start = this.horizontalPositionToPixelMap.get(columnPosition);
            if (start == null) {
                start = this.natLayer.getStartXOfColumnPosition(columnPosition);
                if (columnPosition > 0) {
                    int start2 = this.natLayer.getStartXOfColumnPosition(columnPosition - 1)
                            + this.natLayer.getColumnWidthByPosition(columnPosition - 1);
                    if (start2 > start.intValue()) {
                        start = start2;
                    }
                }
                this.horizontalPositionToPixelMap.put(columnPosition, start);
            }
            return start.intValue();
        } else {
            return this.natLayer.getWidth();
        }
    }

    protected int getStartYOfRowPosition(final int rowPosition) {
        if (rowPosition < this.natLayer.getRowCount()) {
            Integer start = this.verticalPositionToPixelMap.get(rowPosition);
            if (start == null) {
                start = this.natLayer.getStartYOfRowPosition(rowPosition);
                if (rowPosition > 0) {
                    int start2 = this.natLayer.getStartYOfRowPosition(rowPosition - 1)
                            + this.natLayer.getRowHeightByPosition(rowPosition - 1);
                    if (start2 > start.intValue()) {
                        start = start2;
                    }
                }
                this.verticalPositionToPixelMap.put(rowPosition, start);
            }
            return start.intValue();
        } else {
            return this.natLayer.getHeight();
        }
    }

}
