/**
 * A logic puzzle game Copyright 2005 by Frank Buss (fb@frank-buss.de)
 */

import java.applet.*;
import java.awt.*;
import java.awt.event.*;
import java.util.*;

public class Aqueduct extends Applet {
	private Button playButton = new Button("Play");
	private Button editButton = new Button("Edit");
	private Button groundButton = new Button("Ground");
	private Button channelButton = new Button("Channel");
	private Button sourceButton = new Button("Source");
	private Button drainButton = new Button("Drain");
	private Button helpButton = new Button("Help");
	private Panel panel;

	public void init() {
		setBackground(Color.white);
		setFont(new Font("TimesRoman", Font.PLAIN, 16));
		setLayout(new BorderLayout());
		Panel buttons = new Panel(new GridLayout(1, 0));
		buttons.add(playButton);
		buttons.add(editButton);
		buttons.add(groundButton);
		buttons.add(channelButton);
		buttons.add(sourceButton);
		buttons.add(drainButton);
		buttons.add(helpButton);
		add(BorderLayout.NORTH, buttons);
		add(BorderLayout.CENTER, panel);
	}

	// mapping between symbol and data file representation
	private static final Object GROUND = new Object();
	private static final Object CHANNEL = new Object();
	private static final Object SOURCE = new Object();
	private static final Object DRAIN = new Object();
	private static final Object PLAYER = new Object();
	private static final Object[][] cellTypes = { { GROUND, "." }, { CHANNEL, "C" }, { SOURCE, "S" }, { DRAIN, "D" },
			{ PLAYER, "P" }, { null, " " } };

	// the 6 moving directions and opposite directions
	private static final int directions[][] = { { -1, 0, 1, -1 }, { 0, 1, 0, -1 }, { 1, 0, -1, -1 }, { -1, -1, 1, 0 },
			{ 0, -1, 0, 1 }, { 1, -1, -1, 0 } };

	private Image pixmapImage;
	private Graphics pixmap;
	private Object[][] board = {};
	private Object[][] boardTemplate = {};
	private boolean[][] filled = {};
	private boolean[][] targets = {};
	private Object selectedCellType;
	private static final Object PLAY = new Object();
	private static final Object EDIT = new Object();
	private Object mode = PLAY;
	private boolean sourceSelected;
	private int sourceX;
	private int sourceY;

	// copies an object array
	private Object[][] copyArray(Object[][] array) {
		Object[][] copy = new Object[array.length][array[0].length];
		for (int y = 0; y < array.length; y++) {
			for (int x = 0; x < array[0].length; x++) {
				copy[y][x] = array[y][x];
			}
		}
		return copy;
	}

	// copies an boolean array
	private boolean[][] copyArray(boolean[][] array) {
		boolean[][] copy = new boolean[array.length][array[0].length];
		for (int y = 0; y < array.length; y++) {
			for (int x = 0; x < array[0].length; x++) {
				copy[y][x] = array[y][x];
			}
		}
		return copy;
	}

	// sets a cell on the board to the specified type
	private void setCell(Object[][] board, int x, int y, Object cellType) {
		board[y][x] = cellType;
	}

	// checks, if the cell is of the specified type
	private boolean isCell(Object[][] board, int x, int y, Object cellType) {
		return board[y][x] == cellType;
	}

	// returns the with of the board
	private int boardWidth(Object[][] board) {
		return board.length;
	}

	// returns the height of the board
	private int boardHeight(Object[][] board) {
		return board[0].length;
	}

	private Object rassoc(char c) {
		for (int i = 0; i < cellTypes.length; i++) {
			if (((String) cellTypes[i][1]).charAt(0) == c)
				return cellTypes[i][0];
		}
		return null;
	}

	// inits the board by a list of strings
	private void initBoardTemplate(String[] lines) {
		int width = 0;
		for (int i = 0; i < lines.length; i++) {
			String line = lines[i];
			if (line.length() > width)
				width = line.length();
		}
		int height = lines.length;
		boardTemplate = new Object[height][width];
		for (int y = 0; y < height; y++) {
			String line = lines[y];
			for (int x = 0; x < line.length(); x++) {
				boardTemplate[y][x] = rassoc(line.charAt(x));
			}
		}
	}

