/*******************************************************************************
 * Copyright (c) 2020 Ericsson
 *
 * All rights reserved. This program and the accompanying materials are
 * made available under the terms of the Eclipse Public License 2.0 which
 * accompanies this distribution, and is available at
 * https://www.eclipse.org/legal/epl-2.0/
 *
 * SPDX-License-Identifier: EPL-2.0
 *******************************************************************************/

package org.eclipse.tracecompass.internal.tmf.ui.widgets.timegraph;

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

import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable;
import org.eclipse.jface.resource.ColorRegistry;
import org.eclipse.swt.graphics.Color;
import org.eclipse.swt.graphics.Font;
import org.eclipse.swt.graphics.FontData;
import org.eclipse.swt.graphics.GC;
import org.eclipse.swt.graphics.Rectangle;
import org.eclipse.tracecompass.tmf.core.presentation.RGBAColor;
import org.eclipse.tracecompass.tmf.ui.colors.RGBAUtil;
import org.eclipse.tracecompass.tmf.ui.widgets.timegraph.ITimeGraphPresentationProvider;
import org.eclipse.tracecompass.tmf.ui.widgets.timegraph.model.ITimeEvent;
import org.eclipse.tracecompass.tmf.ui.widgets.timegraph.model.ITimeGraphEntry;
import org.eclipse.tracecompass.tmf.ui.widgets.timegraph.widgets.Utils;

/**
 * Deferred draw util, can be used to store draws in a list so that they can be
 * displayed at a later time.
 *
 * @author Matthew Khouzam
 */
@NonNullByDefault
public final class TimeGraphRender {

    private interface IDrawable {
        /**
         * Draw the element
         *
         * @param provider
         *            the presentation provider for post-drawing
         * @param gc
         *            the Graphics context
         */
        public void draw(@Nullable ITimeGraphPresentationProvider provider, GC gc);
    }

    /**
     * Deferred entry, basically a row to draw
     */
    public static class DeferredEntry implements IDrawable {
        private Rectangle fBounds;
        private ITimeGraphEntry fEntry;
        private final List<DeferredItem> fItems = new ArrayList<>();

        /**
         * Constructor
         *
         * @param entry
         *            the entry to draw
         * @param bounds
         *            the bounds of the entry
         */
        public DeferredEntry(ITimeGraphEntry entry, Rectangle bounds) {
            fEntry = entry;
            fBounds = bounds;
        }

        @Override
        public void draw(@Nullable ITimeGraphPresentationProvider provider, GC gc) {

            /* Set the font for this item */
            int height = fBounds.height;
            setFontForHeight(height - getMarginForHeight(height), gc);

            for (DeferredItem item : getItems()) {
                item.draw(provider, gc);
            }
            if (provider != null) {
                provider.postDrawEntry(fEntry, fBounds, gc);
            }
        }

        /**
         * gets the items list
         *
         * @return the items
         */
        public List<DeferredItem> getItems() {
            return fItems;
        }
    }

    /**
     * The deferredItem's responsibility is to set and reset the graphics
     * context so that derived classes can draw in it.
     */
    public abstract static class DeferredItem implements IDrawable {
        /**
         * Style with no border
         */
        public static final int NO_BORDER = Integer.MIN_VALUE;
        private final Collection<PostDrawEvent> fPDEs = new ArrayList<>();
        private final Rectangle fBounds;
        private RGBAColor fBgColor;
        private final RGBAColor fBorderColor;
        private final int fLineWidth;

        /**
         * Copy constructor
         *
         * @param other
         *            other {@link DeferredItem} to copy
         */
        public DeferredItem(DeferredItem other) {
            Rectangle bounds = other.fBounds;
            fBounds = new Rectangle(bounds.x, bounds.y, bounds.width, bounds.height);
            fBgColor = other.getBackgroundColor();
            fPDEs.addAll(other.getPostDrawEvents());
            fBorderColor = other.fBorderColor;
            fLineWidth = other.fLineWidth;

        }

        /**
         * Constructor
         *
         * @param bounds
         *            bounding box of item
         * @param backgroundColor
         *            the background color
         * @param borderColor
         *            the border color
         * @param lineWidth
         *            border width, NO_BORDER if it should not be drawn
         */
        public DeferredItem(Rectangle bounds, RGBAColor backgroundColor, RGBAColor borderColor, int lineWidth) {
            fBounds = bounds;
            fLineWidth = lineWidth;
            fBgColor = backgroundColor;
            fBorderColor = borderColor;
        }

