import java.awt.*;
import java.awt.event.*;
import javax.swing.*;

import java.io.*;
import java.net.*;
import java.util.ArrayList;


/**
 * Opens a window that can be used for a two-way network chat. A BuddyChat window 
 * is opened by the BuddyChat program when either an incoming connection request 
 * arrives or the user wants to send an outgoing request.  This class is meant for
 * use only with BuddyChat.  It does not have a main() program of its own; the
 * user must run BuddyChat.
 * <p>Protocol used for network connection:  As soon as the connection has been 
 * opened, the client (the side that requested the conection) sends two lines of
 * text to the server (the side that received the request).  The first line is 
 * a "password" or "secret" that was created by and provided by the BuddyChatServer
 * as a way for the client to prove that it got its connection information from
 * the BuddyChatServer.  The second line is the client's "handle".  (The handle
 * is not verified in any way; it is just reported to the user.) 
 */
public class BuddyChatWindow extends JFrame {
   
   /**
    * Possible states of the thread that handles the network connection.
    */
   private enum ConnectionState { CONNECTING, CONNECTED, CLOSED };
   
   /**
    * Used to keep track of where on the screen the previous window
    * was opened, so that the next window can be placed at a 
    * different position.
    */
   private static Point previousWindowLocation;
   
   /**
    * The thread that handles the connection; defined by a nested class.
    */
   private ConnectionHandler connection;
   
   /**
    * Control buttons that appear in the window.
    */
   private JButton closeButton, clearButton, saveButton, sendButton;
   
   /**
    * Input box for messages that will be sent to the other side of the
    * network connection.
    */
   private JTextField messageInput;
   
   /**
    * Contains a transcript of messages sent and received, along with
    * information about the progress and state of the connection.
    */
   private JTextArea transcript;

   
   /**
    * A list of all open BuddyChatWindows.
    */
   private static ArrayList<BuddyChatWindow> openWindows = new ArrayList<BuddyChatWindow>();
   
   /**
    * To be called by BuddyChatServer when the user ends the program by
    * clicking on the "Close All Windows and Quit" button.
    */
   public static void closeAll() {
      Object[] windows = openWindows.toArray();
      for (int i = 0; i < windows.length; i++)
         ((JFrame)windows[i]).dispose();
   }
   
   /**
    * Returns the number of currently open BuddyChatWindows.
    */
   public static int openWindowCount() {
      return openWindows.size();
   }

   /**
    * Open a window to communicate over a socket that has already been created (by the 
    * BuddyChat program).  No data has yet been sent over the Socket. The "secret" is
    * a password known to the the BuddyChat program that must be sent BY the remote client
    * TO this window.  This represents an incoming connection request.
    */
   public BuddyChatWindow(Socket connectedSocket, String secret) {
      super("Connection Request Received");
      create();
      connection = new ConnectionHandler(connectedSocket,secret);
   }
   
   /**
    * Create a window that will open a connection to a specified machine.  This is done 
    * by the BuddyChat program when the user wants to open a connection to one of the
    * people in the buddy list.  This represents an outgoing connection request.
    * @param hostName The host for use in opening the socket.
    * @param port The port for use in opening the socket.
    * @param myName The user's "handle" that identifies the user in the buddy list.
    * @param partnerName The "handle" of the buddy list entry to which the user wants to connect.
    * @param secret The remote user's password, which must be sent BY this window TO the remote client.
    */
   public BuddyChatWindow(String hostName, int port, 
                                String myName, String partnerName, String secret) {
      super("Chatting with " + partnerName);
      create();
      connection = new ConnectionHandler(hostName,port,myName,partnerName,secret);
   }
   
