/**
 * @(#)Client.java	11/03/05
 * 
 * Copyright 2005 3GP01, KCL.
 * 
 * This program is free software; you can redistribute it and/or
 * modify it under the terms of the GNU General Public License
 * as published by the Free Software Foundation; either version 2
 * of the License, or (at your option) any later version.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.
 *
 * The GNU General Public Licence should be contained within licence.txt;
 * if not, write to the Free Software Foundation, Inc., 51 Franklin Street,
 * Fifth Floor, Boston, MA  02110-1301, USA.
 */

import java.awt.BorderLayout;
import java.awt.Container;
import java.awt.Dimension;
import java.io.BufferedReader;
import java.io.BufferedWriter;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.FileWriter;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.PrintWriter;
import java.net.Socket;
import java.net.UnknownHostException;
import java.util.ArrayList;
import java.util.Calendar;
import java.util.List;
import java.util.ResourceBundle;
import java.util.StringTokenizer;

import javax.swing.Box;
import javax.swing.BoxLayout;
import javax.swing.ImageIcon;
import javax.swing.JDialog;
import javax.swing.JFrame;
import javax.swing.JLabel;
import javax.swing.JOptionPane;
import javax.swing.JPanel;
import javax.swing.JProgressBar;

import sun.audio.AudioPlayer;
import sun.audio.AudioStream;

/**
 * This class creates a client from the Scrabble lobby.
 * A client can be created from either the server or client lobbies, in the former case
 * a client connects locally.
 * Class provides methods called when the client receives a move (<code>decodeLetterMove</code>),
 * for receiving a dictionary from the server (<code>receiveDictionary</code>), and all
 * other protocol messages.
 * 
 * @author 3GP01 - Hardev Bhamra, Deepak Chandarana, James Tompkin
 * @version 2.0
 * @see Server
 * @see ServerThread
 */

public class Client implements Runnable 
{
	Thread thread;
	
	String name;
	
	int uid;
	
	int turn;
	
	Move move;
	
	PrintWriter out = null;
	
	BufferedReader in = null;
	
	Socket socket;
	
	String serverDictionary;
	
	ScrabbleProtocol sp = new ScrabbleProtocol();
	
	JDialog f;
	
	ResourceBundle messages = getScrabble().getI18n()
	.getCurrentResourceBundle(); //localisation message bundle
	
	boolean gameInProgress = false;
	
	/**
	 * Returns the current <b>Scrabble</b> program instance.
	 * 
	 * @return Scrabble
	 */
	public Scrabble getScrabble() 
	{
		return Scrabble.scrabble;
	}
	
	/**
	 * Returns the dictionary the server is using as a String dictionary name.
	 * 
	 * @return String
	 */
	public String getServerDictionary() 
	{ 
		return serverDictionary;
	}
	
