package jade.core;

import jade.util.Logger;

import java.io.IOException;
import java.io.UnsupportedEncodingException;
import java.net.DatagramPacket;
import java.net.InetAddress;
import java.net.MulticastSocket;
import java.net.SocketTimeoutException;
import java.net.UnknownHostException;
import java.util.ArrayList;
import java.util.Iterator;
import java.util.List;
import java.util.StringTokenizer;

//#J2ME_EXCLUDE_FILE

public class MainDetectionManager {

	private final static Logger logger = Logger.getMyLogger(MainDetectionManager.class.getName());

	/*
	 *  configuration options with defaults
	 */
	private final static String OPT_MCAST_ADDR = "jade_core_MainDetectionManager_mcastaddr";
	private final static String OPT_MCAST_ADDR_DEFAULT = "239.255.10.99";

	private final static String OPT_MCAST_PORT = "jade_core_MainDetectionManager_mcastport";
	private final static String OPT_MCAST_PORT_DEFAULT = "1199";

	private final static String OPT_MCAST_TTL = "jade_core_MainDetectionManager_mcastttl";
	private final static String OPT_MCAST_TTL_DEFAULT = "4";

	private final static String OPT_MCAST_FIRST_TIMEOUT = "jade_core_MainDetectionManager_mcastfirsttimeout";
	private final static String OPT_MCAST_FIRST_TIMEOUT_DEFAULT = "500";

	private final static String OPT_MCAST_TIMEOUT = "jade_core_MainDetectionManager_mcasttimeout";
	private final static String OPT_MCAST_TIMEOUT_DEFAULT = "2500";

	private final static String OPT_MCAST_RETRIES = "jade_core_MainDetectionManager_mcastretries";
	private final static String OPT_MCAST_RETRIES_DEFAULT = "3";

	/*
	 *  protocol data
	 */
	// version string
	public final static String PROTO_VERSION = " MJADE/1.0";

	// charset used to encode/decode packets
	public final static String PROTO_ENCODING = "ISO-8859-1";

	// protocol commands
	public final static String PROTO_CMD_GETMAIN = "get-main";
	public final static String PROTO_CMD_PING = "ping";

	// constants for get-main command
	public final static String PROTO_ADDRESSES_SEPARATOR = ";";
	public final static String PROTO_ADDR_SEPARATOR = ":";

	// protocol responses
	public final static String PROTO_RESP_OK = "200 OK ";
	public final static String PROTO_RESP_ERR = "500 Internal Server Error";
	public final static String PROTO_RESP_NOTFOUND = "404 Not Found";

	// buffer size
	private final static int DGRAM_BUF_LEN = 1024;

	// source port for datagram packets
	private final static int SRC_PORT = 1198;

	private static final String MATCH_ALL_PLATFORMS = "*";

	/*
	 * inner class used to parse and keep addresses from main
	 */
	private static class MainAddr {
		public String protocol;
		public String hostname;
		public int port;

		public MainAddr(String address) {
			StringTokenizer ast;
			/*
			 * Address must be in form "proto:host:port"
			 * If address has a bad format, the following code
			 * will throw an exception. The behaviour is by design.
			 */
			ast = new StringTokenizer(address, PROTO_ADDR_SEPARATOR);
			protocol = ast.nextToken();
			hostname = ast.nextToken();
			port = Integer.parseInt(ast.nextToken());
		}
	}

	/*
	 * inner class used to get multicast parameters from Profile
	 */
	static class MulticastParams {
		String address;
		int port;
		int firstTimeout;
		int timeout;
		int retries;
		int ttl;

		private void checkTrue(boolean condition, String paramName, String paramValue) throws ProfileException {
			if (!condition) {
				throw new ProfileException("Bad value \""+paramValue+"\" for option "+paramName);
			}
		}

		private int parseInt(String paramName, String paramValue) throws ProfileException {
			try {
				return Integer.parseInt(paramValue);
			} catch (NumberFormatException nfe) {
				throw new ProfileException("Bad value \""+paramValue+"\" for option "+paramName+": integer value required", nfe);
			}
		}

		public MulticastParams(Profile p) throws ProfileException {
			String s;

			address = p.getParameter(OPT_MCAST_ADDR, OPT_MCAST_ADDR_DEFAULT);
			checkTrue(address != null && address.length() > 0, OPT_MCAST_ADDR, address);

			s = p.getParameter(OPT_MCAST_PORT, OPT_MCAST_PORT_DEFAULT);
			port = parseInt(OPT_MCAST_PORT, s);
			checkTrue(port > 0, OPT_MCAST_PORT, s);

			s = p.getParameter(OPT_MCAST_FIRST_TIMEOUT, OPT_MCAST_FIRST_TIMEOUT_DEFAULT);
			firstTimeout = parseInt(OPT_MCAST_FIRST_TIMEOUT, s);
			checkTrue(firstTimeout > 0, OPT_MCAST_FIRST_TIMEOUT, s);

			s = p.getParameter(OPT_MCAST_TIMEOUT, OPT_MCAST_TIMEOUT_DEFAULT);
			timeout = parseInt(OPT_MCAST_TIMEOUT, s);
			checkTrue(timeout >= 0, OPT_MCAST_TIMEOUT, s);

			s = p.getParameter(OPT_MCAST_RETRIES, OPT_MCAST_RETRIES_DEFAULT);
			retries = parseInt(OPT_MCAST_RETRIES, s);
			checkTrue(retries >= 0, OPT_MCAST_RETRIES, s);

			s = p.getParameter(OPT_MCAST_TTL, OPT_MCAST_TTL_DEFAULT);
			ttl = parseInt(OPT_MCAST_TTL, s);
			checkTrue(ttl > 0, OPT_MCAST_TTL, s);
		}
	}

