/*****************************************************************************
 * Copyright (c) 2015, 2020 CEA LIST 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:
 *      Dirk Fauth <dirk.fauth@googlemail.com> - Initial API and implementation
 *
 *****************************************************************************/
package org.eclipse.nebula.widgets.nattable.extension.e4.painterfactory;

import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

import org.eclipse.nebula.widgets.nattable.extension.e4.css.NatTableCSSConstants;
import org.eclipse.nebula.widgets.nattable.group.painter.ColumnGroupExpandCollapseImagePainter;
import org.eclipse.nebula.widgets.nattable.group.painter.ColumnGroupHeaderTextPainter;
import org.eclipse.nebula.widgets.nattable.group.painter.RowGroupExpandCollapseImagePainter;
import org.eclipse.nebula.widgets.nattable.group.painter.RowGroupHeaderTextPainter;
import org.eclipse.nebula.widgets.nattable.painter.cell.AbstractTextPainter;
import org.eclipse.nebula.widgets.nattable.painter.cell.BackgroundImagePainter;
import org.eclipse.nebula.widgets.nattable.painter.cell.BackgroundPainter;
import org.eclipse.nebula.widgets.nattable.painter.cell.CellPainterWrapper;
import org.eclipse.nebula.widgets.nattable.painter.cell.CheckBoxPainter;
import org.eclipse.nebula.widgets.nattable.painter.cell.ComboBoxPainter;
import org.eclipse.nebula.widgets.nattable.painter.cell.GradientBackgroundPainter;
import org.eclipse.nebula.widgets.nattable.painter.cell.ICellPainter;
import org.eclipse.nebula.widgets.nattable.painter.cell.ImagePainter;
import org.eclipse.nebula.widgets.nattable.painter.cell.PasswordTextPainter;
import org.eclipse.nebula.widgets.nattable.painter.cell.PercentageBarCellPainter;
import org.eclipse.nebula.widgets.nattable.painter.cell.TableCellPainter;
import org.eclipse.nebula.widgets.nattable.painter.cell.TextPainter;
import org.eclipse.nebula.widgets.nattable.painter.cell.VerticalTextPainter;
import org.eclipse.nebula.widgets.nattable.painter.cell.decorator.BeveledBorderDecorator;
import org.eclipse.nebula.widgets.nattable.painter.cell.decorator.CellPainterDecorator;
import org.eclipse.nebula.widgets.nattable.painter.cell.decorator.CustomLineBorderDecorator;
import org.eclipse.nebula.widgets.nattable.painter.cell.decorator.LineBorderDecorator;
import org.eclipse.nebula.widgets.nattable.painter.cell.decorator.PaddingDecorator;
import org.eclipse.nebula.widgets.nattable.sort.painter.SortIconPainter;
import org.eclipse.nebula.widgets.nattable.sort.painter.SortableHeaderTextPainter;
import org.eclipse.nebula.widgets.nattable.tree.painter.IndentedTreeImagePainter;
import org.eclipse.nebula.widgets.nattable.tree.painter.TreeImagePainter;
import org.eclipse.nebula.widgets.nattable.ui.util.CellEdgeEnum;
import org.eclipse.nebula.widgets.nattable.util.GUIHelper;
import org.eclipse.swt.graphics.Image;

/**
 * Factory that creates {@link ICellPainter} for a specified key in combination
 * with a configuration map.
 */
public class CellPainterFactory {

    // background painter keys

    public static final String BACKGROUND_PAINTER_KEY = "background";
    public static final String BACKGROUND_IMAGE_PAINTER_KEY = "image-background";
    public static final String GRADIENT_BACKGROUND_PAINTER_KEY = "gradient-background";

    // painter decorator keys

    public static final String DECORATOR_KEY = "decorator";
    public static final String LINE_BORDER_DECORATOR_KEY = "line-border";
    public static final String CUSTOM_LINE_BORDER_DECORATOR_KEY = "custom-line-border";
    public static final String BEVELED_BORDER_DECORATOR_KEY = "beveled-border";
    public static final String PADDING_DECORATOR_KEY = "padding";
    public static final String SORTABLE_HEADER_KEY = "sort-header";
    public static final String COLUMN_GROUP_HEADER_KEY = "column-group";
    public static final String ROW_GROUP_HEADER_KEY = "row-group";
    public static final String TREE_STRUCTURE_KEY = "tree";