	/**
	 * Returns the location of the host.
	 * 
	 * @return String The location of the host as an IP.
	 */
	public String getHostLocation() 
	{
		return getScrabble().getHostLocation();
	}
	
	
	/**
	 * Creates an IRS client.  Client will try to connect to the host, and upon success, will begin waiting for messages.
	 * If a connection to the server is at any time lost, the client will fatal exception, present dialog, and close.
	 * 
	 * @param hostLocation The location of the server.
	 */
	public void createClient(String hostLocation) 
	{
		name = getScrabble().getClientName();
		
		try 
		{
			socket = new Socket(hostLocation, 7273);
			out = new PrintWriter(socket.getOutputStream(), true);
			in = new BufferedReader(new InputStreamReader(socket
					.getInputStream()));
		} 
		catch (UnknownHostException e) 
		{
			getScrabble().setConnectionFailed(true);
			JOptionPane.showMessageDialog(
					null,
					messages.getString("Client.0") + hostLocation + messages.getString("Client.1")); //$NON-NLS-1$ //$NON-NLS-2$
			return;
		} 
		catch (IOException e) 
		{
			getScrabble().setConnectionFailed(true);
			JOptionPane.showMessageDialog(
					null,
					messages.getString("Client.0") + hostLocation + messages.getString("Client.3")); //$NON-NLS-1$ //$NON-NLS-2$
			return;
		}
		
		getScrabble().setConnectionSuccessful(true);
		getScrabble().setChatLog(messages.getString("Client.4") + hostLocation + messages.getString("Client.5")); //$NON-NLS-1$ //$NON-NLS-2$
		
		String inputLine;
		String[] processedInput;
		
		try 
		{	// when client receives a message from server
			// respond accordingly
			while ((inputLine = in.readLine()) != null) //output to messageArea when client receives message from server
			{
				processedInput = sp.processInput(inputLine);
				int incomingUid = Integer.parseInt(processedInput[0]); //get name of user from clientNames string array
				int cs = Integer.parseInt(processedInput[1]);
				switch (cs) {
				case 10: //if chat message request
					Calendar rightNow = Calendar.getInstance();
					String time = rightNow.getTime().toString();
					time = time.substring(11, 19); //get time for message output
					getScrabble().getMessageArea().append(getScrabble().clientNames[incomingUid]
													  + "/" + time + "> " + processedInput[2] + "\n"); //userid //$NON-NLS-1$ //$NON-NLS-2$ //$NON-NLS-3$
					getScrabble().getMessageArea().setCaretPosition(getScrabble().getMessageArea().getDocument().getLength());
					
					//flash taskbar to denote that a new message has been received!
					//only works between applications, NOT between JWindows/JFrames
					//only tested in XP, i think this doesnt work for other OS's
					if (getScrabble().getFrame().getExtendedState() == 1) 
					{
						getScrabble().getFrame().toFront();
					}
					if ((getScrabble().getFrame().getExtendedState() == 0)
							&& (getScrabble().getFrame().isFocused() == false)) 
					{
						getScrabble().getFrame().setExtendedState(2);
						getScrabble().getFrame().toFront();
					}
					break;
				case 1: //if user id request
					uid = incomingUid;
					getScrabble().setNumberOfHints(
							Integer.parseInt(processedInput[2])); //set number of hints for game
					out.println(sp.processOutput(uid, 2, name)); //send own name to server thread
					break;
				case 2: //if client has joined
					setNames(incomingUid, processedInput[2]);
					getScrabble().appendPlayerToLobbyList(processedInput[2]);
					getScrabble().getMessageArea().append(
							getScrabble().clientNames[incomingUid]
													  + messages.getString("Client.9")); //$NON-NLS-1$
					getScrabble().getMessageArea().setCaretPosition(
							getScrabble().getMessageArea().getDocument()
							.getLength());
					break;
				case 3: //if dict check request
					//System.out.println(processedInput[2]);
					checkDictionary(processedInput[2]);
					break;
				case 4: //if dict send request
					receiveDictionary(processedInput[2]);	//make sure it writes the first line
					getScrabble().setChatLog(messages.getString("Client.10") + serverDictionary + messages.getString("Client.11")); //$NON-NLS-1$ //$NON-NLS-2$
					break;
				case 5: //if game start
					//load dictionary
					getScrabble().leaveServerButton.setEnabled(false);
					Dictionary.loadFromFile(
							serverDictionary,
							"resources/dictionaries/" + serverDictionary + ".txt"); //load dictionary //$NON-NLS-1$ //$NON-NLS-2$
					getScrabble().setScrabbleBuilder(new ScrabbleBuilder());
					RemoteScrabble.createAndShowGUI(Integer
							.parseInt(processedInput[2]), uid); //processedInput[2] = turn
					gameInProgress = true;
					break;
				case 6: //if seed request
					RemoteScrabble.getRemoteScrabble().startRemoteGame(
							Long.parseLong(processedInput[2])); //pass seed
					break;
				case 7: //received letter move
					//received letter move, do knatty concat tricks
					decodeLetterMove(processedInput[2]);
					break;
				case 8: //swap letters request
					swapLetterMove(processedInput[2]);
					break;
				case 9: //client wants to end the game
					if (gameInProgress) //if game is in progress
					{	
						clientLeftGame(incomingUid);
						clientLeftLobby(incomingUid);

						if(processedInput[2].equals("Server"))
						{	//Server has quit, all clients must be informed, game ends
							if (incomingUid != uid) //if not the user requesting the close
							{	//show error message
								RemoteScrabble.getRemoteScrabble().setMessageLine("Server has quit, the game has ended.");
								JOptionPane.showMessageDialog(null,
									"Server has ended the game abruptly.\nInternational Remote Scrabble will now close.", //$NON-NLS-1$
									"Server error.", //$NON-NLS-1$
									JOptionPane.ERROR_MESSAGE);
							}
							System.exit(0);
						}
					} 
					else //must still be in the lobby
					{
						clientLeftLobby(incomingUid);
					}
					if (incomingUid == uid)//if client has left themselves,
					{	
						getScrabble().setNameButton.setEnabled(true);
						getScrabble().nameLine.setEnabled(true);
						//getScrabble().setServerButton.setEnabled(true);
						//getScrabble().serverLine.setEnabled(true);
						getScrabble().leaveServerButton.setEnabled(false);
						getScrabble().messageLine.setEnabled(false);
						getScrabble().sendMessageButton.setEnabled(false);
						getScrabble().listModel.removeAllElements();
						
						//System.out.println("Closing socket");
						in.close();
						out.close();
						socket.close();
						return;
					}
					break;
				}
			}
		} 
		catch (IOException e1) 
		{
			clientServerException(e1);
		}
	}
	
