package client;

import api.*;
import java.awt.*;
import java.awt.event.*;
import javax.swing.*;
import javax.swing.border.*;
import java.rmi.*;
import java.rmi.server.*;

/**
 * Chat Client application
 * @author Peter Cappello
 */
public final class ClientApp extends UnicastRemoteObject implements Client
{
    // declare view components
    private JFrame frame = new JFrame( "Chat Client" );
    
    private JPanel northPanel = new JPanel();
        private JButton connectButton = new JButton( "Connect" );
        private JTextField connectTextField = new JTextField( 50 );
        private JLabel padLabel = new JLabel("  ");
        //private JLabel loginLabel = new JLabel("Login ");
        private JTextField loginTextField = new JTextField( 10 );
        private JButton loginButton  = new JButton( "Login" );

    private JPanel centerPanel = new JPanel();
        private JPanel chatPanel = new JPanel();
            private JLabel chatLabel = new JLabel( "Chat Record" );
            private JTextArea  chatTextArea   = new JTextArea( 100, 80 );
        private JPanel messagePanel = new JPanel();
            private JLabel messageLabel = new JLabel( "Message Area" );
            private JTextArea messageTextArea = new JTextArea( 3, 80 );

    private JPanel southPanel = new JPanel();
        private JButton clearButton = new JButton( "Clear Chat Record" );
        private JButton listButton  = new JButton( "List Current Chatters" );
        private JButton sendButton  = new JButton( "Send Message" );

    // Client attributes
    private String clientName;
    private Server server;
    private Server serverProxy;

    public ClientApp() throws RemoteException { initComponents(); }

    private void initComponents()
    {
        // layout view
        frame.setSize( 800, 600 );
        frame.setLayout( new BorderLayout() );
        
        northPanel.setLayout( new GridLayout( 1, 5 ) );
        northPanel.add( connectButton );
        northPanel.add( connectTextField );
        northPanel.add( padLabel );
        northPanel.add( loginTextField );
        northPanel.add( loginButton );
        frame.add( northPanel, BorderLayout.NORTH );

        centerPanel.setLayout( new BorderLayout() );
            chatPanel.setLayout( new BorderLayout() );
            chatPanel.add( chatLabel, BorderLayout.NORTH );
            chatPanel.add( chatTextArea, BorderLayout.CENTER );
        centerPanel.add( chatPanel, BorderLayout.CENTER );
            messagePanel.setLayout( new BorderLayout() );
            messagePanel.add( messageLabel, BorderLayout.NORTH );
            messagePanel.add( messageTextArea, BorderLayout.CENTER );
        centerPanel.add( messagePanel, BorderLayout.SOUTH );
        frame.add( centerPanel, BorderLayout.CENTER );

        southPanel.setLayout( new GridLayout( 1, 3 ) );
        southPanel.add( clearButton );
        southPanel.add( listButton );
        southPanel.add( sendButton );
        frame.add( southPanel, BorderLayout.SOUTH );
        
        connectTextField.setText( "localhost" );
        
        chatLabel.setForeground( new Color( 0, 0, 255 ) );
        chatTextArea.setBackground( new Color( 233, 233, 255 ) );
        chatPanel.setBorder( BorderFactory.createEtchedBorder( EtchedBorder.LOWERED ) );
        messageLabel.setForeground( new Color( 0, 127, 0 ) );
        messageTextArea.setBackground( new Color( 233, 255, 233 ) );
        messagePanel.setBorder( BorderFactory.createEtchedBorder( EtchedBorder.LOWERED ) );

        // initial user state == disconnected
        transitFromLoggedinToConnected();
        transitFromConnectedToDisconnected();

        frame.setDefaultCloseOperation( JFrame.EXIT_ON_CLOSE );

        //-------------------------
        // contoller TEMPLATE CODE for each action
        //-------------------------

        // connect/disconnect
        connectButton.addActionListener( new ActionListener()
        {
            public void actionPerformed( ActionEvent actionEvent )
            {
                connectButtonActionPerformed( actionEvent );
            }
        });

        // login/logout
        loginButton.addActionListener( new ActionListener()
        {
            public void actionPerformed( ActionEvent actionEvent )
            {
                loginButtonActionPerformed( actionEvent );
            }
        });

       // clear
        clearButton.addActionListener( new ActionListener()
        {
            public void actionPerformed( ActionEvent actionEvent )
            {
                clearButtonActionPerformed( actionEvent );
            }
        });

        // list
        listButton.addActionListener( new ActionListener()
        {
            public void actionPerformed( ActionEvent actionEvent )
            {
                listButtonActionPerformed( actionEvent );
            }
        });

        // send
        sendButton.addActionListener( new ActionListener()
        {
            public void actionPerformed( ActionEvent actionEvent )
            {
                sendButtonActionPerformed( actionEvent );
            }
        });

        System.setSecurityManager( new RMISecurityManager() );
    }

