/*
 * Copyright (C) 2010 The Android Open Source Project
 *
 * Licensed under the Eclipse Public License, Version 1.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      http://www.eclipse.org/org/documents/epl-v10.php
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package com.android.ide.eclipse.adt.internal.editors.layout.gle2;

import com.android.ide.common.api.DrawingStyle;
import com.android.ide.common.api.IColor;
import com.android.ide.common.api.IGraphics;
import com.android.ide.common.api.IViewRule;
import com.android.ide.common.api.Point;
import com.android.ide.common.api.Rect;

import org.eclipse.swt.SWT;
import org.eclipse.swt.SWTException;
import org.eclipse.swt.graphics.Color;
import org.eclipse.swt.graphics.FontMetrics;
import org.eclipse.swt.graphics.GC;
import org.eclipse.swt.graphics.RGB;

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

/**
 * Wraps an SWT {@link GC} into an {@link IGraphics} interface so that {@link IViewRule} objects
 * can directly draw on the canvas.
 * <p/>
 * The actual wrapped GC object is only non-null during the context of a paint operation.
 */
public class GCWrapper implements IGraphics {

    /**
     * The actual SWT {@link GC} being wrapped. This can change during the lifetime of the
     * object. It is generally set to something during an onPaint method and then changed
     * to null when not in the context of a paint.
     */
    private GC mGc;

    /**
     * Current style being used for drawing.
     */
    private SwtDrawingStyle mCurrentStyle = SwtDrawingStyle.INVALID;

    /**
     * Implementation of IColor wrapping an SWT color.
     */
    private static class ColorWrapper implements IColor {
        private final Color mColor;

        public ColorWrapper(Color color) {
            mColor = color;
        }

        public Color getColor() {
            return mColor;
        }
    }

    /** A map of registered colors. All these colors must be disposed at the end. */
    private final HashMap<Integer, ColorWrapper> mColorMap = new HashMap<Integer, ColorWrapper>();

    /**
     * A map of the {@link SwtDrawingStyle} stroke colors that we have actually
     * used (to be disposed)
     */
    private final Map<DrawingStyle, Color> mStyleStrokeMap = new EnumMap<DrawingStyle, Color>(
            DrawingStyle.class);

    /**
     * A map of the {@link SwtDrawingStyle} fill colors that we have actually
     * used (to be disposed)
     */
    private final Map<DrawingStyle, Color> mStyleFillMap = new EnumMap<DrawingStyle, Color>(
            DrawingStyle.class);

    /** The cached pixel height of the default current font. */
    private int mFontHeight = 0;

    /** The scaling of the canvas in X. */
    private final CanvasTransform mHScale;
    /** The scaling of the canvas in Y. */
    private final CanvasTransform mVScale;

    public GCWrapper(CanvasTransform hScale, CanvasTransform vScale) {
        mHScale = hScale;
        mVScale = vScale;
        mGc = null;
    }

    void setGC(GC gc) {
        mGc = gc;
    }

    private GC getGc() {
        return mGc;
    }

    void checkGC() {
        if (mGc == null) {
            throw new RuntimeException("IGraphics used without a valid context.");
        }
    }

    void dispose() {
        for (ColorWrapper c : mColorMap.values()) {
            c.getColor().dispose();
        }
        mColorMap.clear();

        for (Color c : mStyleStrokeMap.values()) {
            c.dispose();
        }
        mStyleStrokeMap.clear();

        for (Color c : mStyleFillMap.values()) {
            c.dispose();
        }
        mStyleFillMap.clear();
    }

    //-------------

    public IColor registerColor(int rgb) {
        checkGC();

        Integer key = Integer.valueOf(rgb);
        ColorWrapper c = mColorMap.get(key);
        if (c == null) {
            c = new ColorWrapper(new Color(getGc().getDevice(),
                    (rgb >> 16) & 0xFF,
                    (rgb >>  8) & 0xFF,
                    (rgb >>  0) & 0xFF));
            mColorMap.put(key, c);
        }

        return c;
    }

