import javax.imageio.ImageIO;
import javax.sound.sampled.AudioInputStream;
import javax.sound.sampled.AudioSystem;
import javax.sound.sampled.Clip;
import javax.sound.sampled.LineUnavailableException;
import javax.sound.sampled.UnsupportedAudioFileException;
import javax.swing.*;
import java.awt.*;
import java.io.IOException;
import java.net.URL;
import java.awt.event.*;
import java.util.Random;

/* This program creates a sliding puzzle game 
 * 
 * Adam Tran
 * Ethan Bennis
 * Graham Plata
 *
 * 10/31/08
 * 
 */

/* JApplet containing one panel, the game frame
 * 
 */

public class Puzzle extends JApplet{
	
	// >>>>>>> The model <<<<<<<
	
	private Frame frame;
	
	// >>>>>>> The view <<<<<<<<
	
	private JButton shuffleButton = new JButton("Shuffle"); //Shuffle Button
	private JLabel message = new JLabel("Click on a piece to move it"); //Instructions
	
	public void init(){
		
		//New array of Image objects, in the init() so that getDocumentBase() is accessible
		Image[] images = new Image[16];

		for(int file = 0; file < 16; file = file + 1){ //For the 16 pieces, 0-15
			 try {
				images[file] = ImageIO.read(getClass().getResource("files/MapOfTheInternet_" + (file + 1) + ".jpg"));
			} catch (IOException e) {
			} //Get the file name and instantiate Images
		}
		
		frame = new Frame(images); //New frame object, pass in array of Images
		
		//Border layout
		setLayout(new BorderLayout());
		
		//Message above
		add(message, BorderLayout.NORTH);
		
		//Print frame in center
		add(frame, BorderLayout.CENTER);
		
		//New button and listener, add to bottom
		shuffleButton.addActionListener(new buttonListener());
		add(shuffleButton, BorderLayout.SOUTH);
		
		resize(720,760); //Set applet size
	}
	
	// >>>>>>> The controller <<<<<<
	
	private class buttonListener implements ActionListener{
		public void actionPerformed(ActionEvent e){
			frame.shuffle(); //Shuffle!
		}
	}
	
}

//-------------------- End of Puzzle class --------------------//  

/* This class creates a frame containing 16 Tiles and holds width and height for one tile
 * 
 */

class Frame extends JPanel{
	
	private Tile[] tiles; //Array of Tile objects
	private int width;
	private int height;
	
	public Frame(Image[] images){
		
		tiles = new Tile[16];
		
		ImageIcon[] imageIcons; //Array of ImageIcons
		imageIcons = new ImageIcon[16];
		
		/* This loop takes an array of Images from the constructor and creates an array of ImageIcons
		 * 
		 */
		
		for(int file = 0; file < 16; file = file + 1){
			imageIcons[file] = new ImageIcon(images[file]);
		}

		/* This loop creates an array of Tile objects using the array of ImageIcons
		 * 
		 */
		
		for(int tile = 0; tile < 16; tile = tile + 1){
				tiles[tile] = new Tile(imageIcons[tile],tile);
		}
		
		//Same width and height from the first tile
		width = tiles[0].getImage().getIconWidth(); 
		height = tiles[0].getImage().getIconHeight();
		
		shuffle(); //Shuffle
		
		addMouseListener(new PanelListener()); //Mouse events
	}
	

	public void paintComponent(Graphics g){
		super.paintComponent(g);

		//Set font type and size
		Font font = new Font("Courier", Font.BOLD, 48);
		g.setFont(font);
		
		/* This loop plots painting position for each tile, like a grid, and paints the tile 
		 * 
		 */
		
		for(int tile = 0; tile < 16; tile = tile + 1){
			tiles[tile].setX(tile % 4 * width); //The X coordinate, much like a grid
			tiles[tile].setY(tile / 4 * height); //Y coordinate, uses div to get row number
			tiles[tile].setLocation(tile); //Set the location of the tile in the array to an instance variable in the Tile object
			tiles[tile].getImage().paintIcon(this, g, tiles[tile].getX(), tiles[tile].getY()); //Paint away
		}
		
		//If the puzzle is solved, print message
		if (puzzleSolved()){
			g.drawString("CONGRATULATIONS!", width, height);
			this.soundEffect(3);
		}
			
	}
	