	// returns a list of coordinates of the specified cell type
	private Vector findAllCellType(Object cellType) {
		Vector coordinates = new Vector();
		for (int y = 0; y < boardHeight(board); y++) {
			for (int x = 0; x < boardWidth(board); x++) {
				if (isCell(board, x, y, cellType))
					coordinates.addElement(new Point(x, y));
			}
		}
		return coordinates.size() > 0 ? coordinates : null;
	}

	// returns the first occurence of the specified cell type
	private Point findCellType(Object cellType) {
		Vector coordinates = findAllCellType(cellType);
		if (coordinates != null)
			return (Point) coordinates.get(0);
		return null;
	}

	// checks, if (x y) is a valid cell coordinate
	private boolean isValid(int x, int y) {
		int width = boardWidth(board);
		int height = boardHeight(board);
		return x >= 0 && x < width && y >= 0 && y < height;
	}

	// returns the next hexagon coordinates in direction (dx dy)
	private Point advance(int x, int y, int dx, int dy) {
		y += dy;
		if (dx != 0) {
			y += x % 2;
			x += dx;
		}
		return new Point(x, y);
	}

	private boolean member(Object o, Object[] l) {
		for (int i = 0; i < l.length; i++) {
			if (l[i] == o)
				return true;
		}
		return false;
	}

	// fills recursively all specified cell-types, starting at x y
	private void floodImpl(int x, int y, Object[] cellTypes) {
		if (isValid(x, y)) {
			if (!filled[y][x] && member(board[y][x], cellTypes)) {
				filled[y][x] = true;
				for (int i = 0; i < directions.length; i++) {
					int[] direction = directions[i];
					int dx = direction[0];
					int dy = direction[1];
					Point p = advance(x, y, dx, dy);
					floodImpl(p.x, p.y, cellTypes);
				}
			}
		}
	}

	// clears the fill array and fills all specified cell-types, starting at x y
	private void flood(int x, int y, Object[] cellTypes) {
		filled = new boolean[boardHeight(board)][boardWidth(board)];
		floodImpl(x, y, cellTypes);
	}

	// moves the player and a channel, if pushed
	private void onMove(int dx, int dy) {
		Point p = findCellType(PLAYER);
		if (p != null) {
			int x = p.x;
			int y = p.y;
			int oldX = x;
			int oldY = y;
			p = advance(x, y, dx, dy);
			x = p.x;
			y = p.y;
			if (isValid(x, y)) {
				if (isCell(board, x, y, GROUND)) {
					setCell(board, oldX, oldY, GROUND);
					setCell(board, x, y, PLAYER);
				} else {
					if (isCell(board, x, y, CHANNEL)) {
						p = advance(x, y, dx, dy);
						int x2 = p.x;
						int y2 = p.y;
						if (isValid(x2, y2) && isCell(board, x2, y2, GROUND)) {
							setCell(board, oldX, oldY, GROUND);
							setCell(board, x, y, PLAYER);
							setCell(board, x2, y2, CHANNEL);
						}
					}
				}
				panel.repaint();
			}
		}
	}

	// the edge with of a hexagon
	private static final int cellWidth = 15;

	private float[] getHexLines(int x, int y) {
		float a = cellWidth;
		float d = 2 * a;
		float e = (float) (Math.sqrt(3) * a);
		float o = (d - a) / 2.0f;
		float x0 = (a + o) * x + o;
		float y0 = ((float) (x % 2.0f)) * (e / 2.0f) + y * e;
		float x1 = x0 + a;
		float y1 = y0;
		float x2 = x1 + o;
		float y2 = y1 + (e / 2.0f);
		float x3 = x1;
		float y3 = y0 + e;
		float x4 = x0;
		float y4 = y3;
		float x5 = x0 - o;
		float y5 = y2;
		return new float[] { x0, y0, x1, y1, x2, y2, x3, y3, x4, y4, x5, y5 };
	}