    /** Returns the (cached) pixel height of the current font. */
    public int getFontHeight() {
        if (mFontHeight < 1) {
            checkGC();
            FontMetrics fm = getGc().getFontMetrics();
            mFontHeight = fm.getHeight();
        }
        return mFontHeight;
    }

    public IColor getForeground() {
        Color c = getGc().getForeground();
        return new ColorWrapper(c);
    }

    public IColor getBackground() {
        Color c = getGc().getBackground();
        return new ColorWrapper(c);
    }

    public int getAlpha() {
        return getGc().getAlpha();
    }

    public void setForeground(IColor color) {
        checkGC();
        getGc().setForeground(((ColorWrapper) color).getColor());
    }

    public void setBackground(IColor color) {
        checkGC();
        getGc().setBackground(((ColorWrapper) color).getColor());
    }

    public void setAlpha(int alpha) {
        checkGC();
        try {
            getGc().setAlpha(alpha);
        } catch (SWTException e) {
            // This means that we cannot set the alpha on this platform; this is
            // an acceptable no-op.
        }
    }

    public void setLineStyle(LineStyle style) {
        int swtStyle = 0;
        switch (style) {
        case LINE_SOLID:
            swtStyle = SWT.LINE_SOLID;
            break;
        case LINE_DASH:
            swtStyle = SWT.LINE_DASH;
            break;
        case LINE_DOT:
            swtStyle = SWT.LINE_DOT;
            break;
        case LINE_DASHDOT:
            swtStyle = SWT.LINE_DASHDOT;
            break;
        case LINE_DASHDOTDOT:
            swtStyle = SWT.LINE_DASHDOTDOT;
            break;
        default:
            assert false : style;
            break;
        }

        if (swtStyle != 0) {
            checkGC();
            getGc().setLineStyle(swtStyle);
        }
    }

    public void setLineWidth(int width) {
        checkGC();
        if (width > 0) {
            getGc().setLineWidth(width);
        }
    }

    // lines

    public void drawLine(int x1, int y1, int x2, int y2) {
        checkGC();
        useStrokeAlpha();
        x1 = mHScale.translate(x1);
        y1 = mVScale.translate(y1);
        x2 = mHScale.translate(x2);
        y2 = mVScale.translate(y2);
        getGc().drawLine(x1, y1, x2, y2);
    }

    public void drawLine(Point p1, Point p2) {
        drawLine(p1.x, p1.y, p2.x, p2.y);
    }

    // rectangles

    public void drawRect(int x1, int y1, int x2, int y2) {
        checkGC();
        useStrokeAlpha();
        int x = mHScale.translate(x1);
        int y = mVScale.translate(y1);
        int w = mHScale.scale(x2 - x1);
        int h = mVScale.scale(y2 - y1);
        getGc().drawRectangle(x, y, w, h);
    }

    public void drawRect(Point p1, Point p2) {
        drawRect(p1.x, p1.y, p2.x, p2.y);
    }

    public void drawRect(Rect r) {
        checkGC();
        useStrokeAlpha();
        int x = mHScale.translate(r.x);
        int y = mVScale.translate(r.y);
        int w = mHScale.scale(r.w);
        int h = mVScale.scale(r.h);
        getGc().drawRectangle(x, y, w, h);
    }

    public void fillRect(int x1, int y1, int x2, int y2) {
        checkGC();
        useFillAlpha();
        int x = mHScale.translate(x1);
        int y = mVScale.translate(y1);
        int w = mHScale.scale(x2 - x1);
        int h = mVScale.scale(y2 - y1);
        getGc().fillRectangle(x, y, w, h);
    }

    public void fillRect(Point p1, Point p2) {
        fillRect(p1.x, p1.y, p2.x, p2.y);
    }

