/*
 * This Beanshell example demonstrates
 *
 * - how to override mouse listeners on an ImageCanvas
 * - how to reinstate the mouse listeners after we're done
 * - that you can have fun with Fiji
 */
import color.CIELAB;

import fiji.util.gui.GenericDialogPlus;

import ij.gui.Overlay;

import java.awt.event.MouseEvent;
import java.awt.event.MouseListener;
import java.awt.event.MouseMotionListener;

import java.util.Random;

import stitching.CommonFunctions;

class Rainbow {
	protected int[] rainbow, indices;

	public Rainbow(float l, float a0, float b0, float a1, float b1, int n) {
		rainbow = new int[n];
		indices = new int[n];

		float[] lab = new float[3], rgb = new float[3];
		lab[0] = l;
		for (int i = 0; i < n; i++) {
			lab[1] = (a0 * (n - 1 - i) + a1 * i) / (n - 1);
			lab[2] = (b0 * (n - 1 - i) + b1 * i) / (n - 1);
			CIELAB.CIELAB2sRGB(lab, rgb);
			rainbow[i] = packRGB(rgb);
			indices[i] = i;
		}

		permute();
	}

	protected int packRGB(float[] rgb) {
		return (toByte(rgb[0] * 256) << 16) | (toByte(rgb[1] * 256) << 8) | toByte(rgb[2] * 256);
	}

	protected int toByte(float f) {
		if (f < 0)
			return 0;
		if (f > 255)
			return 255;
		return (int)Math.round(f);
	}

	public void paint(ColorProcessor slice, int gap, int x0, int y0, int x1, int y1) {
		int n = rainbow.length;
		for (int i = 0; i < n; i++) {
			int x2 = x0 + (x1 - x0 + gap) * i / n;
			int x3 = x0 + (x1 - x0 + gap) * (i + 1) / n - gap;
			fillRectangle(slice, x2, y0, x3, y1, rainbow[i]);
		}
	}

	protected void fillRectangle(ColorProcessor slice, int x0, int y0, int x1, int y1, int color) {
		int[] pixels = (int[])slice.getPixels();
		int width = slice.getWidth();
		for (int y = y0; y < y1; y++)
			Arrays.fill(pixels, y * width + x0, y * width + x1, color);
	}

	public void permute() {
		Random random = new Random();
		for (int i = rainbow.length - 2; i > 0; i--) {
			int random = random.nextInt(i) + 1;
			if (random != i) {
				int save = rainbow[random];
				rainbow[random] = rainbow[i];
				rainbow[i] = save;
				save = indices[random];
				indices[random] = indices[i];
				indices[i] = save;
			}
		}
	}

	public void move(int from, int to) {
		if (from < 1 || from > rainbow.length - 2 || to < 1 || to > rainbow.length || from == to)
			return;
		if (from < to) {
			int save = rainbow[from];
			int index = indices[from];
			while (from < to) {
				rainbow[from] = rainbow[from + 1];
				indices[from] = indices[++from];
			}
			rainbow[to] = save;
			indices[to] = index;
		}
		else {
			int save = rainbow[from];
			int index = indices[from];
			while (from > to) {
				rainbow[from] = rainbow[from - 1];
				indices[from] = indices[--from];
			}
			rainbow[to] = save;
			indices[to] = index;
		}
	}
}

class Rainbows {
	protected int width, height, squareSize, gap, n;
	protected Rainbow[] rainbows;
	public Rainbows(float l, float[] a, float[] b, int n, int squareSize, int gap) {
		int m = a.length < b.length ? a.length : b.length;
		this.n = n;
		this.squareSize = squareSize;
		this.gap = gap;
		width = gap + n * (squareSize + gap);
		height = gap + m * (squareSize + gap);

		rainbows = new Rainbow[m];
		for (int i = 0; i < m - 1; i++)
			rainbows[i] = new Rainbow(l, a[i], b[i], a[i + 1], b[i + 1], n);
		rainbows[m - 1] = new Rainbow(l, a[m - 1], b[m - 1], a[0], b[0], n);
	}

	public void paint(ColorProcessor slice) {
		for (int i = 0; i < rainbows.length; i++)
			rainbows[i].paint(slice, gap, gap, gap + i * (squareSize + gap), width - gap, gap + (i + 1) * (squareSize + gap) - gap);
	}

	public ColorProcessor createSlice() {
		ColorProcessor slice = new ColorProcessor(width, height);
		paint(slice);
		return slice;
	}

	public Rainbow getRainbow(int y) {
		if (y < 0 || (y % (squareSize + gap)) <= gap)
			return null;
		int index = y / (squareSize + gap);
		if (index >= rainbows.length)
			return null;
		return rainbows[index];
	}

	public int getIndex(int x) {
		if (x < 0 || (x % (squareSize + gap)) <= gap)
			return -1;
		int index = x / (squareSize + gap);
		return index < 1 || index > n - 2 ? -1 : index;
	}