	// returns the center of the specified hexagon
	private float[] getHexCenter(int x, int y) {
		float a = cellWidth;
		float d = 2 * a;
		float e = (float) (Math.sqrt(3) * a);
		float o = (d - a) / 2.0f;
		return new float[] { (a + o) * x + o + (a / 2.0f), ((float) (x % 2)) * (e / 2.0f) + y * e + e / 2.0f };
	}

	// converts screen position to hexagon position
	private Point mouseToCellCoordinates(int x, int y) {
		for (int yb = 0; yb < boardHeight(board); yb++) {
			for (int xb = 0; xb < boardWidth(board); xb++) {
				float a = cellWidth;
				float d = 2.0f * a;
				float e = (float) (Math.sqrt(3) * a);
				float o = (d - a) / 2.0f;
				float x0 = (a + o) * xb + o;
				float y0 = ((float) (xb % 2) * (e / 2.0f)) + yb * e;
				float centerX = x0 + a / 2.0f;
				float centerY = y0 + e / 2.0f;
				float dx = x - centerX;
				float dy = y - centerY;
				float dx2 = dx * dx;
				float dy2 = dy * dy;
				float e2 = e / 2.0f;
				e2 = e2 * e2;
				if (dx2 + dy2 < e2)
					return new Point(xb, yb);
			}
		}
		return null;
	}

	private boolean isSomethingFilledAround(int x, int y) {
		for (int i = 0; i < directions.length; i++) {
			int[] direction = directions[i];
			int dx = direction[0];
			int dy = direction[1];
			Point p = advance(x, y, dx, dy);
			int xf = p.x;
			int yf = p.y;
			if (isValid(xf, yf) && filled[yf][xf])
				return true;
		}
		return false;
	}

	// first checks, if route to source is possible,
	// then moves the player to it, if empty or
	// selects the source, if it is a channel
	private void onSelectSource(int x, int y) {
		Point p = findCellType(PLAYER);
		int xp = p.x;
		int yp = p.y;
		flood(xp, yp, new Object[] { PLAYER, GROUND });
		if (isCell(board, x, y, GROUND)) {
			if (filled[y][x]) {
				setCell(board, xp, yp, GROUND);
				setCell(board, x, y, PLAYER);
			}
		} else if (isCell(board, x, y, CHANNEL)) {
			if (isSomethingFilledAround(x, y)) {
				sourceX = x;
				sourceY = y;
				sourceSelected = true;
			}
		}
	}

	private void getPossibleTargetsImpl(int xp, int yp, int xc, int yc) {
		setCell(board, xp, yp, PLAYER);
		setCell(board, xc, yc, CHANNEL);
		flood(xp, yp, new Object[] { PLAYER, GROUND });
		boolean[][] filled = copyArray(this.filled);
		setCell(board, xp, yp, GROUND);
		setCell(board, xc, yc, GROUND);
		for (int i = 0; i < directions.length; i++) {
			int[] direction = directions[i];
			int dx = direction[0];
			int dy = direction[1];
			int dx2 = direction[2];
			int dy2 = direction[3];
			Point p = advance(xc, yc, dx, dy);
			int xf = p.x;
			int yf = p.y;
			if (isValid(xf, yf) && filled[yf][xf]) {
				p = advance(xc, yc, dx2, dy2);
				int xt = p.x;
				int yt = p.y;
				if (isValid(xt, yt) && !targets[yt][xt] && isCell(board, xt, yt, GROUND)) {
					targets[yt][xt] = true;
					getPossibleTargetsImpl(xc, yc, xt, yt);
				}
			}
		}
	}

	private void getPossibleTargets() {
		Point p = findCellType(PLAYER);
		int xp = p.x;
		int yp = p.y;
		targets = new boolean[boardHeight(board)][boardWidth(board)];
		getPossibleTargetsImpl(xp, yp, sourceX, sourceY);
		setCell(board, xp, yp, PLAYER);
		setCell(board, sourceX, sourceY, CHANNEL);
	}