        /**
         * Add a post draw event
         *
         * @param pde
         *            post draw event
         */
        public void add(PostDrawEvent pde) {
            fPDEs.add(pde);
        }

        @Override
        public final void draw(@Nullable ITimeGraphPresentationProvider provider, GC gc) {
            Rectangle bounds = getBounds();
            // basic sanity
            if (bounds.width <= 0 || bounds.height <= 0) {
                return;
            }
            int prevAlpha = gc.getAlpha();
            Color prevBgColor = gc.getBackground();
            Color prevFgColor = gc.getForeground();
            int prevLineWidth = gc.getLineWidth();
            setContext(gc);
            innerDraw(gc);
            if (fLineWidth != NO_BORDER) {
                setContext(gc);
                gc.setAlpha(fBorderColor.getAlpha());
                drawBorder(gc);
            }
            setContext(gc);
            drawLabel(gc);
            if (provider != null) {
                gc.setLineWidth(prevLineWidth);
                gc.setBackground(prevBgColor);
                gc.setForeground(prevFgColor);
                gc.setAlpha(prevAlpha);
                postDraw(provider, gc);
            }
            // reset context
            gc.setLineWidth(prevLineWidth);
            gc.setBackground(prevBgColor);
            gc.setForeground(prevFgColor);
            gc.setAlpha(prevAlpha);
        }

        /**
         * This function has the context set for it. The most important being
         * ForegroundColor
         *
         * @param gc
         *            the graphics context
         */
        protected void drawBorder(GC gc) {
            gc.drawRectangle(getBounds());
        }

        /**
         * Draw the label if applicable
         *
         * @param gc
         *            the graphics context
         */
        protected void drawLabel(GC gc) {
            // do nothing
        }

        /**
         * Gets the background color
         *
         * @return the background color
         */
        public RGBAColor getBackgroundColor() {
            return fBgColor;
        }

        /**
         * Gets the border color
         *
         * @return the border color
         */
        public @Nullable RGBAColor getBorderColor() {
            return fBorderColor;
        }

        /**
         * Gets the bounds (bounding box)
         *
         * @return the bounds
         */
        public Rectangle getBounds() {
            return fBounds;
        }

        /**
         * Gets the post draw events
         *
         * @return the post draw events
         */
        private Collection<PostDrawEvent> getPostDrawEvents() {
            return fPDEs;
        }

        /**
         * Draw where the line width, background and foreground color and alpha
         * is already set and reset.
         *
         * @param gc
         *            the Graphics context
         */
        protected abstract void innerDraw(GC gc);

        private final void postDraw(ITimeGraphPresentationProvider provider, GC gc) {
            for (PostDrawEvent pde : fPDEs) {
                pde.draw(provider, gc);
            }
        }

        /**
         * Sets the background color, useful for merging states
         *
         * @param color
         *            the background color
         */
        protected void setBackgroundColor(RGBAColor color) {
            fBgColor = color;
        }

        /**
         * Sets the graphical context.
         * <ul>
         * <li>background color</li>
         * <li>alpha</li>
         * <li>foreground color</li>
         * <li>line width</li>
         * </ul>
         *
         * @param gc
         *            the graphics context
         */
        private void setContext(GC gc) {
            RGBAColor backgroundRgba = getBackgroundColor();
            Color bgColor = getColor(backgroundRgba.toInt());
            gc.setAlpha(backgroundRgba.getAlpha());
            gc.setBackground(bgColor);
            RGBAColor foregroundRgba = getBorderColor();
            if (foregroundRgba == null) {
                foregroundRgba = BLACK;
            }
            Color fgColor = getColor(foregroundRgba.toInt());
            gc.setForeground(fgColor);
            if (fLineWidth >= 0) {
                gc.setLineWidth(fLineWidth);
            }
        }
    }

    /**
     * Deferred poly line drawing
     */
    public static class DeferredLine implements IDrawable {
        private Rectangle fBounds;
        private long fMin;
        private List<List<LongPoint>> fSeriesPoints;
        private RGBAColor fColorRGBA;
        private double fScale;

