/*
Datei............: Aufg2018.java
Projekt..........: Einführung in die Java-Programmierung
Erstellt.........: 09.12.97, Guido Krüger
Geändert.........: --
Aufgabe..........: Musterlösung zu Aufgabe 20.18
Kommentare.......:

Dieses Programm besteht aus zwei Klassen Aufg2018 und TrafficLight.
Während TrafficLight das Ampelelement implementiert, stellt Aufg2018 
die Testumgebung zur Verfügung. Dazu wird zunächst ein Fenster erzeugt,
auf dem die Ampeln angezeigt werden können. Anschließend wird das
Fenster skaliert und insgesamt MAXLIGHTS Ampeln erzeugt und gleichmäßig 
auf dem Fenster plaziert. Danach wird aus der Fensterklasse ein neuer
Thread erzeugt, mit dessen Hilfe alle 2 Sekunden eine der drei Ampelaktivitäten
red(), green() oder blink() für ein zufällig ausgewähltes Ampelelement
aufgerufen wird. Da die Ampeln selbst einen eigenen Thread verwenden,
beeinflussen sich auch bei einer Vielzahl von Ampeln die Schaltzeiten
nur unwesentlich.
*/
import java.awt.*;
import java.awt.event.*;
import java.io.*;

public class Aufg2018
extends Frame
implements Runnable
{
  //Konstanten
  static final int MAXLIGHTS = 10;

  //Instanzmerkmale 
  Thread th;
  TrafficLight lights[];

  public static void main(String[] args)
  {
	Aufg2018 wnd = new Aufg2018();
	wnd.setVisible(true);
  }

  public Aufg2018()
  {
	super("Aufg2018");
	setBackground(Color.lightGray);
	//WindowListener registrieren
	addWindowListener(
	  new WindowAdapter() {
        public void windowClosing(WindowEvent event) {
		  if (th != null) {
			th.stop();
		  }
		  System.exit(0);
		}
	  }
    );
	//Testanordnung erstellen und Testthread starten
	setSize(500, 150);
	setLayout(null);
	lights = new TrafficLight[MAXLIGHTS];
	int lightwidth = getSize().width / MAXLIGHTS;
	for (int i = 0; i < MAXLIGHTS; ++i) {
	  lights[i] = new TrafficLight();
	  add(lights[i]);
	  lights[i].setBounds(lightwidth / 2 + lightwidth * i, 50, 20, 60);
	}
	th = new Thread(this);
	th.start();
  }

  public void run()
  {
	while (true) {
	  try { Thread.sleep(2000); } catch (InterruptedException e) {}
	  int light = (int)(Math.random() * MAXLIGHTS);
	  int action = (int)(Math.random() * 3);
	  switch (action) {
	  case 0: 
		System.out.println("Schalte Ampel " + (light + 1) + " auf gruen");
		lights[light].green();
		break;
	  case 1:
		System.out.println("Schalte Ampel " + (light + 1) + " auf blinkend");
		lights[light].blink();
		break;
	  case 2:
		System.out.println("Schalte Ampel " + (light + 1) + " auf rot");
		lights[light].red();
		break;
	  }
	}
  }
}


/**
 * Diese Klasse implementiert ein Dialogelement, das eine Ampel mit den 
 * drei Lampen ROT, GELB und GRÜN repräsentiert. Die Ampel befindet sich
 * stets in einem der drei Zustände ROT, GRÜN oder BLINKEND (gelbes 
 * Blinklicht). Zustandswechsel können durch Aufruf der Methoden red(),
 * green() oder blink() ausgelöst werden. Ein Übergang auf GRÜN schaltet
 * zuvor das rote und gelbe Licht für 2 Sekunden an, ein Übergang auf ROT
 * schaltet vorher das gelbe Licht für 3 Sekunden an. Alle Zustandsübergänge
 * werden in einem eigenen Thread im Hintergrund ausgführt, auch das Blinken
 * erfolgt mit einer Periode von 0,5 Sek. automatisch. 
 *
 * Erreicht wird dies durch einen eigenen Thread, in dem das Ampelelement
 * nach der Instanzierung läuft. Dieser Thread durchläuft ständig eine
 * Schleife, in der im 0,5-Sekunden-Abstand das jeweils nächste Kommando
 * abgearbeitet wird. Steht kein neues Kommando an, so bleibt der aktuelle
 * Darstellungszustand unverändert, andernfalls führt das Kommando zu der
 * oben besprochenen Zustandsänderung. Die Kommunikation zwischen den
 * kommandoerzeugenden Methoden red(), green() und blink() und der 
 * Kommandoschleife erfolgt über die Instanzvariable cmd. Innerhalb 
 * besagter Methoden wird cmd lediglich auf das abzuarbeitende Kommando
 * gesetzt und beim nächsten Schleifendurchlauf reagiert der 
 * Kommandointerpreter darauf. Durch die Verwendung des synchronized-Keywords
 * werden Synchronisationsprobleme durch gleichzeitigen Zugriff auf cmd
 * vermieden. 
 * 
 * Diese Art der Kommunikation ist zwar für unsere Zwecke ausreichend, bei
 * näherer Betrachtung aber nicht vollständig sicher. Es kann beispielsweise
 * passieren, daß während der Abarbeitung eines ROT-GRÜN-Wechsel durch 
 * Aufruf von blink() oder red() ein neues cmd generiert wird. Dieses würde
 * aber am Ende des aktuellen Schleifendurchlaufs wegen der Zuweisung 
 * cmd = CMD_NONE; wieder vernichtet noch bevor es interpretiert wurde. Das
 * wäre dann dem Verlust des Kommandos gleichbedeutend. Es gibt einige
 * Lösungsansätze für dieses Problem, von denen der sauberste die Verwendung
 * einer Kommando-Queue zur Realisierung einer gepufferten Kommunikation
 * zwischen Kommandoerzeugern und Kommandoschleife wäre. 
 * 
 * Die übrigen Architekturmerkmale dieser klasse entsprechen den bekannten
 * Schemata, die bei der Konstruktion eigener Dialogelemente angewendet
 * werden. Die Dokumentationskommentare geben weitere Hinweise.
 */