    public void fillRect(Rect r) {
        checkGC();
        useFillAlpha();
        int x = mHScale.translate(r.x);
        int y = mVScale.translate(r.y);
        int w = mHScale.scale(r.w);
        int h = mVScale.scale(r.h);
        getGc().fillRectangle(x, y, w, h);
    }

    // circles (actually ovals)

    public void drawOval(int x1, int y1, int x2, int y2) {
        checkGC();
        useStrokeAlpha();
        int x = mHScale.translate(x1);
        int y = mVScale.translate(y1);
        int w = mHScale.scale(x2 - x1);
        int h = mVScale.scale(y2 - y1);
        getGc().drawOval(x, y, w, h);
    }

    public void drawOval(Point p1, Point p2) {
        drawOval(p1.x, p1.y, p2.x, p2.y);
    }

    public void drawOval(Rect r) {
        checkGC();
        useStrokeAlpha();
        int x = mHScale.translate(r.x);
        int y = mVScale.translate(r.y);
        int w = mHScale.scale(r.w);
        int h = mVScale.scale(r.h);
        getGc().drawOval(x, y, w, h);
    }

    public void fillOval(int x1, int y1, int x2, int y2) {
        checkGC();
        useFillAlpha();
        int x = mHScale.translate(x1);
        int y = mVScale.translate(y1);
        int w = mHScale.scale(x2 - x1);
        int h = mVScale.scale(y2 - y1);
        getGc().fillOval(x, y, w, h);
    }

    public void fillOval(Point p1, Point p2) {
        fillOval(p1.x, p1.y, p2.x, p2.y);
    }

    public void fillOval(Rect r) {
        checkGC();
        useFillAlpha();
        int x = mHScale.translate(r.x);
        int y = mVScale.translate(r.y);
        int w = mHScale.scale(r.w);
        int h = mVScale.scale(r.h);
        getGc().fillOval(x, y, w, h);
    }


    // strings

    public void drawString(String string, int x, int y) {
        checkGC();
        useStrokeAlpha();
        x = mHScale.translate(x);
        y = mVScale.translate(y);
        // Background fill of text is not useful because it does not
        // use the alpha; we instead supply a separate method (drawBoxedStrings) which
        // first paints a semi-transparent mask for the text to sit on
        // top of (this ensures that the text is readable regardless of
        // colors of the pixels below the text)
        getGc().drawString(string, x, y, true /*isTransparent*/);
    }

    public void drawBoxedStrings(int x, int y, List<?> strings) {
        checkGC();

        x = mHScale.translate(x);
        y = mVScale.translate(y);

        // Compute bounds of the box by adding up the sum of the text heights
        // and the max of the text widths
        int width = 0;
        int height = 0;
        int lineHeight = getGc().getFontMetrics().getHeight();
        for (Object s : strings) {
            org.eclipse.swt.graphics.Point extent = getGc().stringExtent(s.toString());
            height += extent.y;
            width = Math.max(width, extent.x);
        }

        // Paint a box below the text
        int padding = 2;
        useFillAlpha();
        getGc().fillRectangle(x - padding, y - padding, width + 2 * padding, height + 2 * padding);

        // Finally draw strings on top
        useStrokeAlpha();
        int lineY = y;
        for (Object s : strings) {
            getGc().drawString(s.toString(), x, lineY, true /* isTransparent */);
            lineY += lineHeight;
        }
    }

    public void drawString(String string, Point topLeft) {
        drawString(string, topLeft.x, topLeft.y);
    }

    // Styles