    // content painter keys

    public static final String CHECKBOX_PAINTER_KEY = "checkbox";
    public static final String COMBOBOX_PAINTER_KEY = "combobox";
    public static final String IMAGE_PAINTER_KEY = "image";
    public static final String PASSWORD_PAINTER_KEY = "password";
    public static final String PERCENTAGEBAR_PAINTER_KEY = "percentage";
    public static final String TABLE_PAINTER_KEY = "table";
    public static final String TEXT_PAINTER_KEY = "text";

    public static final String NONE = "none";

    private final Map<String, CellPainterWrapperCreator> backgroundPainter = new HashMap<>();
    private final Map<String, CellPainterWrapperCreator> decoratorPainter = new HashMap<>();
    private final Map<String, CellPainterCreator> contentPainter = new HashMap<>();

    /**
     * Singleton instance
     */
    private static CellPainterFactory instance;

    /**
     * @return The singleton instance of {@link CellPainterFactory}
     */
    public static CellPainterFactory getInstance() {
        if (instance == null) {
            instance = new CellPainterFactory();
        }
        return instance;
    }

    /**
     * Private constructor for the singleton pattern
     */
    private CellPainterFactory() {
        // default background painter initializations
        this.backgroundPainter.put(
                BACKGROUND_PAINTER_KEY,
                (painterProperties, underlying) -> new BackgroundPainter(underlying));
        this.backgroundPainter.put(
                BACKGROUND_IMAGE_PAINTER_KEY,
                (painterProperties, underlying) -> {
                    Image image = (Image) painterProperties.get(NatTableCSSConstants.CELL_BACKGROUND_IMAGE);
                    return new BackgroundImagePainter(underlying, image);
                });
        this.backgroundPainter.put(
                GRADIENT_BACKGROUND_PAINTER_KEY,
                (painterProperties, underlying) -> {
                    Boolean vertical = (Boolean) painterProperties.get(NatTableCSSConstants.GRADIENT_BACKGROUND_VERTICAL);
                    if (vertical == null) {
                        vertical = false;
                    }
                    return new GradientBackgroundPainter(underlying, vertical);
                });

        // default decorator painter initializations
        this.decoratorPainter.put(
                LINE_BORDER_DECORATOR_KEY,
                (painterProperties, underlying) -> new LineBorderDecorator(underlying));
        this.decoratorPainter.put(
                CUSTOM_LINE_BORDER_DECORATOR_KEY,
                (painterProperties, underlying) -> new CustomLineBorderDecorator(underlying));
        this.decoratorPainter.put(
                BEVELED_BORDER_DECORATOR_KEY,
                (painterProperties, underlying) -> new BeveledBorderDecorator(underlying));

        this.decoratorPainter.put(
                PADDING_DECORATOR_KEY,
                (painterProperties, underlying) -> {
                    Integer topPadding = (Integer) painterProperties.get(NatTableCSSConstants.PADDING_TOP);
                    if (topPadding == null) {
                        topPadding = 0;
                    }
                    Integer bottomPadding = (Integer) painterProperties.get(NatTableCSSConstants.PADDING_BOTTOM);
                    if (bottomPadding == null) {
                        bottomPadding = 0;
                    }
                    Integer leftPadding = (Integer) painterProperties.get(NatTableCSSConstants.PADDING_LEFT);
                    if (leftPadding == null) {
                        leftPadding = 0;
                    }
                    Integer rightPadding = (Integer) painterProperties.get(NatTableCSSConstants.PADDING_RIGHT);
                    if (rightPadding == null) {
                        rightPadding = 0;
                    }
                    return new PaddingDecorator(underlying, topPadding, rightPadding, bottomPadding, leftPadding, false);
                });
        this.decoratorPainter.put(
                SORTABLE_HEADER_KEY,
                (painterProperties, underlying) -> {
                    boolean invert = false;
                    if (painterProperties.containsKey(NatTableCSSConstants.INVERT_ICONS)) {
                        invert = (Boolean) painterProperties.get(NatTableCSSConstants.INVERT_ICONS);
                    }

                    return new SortableHeaderTextPainter(
                            underlying,
                            CellEdgeEnum.RIGHT,
                            new SortIconPainter(false, invert),
                            false,
                            0,
                            false);
                });
        this.decoratorPainter.put(
                COLUMN_GROUP_HEADER_KEY,
                (painterProperties, underlying) -> {
                    boolean invert = false;
                    if (painterProperties.containsKey(NatTableCSSConstants.INVERT_ICONS)) {
                        invert = (Boolean) painterProperties.get(NatTableCSSConstants.INVERT_ICONS);
                    }

                    return new ColumnGroupHeaderTextPainter(
                            underlying,
                            CellEdgeEnum.RIGHT,
                            new ColumnGroupExpandCollapseImagePainter(false, invert),
                            false,
                            0,
                            false);
                });
        this.decoratorPainter.put(
                ROW_GROUP_HEADER_KEY,
                (painterProperties, underlying) -> {
                    boolean invert = false;
                    if (painterProperties.containsKey(NatTableCSSConstants.INVERT_ICONS)) {
                        invert = (Boolean) painterProperties.get(NatTableCSSConstants.INVERT_ICONS);
                    }

                    return new RowGroupHeaderTextPainter(
                            underlying,
                            CellEdgeEnum.BOTTOM,
                            new RowGroupExpandCollapseImagePainter(false, invert),
                            false,
                            0,
                            true);
                });
        this.decoratorPainter.put(
                TREE_STRUCTURE_KEY,
                (painterProperties, underlying) -> {
                    boolean invert = false;
                    if (painterProperties.containsKey(NatTableCSSConstants.INVERT_ICONS)) {
                        invert = (Boolean) painterProperties.get(NatTableCSSConstants.INVERT_ICONS);
                    }

                    String postFix = ""; //$NON-NLS-1$
                    if (invert)
                        postFix = "_inv"; //$NON-NLS-1$

                    TreeImagePainter imagePainter =
                            new TreeImagePainter(
                                    false,
                                    GUIHelper.getImage("right" + postFix),
                                    GUIHelper.getImage("right_down" + postFix),
                                    null);

                    return new IndentedTreeImagePainter(10, null, CellEdgeEnum.LEFT, imagePainter, false, 2, true);
                });

        // default content painter initializations
        this.contentPainter.put(
                TEXT_PAINTER_KEY,
                (painterProperties, underlying) -> {
                    AbstractTextPainter result = null;
                    String textDirection = (String) painterProperties.get(NatTableCSSConstants.TEXT_DIRECTION);
                    if (!"vertical".equalsIgnoreCase(textDirection)) {
                        result = new TextPainter(false, false);
                    } else {
                        result = new VerticalTextPainter(false, false);
                    }
                    initTextPainter(result, painterProperties);
                    return result;
                });
        this.contentPainter.put(
                IMAGE_PAINTER_KEY,
                (painterProperties, underlying) -> {
                    ImagePainter result = new ImagePainter(false);

                    // init
                    if (painterProperties.containsKey(NatTableCSSConstants.CALCULATE_CELL_HEIGHT)) {
                        result.setCalculateByHeight(
                                (Boolean) painterProperties.get(NatTableCSSConstants.CALCULATE_CELL_HEIGHT));
                    }

                    if (painterProperties.containsKey(NatTableCSSConstants.CALCULATE_CELL_WIDTH)) {
                        result.setCalculateByWidth(
                                (Boolean) painterProperties.get(NatTableCSSConstants.CALCULATE_CELL_WIDTH));
                    }
                    return result;
                });
        this.contentPainter.put(
                CHECKBOX_PAINTER_KEY,
                (painterProperties, underlying) -> {
                    boolean invert = false;
                    if (painterProperties.containsKey(NatTableCSSConstants.INVERT_ICONS)) {
                        invert = (Boolean) painterProperties.get(NatTableCSSConstants.INVERT_ICONS);
                    }
                    return new CheckBoxPainter(false, invert);
                });
        this.contentPainter.put(
                COMBOBOX_PAINTER_KEY,
                (painterProperties, underlying) -> {
                    boolean invert = false;
                    if (painterProperties.containsKey(NatTableCSSConstants.INVERT_ICONS)) {
                        invert = (Boolean) painterProperties.get(NatTableCSSConstants.INVERT_ICONS);
                    }
                    return new ComboBoxPainter(invert);
                });
        this.contentPainter.put(
                PASSWORD_PAINTER_KEY,
                (painterProperties, underlying) -> new PasswordTextPainter(false, false));
        this.contentPainter.put(
                PERCENTAGEBAR_PAINTER_KEY,
                (painterProperties, underlying) -> new PercentageBarCellPainter());
        this.contentPainter.put(
                TABLE_PAINTER_KEY,
                (painterProperties, underlying) -> {
                    TableCellPainter result = new TableCellPainter(new TextPainter(false, false));
                    result.setPaintBg(false);
                    return result;
                });

    }