class TrafficLight
extends Canvas
implements Runnable
{
  //Konstanten
  static final int STATE_OFF       = 0;
  static final int STATE_GREEN     = 1;
  static final int STATE_YELLOW    = 2;
  static final int STATE_RED       = 3;
  static final int STATE_REDYELLOW = 4;

  static final int CMD_NONE        = 0;
  static final int CMD_GREEN       = 1;
  static final int CMD_RED         = 2;
  static final int CMD_BLINK       = 3;

  //Instanzmerkmale
  int state;
  int cmd;

  public TrafficLight()
  {
	state = STATE_RED;
	cmd = CMD_NONE;
	setBackground(Color.darkGray);
	Thread th = new Thread(this);
	th.start();
  }

  /**
   * Liefert die bevorzugte Größe.
   */
  public Dimension getPreferredSize()
  {
	return new Dimension(20, 60);
  }

  /**
   * Liefert die minimale Größe.
   */
  public Dimension getMinimumSize()
  {
	return new Dimension(5, 15);
  }

  /**
   * Wechselt vom aktuellen Zustand nach ROT.
   */
  synchronized public void red()
  {
	cmd = CMD_RED;
  }

  /**
   * Wechselt vom aktuellen Zustand nach GRÜN.
   */
  synchronized public void green()
  {
	cmd = CMD_GREEN;
  }

  /**
   * Wechselt vom aktuellen Zustand nach BLINKEND.
   */
  synchronized public void blink()
  {
	cmd = CMD_BLINK;
  }

  /**
   * Überlagert zur Reduzierung des Bildschirmflimmerns.
   */
  public void update(Graphics g)
  {
	paint(g);
  }

  /**
   * Zeichnet das Dialogelement in Abhängigkeit vom Darstellungsstatus, der
   * in der Instanzvariablen state gespeichert wird.
   */
  public void paint(Graphics g)
  {
	int width = getSize().width;
	int height = getSize().height;
	//Rahmen zeichnen
	g.setColor(Color.black);
	g.drawRect(0, 0, width -1, height - 1);
	//Rote Lampe
	if (state == STATE_RED || state == STATE_REDYELLOW) {
	  g.setColor(Color.red);
	} else {
	  g.setColor(Color.black);
	}
	g.fillOval(1, 1, width - 2, (height / 3) - 2);
	//Gelbe Lampe
	if (state == STATE_YELLOW || state == STATE_REDYELLOW) {
	  g.setColor(Color.yellow);
	} else {
	  g.setColor(Color.black);
	}
	g.fillOval(1, (height / 3) + 1, width - 2, (height / 3) - 2);
	//Grüne Lampe
	if (state == STATE_GREEN) {
	  g.setColor(Color.green);
	} else {
	  g.setColor(Color.black);
	}
	g.fillOval(1, (2 * height / 3) + 1, width - 2, (height / 3) - 2);
  }

  /**
   * Der Hintergrundprozess für die Kommandoschleife.
   */
  public void run()
  {
	boolean lBlinking = false;

	while (true) {
	  //Ampel zeichnen und 0.5 Sek. warten
	  repaint();
	  sleep(500);
	  //Nächstes Kommando bearbeiten
	  if (cmd == CMD_NONE) {
		if (lBlinking) {
		  state = (state == STATE_YELLOW) ? STATE_OFF : STATE_YELLOW;
		}
	  } else if (cmd == CMD_GREEN) {
		if (state != STATE_GREEN) {
		  state = STATE_REDYELLOW;
		  repaint();
		  sleep(2000);
		  state = STATE_GREEN;
		}
		lBlinking = false;
	  } else if (cmd == CMD_RED) {
		if (state != STATE_RED) {
		  state = STATE_YELLOW;
		  repaint();
		  sleep(3000);
		  state = STATE_RED;
		}
		lBlinking = false;
	  } else if (cmd == CMD_BLINK) {
		if (!lBlinking) {
		  state = STATE_OFF;
		  lBlinking = true;
		}
	  }
	  synchronized(this) {
		cmd = CMD_NONE;
	  }
	}
  }

  /**
   * Hält den aktuellen Prozess für die angegebene Zeit an.
   */
  private void sleep(int millis)
  {
	try {	
	  Thread.sleep(millis); 
	} catch (InterruptedException e) { 
	  //nichts
	}
  }
}

