/*
Datei............: Aufg0113.java
Projekt..........: Einführung in die Java-Programmierung
Erstellt.........: 04.10.97, Guido Krüger
Geändert.........: --
Aufgabe..........: Musterlösung zu Aufgabe 01.13
Kommentare.......:

Die Anforderungen der Aufgabe wurden wie folgt erfüllt:

1. Die Farbe der Lücke wurde in der Methode paintField geändert.
   Dazu wurde vor dem Aufruf von fillRect mit g.setColor(Color.blue);
   die Füllfarbe auf blau und nach dem Aufruf wieder auf schwarz
   geändert.

2. Um diese Teilaufgabe portabel umzusetzen, wären Textmetriken
   erforderlich, die erst in Kapitel 15 eingeführt werden.
   Wir wollen uns hier darauf beschränken, die Beschrifung um
   eine knappe Spielsteinbreite weiter nach rechts zu setzen.
   Dazu wird in der Methode paintField imm Abschnitt "Beschriftung"
   anstelle der Zeile

   topleft.x + j * fieldsize.width + 2,

   die Zeile

   topleft.x + (j + 1) * fieldsize.width - 16,

   für die x-Koordinate der Textausgabe verwendet. Der Text wird
   dabei eine komplette Spielsteinbreite minus 16 Pixel weiter
   nach rechts gesetzt. Wie erwähnt, ist dieser Ansatz natürlich
   nicht portabel, sondern geht von einer einheitlichen Breite
   der Ausgabezeichen von ungefähr als 16 Pixeln aus.

   Eine verbesserte Variante kann nach Ende von Kapitel 15 erstellt
   werden, wenn die Textmetriken eingeführt sind. Eine weitere
   Verbesserung könnte dann auch darin bestehen, sowohl ein- als
   auch zweistellige Zahlen rechtsbündig auszugeben

3. Zum Verändern der Rahmenbreite wurde die Methode paintBorder
   angepaßt. In einer einfachen for-Schleife werden die beiden
   Rahmenrechtecke je zweimal ausgegeben, wobei das zweite
   Rechteck gegenüber dem ersten um ein Pixel versetzt und
   vergrößert bzw. verkleinert ist.

Anmerkung:

Auch in dieser Musterlösung wurde die ursprüngliche Klasse
Puzzle umbenannt, um ein Kompilieren innerhalb der Datei
Aufg0113.java zu ermöglichen.
*/
/**
 * @(#)Aufg0113.java   1.000 97/10/04
 * @(#)Puzzle.java   1.000 97/07/23
 *
 * Copyright (c) 1997 Guido Krueger. All Rights Reserved.
 *
 * Dieses Applet ist die Implementierung eines Schiebepuzzles
 * mit 4 x 4 Feldern. Auf den zunächst unsortierten Spielsteinen
 * werden die Bestandteile eines Images angezeigt, die dann per
 * Drag & Drop sortiert werden können. Der einzig erlaubte Zug
 * besteht darin, einen Stein in die benachbarte Lücke zu
 * verschieben. Durch Klicken auf den Rahmen kann die Sortierung
 * umgekehrt werden.
 *
 * Das applet-Tag erwartet folgende Parameter:
 *
 * bordersize = Breite des Spielfeldrandes
 * src        = Name der Bilddatei (gif oder jpeg)
 *
 */
import java.awt.*;
import java.awt.event.*;
import java.applet.*;
import java.util.*;