	private void onSelectTarget(int x, int y) {
		if (!(x == sourceX && y == sourceY)) {
			getPossibleTargets();
			if (targets[y][x]) {
				Point p = findCellType(PLAYER);
				int xp = p.x;
				int yp = p.y;
				if (!(xp == x && yp == y)) {
					setCell(board, xp, yp, GROUND);
					setCell(board, sourceX, sourceY, PLAYER);
					setCell(board, x, y, CHANNEL);
				}
			}
		}
		sourceSelected = false;
	}

	// called on button 1 press in play mode
	private void onPlayButton1Press(int x, int y) {
		if (sourceSelected) {
			onSelectTarget(x, y);
		} else {
			onSelectSource(x, y);
		}
	}

	// called on button 1 press in edit mode
	private void onEditButton1Press(int x, int y) {
		if (selectedCellType != null) {
			if (selectedCellType == SOURCE || selectedCellType == PLAYER) {
				Point p = findCellType(selectedCellType);
				int xs = p.x;
				int ys = p.y;
				if (isValid(xs, ys)) {
					setCell(board, xs, ys, GROUND);
				}
			}
			setCell(board, x, y, selectedCellType);
		}
	}

	// called on button 1 press
	private void onButton1Press(int x, int y) {
		Point p = mouseToCellCoordinates(x, y);
		if (p != null) {
			int px = p.x;
			int py = p.y;
			if (isValid(px, py)) {
				if (mode == PLAY) {
					onPlayButton1Press(px, py);
				} else {
					onEditButton1Press(px, py);
				}
				panel.repaint();
			}
		}
	}

	// clears a cell
	private void onButton3Press(int x, int y) {
		Point p = mouseToCellCoordinates(x, y);
		if (p != null) {
			int px = p.x;
			int py = p.y;
			if (isValid(px, py)) {
				board[py][px] = null;
			}
			panel.repaint();
		}
	}

	// draws a hexagon outline with black
	private void drawHexOutline(int x, int y) {
		pixmap.setColor(Color.black);
		float[] points = getHexLines(x, y);
		pixmap.drawPolygon(new int[] { (int) points[0], (int) points[2], (int) points[4], (int) points[6],
				(int) points[8], (int) points[10] }, new int[] { (int) points[1], (int) points[3], (int) points[5],
				(int) points[7], (int) points[9], (int) points[11] }, 6);
	}

	// fills a hexagon outline with the current color
	private void drawHexFill(int x, int y) {
		float[] points = getHexLines(x, y);
		pixmap.fillPolygon(new int[] { (int) points[0], (int) points[2], (int) points[4], (int) points[6],
				(int) points[8], (int) points[10] }, new int[] { (int) points[1], (int) points[3], (int) points[5],
				(int) points[7], (int) points[9], (int) points[11] }, 6);
	}

	// draws a ground cell
	private void drawGround(int x, int y) {
		Color c;
		if (sourceSelected && (targets[y][x] || (sourceX == x && sourceY == y))) {
			c = Color.yellow;
		} else {
			c = Color.lightGray;
		}
		pixmap.setColor(c);
		drawHexFill(x, y);
	}

	//; draws a player
	private void drawPlayer(int x, int y) {
		drawGround(x, y);
		float[] center = getHexCenter(x, y);
		float x0 = center[0];
		float y0 = center[1];
		pixmap.setColor(Color.red);
		pixmap.fillOval((int) (x0 - cellWidth / 2.0f), (int) (y0 - cellWidth / 2.0f), cellWidth, cellWidth);
	}

	// draws a source
	private void drawSource(int x, int y) {
		drawGround(x, y);
		float[] center = getHexCenter(x, y);
		float x0 = center[0];
		float y0 = center[1];
		pixmap.setColor(Color.white);
		pixmap.fillOval((int) (x0 - cellWidth / 2.0f), (int) (y0 - cellWidth / 2.0f), cellWidth, cellWidth);
	}