    //-------------------------
    // contoller for each action
    //-------------------------
    private void connectButtonActionPerformed( ActionEvent actionEvent )
    {
        if ( connectButton.getText().equals( "Connect" ) )
        {
            if ( connectTextField.getText().length() == 0 )
            {
                return; // Nothing to connect to.
            }
            transitFromDisconnectedToConnected();
        }
        else // disconnect
        {
            assert connectButton.getText().equals( "Disconnect" );
            transitFromConnectedToDisconnected(); 
        }
        connectTextField.setEditable( true );
    }

    private void loginButtonActionPerformed( ActionEvent actionEvent )
    {
        if ( loginButton.getText().equals( "Login" ) )
        {
            transitFromConnectedToLoggedin();
        }
        else // logout
        {
            assert loginButton.getText().equals( "Logout" );
            transitFromLoggedinToConnected();
        }
    }

    private void clearButtonActionPerformed( ActionEvent actionEvent )
    {
        chatTextArea.setText("");
        messageTextArea.requestFocus();
    }

    private void listButtonActionPerformed( ActionEvent actionEvent )
    {
        java.util.List<String> clientNameList = null;
        try
        {
            clientNameList = serverProxy.list();
        }
        catch ( RemoteException ignore ) {}
        chatTextArea.append("SYSTEM: Current chatters:\n");
        for ( String clientName : clientNameList )
        {
            chatTextArea.append("   Name: " + clientName + "\n");
        }
        messageTextArea.requestFocus();
    }

    private void sendButtonActionPerformed( ActionEvent actionEvent )
    {
        String input = messageTextArea.getText().trim();
        if ( input.equals( "" ) ) // Is there a message?
        {
            return; // No.
        }
        try
        {
            serverProxy.update( new Message( clientName, input ));
        }
        catch ( RemoteException ignore ) {}
        messageTextArea.setText( "" );
        messageTextArea.requestFocus();
    }

    /*
     * User has 3 states: DISCONNECTED, CONNECTED, LOGGED_IN
     * The initial state is DISCONNECTED.
     * Each user state transition has a corresponding method
     */
    // enable transit to CONNECTED's successor states: DISCONNECTED, LOGGED_IN
    private void transitFromDisconnectedToConnected()
    {
        connectButton.setText( "Disconnect" );
        loginTextField.requestFocus();
        loginButton.setEnabled( true );
        loginTextField.setEditable( true );
        String domainName = connectTextField.getText().trim();
        connect( domainName );
    }

    private void transitFromConnectedToDisconnected()
    {
        connectButton.setText( "Connect" );
        connectTextField.requestFocus();
        connectTextField.setEditable( true );
        server = null;
    }

    private void transitFromConnectedToLoggedin()
    {
        connectButton.setEnabled( false );
        connectTextField.setEditable( false );
        clientName = loginTextField.getText().trim();  
        loginButton.setText( "Logout" );
        messageTextArea.requestFocus();
        enableMessageActions( true );
        login();
    }

    private void transitFromLoggedinToConnected()
    {
        clientName = null;
        loginButton.setText( "Login" );
        loginTextField.requestFocus();
        chatTextArea.setText("");
        messageTextArea.setText("");
        connectButton.setEnabled( true );
        connectTextField.setEditable( true );
        enableMessageActions( false );
        logout();
    }

    private void enableMessageActions( boolean isEnabled )
    {
        loginTextField.setEditable( ! isEnabled );
        messageTextArea.setEditable( isEnabled );
        clearButton.setEnabled( isEnabled );
        listButton.setEnabled( isEnabled );
        sendButton.setEnabled( isEnabled );
    }