	/*
	 * decode data using the right protocol charset and throw away trailing '\0's
	 */
	public static String decodeData(byte[] data) {
		String result = null;

		try {
			// discard all null bytes at the end of the buffer
			int i;
			for (i = data.length-1; i >= 0; i--) {
				if (data[i] != 0) {
					break;
				}
			}
			if (i > 0) {
				result = new String(data, 0, i+1, PROTO_ENCODING);
			}
		} catch (UnsupportedEncodingException uee) {
			logger.log(Logger.SEVERE, "Cannot decode data with charset "+PROTO_ENCODING, uee);
		}
		return result;
	}

	/*
	 *  check protocol version at the end of request
	 */
	public static int checkProtocolVersion(String request) throws Exception {
		int i = request.lastIndexOf(PROTO_VERSION);
		if (i < 0) {
			throw new Exception("Bad message");
		}
		if (i+PROTO_VERSION.length() != request.length()) {
			throw new Exception("Wrong protocol version");
		}
		return i;
	}

	/*
	 * extract Main address from the list got in response
	 */
	private static MainAddr extractAddress(String response, String proto) {
		logger.log(Logger.FINER, "MainDetectionManager::extractAddress(response=\""+response+"\", proto=\""+proto+"\")");
		MainAddr result = null;

		// parse a list of addresses in form proto_1:hostname_1:port_1;...;proto_n:hostname_n:port_n
		StringTokenizer st = new StringTokenizer(response, PROTO_ADDRESSES_SEPARATOR);
		String address;
		MainAddr ma;
		while (st.hasMoreTokens()) {
			address = st.nextToken();
			try {
				ma = new MainAddr(address);
				result = ma;
				/*
				 * If either no protocol specified or main has an address with
				 * desired protocol, then we are done, otherwise, go on searching
				 * for the right protocol.
				 */
				if (proto == null || proto.equals(ma.protocol)) {
					break;
				}
			} catch (Exception e) {
				// skip malformed address...
				logger.log(Logger.WARNING, "Skipping malformed address", e);
			}
		}
		return result;
	}

	/*
	 * manage responses for get-main query
	 */
	private static MainAddr manageGetMainResponses(List responses, String proto) {
		logger.log(Logger.FINER, "MainDetectionManager::manageGetMainResponses(responses.size()="+responses.size()+", proto=\""+proto+"\")");
		MainAddr mainAddress = null;

		String response;
		String s;
		Iterator iter = responses.iterator();
		byte[] data;
		InetAddress senderHost;
		int senderPort;
		while (iter.hasNext()) {
			DatagramPacket p = (DatagramPacket)iter.next();
			data = p.getData();
			senderHost = p.getAddress();
			senderPort = p.getPort();
			response = decodeData(data);

			try {
				if (response == null) {
					throw new Exception("Response cannot be decoded");
				}
				s = response;

				// first of all, check protocol version in response
				int i = checkProtocolVersion(s);
				s = s.substring(0, i);

				// then, check for errors returned by Main
				i = s.indexOf(PROTO_RESP_OK);
				if (i != 0) {
					throw new Exception("Main container returned Error in response");
				}
				s = s.substring(PROTO_RESP_OK.length());
				mainAddress = extractAddress(s, proto);
				// bail out at first good answer
				break;
			} catch (Exception e) {
				logger.log(Logger.WARNING, "Error managing response \""+response+"\" from "+senderHost+":"+senderPort+"; response discarded", e);
			}
		}
		// at last, extract Main address
		return mainAddress;
	}