   /**
    * Set up the window  and make it visible on the screen.  This method is called 
    * by both constructors.
    */
   private void create() {
      
      ActionListener actionHandler = new ActionHandler();
      closeButton = new JButton("Close");
      closeButton.addActionListener(actionHandler);
      clearButton = new JButton("Clear");
      clearButton.addActionListener(actionHandler);
      sendButton = new JButton("Send");
      sendButton.addActionListener(actionHandler);
      sendButton.setEnabled(false);
      saveButton = new JButton("Save Transcript");
      saveButton.addActionListener(actionHandler);
      messageInput = new JTextField();
      messageInput.addActionListener(actionHandler);
      messageInput.setEditable(false);
      transcript = new JTextArea(20,60);
      transcript.setLineWrap(true);
      transcript.setWrapStyleWord(true);
      transcript.setEditable(false);
      
      JPanel content = new JPanel();
      content.setLayout(new BorderLayout(3,3));
      content.setBackground(Color.GRAY);
      JPanel buttonBar = new JPanel();
      buttonBar.setLayout(new GridLayout(1,3,3,3));
      buttonBar.setBackground(Color.GRAY);
      JPanel inputBar = new JPanel();
      inputBar.setLayout(new BorderLayout(3,3));
      inputBar.setBackground(Color.GRAY);
      
      content.setBorder(BorderFactory.createLineBorder(Color.GRAY, 3));
      content.add(buttonBar, BorderLayout.NORTH);
      content.add(inputBar, BorderLayout.SOUTH);
      content.add(new JScrollPane(transcript), BorderLayout.CENTER);
      buttonBar.add(saveButton);
      buttonBar.add(clearButton);
      buttonBar.add(closeButton);
      inputBar.add(new JLabel("Your Message:"), BorderLayout.WEST);
      inputBar.add(messageInput, BorderLayout.CENTER);
      inputBar.add(sendButton, BorderLayout.EAST);
      
      setContentPane(content);
      
      pack();
      if (previousWindowLocation == null)
         previousWindowLocation = new Point(40,80);
      else {
         Dimension screenSize = Toolkit.getDefaultToolkit().getScreenSize();
         previousWindowLocation.x += 50;
         if (previousWindowLocation.x + getWidth() > screenSize.width)
            previousWindowLocation.x = 10;
         previousWindowLocation.y += 30;
         if (previousWindowLocation.y + getHeight() > screenSize.height)
            previousWindowLocation.y = 50;
      }
      setLocation(previousWindowLocation);
      
      setDefaultCloseOperation(JFrame.DISPOSE_ON_CLOSE);
      
      openWindows.add(this);

      addWindowListener( new WindowAdapter() {
         public void windowClosed(WindowEvent evt) {
            if (connection != null && 
                  connection.getConnectionState() != ConnectionState.CLOSED) {
               connection.close();
            }
            openWindows.remove(this);
            if (openWindows.size() == 0 && !BuddyChat.isRunning()) {
               try {
                  Thread.sleep(1000);
               }
               catch (InterruptedException e) {
               }
               System.exit(0);
            }
         }
      });
      
      setVisible(true);
      
   } // end constructor
   
   
   /**
    * Defines responsed to buttons, and when the user presses return in the message input box.
    */
   private class ActionHandler implements ActionListener {
      public void actionPerformed(ActionEvent evt) {
         Object source = evt.getSource();
         if (source == closeButton) {
            dispose();
         }
         else if (source == clearButton) {
            transcript.setText("");
         }
         else if (source == saveButton) {
            doSave();
         }
         else if (source == sendButton || source == messageInput) {
            if (connection != null && 
                  connection.getConnectionState() == ConnectionState.CONNECTED) {
               connection.send(messageInput.getText());
               messageInput.selectAll();
               messageInput.requestFocus();
            }
         }
      }
   }
   
   
   /**
    * Save the contents of the transcript area to a file selected by the user.
    */
   private void doSave() {
      JFileChooser fileDialog = new JFileChooser(); 
      File selectedFile;  //Initially selected file name in the dialog.
      selectedFile = new File("transcript.txt");
      fileDialog.setSelectedFile(selectedFile); 
      fileDialog.setDialogTitle("Select File to be Saved");
      int option = fileDialog.showSaveDialog(this);
      if (option != JFileChooser.APPROVE_OPTION)
         return;  // User canceled or clicked the dialog's close box.
      selectedFile = fileDialog.getSelectedFile();
      if (selectedFile.exists()) {  // Ask the user whether to replace the file.
         int response = JOptionPane.showConfirmDialog( this,
               "The file \"" + selectedFile.getName()
               + "\" already exists.\nDo you want to replace it?", 
               "Confirm Save",
               JOptionPane.YES_NO_OPTION, 
               JOptionPane.WARNING_MESSAGE );
         if (response == JOptionPane.NO_OPTION)
            return;  // User does not want to replace the file.
      }
      PrintWriter out; 
      try {
         FileWriter stream = new FileWriter(selectedFile); 
         out = new PrintWriter( stream );
      }
      catch (Exception e) {
         JOptionPane.showMessageDialog(this,
            "Sorry, but an error occurred while trying to open the file:\n" + e);
         return;
      }
      try {
         out.print(transcript.getText());  // Write text from the TextArea to the file.
         out.close();
         if (out.checkError())   // (need to check for errors in PrintWriter)
            throw new IOException("Error check failed.");
      }
      catch (Exception e) {
         JOptionPane.showMessageDialog(this,
            "Sorry, but an error occurred while trying to write the text:\n" + e);
      }   
   }
   
   
   /**
    * Add a line of text to the transcript area.
    * @param message text to be added; a line feed is added at the end.
    */
   private void postMessage(String message) {
      transcript.append(message + "\n");
         // The following line is a nasty kludge that was the only way I could find to force
         // the transcript to scroll so that the text that was just added is visible in
         // the window.  Without this, text can be added below the bottom of the visible area
         // of the transcript.
      transcript.setCaretPosition(transcript.getDocument().getLength());
   }
   
   
   /**
    * Defines the thread that handles the connection.  The thread is responsible
    * for opening the connection and for receiving messages.  This class contains
    * several methods that are called by the main class, and that are therefor
    * executed in a different thread.  Note that by using a thread to open the
    * connection, any blocking of the graphical user interface is avoided.  By
    * using a thread for reading messages sent from the other side, the messages
    * can be received and posted to the transcript asynchronously at the same
    * time as the user is typing and sending messages.
    */
   private class ConnectionHandler extends Thread {
      