        /**
         * Constructor
         *
         * @param bounds
         *            bounds of the line
         * @param min
         *            minimum value of the line
         * @param seriesPoints
         *            list of series (list of long points)
         * @param colorRGBA
         *            the color of the line
         * @param scale
         *            the scale of the line
         */
        public DeferredLine(Rectangle bounds, long min, List<List<LongPoint>> seriesPoints, RGBAColor colorRGBA, double scale) {
            fBounds = bounds;
            fMin = min;
            fSeriesPoints = seriesPoints;
            fColorRGBA = colorRGBA;
            fScale = scale;
        }

        @Override
        public void draw(@Nullable ITimeGraphPresentationProvider provider, GC gc) {
            RGBAColor rgba = fColorRGBA;
            int colorInt = rgba.toInt();
            Color color = getColor(colorInt);
            for (int i = 0; i < this.fSeriesPoints.size(); i++) {
                Color prev = gc.getForeground();
                int prevAlpha = gc.getAlpha();
                gc.setAlpha(rgba.getAlpha());
                gc.setForeground(color);
                List<LongPoint> series = fSeriesPoints.get(i);
                int[] points = new int[series.size() * 2];
                for (int point = 0; point < series.size(); point++) {
                    LongPoint longPoint = series.get(point);
                    points[point * 2] = longPoint.x;
                    points[point * 2 + 1] = fBounds.height - (int) ((longPoint.y - fMin) * fScale) + fBounds.y;
                }
                gc.drawPolyline(points);
                gc.setForeground(prev);
                gc.setAlpha(prevAlpha);
            }
        }
    }

    /**
     * Deferred Transparent State
     */
    public static class DeferredTransparentState extends DeferredItem {

        /**
         * Constructor
         *
         * @param bounds
         *            the bounds
         * @param bgColor
         *            the background color
         */
        public DeferredTransparentState(Rectangle bounds, RGBAColor bgColor) {
            super(bounds, bgColor, BLACK, DeferredItem.NO_BORDER);
        }

        @Override
        protected void innerDraw(GC gc) {
            Rectangle drawRect = getBounds();
            if (drawRect.width >= 2) {
                gc.fillRectangle(drawRect);
                if (drawRect.width > 2) {
                    // Draw the top and bottom borders
                    RGBAColor foregroundRGB = BLACK;
                    gc.setAlpha(foregroundRGB.getAlpha());
                    gc.setForeground(getColor(foregroundRGB.toInt()));
                    gc.drawLine(drawRect.x, drawRect.y, drawRect.x + drawRect.width - 1, drawRect.y);
                    gc.drawLine(drawRect.x, drawRect.y + drawRect.height - 1, drawRect.x + drawRect.width - 1, drawRect.y + drawRect.height - 1);
                }
            } else {
                gc.setForeground(gc.getBackground());
                gc.drawLine(drawRect.x, drawRect.y, drawRect.x, drawRect.y + drawRect.height - 1);
            }

        }
    }

    /**
     * Deferred segment, can be a collection of points or a single point. Starts
     * as one point then grows on the X axis
     */
    public static class DeferredSegment implements IDrawable {
        private final int fX;
        private final int fY;
        private int fLength;

        /**
         * Constructor
         *
         * @param x
         *            x coordinate
         * @param y
         *            y coordinate
         */
        public DeferredSegment(int x, int y) {
            fX = x;
            fY = y;
            fLength = 1;
        }

        /**
         * Does the segment contain another point?
         *
         * @param x
         *            the x coordinate
         * @param y
         *            the Y coordinate
         * @return true if the segment crosses that point, false otherwise
         */
        public boolean contains(int x, int y) {
            return (y == fY) && x >= fX && x <= fX + fLength;
        }

        @Override
        public void draw(@Nullable ITimeGraphPresentationProvider provider, GC gc) {
            if (fLength == 1) {
                gc.drawPoint(fX, fY);
            } else {
                gc.drawLine(fX, fY, fX + fLength - 1, fY);
            }
        }

        /**
         * Extend the point if it's at the end
         *
         * @param x
         *            the x coordinate to extend to
         */
        public void extend(int x) {
            if (x == fX + fLength) {
                fLength++;
            }
        }
    }

    /**
     * Deferred State class, the bigger states that can have labels and corners.
     * (in the world of graphics, these are LOD1 and tiny are LOD0)
     */
    public static class DeferredState extends DeferredItem {
        private final int fArc;
        private final @Nullable String fLabel;