	// draws a drain
	private void drawDrain(int x, int y) {
		drawGround(x, y);
		float[] center = getHexCenter(x, y);
		float x0 = center[0];
		float y0 = center[1];
		pixmap.setColor(Color.green);
		pixmap.fillOval((int) (x0 - cellWidth / 2.0f), (int) (y0 - cellWidth / 2.0f), cellWidth, cellWidth);
	}

	// draws a channel
	private void drawChannel(int x, int y) {
		drawGround(x, y);
		float[] center = getHexCenter(x, y);
		float xc = center[0];
		float yc = center[1];
		pixmap.setColor(filled[y][x] ? Color.blue : Color.black);
		pixmap.fillOval((int) (xc - cellWidth / 2.0f), (int) (yc - cellWidth / 2.0f), cellWidth, cellWidth);
		float[] points = getHexLines(x, y);
		float x0 = points[0];
		float y0 = points[1];
		float x1 = points[2];
		float y1 = points[3];
		float x2 = points[4];
		float y2 = points[5];
		float x3 = points[6];
		float y3 = points[7];
		float x4 = points[8];
		float y4 = points[9];
		float x5 = points[10];
		float y5 = points[11];
		pixmap.drawLine((int) xc, (int) yc, (int) ((x0 + x1) / 2.0f), (int) ((y0 + y1) / 2.0f));
		pixmap.drawLine((int) xc, (int) yc, (int) ((x1 + x2) / 2.0f), (int) ((y1 + y2) / 2.0f));
		pixmap.drawLine((int) xc, (int) yc, (int) ((x2 + x3) / 2.0f), (int) ((y2 + y3) / 2.0f));
		pixmap.drawLine((int) xc, (int) yc, (int) ((x3 + x4) / 2.0f), (int) ((y3 + y4) / 2.0f));
		pixmap.drawLine((int) xc, (int) yc, (int) ((x4 + x5) / 2.0f), (int) ((y4 + y5) / 2.0f));
		pixmap.drawLine((int) xc, (int) yc, (int) ((x5 + x0) / 2.0f), (int) ((y5 + y0) / 2.0f));
	}

	private void drawAqueduct() {
		if (pixmap == null) {
			pixmapImage = this.createImage(600, 500);
			pixmap = pixmapImage.getGraphics();
		}
		pixmap.setColor(Color.white);
		pixmap.fillRect(0, 0, 600, 500);
		if (sourceSelected)
			getPossibleTargets();
		Point p = findCellType(SOURCE);
		if (p != null) {
			int x = p.x;
			int y = p.y;
			flood(x, y, new Object[] { SOURCE, DRAIN, CHANNEL });
			for (y = 0; y < boardHeight(board); y++) {
				for (x = 0; x < boardWidth(board); x++) {
					if (isCell(board, x, y, GROUND)) {
						drawGround(x, y);
					} else if (isCell(board, x, y, SOURCE)) {
						drawSource(x, y);
					} else if (isCell(board, x, y, DRAIN)) {
						drawDrain(x, y);
					} else if (isCell(board, x, y, CHANNEL)) {
						drawChannel(x, y);
					} else if (isCell(board, x, y, PLAYER)) {
						drawPlayer(x, y);
					}
					drawHexOutline(x, y);
				}
			}
			Vector v = findAllCellType(DRAIN);
			int count = 0;
			for (int i = 0; i < v.size(); i++) {
				p = (Point) v.elementAt(i);
				if (filled[p.y][p.x])
					count++;
			}
			if (count == v.size()) {
				pixmap.setFont(new Font("TimesRoman", Font.PLAIN, 24));
				pixmap.setColor(Color.red);
				pixmap.drawString("You have won!", 100, 100);
			}
		}
	}

	private static final String[] test = { "                ", "                ", "                ",
			"                ", "         S      ", "    ..   C      ", "    C..C.C      ", "    .C .PC.     ",
			"    .. .C..     ", "      C....     ", "       DC D     ", "                ", "                ",
			"                ", "                ", "                " };