    private JFrame getFrame() { return frame; }

    private void connect( String serverMachineDomainName )
    {
        String url = "//" + serverMachineDomainName + ":" + Server.PORT + "/" + Server.SERVICE_NAME;
        try
        {
            server = (Server) Naming.lookup( url );
        }
        catch ( NotBoundException exception )
        {
            update( new Message( "SYSTEM", "No server is registered on this machine.") );
            exception.printStackTrace();
        }
        catch ( RemoteException exception )
        {
            update( new Message( "SYSTEM", "Server is not responding." ) );
            exception.printStackTrace();
        }
        catch ( Exception exception )
        {
            update( new Message( "SYSTEM", "Cannot proceed. Unexpected exception: \n" + exception ) );
            exception.printStackTrace();
        }
        System.out.println("Connection established.");
    }

    private void login()
    {       
        try
        {
            serverProxy = new ServerProxy( server, this, clientName );
            serverProxy.login( this, clientName );
        }
        catch ( RemoteException exception )
        {
            update( new Message( "SYSTEM", "Server is not responding." ) );
            exception.printStackTrace();
        }
        catch ( Exception exception )
        {
            update( new Message( "SYSTEM", "Cannot proceed. Unexpected exception: \n" + exception ) );
            exception.printStackTrace();
        }
        System.out.println("loginClient complete.");
    }

    private void logout()
    {
        if ( serverProxy == null )
        {
            return;
        }
        try
        {
            serverProxy.logout( this );
        }
        catch ( RemoteException ignore )
        {
            serverProxy = null;
        }
        serverProxy = null; // ignore subsequent server requests
    }

    /**
     * Append message to the view of the chat
     * @param message the message to be appended
     */
    public synchronized void update( Message message )
    {
        chatTextArea.append( message.getName() + ": " + message.getMessage() + "\n" );
    }

    /**
     * Get the name this client uses in this chat session
     * @return client name
     */
    public String getClientName() { return clientName; }


    /**
     * Used to instantiate a client
     * @param args is unused
     */
    public static void main( String[] args )
    {
        EventQueue.invokeLater( new Runnable()
        {
            public void run()
            {
                try
                {
                    new ClientApp().getFrame().setVisible( true );
                }
                catch ( RemoteException exception )
                {
                    exception.printStackTrace();
                }
            }
        }
        );
    }

    //________________________________
    // !! Only to support unit testing
    //________________________________
    public JTextArea getChatTextArea() { return chatTextArea; }

    public void setClientName( String clientName )
    {
        this.clientName = clientName;
    }

    /**
     * The Thread that invokes Remote methods on the server.
     */
    private class ServerProxy extends Thread implements Server
    {
        private final Message ERROR_MESSAGE = new Message( "SYSTEM", "Server is not responding." );

        private Server server;
        private Client client;
        private Q<Message> q = new Q<Message>();

        public ServerProxy( Server server, Client client, String name )
        {
            this.server = server;
            this.client = client;
            q.add( new Message( "SYSTEM", "Connected " + name ) );
            start();
        }

        public void login( Client client, String name )
        {
            assert client != null;
            assert name != null;
            try
            {
                server.login( client, name );
            }
            catch ( RemoteException exception )
            {
                disconnect( ERROR_MESSAGE );
                exception.printStackTrace();
            }
        }

        public void logout( Client client )
        {
            assert client != null;

            try
            {
                server.logout( client );
            }
            catch ( RemoteException exception )
            {
                disconnect( ERROR_MESSAGE );
            }
        }

        public void update( Message message )
        {
            assert message != null;

            q.add( message );
        }

        public java.util.List<String> list()
        {
            try
            {
                return server.list();
            }
            catch ( RemoteException exception )
            {
                disconnect( ERROR_MESSAGE );
            }
            return null;
        }

        @Override
        public void run()
        {
            while ( true )
            {
                try
                {
                    server.update( (Message) q.remove() );
                }
                catch (RemoteException exception )
                {
                    disconnect( ERROR_MESSAGE );
                    return;

                }
            }
        }

        private void disconnect( Message message )
        {
            try
            {
                client.update( message );
            }
            catch ( RemoteException ignore ) {} // impossible: local method
        }
    }
}
