import java.applet.Applet;
import java.awt.*;
import java.io.DataInputStream;
import java.net.*;
import java.util.*;

public class Sokoban extends Applet {
	private Button undoButton = new Button("Zug zurücknehmen");
	private Button restartButton = new Button("Level neu anfangen");
	private Button nextButton = new Button("Nächster Level");
	private Button previousButton = new Button("Vorheriger Level");
	private Button helpButton = new Button("Spielregeln");
	private Button solutionButton = new Button("Lösung ansehen");
	private boolean solution;
	private SolutionViewer solutionViewer;
	private SokobanField field;
	private int currentLevel;
	private int levelCount;
	private Vector levels;
	private Label levelCountLabel = new Label("Level:", Label.CENTER);
	private Label moveCountLabel = new Label("Züge:", Label.CENTER);
	private Label highscoreLabel = new Label("Beste Lösung:", Label.CENTER);

	public Image empty;
	public Image target;
	public Image box;
	public Image player;
	public Color back;

	private Button startButton = new Button("Spiel starten");
	private Panel center;
	private Panel bottom;
	private GridBagConstraints centerConstraints;

	private boolean fieldVisible;

	// geladene PHP-Informationen

	// Liste der besten Spieler für die Levels: key = Integer 0..n, value = String
	private Hashtable levelBestName = new Hashtable();

	// Anzahl Züge der besten Spieler für die Levels: key = Integer 0..n, value = Integer
	private Hashtable levelBestCount = new Hashtable();

	// Lösungszwischenspeicher für ein Level: key = Integer 0..n, value = String
	private Hashtable solutionCache = new Hashtable();

	/**
	 * Liest ein param als String ein.
	 */
	private String getStringParam(String name, String defaultValue) {
		String param = getParameter(name);
		if (param == null)
			return defaultValue;
		return param;
	}

	/**
	 * Liest ein param als int ein, wobei die Zahlenbasis angegeben werden kann.
	 */
	private int getInt(String name, int def, int radix) {
		String param = getParameter(name);
		return param == null ? def : Integer.parseInt(param, radix);
	}

	// Bild holen und warten, bis es geladen ist
	private Image loadImage(String imageName) {
		// Bild holen
		Image image = getImage(getDocumentBase(), imageName);

		// ein MediaTracker-Objekt anlegen und das Bild davon kontrollieren lassen
		MediaTracker tracker = new MediaTracker(this);
		tracker.addImage(image, 0);

		// solange warten, bis das Bild vollständig geladen ist
		try {
			tracker.waitForID(0);
		} catch (InterruptedException e) {
		}

		// Referenz auf das Bild zurückliefern
		return image;
	}

	// Applet initialisieren
	public void init() {
		removeAll();

		back = new Color(getInt("background", 0xc0c0c0, 16));
		setBackground(back);
	}

	private String readFile(String fileName) {
		Cursor old = getCursor();
		setCursor(Cursor.getPredefinedCursor(Cursor.WAIT_CURSOR));
		String result = null;
		try {
			URL url = new URL(getDocumentBase(), fileName);
			DataInputStream file = new DataInputStream(url.openStream());
			String s = null;
			result = "";
			while ((s = file.readLine()) != null)
				result += s;
			System.out.println(result);
		} catch (Exception e) {
			result = null;
		}
		setCursor(old);
		return result;
	}