      private volatile ConnectionState state;
      private String remoteHost;
      private int port;
      private Socket socket;
      private PrintWriter out;
      private BufferedReader in;
      private String secret;
      private String myName;
      
      /**
       * Start a thread for communicating over a socket that is already connected.  
       * The connection comes from the BuddyChat program, but no information has yet been
       * exchanged.  The constructor just starts a thread, which does the actual work.
       */
      ConnectionHandler(Socket connectedSocket, String secret) {
         postMessage("ACCEPTING CONNECTION REQUEST...");
         state = ConnectionState.CONNECTED;
         socket = connectedSocket;
         this.secret = secret;
         start();
      }
      
      /**
       * Open a connection to spedified computer and port.  The constructor just
       * starts a thread, which does the actual work.
       */
      ConnectionHandler(String remoteHost, int port, String myName, String partner, String secret) {
         postMessage("CONNECTING TO " + partner +
               " (at " + remoteHost + ", port " + port + ")...");
         state = ConnectionState.CONNECTING;
         this.remoteHost = remoteHost;
         this.port = port;
         this.secret = secret;
         this.myName = myName;
         start();
      }
      
      /**
       * Returns the current state of the connection.  
       */
      synchronized ConnectionState getConnectionState() {
         return state;
      }
      
      /**
       * Send a message to the other side of the connection, and post the
       * message to the transcript.  This should only be called when the
       * connection state is ConnectionState.CONNECTED; if it is called at
       * other times, it is ignored.  Note that this is called by the
       * Swing event-handling thread.
       */
      synchronized void send(String message) {
         if (state == ConnectionState.CONNECTED) {
            postMessage("SEND:  " + message);
            out.println(message);
            out.flush();
            if (out.checkError()) {
               postMessage("\nERROR OCCURRED WHILE TRYING TO SEND DATA.");
               close();
            }
         }
      }
      
      /**
       * Close the connection. If the socket is non-null, then the socket
       * is closed, which will cause its input method to fail with an
       * error.  This in turn will cause the ConnectionHandler thread to
       * terminate.
       */
      synchronized void close() {
         state = ConnectionState.CLOSED;
         try {
            if (socket != null && !socket.isClosed())
               socket.close();
         }
         catch (IOException e) {
         }
      }
      