    /**
     *
     * @param painterValues
     *            The list of all painter representation values in correct
     *            order.
     * @param painterProperties
     *            The properties to set to the painters
     * @return The {@link ICellPainter} construct that should be used for
     *         rendering
     */
    public ICellPainter getCellPainter(List<String> painterValues, Map<String, Object> painterProperties) {
        String backgroundKey = null;
        List<String> decoratorKeys = new ArrayList<>();
        String contentKey = null;

        if (isBackgroundPainterKey(painterValues.get(0))) {
            backgroundKey = painterValues.get(0);
        }

        String last = painterValues.get(painterValues.size() - 1);
        if (isContentPainterKey(last)
                || CellPainterFactory.NONE.equals(last)) {
            contentKey = last;
        }

        int decoratorStart = (backgroundKey != null) ? 1 : 0;
        int decoratorEnd = (contentKey != null) ? painterValues.size() - 1 : painterValues.size();
        decoratorKeys.addAll(painterValues.subList(decoratorStart, decoratorEnd));

        return getCellPainter(
                backgroundKey,
                decoratorKeys,
                contentKey,
                painterProperties);
    }

    /**
     *
     * @param backgroundKey
     *            The key of the background painter to use
     * @param decoratorKeys
     *            The list of keys of decorator painter to use
     * @param contentKey
     *            The key of the content painter to use
     * @param painterProperties
     *            The properties to set to the painters
     * @return The {@link ICellPainter} construct that should be used for
     *         rendering
     */
    public ICellPainter getCellPainter(
            String backgroundKey, List<String> decoratorKeys, String contentKey, Map<String, Object> painterProperties) {

        ICellPainter painter = null;

        // resolve content painter
        ICellPainter cp = null;
        if (isContentPainterKey(contentKey)) {
            cp = getContentPainter(contentKey, painterProperties);
        } else if (!NONE.equalsIgnoreCase(contentKey)) {
            // fallback for unknown content painter key
            cp = getContentPainter(TEXT_PAINTER_KEY, painterProperties);
        }

        // intermediate result = content painter
        painter = cp;

        // resolve decorators
        String decoratorKey = null;
        for (int i = decoratorKeys.size() - 1; i >= 0; i--) {
            decoratorKey = decoratorKeys.get(i);
            if (DECORATOR_KEY.equalsIgnoreCase(decoratorKey)) {
                CellPainterDecorator decorator = new CellPainterDecorator();
                decorator.setPaintBackground(false);
                decorator.setBaseCellPainter(painter);

                Image image = (Image) painterProperties.get(NatTableCSSConstants.DECORATOR_IMAGE);
                CellEdgeEnum edge = (CellEdgeEnum) painterProperties.get(NatTableCSSConstants.DECORATOR_EDGE);
                Integer spacing = (Integer) painterProperties.get(NatTableCSSConstants.DECORATOR_SPACING);
                Boolean paintDecorationDependent = (Boolean) painterProperties.get(NatTableCSSConstants.PAINT_DECORATION_DEPENDENT);

                decorator.setDecoratorCellPainter(new ImagePainter(image, false));
                decorator.setCellEdge(edge);
                decorator.setSpacing(spacing);
                decorator.setPaintDecorationDependent(paintDecorationDependent);

                painter = decorator;
            } else {
                CellPainterWrapper decorator = getDecoratorPainter(decoratorKey, painterProperties, painter);
                if (decorator != null) {
                    painter = decorator;
                }
            }
        }

        // add background painter
        if (backgroundKey != null) {
            CellPainterWrapper bgPainter = getBackgroundPainter(backgroundKey, painterProperties, painter);
            if (bgPainter != null) {
                bgPainter.setWrappedPainter(painter);
                painter = bgPainter;
            }
        }

        return painter;
    }