        /**
         * Constructor
         *
         * @param bounds
         *            the bounds of the item
         * @param bgColor
         *            the background color
         * @param borderColor
         *            the border color
         * @param arc
         *            the radius of the arc
         * @param lineWidth
         *            the border width
         * @param label
         *            the label to display, can be {@code null}
         */
        public DeferredState(Rectangle bounds, RGBAColor bgColor, RGBAColor borderColor, int arc, int lineWidth, @Nullable String label) {
            super(bounds, bgColor, borderColor, lineWidth);
            fArc = arc;
            fLabel = label;
        }

        @Override
        protected void drawBorder(GC gc) {
            Rectangle bounds = getBounds();
            gc.drawRoundRectangle(bounds.x, bounds.y, bounds.width, bounds.height, fArc, fArc);
        }

        @Override
        protected void drawLabel(GC gc) {
            Rectangle bounds = getBounds();
            RGBAColor backgroundColor = getBackgroundColor();
            if (fLabel != null && !fLabel.isEmpty() && bounds.width > bounds.height) {
                gc.setForeground(Utils.getDistinctColor(RGBAUtil.fromRGBAColor(backgroundColor).rgb));
                Utils.drawText(gc, fLabel, bounds.x, bounds.y, bounds.width, bounds.height, true, true);
            }
        }

        @Override
        protected void innerDraw(GC gc) {
            Rectangle bounds = getBounds();
            gc.fillRoundRectangle(bounds.x, bounds.y, bounds.width, bounds.height, fArc, fArc);
        }
    }

    /**
     * States that are smaller than a pixel
     */
    public static class DeferredTinyState extends DeferredItem {

        /**
         * Blend 2 colors
         *
         * @param alpha
         *            the ratio
         * @param c0
         *            color channel 1, the base
         * @param c1
         *            color channel 2, the top layer
         * @return the blended color
         */
        private static int alphaBlend(int alpha, short c0, short c1) {
            return (int) (c0 * ((255 - alpha) / 255.0) + c1 * ((alpha) / 255.0));
        }

        /**
         * Can the items be merged? (note, right needs to have x >= left's x)
         *
         * @param left
         *            the left element
         * @param right
         *            the right element
         * @return true if the items can be merged
         */
        public static boolean areMergeable(DeferredItem left, DeferredItem right) {
            Rectangle rightBounds = right.getBounds();
            Rectangle largerBounds = new Rectangle(rightBounds.x - 1, rightBounds.y, rightBounds.width + 1, rightBounds.height);
            return Objects.equals(left.getBackgroundColor(), right.getBackgroundColor())
                    && Objects.equals(left.getBorderColor(), right.getBorderColor())
                    && largerBounds.intersects(left.getBounds())
                    && largerBounds.height == right.getBounds().height;
        }

        /**
         * Copy constructor
         *
         * @param other
         *            the other state
         */
        public DeferredTinyState(DeferredTinyState other) {
            super(other);
        }

        /**
         * Constructor
         *
         * @param bounds
         *            the bounds of the state
         * @param backgroundColor
         *            the background color
         * @param borderColor
         *            the border color
         * @param lineWidth
         *            the line width, could be NO_BORDER
         */
        public DeferredTinyState(Rectangle bounds, RGBAColor backgroundColor, RGBAColor borderColor, int lineWidth) {
            super(bounds, backgroundColor, borderColor, lineWidth);
        }

        /**
         * Extend a tiny state if another is overlapping
         *
         * @param other
         *            the other state to overlap
         * @return if the size was grown.
         */
        public boolean extend(DeferredTinyState other) {
            Rectangle bounds = getBounds();
            if (areMergeable(this, other)) {
                bounds.add(other.getBounds());
                return true;
            }
            return false;
        }

        @Override
        protected void innerDraw(GC gc) {
            gc.fillRectangle(getBounds());
        }

