/*
 * Copyright 2008-2014, David Karnok 
 * The file is part of the Open Imperium Galactica project.
 * 
 * The code should be distributed under the LGPL license.
 * See http://www.gnu.org/licenses/lgpl.html for details.
 */

package hu.openig.render;

import hu.openig.core.Pair;

import java.awt.AlphaComposite;
import java.awt.Color;
import java.awt.Composite;
import java.awt.Graphics2D;
import java.awt.Paint;
import java.awt.Point;
import java.awt.Rectangle;
import java.awt.RenderingHints;
import java.awt.Stroke;
import java.awt.TexturePaint;
import java.awt.image.BufferedImage;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Comparator;
import java.util.List;
import java.util.Random;

/**
 * Utility class for common rendering tasks and algorithms. 
 * @author akarnokd
 */
public final class RenderTools {
	/** Utility class. */
	private RenderTools() {
	}
	/** The top status bar height constant. */
	public static final int STATUS_BAR_TOP = 20;
	/** The bottom status bar height constant. */
	public static final int STATUS_BAR_BOTTOM = 18;
	/**
	 * Set the rectangle (X, Y) so that the rectangle is centered on the screen
	 * denoted by the screen width and height.
	 * The centering may consider the effect of the top and bottom status bar
	 * @param current the current rectangle
	 * @param screenWidth the screen width
	 * @param screenHeight the screen height
	 * @param considerStatusbars consider, that the status bars take 20px top and 18 pixels bottom?
	 * @return the updated current rectangle
	 */
	public static Rectangle centerScreen(Rectangle current, int screenWidth, int screenHeight,
			boolean considerStatusbars) {
		if (current == null) {
			throw new IllegalArgumentException("current is null");
		}
		if (current.width == 0) {
			throw new IllegalArgumentException("current.width is zero");
		}
		if (current.height == 0) {
			throw new IllegalArgumentException("current.height is zero");
		}
		current.x = (screenWidth - current.width) / 2;
		if (considerStatusbars) {
			current.y = STATUS_BAR_TOP + (screenHeight - STATUS_BAR_TOP - STATUS_BAR_BOTTOM - current.height) / 2;
		} else {
			current.y = (screenHeight - current.height) / 2;
		}
		
		return current;
	}
	/**
	 * Paint a semi-transparent area around the supplied panel.
	 * @param panel the panel to paint around
	 * @param screenWidth the target screen width
	 * @param screenHeight the target screen height
	 * @param g2 the target graphics object
	 * @param alpha the transparency level
	 * @param considerStatusbars consider, that the status bars take 20px top and 18 pixels bottom?
	 */
	public static void darkenAround(
			Rectangle panel, int screenWidth, int screenHeight, Graphics2D g2, float alpha,
			boolean considerStatusbars) {
		Composite c = null;
		Paint p = null;
		if (fast) {
			p = g2.getPaint();
			BufferedImage fimg = holes((float)Math.min(1.0, alpha * 2));
			g2.setPaint(new TexturePaint(fimg, new Rectangle(panel.x, panel.y, fimg.getWidth(), fimg.getHeight())));
		} else {
			c = g2.getComposite();
			g2.setComposite(AlphaComposite.SrcOver.derive(alpha));
			g2.setColor(Color.BLACK);
			
		}
		if (considerStatusbars) {
			fillRectAbsolute(0, STATUS_BAR_TOP, screenWidth - 1, panel.y - 1, g2);
			fillRectAbsolute(0, panel.y + panel.height, screenWidth - 1, screenHeight - 1 - STATUS_BAR_BOTTOM, g2);
		} else {
			fillRectAbsolute(0, 0, screenWidth - 1, panel.y - 1, g2);
			fillRectAbsolute(0, panel.y + panel.height, screenWidth - 1, screenHeight - 1, g2);
		}
		
		fillRectAbsolute(0, panel.y, panel.x - 1, panel.y + panel.height - 1, g2);
		fillRectAbsolute(panel.x + panel.width, panel.y, screenWidth - 1, panel.y + panel.height - 1, g2);
		if (fast) {
			g2.setPaint(p);
		} else {
			g2.setComposite(c);
		}
	}
	/** Use a faster drawing trick for alpha composition? */
	static boolean fast = false;
	/** The cache for the last alpha blending. */
	static float lastAlpha;
	/** The image to use for the alpha blending. */
	static BufferedImage lastHoles;
	/**
	 * Create a transparent image which has holes.
	 * @param alpha the effective transparency level
	 * @return the image
	 */
	static BufferedImage holes(float alpha) {
		if (alpha != lastAlpha) {
			int r = (int)(255 * (1 - alpha));
			int size = 16; // The pattern size
			int[] pixels = new int[size * size];
			for (int i = 0; i < size; i++) {
				for (int j = 0; j < size; j++) {
					if ((i % 2) == (j % 2)) {
						pixels[i * size + j] = argb(255, r, r, r);
					}
				}
			}
			BufferedImage img = new BufferedImage(size, size, BufferedImage.TYPE_INT_ARGB);
			img.setRGB(0, 0, size, size, pixels, 0, size);
			lastHoles = img;
			lastAlpha = alpha;
			return img;
		}
		return lastHoles;
	}
	/**
	 * Create an ARGB color from its components.
	 * @param a The alpha channel value
	 * @param r the RED component
	 * @param g the GREEN component
	 * @param b the BLUE component
	 * @return the composite color
	 */
	public static int argb(int a, int r, int g, int b) {
		return 
			((a << 24) & 0xFF000000)
			| ((r << 16) & 0x00FF0000)
			| ((g << 8) & 0x0000FF00)
			| ((b) & 0x000000FF)
		;
	}
	/**
	 * Fill the rectangle given by absolute coordinates.
	 * @param x the left start of the filling inclusive
	 * @param y the top start of the filling inclusive
	 * @param x2 the right end of the filling inclusive
	 * @param y2 the bottom end of the filling inclusive
	 * @param g2 the graphics context
	 */
	public static void fillRectAbsolute(int x, int y, int x2, int y2, Graphics2D g2) {
		if (x >= x2 || y >= y2) {
			return;
		}
		g2.fillRect(x, y, x2 - x + 1, y2 - y + 1);
	}
	/**
	 * Paint a 5x5 ABC grid on the given rectangle by equally distributing its cells.
	 * The caller should remember to apply a clipping region if necessary.
	 * @param g2 the graphics object
	 * @param rect the rectangle to fill in
	 * @param gridColor the color of the grid
	 * @param text the text renderer to print the labels
	 */
	public static void paintGrid(Graphics2D g2, Rectangle rect, Color gridColor, TextRenderer text) {
		g2.setColor(gridColor);
		Stroke st = g2.getStroke();
		//FIXME the dotted line rendering is somehow very slow
//		g2.setStroke(gfx.gridStroke);
		
		float fw = rect.width;
		float fh = rect.height;
		float dx = fw / 5;
		float dy = fh / 5;
		float y0 = dy;
		float x0 = dx;
		for (int i = 1; i < 5; i++) {
			g2.drawLine((int)(rect.x + x0), rect.y, (int)(rect.x + x0), (int)(rect.y + fh));
			g2.drawLine(rect.x, (int)(rect.y + y0), (int)(rect.x + fw), (int)(rect.y + y0));
			x0 += dx;
			y0 += dy;
		}
		int i = 0;
		y0 = dy - 6;
		x0 = 2;
		for (char c = 'A'; c < 'Z'; c++) {
			text.paintTo(g2, (int)(rect.x + x0), (int)(rect.y + y0), 5, TextRenderer.GRAY, String.valueOf(c));
			x0 += dx;
			i++;
			if (i % 5 == 0) {
				x0 = 2;
				y0 += dy;
			}
		}
		
		g2.setStroke(st);
	}
	/** Star rendering starting color. */
	private static int startStars = 0x685CA4;
	/** Star rendering end color. */
	private static int endStars = 0xFCFCFC;
	/** Number of stars per layer. */
	private static final int STAR_COUNT = 512;
	/** Number of layers. */
	private static final int STAR_LAYER_COUNT = 4;
	/** A star object. */
	private static class Star {
		/** The star proportional position. */
		public double x;
		/** The star proportional position. */
		public double y;
		/** The star color. */
		public Color color;
	}
	/** The list of stars. */
	private static List<Star> stars = new ArrayList<>();
	/**
	 * Paint the multiple layer of stars.
	 * @param g2 the graphics object
	 * @param rect the target rectangle
	 * @param view the viewport rectangle
	 * @param starmapClip the clipping region to avoid even calling a graphics operation outside there for performance reasons
	 * @param zoomIndex the current zoom index
	 * @param zoomLevelCount the maximum zoom level
	 */
	public static void paintStars(Graphics2D g2, 
			Rectangle rect, Rectangle view, 
			Rectangle starmapClip, int zoomIndex, int zoomLevelCount) {
		int starsize = zoomIndex < zoomLevelCount / 2.5 ? 1 : 2;
		double xf = (view.x - rect.x) * 1.0 / rect.width;
		double yf = (view.y - rect.y) * 1.0 / rect.height;
		Color last = null;
		for (int i = 0; i < stars.size(); i++) {
			Star s = stars.get(i);
            int layerIdx = i / STAR_COUNT;
			double layer = 0.9 - layerIdx * 0.10;
			double w = rect.width * layer;
			double h = rect.height * layer;
			double lx = rect.x;
			double ly = rect.y;
			
			
			int x = (int)(lx + xf * (rect.width - w) + s.x * rect.width);
			int y = (int)(ly + yf * (rect.height - h) + s.y * rect.height);
			if (starmapClip.contains(x, y)) {
				if (last != s.color) {
					g2.setColor(s.color);
					last = s.color;
				}
				g2.fillRect(x, y, starsize, starsize);
			}
		}
	}
	static {
		precalculateStars();
	}
	/**
	 * Precalculates the star background locations and colors.
	 */
	private static void precalculateStars() {
		Random random = new Random(0);
		Color[] colors = new Color[8];
		for (int i = 0; i < colors.length; i++) {
			colors[i] = new Color(mixColors(startStars, endStars, random.nextFloat()));
		}
		for (int i = 0; i < STAR_COUNT * STAR_LAYER_COUNT; i++) {
			Star s = new Star();
			s.x = random.nextDouble();
			s.y = random.nextDouble();
			s.color = colors[random.nextInt(colors.length)];
			stars.add(s);
		}
		Collections.sort(stars, new Comparator<Star>() {
			@Override
			public int compare(Star o1, Star o2) {
				int c1 = o1.color.getRGB() & 0xFFFFFF;
				int c2 = o2.color.getRGB() & 0xFFFFFF;
				return c1 - c2;
			}
		});
	}
	/**
	 * Mix two colors with a factor.
	 * @param c1 the first color
	 * @param c2 the second color
	 * @param rate the mixing factor
	 * @return the mixed color
	 */
	public static int mixColors(int c1, int c2, float rate) {
		return
			((int)((c1 & 0xFF0000) * rate + (c2 & 0xFF0000) * (1 - rate)) & 0xFF0000)
			| ((int)((c1 & 0xFF00) * rate + (c2 & 0xFF00) * (1 - rate)) & 0xFF00)
			| ((int)((c1 & 0xFF) * rate + (c2 & 0xFF) * (1 - rate)) & 0xFF);
	}
	/**
	 * Flood-fill the area of the given rectangle with the given image. 
	 * @param g2 the graphics context
	 * @param rect the target rectangle
	 * @param image the image to use
	 */
	public static void fill(Graphics2D g2, Rectangle rect, BufferedImage image) {
		fill(g2, rect.x, rect.y, rect.width, rect.height, image);
	}
	/**
	 * Flood-fill the area of the given rectangle with the given image. 
	 * @param g2 the graphics context
	 * @param x the left coordinate
	 * @param y the top coordinate
	 * @param width the area width
	 * @param height the area height
	 * @param image the image to use
	 */
	public static void fill(Graphics2D g2, int x, int y, int width, int height, BufferedImage image) {
		Paint save = g2.getPaint();
		TexturePaint tp = new TexturePaint(image, new Rectangle(x, y, image.getWidth(), image.getHeight()));
		g2.setPaint(tp);
		g2.fillRect(x, y, width, height);
		g2.setPaint(save);
	}
	/**
	 * Render the target image centered relative to the given rectangle.
	 * @param g2 the graphics object
	 * @param rect the target rectangle
	 * @param image the image to render
	 */
	public static void drawCentered(Graphics2D g2, Rectangle rect, BufferedImage image) {
		int dw = (rect.width - image.getWidth()) / 2;
		int dh = (rect.height - image.getHeight()) / 2;
		g2.drawImage(image, rect.x + dw, rect.y + dh, null);
	}
	/**
	 * Enable and disable the bilinear interpolation mode for the graphics.
	 * @param g2 the target graphics object.
	 * @param active activate?
	 */
	public static void setInterpolation(Graphics2D g2, boolean active) {
		if (active) {
			g2.setRenderingHint(RenderingHints.KEY_INTERPOLATION, 
					RenderingHints.VALUE_INTERPOLATION_BILINEAR);
		} else {
			g2.setRenderingHint(RenderingHints.KEY_INTERPOLATION, 
					RenderingHints.VALUE_INTERPOLATION_NEAREST_NEIGHBOR);
		}
	}
	/**
	 * Enable and disable the bilinear interpolation mode for the graphics.
	 * @param g2 the target graphics object.
	 * @param active activate?
	 */
	public static void setAntiailas(Graphics2D g2, boolean active) {
		if (active) {
			g2.setRenderingHint(RenderingHints.KEY_TEXT_ANTIALIASING, 
					RenderingHints.VALUE_TEXT_ANTIALIAS_ON);
		} else {
			g2.setRenderingHint(RenderingHints.KEY_TEXT_ANTIALIASING, 
					RenderingHints.VALUE_TEXT_ANTIALIAS_OFF);
		}
	}
	/**
	 * Check if a specified point is inside a specified rectangle.
	 * @param x0 the upper left coordinate
	 * @param y0 the upper left coordinate
	 * @param x1 the lower right coordinate (exclusive)
	 * @param y1 the lower right coordinate (exclusive)
	 * @param x the point to check
	 * @param y the point to check
	 * @return                True if the point is inside the rectangle,
	 *                        false otherwise.
	 */
	public static boolean isPointInsideRectangle(double x0, double y0, double x1, double y1,
			double x, double y) {
		return x >= x0 && x < x1 && y >= y0 && y < y1;
	}