    /**
     * Create the background painter for the given key and properties.
     *
     * @param key
     *            The background painter key.
     * @param painterProperties
     *            The painter properties for painter initialization.
     * @param underlying
     *            The {@link ICellPainter} that should be applied as wrapped
     *            painter to the created decorator.
     * @return The background painter to use
     */
    public CellPainterWrapper getBackgroundPainter(String key, Map<String, Object> painterProperties, ICellPainter underlying) {
        CellPainterWrapper result = null;

        String lowerKey = key.toLowerCase();
        CellPainterWrapperCreator creator = this.backgroundPainter.get(lowerKey);
        if (creator != null) {
            result = creator.createCellPainterWrapper(painterProperties, underlying);
        }

        return result;
    }

    /**
     * Create the decorator painter for the given key and properties.
     *
     * @param key
     *            The decorator painter key.
     * @param painterProperties
     *            The painter properties for painter initialization.
     * @param underlying
     *            the {@link ICellPainter} that should be applied as wrapped
     *            painter to the created decorator.
     * @return The decorator painter to use
     */
    public CellPainterWrapper getDecoratorPainter(String key, Map<String, Object> painterProperties, ICellPainter underlying) {
        CellPainterWrapper result = null;

        String lowerKey = key.toLowerCase();
        CellPainterWrapperCreator creator = this.decoratorPainter.get(lowerKey);
        if (creator != null) {
            result = creator.createCellPainterWrapper(painterProperties, underlying);
        }

        if (result == null) {
            // a background painter can also be a decorator
            result = getBackgroundPainter(key, painterProperties, underlying);
        }

        return result;
    }

