/*
 *
 *   tcpclient.cc
 *
 *   Implements a simple reverse proxy, for testing the
 *   my::multiplexed::tcpsocket class.
 *
 *   Copyright (C) 2001 by Matt Roberts, KK5JY,
 *   All rights reserved.
 *
 *   TODO:
 *      + Add limit controls, so that the number of simultaneous connections
 *        can be controled.
 *      + Add syslog support to make it all very pretty. :)
 *
 *
 */


#include <string>
#include <list>
#include <iostream>
#include <my++/tcpsocket.h>
#include <my++/multiplex.h>
#include <my++/muxtcpsocket.h>

#define LISTEN_HOST "localhost"
#define LISTEN_PORT (2525)
#define REMOTE_HOST "localhost"
#define REMOTE_PORT (25)

/*
 *
 *   class session
 *
 *   Represents one proxy session; holds two TCP sockets.
 *
 */

struct session {
	public:
		my::multiplexed::tcpsocket client;
		my::multiplexed::tcpsocket server;
		bool cleanup;

	public:
		session (void) : cleanup(false) { /* noop */ };

	private:
		session (const session &s) { /* noop */ };
};



/*
 *
 *   main (...)
 *
 */

int main (int argc, char **argv) {
	my::multiplexed::tcpsocket listener;
	my::multiplex mux;
	char buffer [128];
	int count;
	int devs;
	bool do_cleanup, try_again;
	timeval timeout;

	std::list<session*> clients;
	std::list<session*>::iterator ci;

	/* attempt to bind to a well-known port */
	try {
		//listener.bind (my::tcpsocket::address (LISTEN_HOST, LISTEN_PORT));
		listener.bind (my::tcpsocket::address (LISTEN_PORT));
	} catch (my::tcpsocket::exception &e) {
		std::cerr << "Bind failure: " << e.what() << std::endl;
		return 1;
	}

	/* set that socket to listen mode */
	try {
		listener.listen();
	} catch (my::tcpsocket::exception &e) {
		std::cerr << "Listener failure: " << e.what() << std::endl;
		return 1;
	}

	/* start by configuring the mux to watch for inbound connections on the listener */
	mux.add (listener);

	std::cout << "Listening to localhost:" << LISTEN_PORT << std::endl;

	for (;;) {
		// assume that we will have a clean run
		do_cleanup = false;
		try_again = false;

		try {
			// watch for something to happen
			timeout.tv_sec = 4;
			timeout.tv_usec = 0;
			devs = mux.watch(timeout);
			std::cerr << "Time remaining: " << timeout.tv_sec << " sec, " << timeout.tv_usec << " usec." << std::endl;
		} catch (const my::tcpsocket::exception &e) {
			std::cout << "TCP Socket Error: " << e.what() << std::endl;
			for (ci = clients.begin(); ci != clients.end(); ++ci) {
				if (((&(*ci)->client) == e.object()) ||
						((&(*ci)->server) == e.object())) {
					clients.erase(ci);
					break;
				}
			}
			try_again = true;
		} catch (const std::exception &e) {
			std::cout << "Error: " << e.what() << std::endl;
			try_again = true;
		}

		if (try_again) {
			try_again = false;
			continue;
		}

		// if we can read from the listener socket
		if (listener.inbound()) {
			std::cout << "Accept new connection: ";
			std::cout.flush();
			session *new_session = 0;

			// try to accept the client connection request
			try {
				new_session = new session;
				my::multiplexed::tcpsocket::address addr;
				std::string disp_str;
				listener.accept (new_session->client, addr);
				std::cout << addr.to_string(disp_str) << std::endl;
			} catch (const std::exception &e) {
				std::cerr << "Could not accept new connection: reason was "
					<< e.what() << std::endl;
				delete new_session;
				new_session = 0;
			}

			// try to connect to the remote service
			if (new_session != 0) {
				try {
					new_session->server.connect (my::multiplexed::tcpsocket::address (REMOTE_HOST, REMOTE_PORT));
				} catch (const std::exception &e) {
					std::cerr << "Connection attempt to server failed: "
						<< e.what() << std::endl;
					new_session->client.disconnect();
					delete new_session;
					new_session = 0;
				}

				if (new_session != 0) {
					// add the new sockets to the mux
					mux.add (new_session->client);
					mux.add (new_session->server);

					// add the new object to the list of active clients
					clients.push_back(new_session);

					my::multiplexed::tcpsocket::address a1, a2;
					new_session->client.peer(a1);
					new_session->server.peer(a2);
					std::cout << "Added new proxy session: " << a1.to_string()
						<< " <==> " << a2.to_string() << std::endl;
				}
			}
		}

		for (ci = clients.begin(); ci != clients.end(); ++ci) {
			/* this is our current session */
			session *sptr = *ci;

			try {
				/* if we can read from client end */
				while (sptr->client.ready() > 0) {
					/* read from client, write to server */
					count = sptr->client.get (buffer, sizeof(buffer) - 1);
					if (count > 0)
						sptr->server.put (buffer, count);
				}

				if (sptr->client.disconnected()) {
					my::multiplexed::tcpsocket::address client_addr;
					sptr->client.peer(client_addr);
					std::cout << "Client (" << client_addr.to_string() << ") closed connection." << std::endl;
					/* disconnect in both directions */
					sptr->server.disconnect();
					sptr->client.disconnect();
					/* remove both sockets from mux */
					mux.remove(sptr->server);
					mux.remove(sptr->client);
					/* flag this object for cleanup */
					do_cleanup = true;
					sptr->cleanup = true;
				}

				/* if we can read from server end */
				if (!sptr->cleanup) {
					while (sptr->server.ready() > 0) {
						/* read from server, write to client */
						count = sptr->server.get (buffer, sizeof(buffer) - 1);
						if (count > 0)
							sptr->client.put(buffer, count);
					}
				}
				
				if (sptr->server.disconnected()) {
					my::multiplexed::tcpsocket::address server_addr;
					sptr->server.peer(server_addr);
					std::cout << "Server (" << server_addr.to_string() << ") closed connection." << std::endl;
					/* disconnect in both directions */
					sptr->client.disconnect();
					sptr->server.disconnect();
					/* remove both sockets from mux */
					mux.remove(sptr->server);
					mux.remove(sptr->client);
					/* flag this object for cleanup */
					do_cleanup = true;
					sptr->cleanup = true;
				}
			} catch (const std::exception &e) {
				sptr->client.disconnect();
				sptr->server.disconnect();
				mux.remove(sptr->server);
				mux.remove(sptr->client);
				do_cleanup = true;
				sptr->cleanup = true;
			}
		}

		/* only do this if some session ended */
		if (do_cleanup) {
			do_cleanup = false;

			/* look for sessions to destroy */
			for (ci = clients.begin(); ci != clients.end(); ++ci) {
				/* if the node is to be destroyed, move it into the 'pending' list */
				if ((*ci)->cleanup) {
					delete (*ci);
					clients.erase(ci);
					/* at this point, 'ci' is undefined, so we just start again at the
					 * beginning of the list 'clients' */
					ci = clients.begin();
				}
			}
		}
	}

	/* should never reach here */
	return 0;
}