	// Applet initialisieren
	public void init2() {
		// Layout für das Applet ändern
		setLayout(new BorderLayout());

		// Titel
		Label title = new Label("Sokoban", Label.CENTER);
		title.setFont(new Font("Sans Serif", Font.BOLD, 32));
		add(BorderLayout.NORTH, title);

		// die Toolbar erzeugen und im Norden des Applets positionieren
		Panel toolbar = new Panel(new GridLayout(0, 1));
		Font f = new Font("Sans Serif", Font.PLAIN, 12);
		undoButton.setFont(f);
		restartButton.setFont(f);
		nextButton.setFont(f);
		previousButton.setFont(f);
		helpButton.setFont(f);
		solutionButton.setFont(f);

		undoButton.setEnabled(false);
		restartButton.setEnabled(false);
		nextButton.setEnabled(false);
		previousButton.setEnabled(false);
		solutionButton.setEnabled(false);

		startButton.setFont(new Font("Sans Serif", Font.PLAIN, 32));

		toolbar.add(undoButton);
		toolbar.add(restartButton);
		toolbar.add(nextButton);
		toolbar.add(previousButton);
		toolbar.add(helpButton);
		toolbar.add(solutionButton);
		add(BorderLayout.WEST, toolbar);

		bottom = new Panel(new FlowLayout());
		f = new Font("Sans Serif", Font.BOLD, 12);
		moveCountLabel.setFont(f);
		bottom.add(moveCountLabel);
		levelCountLabel.setFont(f);
		bottom.add(levelCountLabel);
		highscoreLabel.setFont(f);
		bottom.add(highscoreLabel);
		add(BorderLayout.SOUTH, bottom);
		highscoreLabel.setText("Highscore: ");

		// loading levels
		String levelfile = getStringParam("levelfile", "level.txt");
		levelCount = 0;
		levels = new Vector();

		try {
			URL url = new URL(getDocumentBase(), levelfile);
			DataInputStream file = new DataInputStream(url.openStream());
			int maxX = 0;
			int maxY = 0;
			while (true) {
				Vector level = new Vector();
				String s = null;
				int width = 0;
				int height = 0;
				while ((s = file.readLine()) != null && s.length() != 0) {
					level.addElement(s);
					if (s.length() > width)
						width = s.length();
					height++;
				}
				if (height == 0)
					break;
				levels.addElement(new SokobanLevel(width, height, level));
				if (maxX < width)
					maxX = width;
				if (maxY < height)
					maxY = height;
				levelCount++;
			}
			file.close();
		} catch (Exception e) {
			e.printStackTrace();
		}

		// Highscore laden
		String highscore = readFile("highscore.php");
		if (highscore != null) {
			StringTokenizer t = new StringTokenizer(highscore, "\t");
			while (t.hasMoreTokens()) {
				Integer level = new Integer(t.nextToken());
				String name = t.nextToken();
				Integer count = new Integer(t.nextToken());
				levelBestName.put(level, name);
				levelBestCount.put(level, count);
			}
		}

		empty = loadImage(getStringParam("empty", "empty.gif"));
		target = loadImage(getStringParam("target", "target.gif"));
		box = loadImage(getStringParam("box", "box.gif"));
		player = loadImage(getStringParam("player", "player.gif"));
		field = new SokobanField(this);
		currentLevel = 0;
		center = new Panel(new GridBagLayout());
		centerConstraints = new GridBagConstraints();
		;
		add(BorderLayout.CENTER, center);
		showStartButton();
	}

	// testet die Schaltflächen
	public boolean handleEvent(Event evt) {
		// nur näher testen, wenn die Nachricht ACTION_EVENT aufgetreten war
		if (evt.id == Event.ACTION_EVENT) {
			if (evt.target == undoButton) {
				field.undo();
			} else if (evt.target == restartButton) {
				if (!fieldVisible)
					previousLevel();
				startLevel();
			} else if (evt.target == startButton) {
				center.removeAll();
				center.add(field, centerConstraints);
				validate();
				repaint();
				field.repaint();
				startLevel();
			} else if (evt.target == nextButton) {
				if (fieldVisible)
					nextLevel();
				startLevel();
			} else if (evt.target == previousButton) {
				if (!fieldVisible)
					previousLevel();
				previousLevel();
				startLevel();
			} else if (evt.target == helpButton) {
				SokobanHelpDialog.showIt(
					"Sokoban Spielregeln",
					"Ziel des Spiels ist es, alle Käsestücke\n"
						+ "in die grau umrandeten Felder zu\n"
						+ "verschieben. \n"
						+ "\n"
						+ "Dazu steuern Sie die Maus\n"
						+ "mit den Cursortasten.\n");
			} else if (evt.target == solutionButton) {
				onSolution();
			}
		}
		updateLabels();

		// Status der Nachrichten-Behandlungs-Methode der Basisklasse zurückliefern
		return super.handleEvent(evt);
	}

	private void onSolution() {
		if (solution) {
			undoButton.setEnabled(true);
			restartButton.setEnabled(true);
			nextButton.setEnabled(true);
			previousButton.setEnabled(true);
			solutionButton.setLabel("Lösung ansehen");
			solutionViewer.setStop(true);
			solution = false;
		} else {
			undoButton.setEnabled(false);
			restartButton.setEnabled(false);
			nextButton.setEnabled(false);
			previousButton.setEnabled(false);
			solutionButton.setLabel("Stop");
			resetLevel();
			solutionViewer =
				new SolutionViewer(
					this,
					readFile("load.php?level=" + currentLevel).trim());
			solution = true;
		}
	}

	public void moveByChar(char c) {
		switch (c) {
			case 'l' :
				field.move(-1, 0);
				break;
			case 'r' :
				field.move(1, 0);
				break;
			case 'u' :
				field.move(0, -1);
				break;
			case 'd' :
				field.move(0, 1);
				break;
		}
		updateLabels();
	}

	public void resetLevel() {
		field.getLevel().restart();
		repaint();
	}