    /**
     * Create the content painter for the given key and properties.
     *
     * @param key
     *            The content painter key.
     * @param painterProperties
     *            The painter properties for painter initialization.
     * @return The content painter to use
     */
    public ICellPainter getContentPainter(String key, Map<String, Object> painterProperties) {
        ICellPainter result = null;

        String lowerKey = key.toLowerCase();
        CellPainterCreator creator = this.contentPainter.get(lowerKey);
        if (creator != null) {
            result = creator.createCellPainter(painterProperties, null);
        }

        return result;
    }

    /**
     * Initialize the given {@link AbstractTextPainter} with the values in the
     * given properties map.
     *
     * @param painter
     *            The {@link AbstractTextPainter} to initialize.
     * @param painterProperties
     *            The painter properties to apply.
     */
    public void initTextPainter(AbstractTextPainter painter, Map<String, Object> painterProperties) {
        boolean wrapWord = false;
        if (painterProperties.containsKey(NatTableCSSConstants.WORD_WRAP)) {
            wrapWord = (Boolean) painterProperties.get(NatTableCSSConstants.WORD_WRAP);
        }
        painter.setWordWrapping(wrapWord);

        boolean wrapText = false;
        if (painterProperties.containsKey(NatTableCSSConstants.TEXT_WRAP)) {
            wrapText = (Boolean) painterProperties.get(NatTableCSSConstants.TEXT_WRAP);
        }
        painter.setWrapText(wrapText);

        boolean trimText = true;
        if (painterProperties.containsKey(NatTableCSSConstants.TEXT_TRIM)) {
            trimText = (Boolean) painterProperties.get(NatTableCSSConstants.TEXT_TRIM);
        }
        painter.setTrimText(trimText);

        String textDirection = (String) painterProperties.get(NatTableCSSConstants.TEXT_DIRECTION);
        boolean vertical = false;
        if ("vertical".equalsIgnoreCase(textDirection)) {
            vertical = true;
        }

        int lineSpacing = 0;
        if (painterProperties.containsKey(NatTableCSSConstants.LINE_SPACING)) {
            lineSpacing = (Integer) painterProperties.get(NatTableCSSConstants.LINE_SPACING);
        }
        painter.setLineSpacing(lineSpacing);

        boolean calculateHeight = false;
        if (painterProperties.containsKey(NatTableCSSConstants.CALCULATE_CELL_HEIGHT)) {
            calculateHeight = (Boolean) painterProperties.get(NatTableCSSConstants.CALCULATE_CELL_HEIGHT);
        }

        boolean calculateWidth = false;
        if (painterProperties.containsKey(NatTableCSSConstants.CALCULATE_CELL_WIDTH)) {
            calculateWidth = (Boolean) painterProperties.get(NatTableCSSConstants.CALCULATE_CELL_WIDTH);
        }

        if (!vertical) {
            painter.setCalculateByTextHeight(calculateHeight);
            painter.setCalculateByTextLength(calculateWidth);
        } else {
            painter.setCalculateByTextLength(calculateHeight);
            painter.setCalculateByTextHeight(calculateWidth);
        }
    }