	/* This method takes a click as an x, y coordinate on the JPanel and converts it to a grid position
	 * 
	 *  1  2  3  4
	 *  5  6  7  8
	 *  9 10 11 12
	 * 13 14 15 16
	 * 
	 */
	
	private int coordToPos(int x, int y){
		int position = 0;
		
		//Split into four columns and test where the click falls 
		if (x < width)
			position = position + 0;
		else if (x < 2 * width)
			position = position + 1;
		else if (x < 3 * width)
			position = position + 2;
		else if (x < 4 * width)
			position = position + 3;
		
		//Split into four rows and test where the click falls
		if (y < height)
			position = position + 0;
		else if (y < 2 * height)
			position = position + 4;
		else if (y < 3 * height)
			position = position + 8;
		else if (y < 4 * height)
			position = position + 12;
		
		return position;
	}
	
	/* These overloaded methods use the integer value of the location of a Tile object in the array to swap Tile reference variables
	 * 
	 */
	
	private void swapTiles(int first, int second){
		swapTiles(first, second, false);
	}
	
	private void swapTiles(int first, int second, boolean sfx){ //If you want a sound effect
		
		Tile temp; //Temporary tile
		
		temp = tiles[first];
		tiles[first] = tiles[second];
		tiles[second] = temp;
		
		if (sfx)
			this.soundEffect(1);
	}
	
	/* This method is executed by the Mouse Adapter. It builds a cross around the click,
	 * tests to see if there is a blank space, and if so, swaps positions with it using the swapTiles method.
	 * 
	 *   x        x
	 * x C x -> x O x 
	 *   O        C
	 *   
	 *   C = Click
	 *   O = Open tile
	 *   
	 *        +4
	 *         ^
	 *         |
	 *   -1 <-- --> +1
	 *         |
	 *         v
	 *        -4
	 *        
	 *  This method uses short-circuit evaluation to avoid null-pointer exception
	 *  
	 */
	
	public void clickAt(int x, int y){
		if (puzzleSolved())
			shuffle();
		int thisTile = coordToPos(x, y); //Convert coordinates of click to a grid coordinate
		if (thisTile + 1 < 16 && tiles[thisTile + 1].getOriginalLocation() == 15) //Check to the right
			swapTiles(thisTile, thisTile + 1, true);
		else if (thisTile - 1 >= 0 && tiles[thisTile - 1].getOriginalLocation() == 15) //Check to the left
			swapTiles(thisTile, thisTile - 1, true);
		else if (thisTile + 4 < 16 && tiles[thisTile + 4].getOriginalLocation() == 15) //Check above
			swapTiles(thisTile, thisTile + 4, true);
		else if (thisTile - 4 >= 0 && tiles[thisTile - 4].getOriginalLocation() == 15) //Check below
			swapTiles(thisTile, thisTile - 4, true);
		else
			this.soundEffect(2);
	}
	
	/* This method tests the tile's original locations through a for loop to determine whether or not the entire puzzle is solved
	 * 
	 */
	
	private boolean puzzleSolved(){
		boolean solved = true;
		for(int tile = 0; tile < 16; tile = tile + 1){
			if (tile != tiles[tile].getOriginalLocation())
				solved = false;
		}
		return solved;
	}
	
	public void soundEffect(int sound)
	{
		Clip clickClip = null;
		try {
			clickClip = AudioSystem.getClip();
		} catch (LineUnavailableException e1) {
		}
		// URL clipURL = Object().getClass().getResource("/data/sounds/sound.wav");
		// (Use if you package your WAV inside a JAR)
		
		URL audio1 = Thread.currentThread().getContextClassLoader().getResource("files/move.wav");
		URL audio2 = Thread.currentThread().getContextClassLoader().getResource("files/badmove.wav");
		URL audio3 = Thread.currentThread().getContextClassLoader().getResource("files/win.wav");
		
		AudioInputStream ais = null;
		switch (sound)
		{	// Determines which sound to play based on parameters
		case 1:
			try {	// Parameter "1" = default move sound
				ais = AudioSystem.getAudioInputStream(audio1);
			} catch (UnsupportedAudioFileException e1) {
			} catch (IOException e1) {
			}
			break;
		case 2:
			try {	// Parameter "2" = bad move sound
				ais = AudioSystem.getAudioInputStream(audio2);
			} catch (UnsupportedAudioFileException e1) {
			} catch (IOException e1) {
			}
			break;
		case 3:
			try {	// Parameter "3" = win sound
				ais = AudioSystem.getAudioInputStream(audio3);
			} catch (UnsupportedAudioFileException e1) {
			} catch (IOException e1) {
			}
			break;
		}
	
		try {
			clickClip.open(ais);
		} catch (LineUnavailableException e1) {
		} catch (IOException e1) {
		}
		clickClip.start();
	}
	