      /**
       * This is called by the run() method when a message is received from
       * the other side of the connection.  The message is posted to the
       * transcript, but only if the connection state is CONNECTED.  (This
       * is because a message might be received after the user has clicked
       * the "Disconnect" button; that message should not be seen by the
       * user.)
       */
      synchronized private void received(String message) {
         if (state == ConnectionState.CONNECTED)
            postMessage("RECV:  " + message);
      }
      
      /**
       * This is called by the run() method when the connection has been
       * successfully opened.  It enables the correct buttons, writes a
       * message to the transcript, and sets the connected state to CONNECTED.
       */
      synchronized private void connectionOpened() throws IOException {
         postMessage("CONNECTION ESTABLISHED.\n");
         state = ConnectionState.CONNECTED;
         sendButton.setEnabled(true);
         messageInput.setEditable(true);
         messageInput.setText("");
         messageInput.requestFocus();
      }
      
      /**
       * This is called by the run() method when the connection is closed
       * from the other side.  (This is detected when an end-of-stream is
       * encountered on the input stream.)  It posts a mesaage to the
       * transcript and sets the connection state to CLOSED.  After calling
       * this method, the ConnectionHandler thread terminates.
       */
      synchronized private void connectionClosedFromOtherSide() {
         if (state == ConnectionState.CONNECTED) {
            postMessage("\nCONNECTION CLOSED FROM OTHER SIDE\n");
            state = ConnectionState.CLOSED;
         }
      }
      
      /**
       * Clean up after connection is closed.  Called from the finally
       * clause in the run() method.
       */
      synchronized private void cleanup() {
         state = ConnectionState.CLOSED;
         sendButton.setEnabled(false);
         messageInput.setEditable(false);
         postMessage("\n*** CONNECTION CLOSED ***");
         if (socket != null && !socket.isClosed()) {
               // Make sure that the socket, if any, is closed.
            try {
               socket.close();
            }
            catch (IOException e) {
            }
         }
         socket = null;
         in = null;
         out = null;
      }
      
      /**
       * The run() method that is executed by the ConnectionHandler thread.  The thread
       * opens the connection and then reads messages from the other side and posts it
       * to the transcript until the connection is closed or an error occurs.  (Note
       * that outgoing messages are sent by the event-handling thread, in response to
       * user actions.)
       */
      public void run() {
         try {
            if (state == ConnectionState.CONNECTED) {
                   // The socket was provided by the BuddyChat server and is already
                   // connected.  It represents an incoming connection request.
                   // Go through a "handshake" to identify and validate the remote user.
               InetAddress addr = socket.getInetAddress();
               int port = socket.getPort();
               postMessage("   (from IP address " + addr + ", port " + port +")");
               in = new BufferedReader(new InputStreamReader(socket.getInputStream()));
               out = new PrintWriter(socket.getOutputStream());
               String secret = in.readLine();
               if (secret == null || !secret.equals(this.secret))
                  throw new Exception("Connection request does not come from a validated user!");
               String partner = in.readLine();
               if (partner == null)
                  throw new Exception("Connection unexpectedly closed from other side.");
               postMessage("Connection opened to " + partner);
               setTitle("Chatting with " + partner);
            }
            else if (state == ConnectionState.CONNECTING) {
                  // The user has requested a request to a remote user.  Open a connection
                  // to the user and send handshake info.
               socket = new Socket(remoteHost,port);
               in = new BufferedReader(new InputStreamReader(socket.getInputStream()));
               out = new PrintWriter(socket.getOutputStream());
               out.println(secret);
               out.println(myName);
               out.flush();
            }
            connectionOpened();  // Set up to use the connection.
            while (state == ConnectionState.CONNECTED) {
                  // Read one line of text from the other side of
                  // the connection, and report it to the user.
               String input = in.readLine();
               if (input == null)
                  connectionClosedFromOtherSide();
               else
                  received(input);  // Report message to user.
            }
         }
         catch (Exception e) {
               // An error occurred.  Report it to the user, but not
               // if the connection has been closed (since the error
               // might be the expected error that is generated when
               // a socket is closed).
            if (state != ConnectionState.CLOSED)
               postMessage("\n\n ERROR:  " + e);
         }
         finally {  // Clean up before terminating the thread.
            cleanup();
         }
      }
      
   } // end nested class ConnectionHandler



}