    public void useStyle(DrawingStyle style) {
        checkGC();

        // Look up the specific SWT style which defines the actual
        // colors and attributes to be used for the logical drawing style.
        SwtDrawingStyle swtStyle = SwtDrawingStyle.of(style);
        RGB stroke = swtStyle.getStrokeColor();
        if (stroke != null) {
            Color color = getStrokeColor(style, stroke);
            mGc.setForeground(color);
        }
        RGB fill = swtStyle.getFillColor();
        if (fill != null) {
            Color color = getFillColor(style, fill);
            mGc.setBackground(color);
        }
        mGc.setLineWidth(swtStyle.getLineWidth());
        mGc.setLineStyle(swtStyle.getLineStyle());
        if (swtStyle.getLineStyle() == SWT.LINE_CUSTOM) {
            mGc.setLineDash(new int[] {
                    8, 4
            });
        }
        mCurrentStyle = swtStyle;
    }

    /** Uses the stroke alpha for subsequent drawing operations. */
    private void useStrokeAlpha() {
        mGc.setAlpha(mCurrentStyle.getStrokeAlpha());
    }

    /** Uses the fill alpha for subsequent drawing operations. */
    private void useFillAlpha() {
        mGc.setAlpha(mCurrentStyle.getFillAlpha());
    }

    /**
     * Get the SWT stroke color (foreground/border) to use for the given style,
     * using the provided color description if we haven't seen this color yet.
     * The color will also be placed in the {@link #mStyleStrokeMap} such that
     * it can be disposed of at cleanup time.
     *
     * @param style The drawing style for which we want a color
     * @param defaultColorDesc The RGB values to initialize the color to if we
     *            haven't seen this color before
     * @return The color object
     */
    private Color getStrokeColor(DrawingStyle style, RGB defaultColorDesc) {
        return getStyleColor(style, defaultColorDesc, mStyleStrokeMap);
    }

    /**
     * Get the SWT fill (background/interior) color to use for the given style,
     * using the provided color description if we haven't seen this color yet.
     * The color will also be placed in the {@link #mStyleStrokeMap} such that
     * it can be disposed of at cleanup time.
     *
     * @param style The drawing style for which we want a color
     * @param defaultColorDesc The RGB values to initialize the color to if we
     *            haven't seen this color before
     * @return The color object
     */
    private Color getFillColor(DrawingStyle style, RGB defaultColorDesc) {
        return getStyleColor(style, defaultColorDesc, mStyleFillMap);
    }

    /**
     * Get the SWT color to use for the given style, using the provided color
     * description if we haven't seen this color yet. The color will also be
     * placed in the map referenced by the map parameter such that it can be
     * disposed of at cleanup time.
     *
     * @param style The drawing style for which we want a color
     * @param defaultColorDesc The RGB values to initialize the color to if we
     *            haven't seen this color before
     * @param map The color map to use
     * @return The color object
     */
    private Color getStyleColor(DrawingStyle style, RGB defaultColorDesc,
            Map<DrawingStyle, Color> map) {
        Color color = map.get(style);
        if (color == null) {
            color = new Color(getGc().getDevice(), defaultColorDesc);
            map.put(style, color);
        }

        return color;
    }

    // dots

    public void drawPoint(int x, int y) {
        checkGC();
        useStrokeAlpha();
        x = mHScale.translate(x);
        y = mVScale.translate(y);

        getGc().drawPoint(x, y);
    }

    // arrows

    private static final int MIN_LENGTH = 10;