	public boolean keyDown(Event evt, int key) {
		if (fieldVisible && !solution) {
			switch (key) {
				case Event.UP :
					field.move(0, -1);
					break;
				case Event.DOWN :
					field.move(0, 1);
					break;
				case Event.LEFT :
					field.move(-1, 0);
					break;
				case Event.RIGHT :
					field.move(1, 0);
					break;
			}
			if (evt.shiftDown()) {
				switch (key) {
					case Event.F1 :
						SokobanHelpDialog.showIt(
							"Bisherige Züge",
							field.getLevel().getMoves());
						break;
					case Event.F2 :
						SokobanHelpDialog.showIt(
							"Eingabe der Züge, bestätigen mit Shift-F3",
							"");
						break;
					case Event.F3 :
						String result = SokobanHelpDialog.textarea.getText();
						startLevel();
						for (int i = 0; i < result.length(); i++)
							moveByChar(result.charAt(i));
						break;
				}
			}
			updateLabels();
			if (field.getLevel().isEnd()) {
				won();
			}
		}
		return true;
	}

	private void won() {
		Integer cl = new Integer(currentLevel);
		Integer count = (Integer) levelBestCount.get(cl);
		int c = 1 << 30;
		if (count != null)
			c = count.intValue();
		SokobanLevel level = field.getLevel();
		int currentCount = level.getMoveCount();
		if (currentCount < c) {
			InputDialog d = new InputDialog();
			d.show();
			String name = d.getName().trim();
			if (name.length() > 0) {
				Cursor old = getCursor();
				setCursor(Cursor.getPredefinedCursor(Cursor.WAIT_CURSOR));
				String moves = level.getMoves();
				String result =
					readFile(
						"save.php?level="
							+ cl.toString()
							+ "&name="
							+ URLEncoder.encode(name)
							+ "&moves="
							+ moves);
				String mark = "save.php result: ";
				setCursor(old);
				if (result.indexOf(mark + "ok") >= 0) {
					SokobanHelpDialog.showIt(
						"Sokoban",
						"Die neue Lösung wurde\n"
							+ "auf dem Server gespeichert\n");
					levelBestCount.put(cl, new Integer(moves.length()));
					levelBestName.put(cl, name);
				} else if (result.indexOf(mark + "invalid") >= 0) {
					SokobanHelpDialog.showIt(
						"Sokoban",
						"Die Lösung ist nicht\n"
							+ "gültig. Die Ursache dafür\n"
							+ "können andere Leveldaten\n"
							+ "auf dem Server als im\n"
							+ "Applet sein.\n"
							+ result);
				} else {
					SokobanHelpDialog.showIt(
						"Sokoban",
						"Ungültige Serverrückmelung.\n"
							+ "Eine Speicherung der\n"
							+ "Lösung ist nicht\n"
							+ "garantiert.\n"
							+ result);
				}

				setCursor(old);
			}
		}
		nextLevel();
		showStartButton();
	}

	public void startLevel() {
		undoButton.setEnabled(true);
		restartButton.setEnabled(true);
		nextButton.setEnabled(true);
		previousButton.setEnabled(true);
		solutionButton.setEnabled(
			levelBestName.get(new Integer(currentLevel)) != null);

		startButton.setLabel("Nächsten Level spielen");
		fieldVisible = true;
		field.show((SokobanLevel) levels.elementAt(currentLevel));
		field.requestFocus();
		validate();
		repaint();
		field.repaint();
		bottom.repaint();
	}

	private void showStartButton() {
		fieldVisible = false;
		center.removeAll();
		center.add(startButton, centerConstraints);
		validate();
		repaint();
	}

	private void nextLevel() {
		if (++currentLevel == levelCount)
			currentLevel = 0;
		field.show((SokobanLevel) levels.elementAt(currentLevel));
	}

	private void previousLevel() {
		if (currentLevel-- == 0)
			currentLevel = levelCount - 1;
		field.show((SokobanLevel) levels.elementAt(currentLevel));
	}

	private void updateLabels() {
		if (field != null && fieldVisible && field.getLevel() != null) {
			Integer cl = new Integer(currentLevel);
			String name = (String) levelBestName.get(cl);
			Integer count = (Integer) levelBestCount.get(cl);
			String solutionText = "Beste Lösung: ";
			if (name != null) {
				solutionText += name + ", " + count.toString() + " Züge";
			} else {
				solutionText += "keine gespeichert";
			}

			moveCountLabel.setText(
				"Züge: " + String.valueOf(field.getLevel().getMoveCount()));
			levelCountLabel.setText(
				"Level: " + String.valueOf(currentLevel + 1));
			highscoreLabel.setText(solutionText);
			moveCountLabel.invalidate();
			levelCountLabel.invalidate();
			highscoreLabel.invalidate();
			bottom.validate();
			bottom.invalidate();
			bottom.repaint();
		}
	}

	public void paint(Graphics g) {
		super.paint(g);
		if (field == null) {
			g.setFont(new Font("Sans Serif", Font.BOLD, 20));
			g.drawString("Applet wird geladen. Bitte warten...", 100, 80);
			init2();
		}
	}
}