	/**
	 * Sets the name locally of a specific player in the game.
	 * 
	 * @param uid The unique identifier of the player.
	 * @param name The name of the player.
	 */
	public void setNames(int uid, String name) 
	{
		getScrabble().clientNames[uid] = name;
	}
	
	/**
	 * Method to write to the <b>ServerThread</b>.  Method will process information depending on the ScrabbleProtocol before sending it out.
	 * 
	 * @param messageType The type of message required for sending.
	 * @param messageOutput The body of the message, as a String.
	 */
	public void setMessageOutput(int messageType, String messageOutput) 
	{	//client sending out message to server
		String processedOutput = sp.processOutput(uid, messageType,	messageOutput);
		out.println(processedOutput);
	}
	
	
	/**
	 * Passes information to the RemoteScrabble game, informing it that a client with specific <b>uid</b> has left the game.
	 * 
	 * @param uid The unique identifier of the player.
	 */
	public void clientLeftGame(int uid) 
	{
		RemoteScrabble.getRemoteScrabble().clientLeftGame(uid);
	}
	
	/**
	 * Removes a specific client with <b>uid</b> from the lobby interface.
	 * 
	 * @param uid The unique identifier of the player.
	 */
	public void clientLeftLobby(int uid) 
	{
		getScrabble().removePlayerFromLobbyList(getScrabble().clientNames[uid]);
		getScrabble().setChatLog(getScrabble().clientNames[uid]
										  + messages.getString("Client.6"));
		getScrabble().clientNames[uid] = null;
	}
	
	/**
	 * A method to handle an incoming swap letters move.
	 * The method removes letters from the rack, replaces them into the bag,
	 * and takes the necessary new letters.
	 * 
	 * @param swapMove The required letters to be swapped, formed as a String.
	 */
	public void swapLetterMove(String swapMove) 
	{
		List swap = new ArrayList();
		StringTokenizer st = new StringTokenizer(swapMove);
		while (st.hasMoreTokens()) //while our input has more tokens
		{
			Letter l = getScrabble().getScrabbleBuilder().getLetter(st.nextToken());
			swap.add(l);
		}
		
		//remote letters from rack
		RemoteScrabble.getRemoteScrabble().getGame().getRack().removeLetters(
				swap);
		//replace letters to end of bag
		RemoteScrabble.getRemoteScrabble().getGame().bag.replaceLetters(swap);
		//take new letters
		RemoteScrabble.getRemoteScrabble().getGame().updateCurrentPlayersRack();
		//call the client to end the turn
		RemoteScrabble.getRemoteScrabble().endPlayerTurn(null, 1); //pass null move as we are swapping - will be caught in endPlayerTurn as a swap
	}
	