    public void drawArrow(int x1, int y1, int x2, int y2, int size) {
        int arrowWidth = size;
        int arrowHeight = size;

        checkGC();
        useStrokeAlpha();
        x1 = mHScale.translate(x1);
        y1 = mVScale.translate(y1);
        x2 = mHScale.translate(x2);
        y2 = mVScale.translate(y2);
        GC graphics = getGc();

        // Make size adjustments to ensure that the arrow has enough width to be visible
        if (x1 == x2 && Math.abs(y1 - y2) < MIN_LENGTH) {
            int delta = (MIN_LENGTH - Math.abs(y1 - y2)) / 2;
            if (y1 < y2) {
                y1 -= delta;
                y2 += delta;
            } else {
                y1 += delta;
                y2-= delta;
            }

        } else if (y1 == y2 && Math.abs(x1 - x2) < MIN_LENGTH) {
            int delta = (MIN_LENGTH - Math.abs(x1 - x2)) / 2;
            if (x1 < x2) {
                x1 -= delta;
                x2 += delta;
            } else {
                x1 += delta;
                x2-= delta;
            }
        }

        graphics.drawLine(x1, y1, x2, y2);

        // Arrowhead:

        if (x1 == x2) {
            // Vertical
            if (y2 > y1) {
                graphics.drawLine(x2 - arrowWidth, y2 - arrowHeight, x2, y2);
                graphics.drawLine(x2 + arrowWidth, y2 - arrowHeight, x2, y2);
            } else {
                graphics.drawLine(x2 - arrowWidth, y2 + arrowHeight, x2, y2);
                graphics.drawLine(x2 + arrowWidth, y2 + arrowHeight, x2, y2);
            }
        } else if (y1 == y2) {
            // Horizontal
            if (x2 > x1) {
                graphics.drawLine(x2 - arrowHeight, y2 - arrowWidth, x2, y2);
                graphics.drawLine(x2 - arrowHeight, y2 + arrowWidth, x2, y2);
            } else {
                graphics.drawLine(x2 + arrowHeight, y2 - arrowWidth, x2, y2);
                graphics.drawLine(x2 + arrowHeight, y2 + arrowWidth, x2, y2);
            }
        } else {
            // Compute angle:
            int dy = y2 - y1;
            int dx = x2 - x1;
            double angle = Math.atan2(dy, dx);
            double lineLength = Math.sqrt(dy * dy + dx * dx);

            // Imagine a line of the same length as the arrow, but with angle 0.
            // Its two arrow lines are at (-arrowWidth, -arrowHeight) relative
            // to the endpoint (x1 + lineLength, y1) stretching up to (x2,y2).
            // We compute the positions of (ax,ay) for the point above and
            // below this line and paint the lines to it:
            double ax = x1 + lineLength - arrowHeight;
            double ay = y1 - arrowWidth;
            int rx = (int) (Math.cos(angle) * (ax-x1) - Math.sin(angle) * (ay-y1) + x1);
            int ry = (int) (Math.sin(angle) * (ax-x1) + Math.cos(angle) * (ay-y1) + y1);
            graphics.drawLine(x2, y2, rx, ry);

            ay = y1 + arrowWidth;
            rx = (int) (Math.cos(angle) * (ax-x1) - Math.sin(angle) * (ay-y1) + x1);
            ry = (int) (Math.sin(angle) * (ax-x1) + Math.cos(angle) * (ay-y1) + y1);
            graphics.drawLine(x2, y2, rx, ry);
        }

        /* TODO: Experiment with filled arrow heads?
        if (x1 == x2) {
            // Vertical
            if (y2 > y1) {
                for (int i = 0; i < arrowWidth; i++) {
                    graphics.drawLine(x2 - arrowWidth + i, y2 - arrowWidth + i,
                            x2 + arrowWidth - i, y2 - arrowWidth + i);
                }
            } else {
                for (int i = 0; i < arrowWidth; i++) {
                    graphics.drawLine(x2 - arrowWidth + i, y2 + arrowWidth - i,
                            x2 + arrowWidth - i, y2 + arrowWidth - i);
                }
            }
        } else if (y1 == y2) {
            // Horizontal
            if (x2 > x1) {
                for (int i = 0; i < arrowHeight; i++) {
                    graphics.drawLine(x2 - arrowHeight + i, y2 - arrowHeight + i, x2
                            - arrowHeight + i, y2 + arrowHeight - i);
                }
            } else {
                for (int i = 0; i < arrowHeight; i++) {
                    graphics.drawLine(x2 + arrowHeight - i, y2 - arrowHeight + i, x2
                            + arrowHeight - i, y2 + arrowHeight - i);
                }
            }
        } else {
            // Arbitrary angle -- need to use trig
            // TODO: Implement this
        }
        */
    }
}