        /**
         * Squash two pixels, assume the height is the largest.
         *
         * @param other
         *            the state to squash.
         * @return true if the state is squashed
         */
        public boolean squash(DeferredTinyState other) {
            Rectangle bounds = getBounds();
            Rectangle otherBounds = other.getBounds();
            if (bounds.x != otherBounds.x || bounds.width != otherBounds.width) {
                return false;
            }
            // if the color or the height change here, areMergeable will no
            // longer hold true.
            RGBAColor prevColor = getBackgroundColor();
            RGBAColor newColor = other.getBackgroundColor();
            bounds.y = Math.min(bounds.y, otherBounds.y);
            bounds.height = Math.max(bounds.height, otherBounds.height);
            int alpha = newColor.getAlpha() / 2;
            setBackgroundColor(new RGBAColor(alphaBlend(alpha, prevColor.getRed(), newColor.getRed()),
                    alphaBlend(alpha, prevColor.getGreen(), newColor.getGreen()),
                    alphaBlend(alpha, prevColor.getBlue(), newColor.getBlue()),
                    alphaBlend(alpha, prevColor.getAlpha(), newColor.getAlpha())));
            return true;
        }
    }

    /**
     * Long point, a point with a Y value that's long rather than an int.
     */
    public static class LongPoint {
        final int x;
        final long y;

        /**
         * Constructor
         *
         * @param x
         *            x value
         * @param y
         *            y value
         */
        public LongPoint(int x, long y) {
            this.x = x;
            this.y = y;
        }

        @Override
        public boolean equals(@Nullable Object obj) {
            if (obj instanceof LongPoint) {
                LongPoint longPoint = (LongPoint) obj;
                return longPoint.x == x && longPoint.y == y;
            }
            return false;
        }

        @Override
        public int hashCode() {
            return Objects.hash(x, y);
        }
    }

    /**
     * Post draw event
     */
    public static class PostDrawEvent implements IDrawable {
        private Rectangle fBounds;
        private ITimeEvent fEvent;

        /**
         * Constructor
         *
         * @param event
         *            the event
         * @param bounds
         *            the bounds of the event
         */
        public PostDrawEvent(ITimeEvent event, Rectangle bounds) {
            fEvent = event;
            fBounds = bounds;
        }

        @Override
        public void draw(@Nullable ITimeGraphPresentationProvider provider, GC gc) {
            if (provider != null) {
                provider.postDrawEvent(fEvent, fBounds, gc);
            }
        }

    }

    private static final RGBAColor BLACK = new RGBAColor(0, 0, 0, 255);

    private static final ColorRegistry COLOR_REGISTRY = new ColorRegistry();
    private static final Map<Integer, Font> FONTS = new HashMap<>();

    /** Dots per inch */
    private static final int DPI = 96;

    /** points per inch */
    private static final int PPI = 72;

    /**
     * Get the color for a given color integer
     *
     * @param colorInt
     *            the color integer (0xRRGGBBAA)
     * @return the {@link Color}
     */
    public static Color getColor(int colorInt) {
        String hexRGB = Integer.toHexString((colorInt >> 8) & 0xffffff);
        Color color = COLOR_REGISTRY.get(hexRGB);
        if (color == null) {
            COLOR_REGISTRY.put(hexRGB, RGBAUtil.fromInt(colorInt).rgb);
            color = Objects.requireNonNull(COLOR_REGISTRY.get(hexRGB));
        }
        return color;
    }

    /**
     * Sets the font height for a given height
     *
     * @param pixels
     *            the height in pixels
     * @param gc
     *            the graphics context
     */
    public static void setFontForHeight(int pixels, GC gc) {
        /* convert font height from pixels to points */
        int height = Math.max(pixels * PPI / DPI, 1);
        Font font = FONTS.computeIfAbsent(height, fontHeight -> {
            FontData fontData = gc.getFont().getFontData()[0];
            fontData.setHeight(fontHeight);
            return new Font(gc.getDevice(), fontData);
        });
        gc.setFont(font);
    }

    /**
     * Get the margin for a height
     *
     * @param height
     *            the height
     * @return the margin
     */
    public static int getMarginForHeight(int height) {
        /*
         * State rectangle is smaller than the item bounds when height is > 4.
         * Don't use any margin if the height is below or equal that threshold.
         * Use a maximum of 6 pixels for both margins, otherwise try to use 13
         * pixels for the state height, but with a minimum margin of 1.
         */
        final int MARGIN_THRESHOLD = 4;
        final int PREFERRED_HEIGHT = 13;
        final int MIN_MARGIN = 1;
        final int MAX_MARGIN = 6;
        return height <= MARGIN_THRESHOLD ? 0 : Math.max(Math.min(height - PREFERRED_HEIGHT, MAX_MARGIN), MIN_MARGIN);
    }
}