	/**
	 * A method to decode a received letter move, and call the end of the turn.
	 * 
	 * @param letterMoves The required letters to be placed, formed as a String.
	 */	
	public void decodeLetterMove(String letterMoves) 
	{
		List lettersList = new ArrayList(); //new list to hold letterMoves
		String s = ""; //$NON-NLS-1$
		StringTokenizer st = new StringTokenizer(letterMoves);
		//option for skipping
		int option = 0;
		while (st.hasMoreTokens()) //while our input has more tokens
		{
			s = st.nextToken();
	
			if(s.equals("skip"))
			{	
				option = 2;
			}
			else
			{	//if not a skip, decode the letterMoves
				int x = Integer.parseInt(s.substring(0, 2)); //get x
				int y = Integer.parseInt(s.substring(2, 4)); //get y
				s = s.substring(4, 5); //get letter from symbol
				Letter l = getScrabble().getScrabbleBuilder().getLetter(s);
				LetterMove lm = new LetterMove(x, y, l); //construct letter move
				lettersList.add(lettersList.size(), lm); //append to list
				//System.out.println(lm);
			}
		}
		
		Player pl = null;
		move = new Move(pl, lettersList);
		RemoteScrabble.getRemoteScrabble().endPlayerTurn(move, option); //call the client to end the turn
		//do all post turn ops in 'endPlayerTurn' in RemoteScrabble
		move = null; //reset move for next transmission
	}
	
	/**
	 * Returns a <b>Move</b>, stored locally as move.
	 * Called when the client needs to wait for response of a move.
	 * 
	 * @return Move
	 */
	public Move getMove() 
	{
		while (move == null) 
		{
			try 
			{
				Thread.sleep(100);
			} 
			catch (InterruptedException e) 
			{ 	System.exit(0);	//thread error
				//e.printStackTrace();
			}
		}
		return move;
	}
	
	/**
	 *	Method that checks to see whether the client has the dictionary that the server is using.
	 *	If it is not, it sends a message to the ServerThread telling it to send the dictionary
	 *  
	 * @param dictionary
	 */
	public void checkDictionary(String dictionary) 
	{
		boolean hasDict = false;
		String[][] dictionaries = Scrabble.getDictionaries();
		for (int i = 0; i < dictionaries.length; i++) 
		{
			if (dictionary.equals(dictionaries[i][1])) 
			{
				hasDict = true;
				break;
			}
			else 
			{
				hasDict = false;
			}
		}
		if (!hasDict) 
		{
			getScrabble().setChatLog(messages.getString("Client.15")); //$NON-NLS-1$
			serverDictionary = dictionary;
			String processedOutput = sp.processOutput(uid, 4, null);
			out.println(processedOutput);
		} 
		else //client has server dictionary
		{
			serverDictionary = dictionary;
		}
	}
	
	/**
	 * Method that accepts the incoming dictionary set from the ServerThread.
	 *
	 *@param firstLine The first line of the dictionary - ensures it is written.
	 */
	public void receiveDictionary(String firstLine) 
	{
		String inputLine = ""; //$NON-NLS-1$
		
		createDownloadingDialog();
		f.requestFocus();
		
		try 
		{
			BufferedWriter writer = new BufferedWriter(new FileWriter(System.getProperty("user.dir")
												+ File.separator + "resources"
												+ File.separator + "dictionaries"
												+ File.separator + serverDictionary + ".txt")); //$NON-NLS-1$ //$NON-NLS-2$
			//ensure the first line is written
			writer.write(firstLine);
			writer.newLine();
			
			while ((inputLine = in.readLine()) != null)
			{	
				String[] processedInput = sp.processInput(inputLine);

				if (processedInput[2].equals("%")) //if end of file, stop writing //$NON-NLS-1$
				{
					break;
				}
				writer.write(processedInput[2]); //write line to file
				writer.newLine(); //write a new line
			}
			writer.close(); //close writer connection
		} 
		catch (IOException e) 
		{ //play a sound, as per deepak's request
			try 
			{
				InputStream in = new FileInputStream(
				"resources/sounds/error.wav");
				AudioStream as = new AudioStream(in);
				AudioPlayer.player.start(as);
			} 
			catch (FileNotFoundException fnf) 
			{	//no need to print stack trace, sound will just not play
				//ioe.printStackTrace();
			} 
			catch (IOException ioe) 
			{	//no need to print stack trace, sound will just not play
				//ioe.printStackTrace();
			}
			//Show a dictionary copy error
			JOptionPane.showMessageDialog(null,
					messages.getString("Client.20") + inputLine.hashCode() + System.currentTimeMillis() + ")." + //$NON-NLS-1$ //$NON-NLS-2$
					messages.getString("Client.22"), //$NON-NLS-1$
					messages.getString("Client.23"), //$NON-NLS-1$
					JOptionPane.ERROR_MESSAGE);
			System.exit(0);
		}
		
		f.dispose();
	}
	