    /**
     * Check the given key if it represents a background painter.
     *
     * @param key
     *            The key to check.
     * @return <code>true</code> if the given key represents a background
     *         painter, <code>false</code> if not.
     */
    public boolean isBackgroundPainterKey(String key) {
        return key != null && this.backgroundPainter.containsKey(key.toLowerCase());
    }

    /**
     * Check the given key if it represents a decorator painter.
     *
     * @param key
     *            The key to check.
     * @return <code>true</code> if the given key represents a decorator
     *         painter, <code>false</code> if not.
     */
    public boolean isDecoratorPainterKey(String key) {
        return key != null && this.decoratorPainter.containsKey(key.toLowerCase());
    }

    /**
     * Check the given key if it represents a content painter.
     *
     * @param key
     *            The key to check.
     * @return <code>true</code> if the given key represents a content painter,
     *         <code>false</code> if not.
     */
    public boolean isContentPainterKey(String key) {
        return key != null && this.contentPainter.containsKey(key.toLowerCase());
    }

    /**
     * Register a {@link CellPainterWrapperCreator} to create a background
     * painter for a given key. This way custom painters can be registered with
     * the NatTable CSS mechanism.
     *
     * @param key
     *            The key for which the background painter should be created.
     * @param creator
     *            The {@link CellPainterWrapperCreator} that should be
     *            registered for the given key.
     */
    public void registerBackgroundPainter(String key, CellPainterWrapperCreator creator) {
        this.backgroundPainter.put(key, creator);
    }

    /**
     * Registers a {@link CellPainterWrapperCreator} to create a decorator
     * painter for a given key. This way custom painters can be registered with
     * the NatTable CSS mechanism.
     *
     * @param key
     *            The key for which the decorator painter should be created.
     * @param creator
     *            The {@link CellPainterWrapperCreator} that should be
     *            registered for the given key.
     */
    public void registerDecoratorPainter(String key, CellPainterWrapperCreator creator) {
        this.decoratorPainter.put(key, creator);
    }

    /**
     * Registers a {@link CellPainterCreator} to create a content painter for a
     * given key. This way custom painters can be registered with the NatTable
     * CSS mechanism.
     *
     * @param key
     *            The key for which the content painter should be created.
     * @param creator
     *            The {@link CellPainterCreator} that should be registered for
     *            the given key.
     */
    public void registerContentPainter(String key, CellPainterCreator creator) {
        this.contentPainter.put(key, creator);
    }
}