	/**
	 * Return true if c is between a and b.
	 * @param a range value (inclusive)
	 * @param b range value (inclusive)
	 * @param c the value to test
	 * @return true if between
	 */
	private static boolean isBetween(double a, double b, double c) {
		return b > a ? c >= a && c <= b : c >= b && c <= a;
	}
	/**
	 * Check if two points are on the same side of a given line.
	 * Algorithm from Sedgewick page 350.
	 *
	 * @param x0 the line start X
	 * @param y0 the line start Y
	 * @param x1 the line end X
	 * @param y1 the line end Y
	 * @param px0 the first point X
	 * @param py0 the first point Y
	 * @param px1 the second point X
	 * @param py1 the second point Y
	 * @return                <0 if points on opposite sides.
	 *                        =0 if one of the points is exactly on the line
	 *                        >0 if points on same side.
	 */
	private static int sameSide(double x0, double y0, double x1, double y1,
			double px0, double py0, double px1, double py1)	{
		int  sameSide = 0;

		double dx  = x1  - x0;
		double dy  = y1  - y0;
		double dx1 = px0 - x0;
		double dy1 = py0 - y0;
		double dx2 = px1 - x1;
		double dy2 = py1 - y1;

		// Cross product of the vector from the endpoint of the line to the point
		double c1 = dx * dy1 - dy * dx1;
		double c2 = dx * dy2 - dy * dx2;

		if (c1 != 0 && c2 != 0) {
			sameSide = c1 < 0 != c2 < 0 ? -1 : 1;
		} else 
		if (dx == 0 && dx1 == 0 && dx2 == 0) {
			sameSide = !isBetween(y0, y1, py0) && !isBetween(y0, y1, py1) ? 1 : 0;
		} else 
		if (dy == 0 && dy1 == 0 && dy2 == 0) {
			sameSide = !isBetween(x0, x1, px0) && !isBetween(x0, x1, px1) ? 1 : 0;
		}
		return sameSide;
	}
	/**
	 * Check if two line segments intersects.
	 *
	 * @param x0 the first line start X
	 * @param y0 the first line start Y
	 * @param x1 the first line end X
	 * @param y1 the first line end Y
	 * @param x2 the second line start X
	 * @param y2 the second line start Y
	 * @param x3 the second line end X
	 * @param y3 the second line end Y
	 * @return True if the two lines intersects.
	 */
	public static boolean isLineIntersectingLine(double x0, double y0, double x1, double y1,
			double x2, double y2, double x3, double y3) {
		int s1 = sameSide(x0, y0, x1, y1, x2, y2, x3, y3);
		int s2 = sameSide(x2, y2, x3, y3, x0, y0, x1, y1);

		return s1 <= 0 && s2 <= 0;
	}
	/**
	 * Check if a specified line intersects a specified rectangle.
	 * @param lx0 the line start X
	 * @param ly0 the line start Y
	 * @param lx1 the line end X
	 * @param ly1 the line end Y
	 * @param x0 the rectangle left
	 * @param y0 the rectangle top
	 * @param x1 the rectangle right (excluded)
	 * @param y1 the rectangle bottom (excluded)
	 * @return                True if the line intersects the rectangle,
	 *                        false otherwise.
	 */
	public static boolean isLineIntersectingRectangle(double lx0, double ly0,
			double lx1, double ly1, double x0, double y0, double x1, double y1)	{
		// Is one of the line endpoints inside the rectangle
		if (isPointInsideRectangle(x0, y0, x1, y1, lx0, ly0) 
				|| isPointInsideRectangle(x0, y0, x1, y1, lx1, ly1)) {
			return true;
		}
		// If it intersects it goes through. Need to check three sides only.

		// Check against top rectangle line
		if (isLineIntersectingLine(lx0, ly0, lx1, ly1,
				x0, y0, x1, y0)) {
			return true;
		}
		// Check against left rectangle line
		if (isLineIntersectingLine(lx0, ly0, lx1, ly1,
				x0, y0, x0, y1)) {
			return true;
		}
		// Check against bottom rectangle line
        return isLineIntersectingLine(lx0, ly0, lx1, ly1,
                x0, y1, x1, y1);
    }
	/**
	 * Compute the scaling factor and the top-left point where the rendering should scale from
	 * in order to keep the content fit the enclosing window. 
	 * @param windowWidth the window width
	 * @param windowHeight the window height
	 * @param contentWidth the content width
	 * @param contentHeight the content height
	 * @return the origin and scale
	 */
	public static Pair<Point, Double> fitWindow(int windowWidth, int windowHeight, int contentWidth, int contentHeight) {
		double sx = windowWidth * 1.0 / contentWidth;
		double sy = windowHeight * 1.0 / contentHeight;
		double scalex = Math.min(sx, sy);
		double scaley = scalex;
		
		return Pair.of(new Point(
				(int)(windowWidth - contentWidth * scalex) / 2, 
				(int)(windowHeight - contentHeight * scaley) / 2
				), scalex);
	}
}