	/**
	 * Method to create a dialog showing the player that a dictionary is being downloaded from the server.
	 *
	 */
	public void createDownloadingDialog() 
	{
		JFrame frame = new JFrame();
		f = new JDialog(frame);
		
		Container contentPane = f.getContentPane();
		contentPane.setLayout(new BorderLayout());
		
		JPanel panel = new JPanel();
		panel.setLayout(new BoxLayout(panel, BoxLayout.Y_AXIS));
		
		JLabel label = new JLabel(
				messages.getString("Client.24") + serverDictionary + messages.getString("Client.25")); //$NON-NLS-1$ //$NON-NLS-2$
		JProgressBar pb = new JProgressBar();
		pb.setIndeterminate(true);
		pb.setString(serverDictionary);
		
		panel.add(Box.createRigidArea(new Dimension(0, 5)));
		panel.add(label);
		panel.add(Box.createRigidArea(new Dimension(0, 5)));
		panel.add(pb);
		panel.add(Box.createRigidArea(new Dimension(0, 8)));
		contentPane.add(panel);
		
		f.setTitle(messages.getString("Client.26")); //$NON-NLS-1$
		frame.setIconImage(new ImageIcon("resources/images/icon.png").getImage()); //$NON-NLS-1$

		
		f.pack();
		f.setLocationRelativeTo(null); //centres the frame on the screen
		f.setVisible(true);
	}
	
	/**
	 * Method to display an exception dialog to the client, informing them that the server has errored.
	 * 
	 * @param e The exception caused.
	 */
	public void clientServerException(Exception e)
	{
		int error = e.hashCode();
		//play a sound, as per deepak's request
		try {
			InputStream in = new FileInputStream(
			"resources/sounds/error.wav");
			AudioStream as = new AudioStream(in);
			AudioPlayer.player.start(as);
		} 
		catch (FileNotFoundException fnf) 
		{	//no need to print stack trace, sound will just not play
			//fnf.printStackTrace();
		}
		catch (IOException ioe) 
		{	//no need to print stack trace, sound will just not play
			//ioe.printStackTrace();
		}
		//Show a message displaying server exception.
		JOptionPane.showMessageDialog(null,
				messages.getString("Client.12") + error + System.currentTimeMillis() + ")." + //$NON-NLS-1$ //$NON-NLS-2$
				messages.getString("Server.3"), //$NON-NLS-1$
				messages.getString("Server.4"), //$NON-NLS-1$
				JOptionPane.ERROR_MESSAGE);
		System.exit(0);
	}
	
	/**
	 * Method to close the client's input/output streams, and finally close the client's socket.
	 *
	 */
	public void close() 
	{
		try 
		{
			socket.shutdownInput();
			socket.shutdownOutput();
			socket.close();
		}
		catch (IOException e) 
		{	clientServerException(e);
		}
	}
	
	/**
	 * Client thread start.
	 *
	 */
	public void start() 
	{
		thread = new Thread(this, "client"); //$NON-NLS-1$
		thread.start();
	}
	
	/**
	 * Client run, starts <b>createClient</b> method.
	 */
	public void run() 
	{
		createClient(getHostLocation());
	}
}