/*
 * NMSTL, the Networking, Messaging, Servers, and Threading Library for C++
 * Copyright (c) 2002 Massachusetts Institute of Technology
 *
 * This library is free software; you can redistribute it and/or
 * modify it under the terms of the GNU Lesser General Public
 * License as published by the Free Software Foundation; either
 * version 2.1 of the License, or (at your option) any later version.
 *
 * This library 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
 * Lesser General Public License for more details.
 * 
 * You should have received a copy of the GNU Lesser General Public
 * License along with this library; if not, write to the Free Software
 * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
 *
 */

#include <nmstl/netioevent>
#include <nmstl/terminal>

// Declarations related to our network protocol
#include "chatproto.h"

using namespace nmstl;
using namespace std;



// The event loop that will dispatch all network and terminal I/O events.
io_event_loop loop;

// A handler for terminal input and output.
term_handler term(loop, "chatclient> ");



/*
 * A handler for a single connection to a chat server.
 *
 * We subclass msg_handler and override a few of its virtual methods:
 *
 *   - connected() is invoked when a connection we try either
 *     succeeds or fails.
 *   - incoming_message() is invoked whenever a complete message
 *     is received.
 *   - end_messages() is in invoked when the stream closes.
 *
 * chat_header, defined in chatproto.h, is our header type; it
 * contains a type field (LOGIN, SHOUT, WHISPER, etc...) and
 * a length field.  (The length field is automatically filled in
 * by msg_handler.)
 *
 * We use serialization to read and write message payloads (the
 * actual content of messages).  So if I write out a message
 * like
 *
 *     write( chat_header(LOGIN), omessage() << user << password );
 *
 * then the peer will receive a message in incoming_message()
 * such that header.type == LOGIN; to read the packet, the peer
 * can say
 *
 *     string user, password;
 *     imessage(data) >> user >> password;
 * 
 * See incoming_message() in chatserver.cc.
 *
 */

class chat_client : public msg_handler<chat_header> {
    // Have I logged into the server yet?
    bool authenticated;

  public:
    // Create the client object.  No connection yet.
    chat_client(io_event_loop& loop) :
	msg_handler<chat_header>(loop),
	authenticated(false)
    {
    }

    // Attempt to connect to a remote host.
    status connect(string host, string port) {
	if (host.empty()) host = "localhost";
	if (port.empty()) port = "8002";

	inet_address peer(host, port);
	if (!peer) return status::bad("Invalid address");

        // In handlers, need to use nonblocking connections
        // (otherwise we'd stall all the other handlers,
        // e.g., the terminal handler).
	set_socket(tcpsocket(peer, tcpsocket::nonblocking));

	return status::good();
    }

    status login(string username, string password) {
	if (!is_connected()) return status::bad("Not connected (use \"connect\")");
	if (authenticated) return status::bad("No need to login; already authenticated");

	/*
         * The following write(...) line constructs a header, and
         * serializes username and password into a payload, and
         * write it out.  It is equivalent to:
	 *
	 *     chat_header header(LOGIN);
	 *
	 *     oserialstring oss;
	 *     oss << username << password;
	 *
	 *     write(header, iss.str());
	 *
	 * omessage() << ... << ... is just a convenient way to construct
	 * a serialized message payload.
         *
         * To see where these messages are used, look at incoming_message()
         * in chatserver.cc.
         *
         */

	write(chat_header(LOGIN), omessage() << username << password);
	
	return status::good();
    }

    status disconnect() {
	if (!is_connected()) return status::bad("Not connected (use \"connect\")");

        // tcpsocket() is an "empty" socket, just like string() is
        // an empty string.  set_socket( tcpsocket() ) is a way of
        // closing the current connection.

	set_socket( tcpsocket() );
	return status::good();
    }

    status shout(string msg) {
	if (!is_connected()) return status::bad("Not connected (use \"connect\")");
	if (!authenticated) return status::bad("Not authenticated.");

	write(chat_header(SHOUT), omessage() << msg);
	return status::good();
    }

    status whisper(string target, string msg) {
	if (!is_connected()) return status::bad("Not connected (use \"connect\")");
	if (!authenticated) return status::bad("Not authenticated.");

	write(chat_header(WHISPER), omessage() << target << msg);
	return status::good();
    }

    // Note that the virtual methods are protected - no one is allowed
    // to call them except for msg_handler (our superclass).

  protected:
    // Handle success/failure of a connection attempt.
    void connected(status stat) {
	if (stat) {
	    term << "Connected to remote server; use login "
                "<username> <password> to login." << endl;
	} else {
	    term << "Failed to connect to remote server: " <<
                stat << endl;
	}
    }

    // Handle an incoming message.
    void incoming_message(const chat_header& header, constbuf data) {
	switch (header.type) {
	  case AUTH_ACK: 
	    {
		term << "Authentication succeeded." << endl;
		authenticated = true;
		break;
	    }

	  case AUTH_NAK:
	    {
		term << "Authentication failed." << endl;
		break;
	    }

	  case SHOUT:
	  case WHISPER:
	    {
		string from;
		string msg;
		if (!(imessage(data) >> from >> msg)) break;

		term << from << ( header.type == SHOUT ? " shouts: " : " whispers: " ) << msg << endl;
		break;
	    }

	  default:
	    {
		term << "Unknown message type " << header.type << endl;
		break;
	    }
	}
    }

    // End of message stream.
    void end_messages(unsigned int bytes_remaining) {
	term << "Connection closed";
	if (bytes_remaining)
	    term << " (while part of a message was en route)";

	authenticated = false;
	term << endl;
    }
};







int main()
{
    // Create the client handler (doesn't establish the
    // connection yet; use chat_client::conect for that.
    chat_client chat(loop);

    // Add a bunch of commands to the terminate.
    // "quit," "exit," and "help" are already there.
    //
    // Arguments to add_command are
    //
    //   - name and usage
    //   - minimum # of arguments
    //   - maximum # of arguments
    //   - the function or method to call when the command is invoked
    //
    // The maximum # of arguments can be bitwise ORed with
    // term_handler::text_arg to mean that all extra arguments
    // should just be added to the last argument (so that
    // "shout foo bar" is the same as "shout 'foo bar'").
    // Or term_handler::var_arg means that *any* number of
    // arguments is permissible.

    term.add_command(string("connect <host> <port> (defaults to localhost 8002)"), 0, 2,
		     wrap(chat, &chat_client::connect));
    term.add_command("login <user> <password> (hint: the password is \"foobar\")", 2, 2,
		     wrap(chat, &chat_client::login));
    term.add_command("disconnect", 0, 0,
		     wrap(chat, &chat_client::disconnect));
    term.add_command("shout \"<message>\"", 1, 1 | term_handler::text_arg,
		     wrap(chat, &chat_client::shout));
    term.add_command("whisper <user> \"<message>\"", 2, 2 | term_handler::text_arg,
		     wrap(chat, &chat_client::whisper));

    term << "Welcome.  Type \"help\" for help." << endl;

    // Run the event loop.  Returns only when io_event_loop::terminate
    // is invoked (because the user types "quit" or "exit" or
    // control-D).
    loop.run();
}
