/*
 * Copyright 2010-2015 Institut Pasteur.
 * 
 * This file is part of Icy.
 * 
 * Icy is free software: you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation, either version 3 of the License, or
 * (at your option) any later version.
 * 
 * Icy is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
 * GNU General Public License for more details.
 * 
 * You should have received a copy of the GNU General Public License
 * along with Icy. If not, see <http://www.gnu.org/licenses/>.
 */
package icy.util;

import icy.image.ImageUtil;
import icy.util.ShapeUtil.ShapeConsumer;

import java.awt.AlphaComposite;
import java.awt.BasicStroke;
import java.awt.Color;
import java.awt.Component;
import java.awt.Composite;
import java.awt.Dimension;
import java.awt.Font;
import java.awt.FontMetrics;
import java.awt.GradientPaint;
import java.awt.Graphics;
import java.awt.Graphics2D;
import java.awt.Image;
import java.awt.Point;
import java.awt.Rectangle;
import java.awt.RenderingHints;
import java.awt.Shape;
import java.awt.geom.PathIterator;
import java.awt.geom.Rectangle2D;
import java.awt.geom.RoundRectangle2D;

/**
 * Graphics utilities class.
 * 
 * @author Stephane
 */
public class GraphicsUtil
{
    public static float getAlpha(Graphics2D g)
    {
        final Composite composite = g.getComposite();

        if (composite instanceof AlphaComposite)
            return ((AlphaComposite) composite).getAlpha();

        return 1f;
    }

    public static void mixAlpha(Graphics2D g, float factor)
    {
        mixAlpha(g, 0, factor);
    }

    public static float mixAlpha(Graphics2D g, int rule, float factor)
    {
        final Composite composite = g.getComposite();

        if (composite instanceof AlphaComposite)
        {
            final AlphaComposite alphaComposite = (AlphaComposite) composite;
            final float alpha = Math.min(1f, Math.max(0f, alphaComposite.getAlpha() * factor));

            if (rule == 0)
                g.setComposite(AlphaComposite.getInstance(alphaComposite.getRule(), alpha));
            else
                g.setComposite(AlphaComposite.getInstance(rule, alpha));

            return alpha;
        }

        g.setComposite(AlphaComposite.getInstance(AlphaComposite.SRC_OVER, factor));

        return factor;
    }

    /**
     * Draw ICY style background on the specified Graphics object with specified dimension.
     */
    public static void paintIcyBackGround(int width, int height, Graphics g)
    {
        final Graphics2D g2 = (Graphics2D) g.create();

        final float ray = Math.max(width, height) * 0.05f;
        final RoundRectangle2D roundRect = new RoundRectangle2D.Double(0, 0, width, height, Math.min(ray * 2, 20),
                Math.min(ray * 2, 20));

        g2.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON);

        g2.setPaint(new GradientPaint(0, 0, Color.white.darker(), 0, height / 1.5f, Color.black));
        g2.fill(roundRect);

        g2.setPaint(Color.black);
        g2.setColor(Color.black);
        mixAlpha(g2, AlphaComposite.SRC_OVER, 1f / 3f);
        // g2.setComposite(AlphaComposite.getInstance(AlphaComposite.SRC_OVER, 0.3f));
        g2.fillOval(-width + (width / 2), height / 2, width * 2, height);

        mixAlpha(g2, AlphaComposite.SRC_OVER, 3f / 1f);
        // g2.setComposite(AlphaComposite.getInstance(AlphaComposite.SRC_OVER, 1f));
        g2.setStroke(new BasicStroke(Math.max(1f, Math.min(5f, ray))));
        g2.draw(roundRect);

