// Program: impuzzle.java
// Purpose: This program implements an image puzzle
// Author : Chunyen Liu

import java.awt.*;
import java.awt.image.*;
import java.awt.event.*;
import java.applet.*;
import java.net.*;
import java.io.*;
import java.util.*;

public class impuzzle extends Applet implements Runnable
   {
   Image photo;
   Thread t = null;
   int width, height;
   String picfile;
   int picwidth, picheight;
   Color bg = new Color(200, 200, 200);
   Color fg = Color.black;
   int row = 4, col = 4;
   Button buttonNew, buttonSolve;
   impuzzlephoto rp;
   AudioClip moveaudio;
   String movefile; 
   String NewGameText = "New", SolvePuzzleText = "Solve";
         
   public void init()
      {
      super.init();
      int[] ints;

      // set up dimensions
      width = size().width;
      height = size().height;

      // read params
      picfile = getParameter("photo");   
      movefile = getParameter("movefile");
      row = new Integer(getParameter("row")).intValue();
      col = new Integer(getParameter("col")).intValue();  

      // set up audio files
      moveaudio = getAudioClip(getDocumentBase(), movefile);

      // set up the main display area
      readPhoto(picfile);
      rp = new impuzzlephoto(photo, picwidth, picheight, row, col, this);   
      rp.bg = bg;
      rp.fg = fg;
      rp.moveaudio = moveaudio;  
      rp.repaint();

      // control panel
      Panel ct = new Panel();  
      buttonNew = new Button(NewGameText);     
      buttonSolve = new Button(SolvePuzzleText);
      ct.add(buttonNew);
      ct.add(buttonSolve);

      // combine all panels 
      setBackground(bg);
      setForeground(fg);  
      GridBagLayout gbl = new GridBagLayout();
      GridBagConstraints gbc = new GridBagConstraints();
      gbc.fill = GridBagConstraints.NONE;
      gbc.gridwidth = GridBagConstraints.REMAINDER;
      gbc.anchor = GridBagConstraints.CENTER;
      gbl.setConstraints(ct, gbc);
      gbl.setConstraints(rp, gbc);        
      add(ct);
      add(rp);
      resize(width, height);           
      }

   public void start()
      {
      if (t == null) 
         {
         t = new Thread(this);
         t.start();
         }
      }

   public void stop()
      {
      if (t != null && t.isAlive())
         t.stop();
      t = null;
      photo =null;
      rp = null;
      }

   public void run()
      {
      while (true)
         {
         try
            {
            Thread.sleep(1000);
            }
         catch (InterruptedException e) {}
         }
      }

   // read an image file
   public void readPhoto(String file)
      {
      showStatus("Reading image " + file);
      photo = null;
      photo = getImage(getDocumentBase(), file);
      picwidth = picheight = -1;
      while(picwidth < 0 || picheight < 0)
         {
         picwidth = photo.getWidth(this);
         picheight = photo.getHeight(this);
         try
            {
            Thread.sleep(20);
            }
         catch(Exception e) {}
         }
      showStatus("Photo " + file + " (" + picwidth + "x" + picheight + 
        ") ready");
      }

   public void update(Graphics g)
      {
      paint(g);
      }

   public void paint(Graphics g)
      {
      g.setColor(bg);
      g.draw3DRect(0, 0, width - 1, height - 1, true); 
      g.draw3DRect(1, 1, width - 3, height - 3, true);     
      }

   public Insets insets() 
      {
      return new Insets(0, 0, 0, 0);
      }

    // dimensions
   public Dimension minimizeSize()
      {
      return new Dimension(width, height);
      }
   public Dimension preferredSize()
      {
      return new Dimension(width, height);
      }

   // process button actions
   public boolean action(Event event, Object object)
      {
      repaint();
      if (event.target == buttonNew)
         {   
         rp.rescramble();
         rp.moves = 0;
         rp.gameover = false;
         return true;
         }
      else if (event.target == buttonSolve)
         {
         rp.resetPositions();
         rp.moves = 0;
         rp.gameover = false;
         return true;
         }
      return false;
      }
   }