	/* These overloaded methods shuffle the puzzle by moving the blank space in random directions.
	 * Intensity is maximum swaps. 
	 */
	
	public void shuffle(){
		shuffle(100);
	}
	
	public void shuffle(int intensity){
				
		//Instantiate random number generator
		Random gen = new Random();
		
		//Local variables, position of blank tile and random integer from 0-3
		int blankPos = 0;
		int rand;
		
		//This loop returns the position of the blank tile
		for(int tile = 0; tile < 16; tile = tile + 1){
			if (tiles[tile].getOriginalLocation() == 15){
				blankPos = tile;
				break;
			}
		}
		
		//For intensity loops, swap the blank tile in a random direction, or not at all
		for(int count = 0; count < intensity; count = count + 1){
			rand = gen.nextInt(4); //Random integer from 0-3
			
			switch (rand){
			case 0:
				if (blankPos + 1 < 16 && blankPos / 4 == (blankPos + 1) / 4){ //If the blank tile does not go into a new row or off the frame
					swapTiles(blankPos, blankPos + 1); //Swap right
					blankPos = blankPos + 1; //Update blankPos
				}
				break;
			case 1:
				if (blankPos - 1 >= 0 && blankPos / 4 == (blankPos - 1) / 4){
					swapTiles(blankPos, blankPos - 1); //Swap left
					blankPos = blankPos - 1;
				}
				break;
			case 2:
				if (blankPos + 4 < 16 && blankPos % 4 == (blankPos + 4) % 4){ //If the blank tile does not go into a new column or off the frame
					swapTiles(blankPos, blankPos + 4); //Swap up
					blankPos = blankPos + 4;
				}
				break;
			case 3:
				if (blankPos - 4 >= 0 && blankPos % 4 == (blankPos - 4) % 4){
					swapTiles(blankPos, blankPos - 4); //Swap down
					blankPos = blankPos - 4;
				}
				break;
			default: break;
			}			
			
		}
		
		repaint();
	}
	
	private class PanelListener extends MouseAdapter{
		
		public void mousePressed(MouseEvent e){
			clickAt(e.getX(), e.getY()); //Call clickAt method with coordinates of click
			repaint(); //Call paintComponent
		}
	}
	
	public String toString(){
		return "Tiles: " + tiles + "\nWidth: " + width + "\nHeight: " + height;
	}
	
}

//-------------------- End of Frame class --------------------//  

/* This class holds the ImageIcon for a tile, holds its location in the array, original location, and x and y coordinates.
 * 
 */

class Tile {
	
	private ImageIcon imageIcon; //Image icon associated with tile
	private int location; //Tile's location in the array, also its grid location
	private int x; //Up left location of image
	private int y; //Up right location of image
	private int originalLoc; //Where the tile was originally

	
	public Tile(){
		this(null, 0);
	}
	
	//Constructor for non-blank
	public Tile(ImageIcon imageIcon, int location){
		this.imageIcon = imageIcon;
		this.location = location;
		originalLoc = location;
	}

	//Returns ImageIcon object
	public ImageIcon getImage(){
		return imageIcon;
	}

	//Returns location in the array
	public int getLocation(){
		return location;
	}

	//Set the location in the array
	public void setLocation(int location){
		this.location = location;
	}
	
	//Return true if the tile is blank
	public int getOriginalLocation(){
		return originalLoc;
	}

	//Set blank to true or false
	public void setOriginalLocation(int origLoc){
		originalLoc = origLoc;
	}
	
	/* Capital coordinates denote input from arguments
	 * 
	 */
	
	//Get X and Y for up left of tile
	
	public int getX(){
		return x;
	}
	
	public int getY(){
		return y;
	}
	
	//Set X and Y for up left of tile
	
	public void setX(int X){
		x = X;
	}
	
	public void setY(int Y){
		y = Y;
	}
	
	public String toString(){
		return "Location: " + location + "\nX: " + x + "\nY: " + y + "\nOriginal Location:" + originalLoc;
	}
}