public class Aufg0113
extends Applet
{
   int       aFields[][];      //Brett mit allen Feldern
   Image     image;            //Bildspeicher
   int       bordersize;       //Randbreite
   Dimension fieldsize;        //Größe eines Feldes
   Dimension imagesize;        //Größe des Bildes
   Point     sourcefield;      //Bei Mausklick ausgewähltes Feld
   Point     lastpoint;        //Ursprung des letzten Rechtecks
   Point     drawoffset;       //Offset zur Mausdragposition

   public void init()
   {
      aFields     = new int[4][4];
      sourcefield = new Point(-1, -1);
      lastpoint   = new Point(-1, -1);
      drawoffset  = new Point(0,0);
      bordersize  = Integer.parseInt(getParameter("bordersize"));
      if (bordersize < 1 || bordersize > 50) {
         bordersize = 5;
      }
      setBackground(Color.lightGray);
      addMouseListener(new MyMouseListener());
      addMouseMotionListener(new MyMouseMotionListener());
      prepareImage();
      randomizeField(true);
   }

   public void update(Graphics g)
   {
      Image     dbImage;
      Graphics  dbGraphics;

      //Double-Buffer initialisieren
      dbImage = createImage(getSize().width,getSize().height);
      dbGraphics = dbImage.getGraphics();
      //Hintergrund löschen
      dbGraphics.setColor(getBackground());
      dbGraphics.fillRect(0,0,getSize().width,getSize().height);
      //Vordergrund zeichnen
      dbGraphics.setColor(getForeground());
      paint(dbGraphics);
      //Offscreen-Image anzeigen
      g.drawImage(dbImage,0,0,this);
      dbGraphics.dispose();
   }

   public void paint(Graphics g)
   {
      paintBorder(g);
      paintField(g);
   }

   /**
    * Zeichnet den Rahmen des Spielbretts.
    */
   private void paintBorder(Graphics g)
   {
      Insets insets    = getInsets();
      Dimension size   = getSize();
      size.height      -= insets.top + insets.bottom;
      size.width       -= insets.left + insets.right;
      fieldsize        = new Dimension();
      fieldsize.width  = (size.width - (2 * bordersize)) / 4;
      fieldsize.height = (size.height - (2 * bordersize)) / 4;
      g.setColor(Color.black);
      for (int i = 0; i <= 1 ; ++i) {
        g.drawRect(
           insets.left + i,
           insets.top + i,
           size.width  - 1 - 2 * i,
           size.height - 2 - 2 * i
        );
        g.drawRect(
           insets.left + bordersize - i,
           insets.top  + bordersize - i,
           4 * fieldsize.width + 2 * i,
           4 * fieldsize.height + 2 * i
        );
      }
   }

   /**
    * Zeichnet die Spielsteine auf dem Brett.
    */
   private void paintField(Graphics g)
   {
      int imagenumber, image_i, image_j;
      Insets insets = getInsets();
      Point topleft = new Point();
      topleft.x     = insets.left + bordersize;
      topleft.y     = insets.top  + bordersize;
      g.setColor(Color.black);
      for (int i = 0; i <= 3; ++i) {
         for (int j = 0; j <= 3; ++j) {
            imagenumber = aFields[i][j];
            if (imagenumber == 15) {
               //Lücke zeichnen
               g.setColor(Color.blue);
               g.fillRect(
                  topleft.x + j * fieldsize.width,
                  topleft.y + i * fieldsize.height,
                  fieldsize.width,
                  fieldsize.height
               );
               g.setColor(Color.black);
            } else {
               //Image darstellen
               image_i = imagenumber / 4;
               image_j = imagenumber % 4;
               g.drawImage(
                  image,
                  topleft.x + j * fieldsize.width,
                  topleft.y + i * fieldsize.height,
                  topleft.x + j * fieldsize.width + fieldsize.width,
                  topleft.y + i * fieldsize.height + fieldsize.height,
                  image_j * (imagesize.width / 4),
                  image_i * (imagesize.height / 4),
                  image_j * (imagesize.width / 4) + imagesize.width / 4,
                  image_i * (imagesize.height / 4) + imagesize.height / 4,
                  this
               );
               //Rahmen
               g.drawRect(
                  topleft.x + j * fieldsize.width,
                  topleft.y + i * fieldsize.height,
                  fieldsize.width,
                  fieldsize.height
               );
               //Beschriftung
               g.drawString(
                  "" + imagenumber,
                  topleft.x + (j + 1) * fieldsize.width - 16,
                  topleft.y + i * fieldsize.height + 12
               );
            }
         }
      }
   }

   /**
    * Lädt das Bild.
    */
   private void prepareImage()
   {
      //Bild laden
      image = getImage(getDocumentBase(),getParameter("src"));
      MediaTracker mt = new MediaTracker(this);
      mt.addImage(image, 0);
      try {
         //Warten, bis das Image vollständig geladen ist,
         mt.waitForAll();
      } catch (InterruptedException e) {
         //nothing
      }
      imagesize        = new Dimension();
      imagesize.height = image.getHeight(this);
      imagesize.width  = image.getWidth(this);
   }

   /**
    * Mischt die Steine auf dem Spielfeld.
    */
   private void randomizeField(boolean unordered)
   {
      int i, j, k, tmp;

      //Zuerst sortieren...
      for (i = 0; i <= 15; ++i) {
         aFields[i / 4][i % 4] = i;
      }
      //Dann mischen...
      if (unordered) {
         Random rand = new Random(System.currentTimeMillis());
         for (i = 0; i < 20; ++i) {
            j = Math.abs(rand.nextInt()) % 16;
            k = Math.abs(rand.nextInt()) % 16;
            tmp = aFields[j / 4][j % 4];
            aFields[j / 4][j % 4] = aFields[k / 4][k % 4];
            aFields[k / 4][k % 4] = tmp;
         }
      }
   }

   class MyMouseListener
   extends MouseAdapter
   {
      /**
       * Maustaste gedrückt.
       */
      public void mousePressed(MouseEvent event)
      {
         sourcefield = getFieldFromCursor(event.getX(), event.getY());
         if (sourcefield.x == -1 || sourcefield.y == -1) {
            swapRandomization();
            repaint();
         }
         lastpoint.x = -1;
         lastpoint.y = -1;
      }

      /**
       * Maustaste losgelassen.
       */
      public void mouseReleased(MouseEvent event)
      {
         if (sourcefield.x != -1 && sourcefield.y != -1) {
            Point destfield;
            destfield = getFieldFromCursor(event.getX(), event.getY());
            if (destfield.x != -1 && destfield.y != -1) {
               if (aFields[destfield.y][destfield.x] == 15) {
                  if (areNeighbours(sourcefield, destfield)) {
                     aFields[destfield.y][destfield.x] =
                        aFields[sourcefield.y][sourcefield.x];
                     aFields[sourcefield.y][sourcefield.x] = 15;
                  }
               }
            }
            repaint();
         }
         sourcefield.x = -1;
         sourcefield.y = -1;
      }

      /**
       * Liefert den zur Mausposition passenden horizontalen und
       * vertikalen Index des darunterliegenden Steins. Liegt der
       * Punkt auf dem Rahmen, wird (-1,-1) zurückgegeben.
       */
      private Point getFieldFromCursor(int x, int y)
      {
         Insets insets = getInsets();
         Point topleft = new Point();
         topleft.x     = insets.left + bordersize;
         topleft.y     = insets.top  + bordersize;
         Point ret     = new Point(-1, -1);
         if (x >= topleft.x) {
            if (x < topleft.x + 4 * fieldsize.width) {
               if (y >= topleft.y) {
                  if (y < topleft.y + 4 * fieldsize.height) {
                     ret.x = (x - topleft.x) / fieldsize.width;
                     ret.y = (y - topleft.y) / fieldsize.height;
                     drawoffset.x = x - topleft.x -
                                    ret.x * fieldsize.width;
                     drawoffset.y = y - topleft.y -
                                    ret.y * fieldsize.height;
                  }
               }
            }
         }
         return ret;
      }

      /**
       * Testet, ob die durch p1 und p2 bezeichneten Spielsteine
       * Nachbarn sind.
       */
      private boolean areNeighbours(Point p1, Point p2)
      {
         int aNeighbours[][] = {{-1,0},{0,-1},{0,1},{1,0}};
         for (int i = 0; i < aNeighbours.length; ++i) {
            if (p1.x + aNeighbours[i][0] == p2.x) {
               if (p1.y + aNeighbours[i][1] == p2.y) {
                  return true;
               }
            }
         }
         return false;
      }

      /**
       * Kehrt die Steineordnung um: falls sie sortiert sind,
       * werden sie gemischt und umgekehrt.
       */
      private void swapRandomization()
      {
         //Sind die Felder sortiert?
         boolean sorted = true;
         for (int i = 0; i <= 15; ++i) {
            if (aFields[i / 4][i % 4] != i) {
               sorted = false;
               break;
            }
         }
         //Neu mischen bzw. sortieren
         randomizeField(sorted);
      }
   }

   class MyMouseMotionListener
   extends MouseMotionAdapter
   {
      /**
       * Maus wurde bei gedrückter Taste bewegt.
       */
      public void mouseDragged(MouseEvent event)
      {
         if (sourcefield.x != -1 && sourcefield.y != -1) {
            Graphics g = getGraphics();
            g.setXORMode(getBackground());
            g.setColor(Color.black);
            //Das zuletzt gezeichnete Rechteck entfernen
            if (lastpoint.x != -1) {
               g.drawRect(
                  lastpoint.x - drawoffset.x,
                  lastpoint.y - drawoffset.y,
                  fieldsize.width,
                  fieldsize.height
               );
            }
            //Neues Rechteck zeichnen
            g.drawRect(
               event.getX() - drawoffset.x,
               event.getY() - drawoffset.y,
               fieldsize.width,
               fieldsize.height
            );
            lastpoint.x = event.getX();
            lastpoint.y = event.getY();
            g.dispose();
         }
      }
   }
}