	public void showOverlay(ImagePlus image) {
		Overlay overlay = new Overlay();
		for (int i = 0; i < rainbows.length; i++) {
			int y = gap + i * (squareSize + gap);
			Rainbow rainbow = rainbows[i];
			for (int j = 0; j < n; j++) {
				int x = gap + j * (squareSize + gap);
				overlay.add(new TextRoi(x, y, "" + rainbow.indices[j]));
			}
		}
		image.setOverlay(overlay);
	}

	public boolean showMistakes(ImagePlus image) {
		int score = 0;
		Roi roi = null;
		for (int i = 0; i < rainbows.length; i++) {
			int y = gap + i * (squareSize + gap);
			Rainbow rainbow = rainbows[i];
			for (int j = 1; j < n - 1; j++) {
				int x = gap + j * (squareSize + gap);
				if (rainbow.indices[j] != j) {
					score += Math.abs(rainbow.indices[j] - j);
					if (roi == null)
						roi = new Roi(x, y, squareSize, squareSize);
					else {
						if (!(roi instanceof ShapeRoi))
							roi = new ShapeRoi(roi);
						roi.or(new ShapeRoi(new Roi(x, y, squareSize, squareSize)));
					}
				}
			}
		}
		if (score > 0 && !IJ.showMessageWithCancel("Really?", "Do you really want to quit playing? (Your score is " + score + "...)"))
			return false;
		image.setRoi(roi);
		showOverlay(image);
		IJ.showMessage((score == 0 ? "Congratulations!" : "Sorry!") + "\nYour score is" + (score > 30 ? " only" : "") + ": " + score);
		IJ.showStatus("Score: " + score);
		return true;
	}
}

class Game implements MouseListener, MouseMotionListener {
	protected Rainbows rainbows;
	protected ColorProcessor slice;
	protected ImagePlus image;
	protected ImageCanvas canvas;
	protected MouseListener[] mouseListeners;
	protected MouseMotionListener[] mouseMotionListeners;
	protected Rainbow rainbow;
	protected int index = -1;

	public Game(float l, float[] a, float[] b, int n, int squareSize, int gap) {
		rainbows = new Rainbows(l, a, b, n, squareSize, gap);
		slice = rainbows.createSlice();
		image = new ImagePlus("Game", slice);
	}

	/* for debugging */
	public void stopPreviousGames() {
		for (int i = WindowManager.getImageCount(); i > 0; i--) {
			image = WindowManager.getImage(i);
			if (image.getTitle().startsWith("Game"))
				image.close();
		}
	}

	public void play() {
		image.show();
		canvas = image.getCanvas();
		mouseListeners = canvas.getMouseListeners();
		for (MouseListener listener : mouseListeners)
			canvas.removeMouseListener(listener);
		mouseMotionListeners = canvas.getMouseMotionListeners();
		for (MouseMotionListener listener : mouseMotionListeners)
			canvas.removeMouseMotionListener(listener);
		canvas.addMouseListener(this);
		canvas.addMouseMotionListener(this);
	}

	public void stop() {
		index = Integer.MIN_VALUE;
		canvas.removeMouseListener(this);
		canvas.removeMouseMotionListener(this);
		for (MouseListener listener : mouseListeners)
			canvas.addMouseListener(listener);
		for (MouseMotionListener listener : mouseMotionListeners)
			canvas.addMouseMotionListener(listener);
	}

	protected int getX(MouseEvent e) {
		return canvas.offScreenX(e.getX());
	}

	protected int getY(MouseEvent e) {
		return canvas.offScreenY(e.getY());
	}

	public void mousePressed(MouseEvent event) {
		if (index < -1)
			return;
		index = rainbows.getIndex(getX(event));
		if (index > 0)
			rainbow = rainbows.getRainbow(getY(event));
	}

	public void mouseDragged(MouseEvent event) {
		if (index < 0)
			return;
		int newIndex = rainbows.getIndex(getX(event));
		if (newIndex < 0 || newIndex == index)
			return;
		rainbow.move(index, newIndex);
		index = newIndex;
		rainbows.paint(slice);
		image.updateAndDraw();
	}

	public void mouseClicked(MouseEvent event) {
		if (event.getClickCount() > 1) {
			if (rainbows.showMistakes(image))
				stop();
		}
	}

	public void mouseReleased(MouseEvent event) {}
	public void mouseMoved(MouseEvent event) {}
	public void mouseEntered(MouseEvent event) {}
	public void mouseExited(MouseEvent event) {}
}

String url = "http://www.xrite.com/custom_page.aspx?PageID=77&Lang=en";
gd = new GenericDialogPlus("The Hue Game");
gd.addMessage("Rules:\n \nOrder the tiles by hue by dragging the fields horizontally.\nThe outermost tiles cannot be moved.\n \nDouble-click to finish and see your score!\n \nThis game is based on the idea and the colors presented at:\n" + url);
gd.addNumericField("Difficulty?", 22, 0);
CommonFunctions.addHyperLinkListener(gd.getMessage(), url);
gd.showDialog();
if (!gd.wasCanceled()) {
	int n = (int)gd.getNextNumber();
	new Game(72, new float[] { 26, -10, -30, 5 }, new float[] { 12, 43, 0, -21 }, n, 40, 3).play();
}