	// a solution for test: 341331144545633344412653362213

	// loads a new map template
	private void onLoad() {
		// TODO
	}

	private void onSave() {
		// TODO
	}

	// sets the current displayed map to the map template
	private void onEdit() {
		mode = EDIT;
		board = boardTemplate;
		sourceSelected = false;
		panel.repaint();
	}

	// copies the map template and sets the copy to the current displayed map
	private void onPlay() {
		mode = PLAY;
		board = copyArray(boardTemplate);
		sourceSelected = false;
		panel.repaint();
	}

	// changes the cell type on left mouse click to GROUND
	private void onGround() {
		selectedCellType = GROUND;
	}

	// changes the cell type on left mouse click to CHANNEL
	private void onChannel() {
		selectedCellType = CHANNEL;
	}

	// changes the cell type on left mouse click to SOURCE
	private void onSource() {
		selectedCellType = SOURCE;
	}

	// changes the cell type on left mouse click to DRAIN
	private void onDrain() {
		selectedCellType = DRAIN;
	}

	// changes the cell type on left mouse click to PLAYER
	private void onPlayer() {
		selectedCellType = PLAYER;
	}

	// shows a help text
	private void onHelp() {
		// TODO
		/*
		 * (capi:display-message "Aqueduct V 0.2
		 * 
		 * You are the red circle. Move with the number keys:
		 * 
		 * 1: bottom left 2: bottom 3: bottom right 4: top left 5: top 6: top
		 * right
		 * 
		 * You can move with the mouse, to. Just click in an empty field to
		 * move, or you can click a channel and then all possible destinations
		 * are calculated, which you can select with a second click. If you want
		 * to select another source, click again a channel.
		 * 
		 * The goal is to connect all green circles to the white circle by
		 * moving around the channel pieces.
		 * 
		 * A solution for the initial displayed map:
		 * 341331144545633344412653362213
		 * 
		 * Load: Loads a level and starts the play mode. Every time you click on
		 * 'Play', the loaded level is restored.
		 * 
		 * Save: Saves an edited level.
		 * 
		 * Edit: Switches to edit mode. Select the piece you want to place by
		 * clicking to 'Ground', 'Channel', 'Source', 'Drain' or 'Player' and
		 * place it by left clicking in the map. Right click deletes a piece.
		 * You can test the level by clicking on 'Play'. If you want to change
		 * it, click again on 'Edit'."))
		 */
	}

	// initialization after interface construction
	public Aqueduct() {
		panel = new Panel() {
			public void paint(Graphics g) {
				drawAqueduct();
				g.drawImage(pixmapImage, 0, 0, this);
			}

			public void update(Graphics g) {
				paint(g);
			}
		};
		panel.addMouseListener(new MouseAdapter() {
			public void mouseClicked(MouseEvent e) {
				switch (e.getModifiers()) {
				case InputEvent.BUTTON1_MASK: {
					onButton1Press(e.getX(), e.getY());
					break;
				}
				case InputEvent.BUTTON3_MASK: {
					onButton1Press(e.getX(), e.getY());
					break;
				}
				}
			}
		});
		initBoardTemplate(test);
		onPlay();
		panel.repaint();
	}

	// key handling
	public boolean keyDown(Event evt, int key) {
		key -= 49;
		if (key <= 5 && key >= 0) {
			int[] direction = directions[key];
			int dx = direction[0];
			int dy = direction[1];
			onMove(dx, dy);
		}
		return super.keyDown(evt, key);
	}

	public boolean handleEvent(Event e) {
		if (e.id == Event.ACTION_EVENT) {
			if (e.target == playButton) {
				onPlay();
			} else if (e.target == editButton) {
				onEdit();
			} else if (e.target == groundButton) {
				onGround();
			} else if (e.target == channelButton) {
				onChannel();
			} else if (e.target == sourceButton) {
				onSource();
			} else if (e.target == drainButton) {
				onDrain();
			} else if (e.target == helpButton) {
				onHelp();
			}
		}
		return super.handleEvent(e);
	}
}