	/*
	 * send multicast request, managing timeout and retries, and return decoded response
	 */
	private static List multicAsk(String request, Profile p) throws ProfileException {
		logger.log(Logger.FINER, "MainDetectionManager::multicAsk(...)");
		List result = null;
		/*
		 * usually we expect only one Main, but when more are active,
		 * we collect all responses and choose the one we like more
		 */
		List responses = new ArrayList(3);

		// parse parameters from profile
		MulticastParams mcast = new MulticastParams(p);

		logger.log(Logger.FINER, "MainDetectionManager::multicAsk(): prepared msg=\""+request+"\"");

		InetAddress mcastGroupAddress;
		try {
			mcastGroupAddress = InetAddress.getByName(mcast.address);
		} catch (UnknownHostException e) {
			throw new ProfileException("Cannot resolve address "+mcast.address, e);
		}
		if (!mcastGroupAddress.isMulticastAddress()) {
			throw new ProfileException("Address "+mcast.address+" is not a multicast address");
		}

		MulticastSocket socket = null;
		try {
			try {
				socket = new MulticastSocket(SRC_PORT);
				socket.setTimeToLive(mcast.ttl);
				socket.setSoTimeout(mcast.firstTimeout);
			} catch (IOException ioe) {
				throw new ProfileException("Error setting up multicast socket", ioe);
			}
			DatagramPacket packet;
			int retries = mcast.retries+1;
			do {
				try {
					logger.log(Logger.FINER, "MainDetectionManager::multicAsk(): sending msg =\""+request+"\" to "+mcast.address+":"+mcast.port);
					packet = new DatagramPacket(request.getBytes(PROTO_ENCODING), request.length(), mcastGroupAddress, mcast.port);
					socket.send(packet);

					do {
						try {
							// get responses
							byte[] buf = new byte[DGRAM_BUF_LEN];
							DatagramPacket recv = new DatagramPacket(buf, buf.length);
							socket.receive(recv);
							logger.log(Logger.FINER, "MainDetectionManager::multicAsk(): received "+recv.getLength()+" bytes");
							responses.add(recv);
						} catch (SocketTimeoutException ste) {
							socket.setSoTimeout(mcast.timeout);
							if (responses.size() > 0) {
								// we received at least one answer, it's enough
								break;
							}
							throw ste;
						}
					} while(true);
					if (responses.size() > 0) {
						// bail out
						break;
					}
				} catch (SocketTimeoutException ste) {
					// timeout, go for retry
					--retries;
					logger.log(Logger.FINER, "MainDetectionManager::multicAsk(): timeout, "+retries+" retries left");
				}
			} while (retries > 0);
			if (responses.size() > 0) {
				// we got at least a response!
				result = responses;
			}
		} catch (Exception e) {
			throw new ProfileException("Error during multicast querying", e);
		} finally {
			if (socket != null) {
				socket.close();
			}
		}
		return result;
	}

	/*
	 * build request for get-main command
	 */
	private static String buildGetMainRequest(String platformName, String proto) {
		// build request
		StringBuffer msg = new StringBuffer(PROTO_CMD_GETMAIN);
		if (platformName != null) {
			msg.append('@');
			msg.append(platformName);
		}
		if (proto != null) {
			msg.append(':');
			msg.append(proto);
		}
		msg.append(PROTO_VERSION);
		return msg.toString();
	}

	/*
	 * search for a main container
	 */
	private static MainAddr getMainAddress(Profile profile) throws ProfileException {
		logger.log(Logger.FINER, "MainDetectionManager::getMainAddress(...)");
		MainAddr result = null;
		// get platform name and protocol
		String platformName = profile.getParameter(Profile.PLATFORM_ID, null);
		if (platformName == null) {
			throw new ProfileException("platform id is mandatory when using automatic main detection; use \""+MATCH_ALL_PLATFORMS+"\" to match all");
		}
		if (MATCH_ALL_PLATFORMS.equals(platformName)) {
			platformName = null;
		}
		String proto = profile.getParameter(Profile.MAIN_PROTO, null);

		// build request
		String msg = buildGetMainRequest(platformName, proto);

		// send multicast query to search for a main
		List responses = multicAsk(msg, profile);
		if (responses != null) {
			// parse the response
			result = manageGetMainResponses(responses, proto);
		}
		return result;
	}

	public static void detect(ProfileImpl profile) throws ProfileException {
		logger.log(Logger.FINER, "MainDetectionManager::detect(...)");
		if (!profile.isMasterMain()) {
			MainAddr mainAddress = getMainAddress(profile);
			if (mainAddress == null) {
				// FIXME is the right exception to throw?
				throw new ProfileException("Cannot detect Main Container");
			}
			logger.log(Logger.CONFIG, "setting "+Profile.MAIN_PROTO+"="+mainAddress.protocol);
			profile.setParameter(Profile.MAIN_PROTO, mainAddress.protocol);
			logger.log(Logger.CONFIG, "setting "+Profile.MAIN_HOST+"="+mainAddress.hostname);
			profile.setParameter(Profile.MAIN_HOST, mainAddress.hostname);
			logger.log(Logger.CONFIG, "setting "+Profile.MAIN_PORT+"="+mainAddress.port);
			profile.setParameter(Profile.MAIN_PORT, Integer.toString(mainAddress.port));
		}
	}

	public static MulticastMainDetectionListener createListener(ProfileImpl profile, IMTPManager m) throws ProfileException {
		logger.log(Logger.FINER, "MainDetectionManager::export(...)");
		MulticastMainDetectionListener listener = new MulticastMainDetectionListener(profile, m);
		Thread listenerThread = new Thread(listener);
		listenerThread.start();
		return listener;
	}
}