        g2.dispose();
    }

    /**
     * Draw ICY style background on the specified Graphics object with specified component
     * dimension.
     */
    public static void paintIcyBackGround(Component component, Graphics g)
    {
        paintIcyBackGround(component.getWidth(), component.getHeight(), g);
    }

    /**
     * Draw ICY style background in the specified Image
     */
    public static void paintIcyBackGround(Image image)
    {
        final Graphics g = image.getGraphics();
        // be sure image data are ready
        ImageUtil.waitImageReady(image);
        // draw background in image
        paintIcyBackGround(image.getWidth(null), image.getHeight(null), g);
        g.dispose();
    }

    /**
     * Returns true if the specified region is visible in the specified {@link Graphics} object.<br>
     * Internally use the {@link Graphics} clip area to determine if region is visible.
     */
    public static boolean isVisible(Graphics g, Rectangle region)
    {
        if ((g == null) || (region == null))
            return false;

        final Rectangle clipRegion = g.getClipBounds();

        // no clip region --> return true
        if (clipRegion == null)
            return true;

        if (region.width == 0)
        {
            // special case of single point region
            if (region.height == 0)
                return clipRegion.contains(region.x, region.y);
            else
                // special case of null width region
                return clipRegion.contains(region.x, region.getMinY())
                        || clipRegion.contains(region.x, region.getMaxY());

        }
        else if (region.height == 0)
            // special case of null height region
            return clipRegion.contains(region.getMinX(), region.y) || clipRegion.contains(region.getMaxX(), region.y);
        else
            return clipRegion.intersects(region);
    }

    /**
     * Returns true if the specified region is visible in the specified {@link Graphics} object.<br>
     * Internally use the {@link Graphics} clip area to determine if region is visible.
     */
    public static boolean isVisible(Graphics g, Rectangle2D region)
    {
        if ((g == null) || (region == null))
            return false;

        final Rectangle clipRegion = g.getClipBounds();

        // no clip region --> return true
        if (clipRegion == null)
            return true;

        if (region.getWidth() == 0d)
        {
            // special case of single point region
            if (region.getHeight() == 0d)
                return clipRegion.contains(region.getX(), region.getY());

            // special case of null width region
            return clipRegion.contains(region.getX(), region.getMinY())
                    || clipRegion.contains(region.getX(), region.getMaxY());
        }
        else if (region.getHeight() == 0d)
            // special case of null height region
            return clipRegion.contains(region.getMinX(), region.getY())
                    || clipRegion.contains(region.getMaxX(), region.getY());
        else
            return clipRegion.intersects(region);
    }

    /**
     * Returns bounds to draw specified string in the specified Graphics context
     * with specified font.<br>
     * This function handle multi lines string ('\n' character used a line separator).
     */
    public static Rectangle2D getStringBounds(Graphics g, Font f, String text)
    {
        Rectangle2D result = new Rectangle2D.Double();

        if (g != null)
        {
            final FontMetrics fm;

            if (f == null)
                fm = g.getFontMetrics();
            else
                fm = g.getFontMetrics(f);

            for (String s : text.split("\n"))
            {
                final Rectangle2D r = fm.getStringBounds(s, g);

                if (result.isEmpty())
                    result = r;
                else
                    result.setRect(result.getX(), result.getY(), Math.max(result.getWidth(), r.getWidth()),
                            result.getHeight() + r.getHeight());
            }
        }

        return result;
    }

    /**
     * Limit the single line string so it fits in the specified component width.
     */
    public static String limitStringFor(Component c, String text, int width)
    {
        if (width <= 0)
            return "";

        final int w = width - 20;

        if (w <= 0)
            return "..";

        String str = text;
        int textWidth = (int) getStringBounds(c, str).getWidth();
        boolean changed = false;

        while (textWidth > w)
        {
            str = str.substring(0, (str.length() * w) / textWidth);
            textWidth = (int) getStringBounds(c, str).getWidth();
            changed = true;
        }

        if (changed)
            return str.trim() + "...";

        return text;
    }

    /**
     * Return bounds to draw specified string in the specified component.<br>
     * This function handle multi lines string ('\n' character used a line separator).
     */
    public static Rectangle2D getStringBounds(Component c, String text)
    {
        return getStringBounds(c.getGraphics(), text);
    }

    /**
     * Return bounds to draw specified string in the specified Graphics context
     * with current font.<br>
     * This function handle multi lines string ('\n' character used a line separator).
     */
    public static Rectangle2D getStringBounds(Graphics g, String text)
    {
        return getStringBounds(g, null, text);
    }

    /**
     * Draw a text in the specified Graphics context and at the specified position.<br>
     * This function handles multi lines string ('\n' character used a line separator).
     */
    public static void drawString(Graphics g, String text, int x, int y, boolean shadow)
    {
        if (StringUtil.isEmpty(text))
            return;

        final Color color = g.getColor();
        final Color shadowColor;
        if (ColorUtil.getLuminance(color) > 128)
            shadowColor = ColorUtil.sub(color, Color.gray);
        else
            shadowColor = ColorUtil.add(color, Color.gray);
        final Rectangle2D textRect = getStringBounds(g, "M");

        // get height for a single line of text
        final double lineH = textRect.getHeight();
        final int curX = (int) (x - textRect.getX());
        double curY = y - textRect.getY();

        for (String s : text.split("\n"))
        {
            if (shadow)
            {
                g.setColor(shadowColor);
                g.drawString(s, curX + 1, (int) (curY + 1));
                g.setColor(color);
            }
            g.drawString(s, curX, (int) curY);
            curY += lineH;
        }
    }

    /**
     * Draw a horizontal centered text on specified position.
     * This function handle multi lines string ('\n' character used a line separator).
     */
    public static void drawHCenteredString(Graphics g, String text, int x, int y, boolean shadow)
    {
        if (StringUtil.isEmpty(text))
            return;

        final Color color = g.getColor();
        final Color shadowColor = ColorUtil.mix(color, Color.black);
        final Rectangle2D textRect = getStringBounds(g, "M");

        final double offX = textRect.getX();
        double curY = y - textRect.getY();

        for (String s : text.split("\n"))
        {
            final Rectangle2D r = getStringBounds(g, s);
            final int curX = (int) (x - (offX + (r.getWidth() / 2)));

            if (shadow)
            {
                g.setColor(shadowColor);
                g.drawString(s, curX + 1, (int) (curY + 1));
                g.setColor(color);
            }
            g.drawString(s, curX, (int) curY);
            curY += r.getHeight();
        }
    }

    /**
     * Draw a horizontal and vertical centered text on specified position.
     * This function handle multi lines string ('\n' character used a line separator).
     */
    public static void drawCenteredString(Graphics g, String text, int x, int y, boolean shadow)
    {
        if (StringUtil.isEmpty(text))
            return;

        final Color color = g.getColor();
        final Color shadowColor = ColorUtil.mix(color, Color.black);
        final Rectangle2D textRect = getStringBounds(g, text);

        final double offX = textRect.getX();
        double curY = y - (textRect.getY() + (textRect.getHeight() / 2));

        for (String s : text.split("\n"))
        {
            final Rectangle2D r = getStringBounds(g, s);
            final int curX = (int) (x - (offX + (r.getWidth() / 2)));

            if (shadow)
            {
                g.setColor(shadowColor);
                g.drawString(s, curX + 1, (int) (curY + 1));
                g.setColor(color);
            }
            g.drawString(s, curX, (int) curY);
            curY += r.getHeight();
        }
    }

    /**
     * Returns the size to draw a Hint type text in the specified Graphics context.
     */
    public static Dimension getHintSize(Graphics2D g, String text)
    {
        final Rectangle2D stringRect = getStringBounds(g, text);
        return new Dimension((int) Math.ceil(stringRect.getWidth() + 10d), (int) Math.ceil(stringRect.getHeight() + 8d));
    }

    /**
     * Returns the bounds to draw a Hint type text in the specified Graphics context<br>
     * at the specified location.
     */
    public static Rectangle getHintBounds(Graphics2D g, String text, int x, int y)
    {
        final Dimension dim = getHintSize(g, text);
        return new Rectangle(x, y, dim.width, dim.height);
    }

    /**
     * Draw multi line Hint type text in the specified Graphics context<br>
     * at the specified location.
     */
    public static void drawHint(Graphics2D g, String text, int x, int y, Color bgColor, Color textColor)
    {
        final Graphics2D g2 = (Graphics2D) g.create();

        final Rectangle2D stringRect = getStringBounds(g, text);
        // calculate hint rect
        final RoundRectangle2D backgroundRect = new RoundRectangle2D.Double(x, y, (int) (stringRect.getWidth() + 10),
                (int) (stringRect.getHeight() + 8), 8, 8);

        g2.setStroke(new BasicStroke(1.3f));
        // draw translucent background
        g2.setColor(bgColor);
        mixAlpha(g2, AlphaComposite.SRC_OVER, 1f / 2f);
        // g2.setComposite(AlphaComposite.getInstance(AlphaComposite.SRC_OVER, 0.5f));
        g2.fill(backgroundRect);
        // draw background border
        g2.setColor(ColorUtil.mix(bgColor, Color.black));
        mixAlpha(g2, AlphaComposite.SRC_OVER, 2f / 1f);
        // g2.setComposite(AlphaComposite.Src);
        g2.draw(backgroundRect);
        // draw text
        g2.setColor(textColor);
        drawString(g2, text, x + 5, y + 4, false);

        g2.dispose();
    }

    /**
     * Returns the bounds to draw specified box dimension at specified position.<br>
     * By default the draw process is done from specified position to right/bottom.
     * 
     * @param origin
     *        the initial desired position we want to draw the box.
     * @param dim
     *        box dimension
     * @param xSpace
     *        horizontal space between the position and box
     * @param ySpace
     *        vertical space between the position and box
     * @param top
     *        box is at top of position
     * @param left
     *        box is at left of position
     */
    public static Rectangle getBounds(Point origin, Dimension dim, int xSpace, int ySpace, boolean top, boolean left)
    {
        int x = origin.x;
        int y = origin.y;

        if (left)
            x -= dim.width + xSpace;
        else
            x += xSpace;
        if (top)
            y -= dim.height + ySpace;
        else
            y += ySpace;

        return new Rectangle(x, y, dim.width, dim.height);
    }

    /**
     * Returns the bounds to draw specified box dimension at specified position.
     * By default the draw process is done from specified position to right/bottom.
     * 
     * @param origin
     *        the initial desired position we want to draw the box.
     * @param dim
     *        box dimension
     * @param top
     *        box is at top of position
     * @param left
     *        box is at left of position
     */
    public static Rectangle getBounds(Point origin, Dimension dim, boolean top, boolean left)
    {
        return getBounds(origin, dim, 0, 0, top, left);
    }

    /**
     * Returns the best position to draw specified box in specified region with initial desired
     * position.
     * 
     * @param origin
     *        the initial desired position we want to draw the box.
     * @param dim
     *        box dimension
     * @param region
     *        the rectangle defining the region where we want to draw
     * @param xSpace
     *        horizontal space between the position and box
     * @param ySpace
     *        vertical space between the position and box
     * @param top
     *        by default box is at top of position
     * @param left
     *        by default box is at left of position
     */
    public static Point getBestPosition(Point origin, Dimension dim, Rectangle region, int xSpace, int ySpace,
            boolean top, boolean left)
    {
        final Rectangle bounds1 = getBounds(origin, dim, xSpace, ySpace, top, left);
        if (region.contains(bounds1))
            return new Point(bounds1.x, bounds1.y);

        final Rectangle bounds2 = getBounds(origin, dim, xSpace, ySpace, top, !left);
        if (region.contains(bounds2))
            return new Point(bounds2.x, bounds2.y);

        final Rectangle bounds3 = getBounds(origin, dim, xSpace, ySpace, !top, left);
        if (region.contains(bounds3))
            return new Point(bounds3.x, bounds3.y);

        final Rectangle bounds4 = getBounds(origin, dim, xSpace, ySpace, !top, !left);
        if (region.contains(bounds4))
            return new Point(bounds4.x, bounds4.y);

        Rectangle r;

        r = region.intersection(bounds1);
        final long size1 = r.width * r.height;
        r = region.intersection(bounds2);
        final long size2 = r.width * r.height;
        r = region.intersection(bounds3);
        final long size3 = r.width * r.height;
        r = region.intersection(bounds4);
        final long size4 = r.width * r.height;

        long maxSize = Math.max(size1, Math.max(size2, Math.max(size3, size4)));

        if (maxSize == size1)
            return new Point(bounds1.x, bounds1.y);
        if (maxSize == size2)
            return new Point(bounds2.x, bounds2.y);
        if (maxSize == size3)
            return new Point(bounds3.x, bounds3.y);

        return new Point(bounds4.x, bounds4.y);
    }

    /**
     * Returns the best position to draw specified box in specified region with initial desired
     * position.
     * 
     * @param origin
     *        the initial desired position we want to draw the box.
     * @param dim
     *        box dimension
     * @param region
     *        the rectangle defining the region where we want to draw
     * @param xSpace
     *        horizontal space between the position and box
     * @param ySpace
     *        vertical space between the position and box
     */
    public static Point getBestPosition(Point origin, Dimension dim, Rectangle region, int xSpace, int ySpace)
    {
        return getBestPosition(origin, dim, region, xSpace, ySpace, false, false);
    }

    /**
     * Returns the best position to draw specified box in specified region with initial desired
     * position.
     * 
     * @param origin
     *        the initial desired position we want to draw the box.
     * @param dim
     *        box dimension
     * @param region
     *        the rectangle defining the region where we want to draw
     */
    public static Point getBestPosition(Point origin, Dimension dim, Rectangle region)
    {
        return getBestPosition(origin, dim, region, 0, 0, false, false);
    }

    /**
     * Draw the specified PathIterator in the specified Graphics2D context
     */
    public static void drawPathIterator(PathIterator path, final Graphics2D g)
    {
        ShapeUtil.consumeShapeFromPath(path, new ShapeConsumer()
        {
            @Override
            public boolean consume(Shape shape)
            {
                // draw shape
                g.draw(shape);
                return true;
            }
        });
    }

}