// class to store and scale a photo
class impuzzlephoto extends Canvas 
   {
   Image oi;
   Graphics og;
   Image photo;
   Color bg, fg;
   Applet app;
   int width, height, w, h, row, col;
   int[] data;
   Image [] pieces;
   boolean gameover = false;
   int [] order;
   int moves = 0, movex = -1, movey = -1;
   AudioClip moveaudio;
   String MovesCountText = "Count:", FinishText = "Great Job!";

   public impuzzlephoto(Image photo, int width, int height, int row, int col, Applet app)
      {
      this.photo = photo;
      this.app = app;
      this.width = width;
      this.height = height;
      this.row = row;
      this.col = col;      
      resize(width, height);
      data = new int[width * height];
      grabPixels();
      makePieces(row, col);
      rescramble();
      }

   // make each puzzle piece   
   public void makePieces(int row, int col)
      {
      int [] tt;
      int idx = 0, xl = 0, yl = 0;
      int r = 0, g = 0, b = 0;
      int ww = -1, hh = -1;

      this.row = row;
      this.col = col;
      w = width / col;
      h = height / row;
      pieces = null;
      pieces = new Image[row * col];
      order = null;
      order = new int[row * col];
      for (int i = 0; i < row * col; i++)
         {
         tt = new int[w * h];        
         xl = (i % col) * w;
         yl = (int)(i / col) * h;
         for (int y = 0; y < h; y++)
            for (int x = 0; x < w; x++)
               {           
               idx = (y + yl) * width + (x + xl);
               r = (data[idx] & 0x00FF0000) >> 16;
               g = (data[idx] & 0x0000FF00) >> 8;
               b = data[idx] & 0x000000FF;
               tt[y * w + x] = (new Color(r,g,b)).getRGB();
               }
         pieces[i] =  createImage(new MemoryImageSource(w, h, tt, 0, w));
         ww = hh = -1;
         while (ww < 0 || hh < 0)
            {
            ww = pieces[i].getWidth(this);
            hh = pieces[i].getHeight(this);
            }
         order[i] = i;
         }
      }

   // restore all the puzzle positions   
   public void resetPositions()
      { 
      for (int i = 0; i < row * col; i++)
         order[i] = i;
      repaint();
      }

   // rescramble the puzzle
   public void rescramble()
      {
      int p = 0, q = 0, tem = 0;
      int total = row * col;

      for (int i = 0; i < total; i++)
         { 
         p = (int)((Math.round(Math.random() * 10 * total * total)) % total); 
         q = (int)((Math.round(Math.random() * 10 * total * total)) % total); 
         if (p == q) q = (p + 1) % total;
         tem = order[p];
         order[p] = order[q];
         order[q] = tem;
         }
      repaint();
      }

   public void update(Graphics g)
      {
      paint(g);
      }

   // grab pixels into a 1D array
   private void grabPixels()
      {
      PixelGrabber imagegrabber;

      imagegrabber = new PixelGrabber(photo, 0, 0, width, height,
         data, 0, width);
      try
         {
         imagegrabber.grabPixels();
         }
      catch(InterruptedException e) {}
      }

   // dimensions
   public Dimension minimizeSize()
      {
      return new Dimension(width, height);
      }
   public Dimension preferredSize()
      {
      return new Dimension(width, height);
      }

   public Insets insets() 
      {
      return new Insets(0, 0, 0, 0);
      }

   public void paint(Graphics g)
      {
      int xl = 0, yl = 0;
      int total = row * col;

      // create an offscreen buffer
      if (og == null)
         {
         oi = createImage(width, height);
         og = oi.getGraphics();
         }

      // clean up the puzzle display
      og.setColor(bg);
      og.fillRect(0, 0, width, height);

      // draw each puzzle piece
      for (int i = 0; i < total; i++)
         {         
         if (order[i] != total - 1)
            {
            xl = (i % col) * w;
            yl = (int)(i / col) * h;
            og.drawImage(pieces[order[i]], xl, yl, this);

            // draw the border for each piece
            og.setColor(Color.white);
            og.drawRect(xl, yl, w - 1, h - 1);           
            }
         }

      // show the number of moves
      if (movex > -1 && movey > -1) 
         {
         og.setFont(new Font("Helvetica", Font.BOLD, 12));
         String s = MovesCountText + moves;
         og.setColor(Color.blue);
         og.drawString(s, movex - 1, movey - 1);
         og.drawString(s, movex - 1, movey + 1);
         og.drawString(s, movex + 1, movey - 1);
         og.drawString(s, movex + 1, movey + 1);
         og.setColor(Color.yellow);
         og.drawString(s, movex, movey);
         }

      // display the finish message
      if (gameover)
         {
         og.setFont(new Font("Helvetica", Font.BOLD, 20));       
         int myx = (width - (og.getFontMetrics()).stringWidth(FinishText)) / 2;
         int myy = height / 2;
         og.setColor(Color.blue);
         og.drawString(FinishText, myx - 1, myy - 1);
         og.drawString(FinishText, myx - 1, myy + 1);
         og.drawString(FinishText, myx + 1, myy - 1);
         og.drawString(FinishText, myx + 1, myy + 1);
         og.setColor(Color.cyan);
         og.drawString(FinishText, myx, myy);
         }

      // display the result image 
      g.drawImage(oi, 0, 0, this);
      }

   // do when mouse is down
   public boolean mouseDown(java.awt.Event evt, int x, int y)
      {
      int xl = 0, yl = 0;
      int target = -1;
      boolean done = false;
      int total = row * col;

      // check if we hit a valid puzzle piece      
      for (int i = 0; i < total; i++)
         {      
         xl = (i % col) * w;
         yl = (int)(i / col) * h;
         if (x >= xl && x < xl + w && y >= yl && y < yl + h)
            {       
            moveaudio.play();     
            target = i;
            break;            
            }
         }
         
      // left neighbor
      if (target > -1 && (target % col) != 0)
         {
         if (order[target - 1] == total - 1)
            {
            order[target - 1] = order[target];
            order[target] = total - 1;
            done = true;
            }
         }

      // right neighbor
      if (!done && target > -1 && (target % col) != col - 1)
         {
         if (order[target + 1] == total - 1)
            {
            order[target + 1] = order[target];
            order[target] = total - 1;
            done = true;
            }
         }

      // top neighbor
      if (!done && target > -1 && target / col  >= 1)
         {
         if (order[target - col] == total - 1)
            {
            order[target - col] = order[target];
            order[target] = total - 1;
            done = true;
            }
         }
 
      // bottom neighbor
      if (!done && target > -1 && target / col < row - 1)
         {
         if (order[target + col] == total - 1)
            {
            order[target + col] = order[target];
            order[target] = total - 1;
            done = true;
            }
         }

      // we have hit a movable puzzle liece
      if (done)
         moves++;
      repaint();

      // do we have a winner?      
      if (moves > 0)
         {
         for (int i = 0; i < total; i++) 
            {
            if (order[i] != i)
               return true;
            }         
         gameover = true;         
         }

      return true;
      }

   // do when mouse is out of bound
   public boolean mouseExit(java.awt.Event evt, int x, int y)
      {
      repaint();
      movex = -1;
      movey = -1;
      return true;
      }

   // do when mouse is moving in the image
   public boolean mouseMove(java.awt.Event evt, int x, int y)
      {
      movex = x; 
      movey = y;         
      repaint();
      return true;
      }
    }
