/*
 * Copyright (C) 2015 AVM GmbH <info@avm.de>
 * Copyright (C) 2009 The Sipdroid Open Source Project
 * Copyright (C) 2005 Luca Veltri - University of Parma - Italy
 * 
 * This file is part of MjSip (http://www.mjsip.org)
 * 
 * MjSip is free software; you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation; either version 2 of the License, or
 * (at your option) any later version.
 * 
 * MjSip 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 General Public License for more details.
 * 
 * You should have received a copy of the GNU General Public License
 * along with MjSip; if not, write to the Free Software
 * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
 * 
 * Author(s):
 * Luca Veltri (luca.veltri@unipr.it)
 * Nitin Khanna, Hughes Systique Corp. (Reason: Android specific change, optmization, bug fix) 
 */

package org.zoolu.sip.provider;

import org.zoolu.net.*;
import org.zoolu.sip.header.*;
import org.zoolu.sip.message.Message;
import org.zoolu.sip.message.MessageFactory;
import org.zoolu.sip.message.SipResponses;
import org.zoolu.sip.address.*;
import org.zoolu.tools.Configure;
import org.zoolu.tools.Configurable;
import org.zoolu.tools.Parser;
import org.zoolu.tools.Random;
import org.zoolu.tools.Log;
import org.zoolu.tools.LogLevel;
import org.zoolu.tools.RotatingLog;
import org.zoolu.tools.SimpleDigest;
import org.zoolu.tools.DateFormat;

import java.util.Hashtable;
import java.io.IOException;
import org.zoolu.tools.HashSet;
import org.zoolu.tools.Iterator;

import de.avm.android.fritzapp.sipua.ui.Receiver;
import de.avm.android.fritzapp.sipua.ui.Sipdroid;
import de.avm.fundamentals.logger.FileLog;

import android.content.Context;
import android.os.PowerManager;

import java.util.Enumeration;
import java.util.Date;
import java.util.Locale;

/**
 * SipProvider implements the SIP transport layer, that is the layer responsable
 * for sending and receiving SIP messages. Messages are received by the callback
 * function defined in the interface SipProviderListener.
 * <p>
 * SipProvider implements also multiplexing/demultiplexing service through the
 * use of SIP interface identifiers and <i>onReceivedMessage()<i/> callback
 * function of specific SipProviderListener.
 * <p>
 * A SipProviderListener can be added to a SipProvider through the
 * addSipProviderListener(id,listener) method, where: <b> - <i>id<i/> is the
 * SIP interface identifier the listener has to be bound to, <b> - <i>listener<i/>
 * is the SipProviderListener that received messages are passed to. <p/> The SIP
 * interface identifier specifies the type of messages the listener is going to
 * receive for. Together with the specific SipProvider, it represents the
 * complete SIP Service Access Point (SAP) address/identifier used for
 * demultiplexing SIP messages at receiving side. <p/> The identifier can be of
 * one of the three following types: transaction_id, dialog_id, or method_id.
 * These types of identifiers characterize respectively: <br> - messages within
 * a specific transaction, <br> - messages within a specific dialog, <br> -
 * messages related to a specific SIP method. It is also possible to use the the
 * identifier ANY to specify <br> - all messages that are out of any
 * transactions, dialogs, or already specified method types.
 * <p>
 * When receiving a message, the SipProvider first tries to look for a matching
 * transaction, then looks for a matching dialog, then for a matching method
 * type, and finally for a default listener (i.e. that with identifier ANY). For
 * the matched SipProviderListener, the method <i>onReceivedMessage()</i> is
 * fired.
 * <p>
 * Note: no 482 (Loop Detected) responses are generated for requests that does
 * not properly match any ongoing transactions, dialogs, nor method types.
 */
public class SipProvider implements Configurable, TransportListener,
		TcpServerListener {

    private static final String TAG = "SipProvider";

	// **************************** Constants ****************************

	/** UDP protocol type */
	public static final String PROTO_UDP = "udp";
	/** TCP protocol type */
	public static final String PROTO_TCP = "tcp";
	/** TLS protocol type */
	public static final String PROTO_TLS = "tls";
	/** SCTP protocol type */
	public static final String PROTO_SCTP = "sctp";

	/**
	 * String value "auto-configuration" used for auto configuration of the host
	 * address.
	 */
	public static final String AUTO_CONFIGURATION = "AUTO-CONFIGURATION";

	/**
	 * String value "auto-configuration" used for auto configuration of the host
	 * address.
	 */
	public static final String ALL_INTERFACES = "ALL-INTERFACES";

	/** String value "NO-OUTBOUND" used for setting no outbound proxy. */
	// public static final String NO_OUTBOUND="NO-OUTBOUND";
	/**
	 * Identifier used as listener id for capturing ANY incoming messages that
	 * does not match any active method_id, transaction_id, nor dialog_id. <br>
	 * In this context, "active" means that there is a active listener for that
	 * specific method, transaction, or dialog.
	 */
	public static final Identifier ANY = new Identifier("ANY");

	/**
	 * Identifier used as listener id for capturing any incoming messages in
	 * PROMISQUE mode, that means that messages are passed to the present
	 * listener regardless of any other active SipProviderListeners for specific
	 * messages. <p/> More than one SipProviderListener can be added and be
	 * active concurrently for capturing messages in PROMISQUE mode.
	 */
	public static final Identifier PROMISQUE = new Identifier("PROMISQUE");

	/** Minimum length for a valid SIP message. */
	private static final int MIN_MESSAGE_LENGTH = 12;

	// ***************** Readable/configurable attributes *****************

	/**
	 * Via address/name. Use 'auto-configuration' for auto detection, or let it
	 * undefined.
	 */
	String via_addr = null;

	/** Local SIP port */
	int host_port = 0;

	/**
	 * Network interface (IP address) used by SIP. Use 'ALL-INTERFACES' for
	 * binding SIP to all interfaces (or let it undefined).
	 */
	String host_ifaddr = null;

	/** Transport protocols (the first protocol is used as default) */
	String[] transport_protocols = null;

	/** Max number of (contemporary) open connections */
	int nmax_connections = 0;

	/**
	 * Outbound proxy (host_addr[:host_port]). Use 'NONE' for not using an
	 * outbound proxy (or let it undefined).
	 */
	SocketAddress outbound_proxy = null;

	/** Whether logging all packets (including non-SIP keepalive tokens). */
	boolean log_all_packets = false;

	// for backward compatibility:

	/** Outbound proxy addr (for backward compatibility). */
	private String outbound_addr = null;
	/** Outbound proxy port (for backward compatibility). */
	private int outbound_port = -1;

	// ********************* Non-readable attributes *********************

	/** Event Loger */
	protected Log event_log = null;

	/** Message Loger */
	protected Log message_log = null;

	/** Network interface (IP address) used by SIP. */
	IpAddress host_ipaddr = null;

	/** Default transport */
	String default_transport = null;

	/** Whether using UDP as transport protocol */
	boolean transport_udp = false;
	/** Whether using TCP as transport protocol */
	boolean transport_tcp = false;
	/** Whether using TLS as transport protocol */
	boolean transport_tls = false;
	/** Whether using SCTP as transport protocol */
	boolean transport_sctp = false;

	/** Whether adding 'rport' parameter on outgoing requests. */
	boolean rport = true;

	/**
	 * Whether forcing 'rport' parameter on incoming requests ('force-rport'
	 * mode).
	 */
	boolean force_rport = false;

	/** List of provider listeners */
	Hashtable<Identifier, SipProviderListener> listeners = null;

	/** List of exception listeners */
	HashSet exception_listeners = null;

	/** UDP transport */
	UdpTransport udp = null;

	/** Tcp server */
	TcpServer tcp_server = null;

	/** Connections */
	Hashtable<ConnectionIdentifier, ConnectedTransport> connections = null;

	// *************************** Costructors ***************************

	/** Creates a void SipProvider. */
	/*
	 * protected SipProvider() { }
	 */

	/** Creates a new SipProvider. */
	public SipProvider(String via_addr, int port) {
		init(via_addr, port, null, null);
		initlog();
		startTrasport();
	}

	/**
	 * Creates a new SipProvider. Costructs the SipProvider, initializing the
	 * SipProviderListeners, the transport protocols, and other attributes.
	 */
	public SipProvider(String via_addr, int port, String[] protocols,
			String ifaddr) {
		init(via_addr, port, protocols, ifaddr);
		initlog();
		startTrasport();
	}

	/**
	 * Creates a new SipProvider. The SipProvider attributres are read from
	 * file.
	 */
	public SipProvider(String file) {
		if (!SipStack.isInit())
			SipStack.init(file);
		new Configure(this, file);
		init(via_addr, host_port, transport_protocols, host_ifaddr);
		initlog();
		startTrasport();
	}

	/**
	 * Inits the SipProvider, initializing the SipProviderListeners, the
	 * transport protocols, the outbound proxy, and other attributes.
	 */
	private void init(String viaddr, int port, String[] protocols, String ifaddr) {
		if (!SipStack.isInit())
			SipStack.init();
		via_addr = viaddr;
		if (via_addr == null || via_addr.equalsIgnoreCase(AUTO_CONFIGURATION))
			via_addr = IpAddress.getLocalIpAddress();
		host_port = port;
		if (host_port < 0) // modified
			host_port = SipStack.default_port;
		host_ipaddr = null;
		if (ifaddr != null && !ifaddr.equalsIgnoreCase(ALL_INTERFACES)) {
			try {
				host_ipaddr = IpAddress.getByName(ifaddr);
			} catch (IOException e) {
                FileLog.w(TAG, e.getMessage(), e);
				host_ipaddr = null;
			}
		}
		transport_protocols = protocols;
		if (transport_protocols == null)
			transport_protocols = SipStack.default_transport_protocols;
		default_transport = transport_protocols[0];
		for (int i = 0; i < transport_protocols.length; i++) {
			transport_protocols[i] = transport_protocols[i].toLowerCase(Locale.US);
			if (transport_protocols[i].equals(PROTO_UDP))
				transport_udp = true;
			else if (transport_protocols[i].equals(PROTO_TCP))
				transport_tcp = true;
			/*
			 * else if (transport_protocols[i].equals(PROTO_TLS))
			 * transport_tls=true; else if
			 * (transport_protocols[i].equals(PROTO_SCTP)) transport_sctp=true;
			 */
		}
		if (nmax_connections <= 0)
			nmax_connections = SipStack.default_nmax_connections;

		// just for backward compatibility..
		if (outbound_port < 0)
			outbound_port = SipStack.default_port;
		if (outbound_addr != null) {
			if (outbound_addr.equalsIgnoreCase(Configure.NONE)
					|| outbound_addr.equalsIgnoreCase("NO-OUTBOUND"))
				outbound_proxy = null;
			else
				outbound_proxy = new SocketAddress(outbound_addr, outbound_port);
		}

		rport = SipStack.use_rport;
		force_rport = SipStack.force_rport;

		exception_listeners = new HashSet();
		listeners = new Hashtable<Identifier, SipProviderListener>(10);

		connections = new Hashtable<ConnectionIdentifier, ConnectedTransport>(10);
	}

	/** Inits logs. */
	private void initlog() {
		if (SipStack.debug_level > 0) {
			String filename = SipStack.log_path + "//" + via_addr + "."
					+ host_port;
			event_log = new RotatingLog(filename + "_events.log", null,
					SipStack.debug_level, SipStack.max_logsize * 1024,
					SipStack.log_rotations, SipStack.rotation_scale,
					SipStack.rotation_time);
			message_log = new RotatingLog(filename + "_messages.log", null,
					SipStack.debug_level, SipStack.max_logsize * 1024,
					SipStack.log_rotations, SipStack.rotation_scale,
					SipStack.rotation_time);
		}
		printLog("Date: " + DateFormat.formatHHMMSS(new Date()), LogLevel.HIGH);
		printLog("SipStack: " + SipStack.release, LogLevel.HIGH);
		printLog("new SipProvider(): " + toString(), LogLevel.HIGH);
	}

	/** Starts the transport services. */
	private void startTrasport() {
		// start udp
		if (transport_udp) {
			try {
				if (host_ipaddr == null)
					udp = new UdpTransport(host_port, this);
				else
					udp = new UdpTransport(host_port, host_ipaddr, this);
				host_port = udp.getPort();
				printLog("udp is up", LogLevel.MEDIUM);
			} catch (Exception e) {
				printException(e, LogLevel.HIGH);
			}
		}
		// start tcp
		if (transport_tcp) {
			try {
				if (host_ipaddr == null)
					tcp_server = new TcpServer(host_port, this);
				else
					tcp_server = new TcpServer(host_port, host_ipaddr, this);
				host_port = tcp_server.getPort();
				printLog("tcp is up", LogLevel.MEDIUM);
			} catch (Exception e) {
				printException(e, LogLevel.HIGH);
			}
		}
		// printLog("transport is up",LogLevel.MEDIUM);
	}

	/** Stops the transport services. */
	private void stopTrasport() {
		// stop udp
		if (udp != null) {
			printLog("udp is going down", LogLevel.LOWER);
			udp.halt();
			udp = null;
		}
		// stop tcp
		if (tcp_server != null) {
			printLog("tcp is going down", LogLevel.LOWER);
			tcp_server.halt();
			tcp_server = null;
		}
		haltConnections();
		connections = null;
	}

	public void haltConnections() { // modified
		if (connections != null) {
			printLog("connections are going down", LogLevel.LOWER);
			for (Enumeration<ConnectedTransport> e = connections.elements(); e
					.hasMoreElements();) {
				ConnectedTransport c = e.nextElement();
				c.halt();
			}
			connections = new Hashtable<ConnectionIdentifier, ConnectedTransport>(10);
		}
	}
	
	/** Stops the SipProviders. */
	public void halt() {
		printLog("halt: SipProvider is going down", LogLevel.MEDIUM);
		stopTrasport();
		listeners = new Hashtable<Identifier, SipProviderListener>(10);
		exception_listeners = new HashSet();
	}

	/** Parses a single line (loaded from the config file) */
	public void parseLine(String line) {
		String attribute;
		Parser par;
		int index = line.indexOf("=");
		if (index > 0) {
			attribute = line.substring(0, index).trim();
			par = new Parser(line, index + 1);
		} else {
			attribute = line;
			par = new Parser("");
		}
		char[] delim = { ' ', ',' };

		if (attribute.equals("via_addr")) {
			via_addr = par.getString();
			return;
		}
		if (attribute.equals("host_port")) {
			host_port = par.getInt();
			return;
		}
		if (attribute.equals("host_ifaddr")) {
			host_ifaddr = par.getString();
			return;
		}
		if (attribute.equals("transport_protocols")) {
			transport_protocols = par.getWordArray(delim);
			return;
		}
		if (attribute.equals("nmax_connections")) {
			nmax_connections = par.getInt();
			return;
		}
		/*
		if (attribute.equals("outbound_proxy")) {
			String soaddr = par.getString();
			if (soaddr == null || soaddr.length() == 0
					|| soaddr.equalsIgnoreCase(Configure.NONE)
					|| soaddr.equalsIgnoreCase("NO-OUTBOUND"))
				outbound_proxy = null;
			else
				outbound_proxy = new SocketAddress(soaddr);
			return;
		}
		*/
		if (attribute.equals("log_all_packets")) {
			log_all_packets = (par.getString().toLowerCase(Locale.US).startsWith("y"));
			return;
		}

		// old parameters
		if (attribute.equals("host_addr"))
			System.err
					.println("WARNING: parameter 'host_addr' is no more supported; use 'via_addr' instead.");
		if (attribute.equals("all_interfaces"))
			System.err
					.println("WARNING: parameter 'all_interfaces' is no more supported; use 'host_iaddr' for setting a specific interface or let it undefined.");
		if (attribute.equals("use_outbound"))
			System.err
					.println("WARNING: parameter 'use_outbound' is no more supported; use 'outbound_proxy' for setting an outbound proxy or let it undefined.");
		if (attribute.equals("outbound_addr")) {
			System.err
					.println("WARNING: parameter 'outbound_addr' has been deprecated; use 'outbound_proxy=<host_addr>[:<host_port>]' instead.");
			outbound_addr = par.getString();
			return;
		}
		if (attribute.equals("outbound_port")) {
			System.err
					.println("WARNING: parameter 'outbound_port' has been deprecated; use 'outbound_proxy=<host_addr>[:<host_port>]' instead.");
			outbound_port = par.getInt();
			return;
		}
	}

	/** Converts the entire object into lines (to be saved into the config file) */
	protected String toLines() { // currently not implemented..
		return toString();
	}

	/** Gets a String with the list of transport protocols. */
	private String transportProtocolsToString() {
		String list = transport_protocols[0];
		for (int i = 1; i < transport_protocols.length; i++)
			list += "/" + transport_protocols[i];
		return list;
	}

	// ************************** Public methods *************************

	/** Gets via address. */
	public String getViaAddress() {
		via_addr = IpAddress.getLocalIpAddress();

		return via_addr;
	}

	/** Sets via address. */
	/*
	 * public void setViaAddress(String addr) { via_addr=addr; }
	 */

	/** Gets host port. */
	public int getPort() {
		return host_port;
	}

	/**
	 * Whether binding the sip provider to all interfaces or only on the
	 * specified host address.
	 */
	public boolean isAllInterfaces() {
		return host_ipaddr == null;
	}

	/** Gets host interface IpAddress. */
	public IpAddress getInterfaceAddress() {
		return host_ipaddr;
	}

	/** Gets array of transport protocols. */
	public String[] getTransportProtocols() {
		return transport_protocols;
	}

	/** Gets the default transport protocol. */
	public String getDefaultTransport() {
		return default_transport;
	}

	/** Gets the default transport protocol. */
	public void setDefaultTransport(String proto) {
		default_transport = proto;
	}

	/** Sets rport support. */
	public void setRport(boolean flag) {
		rport = flag;
	}

	/** Whether using rport. */
	public boolean isRportSet() {
		return rport;
	}

	/** Sets 'force-rport' mode. */
	public void setForceRport(boolean flag) {
		force_rport = flag;
	}

	/** Whether using 'force-rport' mode. */
	public boolean isForceRportSet() {
		return force_rport;
	}

	/** Whether has outbound proxy. */
	public boolean hasOutboundProxy() {
		return outbound_proxy != null;
	}

	/** Gets the outbound proxy. */
	public SocketAddress getOutboundProxy() {
		return outbound_proxy;
	}

	/** Sets the outbound proxy. Use 'null' for not using any outbound proxy. */
	public void setOutboundProxy(SocketAddress soaddr) {
		outbound_proxy = soaddr;
	}

	/** Removes the outbound proxy. */
	/*
	 * public void removeOutboundProxy() { setOutboundProxy(null); }
	 */

	/** Gets the max number of (contemporary) open connections. */
	public int getNMaxConnections() {
		return nmax_connections;
	}

	/** Sets the max number of (contemporary) open connections. */
	public void setNMaxConnections(int n) {
		nmax_connections = n;
	}

	/** Gets event log. */
	public Log getLog() {
		return event_log;
	}

	/** Returns the list (Hashtable) of active listener_IDs. */
	public Hashtable<Identifier, SipProviderListener> getListeners() {
		return listeners;
	}

	/**
	 * Adds a new listener to the SipProvider for caputering any message in
	 * PROMISQUE mode. It is the same as using method
	 * addSipProviderListener(SipProvider.PROMISQUE,listener). <p/> When
	 * capturing messages in promisque mode all messages are passed to the
	 * SipProviderListener before passing them to the specific listener (if
	 * present). <br/> Note that more that one SipProviderListener can be active
	 * in promisque mode at the same time;in that case the same message is
	 * passed to all PROMISQUE SipProviderListeners.
	 * 
	 * @param listener
	 *            is the SipProviderListener.
	 * @return It returns <i>true</i> if the SipProviderListener is added,
	 *         <i>false</i> if the listener_ID is already in use.
	 */
	public boolean addSipProviderPromisqueListener(SipProviderListener listener) {
		return addSipProviderListener(PROMISQUE, listener);
	}

	/**
	 * Adds a new listener to the SipProvider for caputering ANY message. It is
	 * the same as using method
	 * addSipProviderListener(SipProvider.ANY,listener).
	 * 
	 * @param listener
	 *            is the SipProviderListener.
	 * @return It returns <i>true</i> if the SipProviderListener is added,
	 *         <i>false</i> if the listener_ID is already in use.
	 */
	public boolean addSipProviderListener(SipProviderListener listener) {
		return addSipProviderListener(ANY, listener);
	}

	/**
	 * Adds a new listener to the SipProvider.
	 * 
	 * @param id
	 *            is the unique identifier for the messages which the listener
	 *            as to be associated to. It is used as key. It can identify a
	 *            method, a transaction, or a dialog. Use SipProvider.ANY to
	 *            capture all messages. Use SipProvider.PROMISQUE if you want to
	 *            capture all message in promisque mode (letting other listeners
	 *            to capture the same received messages).
	 * @param listener
	 *            is the SipProviderListener for this message id.
	 * @return It returns <i>true</i> if the SipProviderListener is added,
	 *         <i>false</i> if the listener_ID is already in use.
	 */
	public boolean addSipProviderListener(Identifier id,
			SipProviderListener listener) {
		printLog("adding SipProviderListener: " + id, LogLevel.MEDIUM);
		boolean ret;
		Identifier key = id;
		if (listeners.containsKey(key)) {
			printWarning(
					"trying to add a SipProviderListener with a id that is already in use.",
					LogLevel.HIGH);
			ret = false;
		} else {
			listeners.put(key, listener);
			ret = true;
		}

		/*
		if (listeners != null) {
			String list = "";
			for (Enumeration<Identifier> e = listeners.keys(); e
					.hasMoreElements();)
				list += e.nextElement() + ", ";
			printLog(listeners.size() + " listeners: " + list, LogLevel.LOW);
		}
		*/
		return ret;
	}

	/**
	 * Removes a SipProviderListener.
	 * 
	 * @param id
	 *            is the unique identifier used to select the listened messages.
	 * @return It returns <i>true</i> if the SipProviderListener is removed,
	 *         <i>false</i> if the identifier is missed.
	 */
	public boolean removeSipProviderListener(Identifier id) {
		printLog("removing SipProviderListener: " + id, LogLevel.MEDIUM);
		boolean ret;
		Identifier key = id;
		if (!listeners.containsKey(key)) {
			printWarning("trying to remove a missed SipProviderListener.",
					LogLevel.HIGH);
			ret = false;
		} else {
			listeners.remove(key);
			ret = true;
		}

		if (listeners != null) {
			String list = "";
			for (Enumeration<Identifier> e = listeners.keys(); e
					.hasMoreElements();)
				list += e.nextElement() + ", ";
			printLog(listeners.size() + " listeners: " + list, LogLevel.LOW);
		}
		return ret;
	}

	/**
	 * Sets the SipProviderExceptionListener. The SipProviderExceptionListener
	 * is the listener for all exceptions thrown by the SipProviders.
	 * 
	 * @param e_listener
	 *            is the SipProviderExceptionListener.
	 * @return It returns <i>true</i> if the SipProviderListener has been
	 *         correctly set, <i>false</i> if the SipProviderListener was
	 *         already set.
	 */
	public boolean addSipProviderExceptionListener(
			SipProviderExceptionListener e_listener) {
		printLog("adding SipProviderExceptionListener", LogLevel.MEDIUM);
		if (exception_listeners.contains(e_listener)) {
			printWarning(
					"trying to add an already present SipProviderExceptionListener.",
					LogLevel.HIGH);
			return false;
		} else {
			exception_listeners.add(e_listener);
			return true;
		}
	}

	/**
	 * Removes a SipProviderExceptionListener.
	 * 
	 * @param e_listener
	 *            is the SipProviderExceptionListener.
	 * @return It returns <i>true</i> if the SipProviderExceptionListener has
	 *         been correctly removed, <i>false</i> if the
	 *         SipProviderExceptionListener is missed.
	 */
	public boolean removeSipProviderExceptionListener(
			SipProviderExceptionListener e_listener) {
		printLog("removing SipProviderExceptionListener", LogLevel.MEDIUM);
		if (!exception_listeners.contains(e_listener)) {
			printWarning(
					"trying to remove a missed SipProviderExceptionListener.",
					LogLevel.HIGH);
			return false;
		} else {
			exception_listeners.remove(e_listener);
			return true;
		}
	}

	/**
	 * Sends a Message, specifing the transport portocol, nexthop address and
	 * port.
	 * <p>
	 * This is a low level method and forces the message to be routed to a
	 * specific nexthop address, port and transport, regardless whatever the
	 * Via, Route, or request-uri, address to.
	 * <p>
	 * In case of connection-oriented transport, the connection is selected as
	 * follows: <br> - if an existing connection is found matching the
	 * destination end point (socket), such connection is used, otherwise <br> -
	 * a new connection is established
	 * 
	 * @return It returns a Connection in case of connection-oriented delivery
	 *         (e.g. TCP) or null in case of connection-less delivery (e.g. UDP)
	 */
	public ConnectionIdentifier sendMessage(Message msg, String proto,
			String dest_addr, int dest_port, int ttl) {
		if (log_all_packets || msg.getLength() > MIN_MESSAGE_LENGTH)
			printLog("Resolving host address '" + dest_addr + "'",
					LogLevel.MEDIUM);
		try {
			IpAddress dest_ipaddr = IpAddress.getByName(dest_addr);
			return sendMessage(msg, proto, dest_ipaddr, dest_port, ttl);
		} catch (Exception e) {
			printException(e, LogLevel.HIGH);
			return null;
		}
	}

	/**
	 * Sends a Message, specifing the transport portocol, nexthop address and
	 * port.
	 */
	private ConnectionIdentifier sendMessage(Message msg, String proto,
			IpAddress dest_ipaddr, int dest_port, int ttl) {
		ConnectionIdentifier conn_id = new ConnectionIdentifier(proto,
				dest_ipaddr, dest_port);
		if (log_all_packets || msg.getLength() > MIN_MESSAGE_LENGTH)
			printLog("Sending message to " + conn_id, LogLevel.MEDIUM);

		if (transport_udp && proto.equals(PROTO_UDP)) { // UDP
			// printLog("using UDP",LogLevel.LOW);
			conn_id = null;
			try { // if (ttl>0 && multicast_address) do something?
				udp.sendMessage(msg, dest_ipaddr, dest_port);
			} catch (IOException e) {
				printException(e, LogLevel.HIGH);
				return null;
			}
		} else if (transport_tcp && proto.equals(PROTO_TCP)) { // TCP
			// printLog("using TCP",LogLevel.LOW);
			if (!connections.containsKey(conn_id)) {
				printLog("no active connection found matching " + conn_id,
						LogLevel.MEDIUM);
				printLog("open " + proto + " connection to " + dest_ipaddr
						+ ":" + dest_port, LogLevel.MEDIUM);
				TcpTransport conn = null;
				try {
					conn = new TcpTransport(dest_ipaddr, dest_port, this);
				} catch (Exception e) {
					printLog("connection setup FAILED", LogLevel.HIGH);
					return null;
				}
				printLog("connection " + conn + " opened", LogLevel.HIGH);
				addConnection(conn);
			} else {
				printLog("active connection found matching " + conn_id,
						LogLevel.MEDIUM);
			}
			ConnectedTransport conn = (ConnectedTransport) connections
					.get(conn_id);
			if (conn != null) {
				printLog("sending data through conn " + conn, LogLevel.MEDIUM);
				try {
					conn.sendMessage(msg);
					conn_id = new ConnectionIdentifier(conn);
				} catch (IOException e) {
					printException(e, LogLevel.HIGH);
					return null;
				}
			} else { // this point has not to be reached
				printLog("ERROR: conn " + conn_id + " not found: abort.",
						LogLevel.MEDIUM);
				return null;
			}
		} else { // otherwise
			printWarning("Unsupported protocol (" + proto
					+ "): Message discarded", LogLevel.HIGH);
			return null;
		}
		// logs
		String dest_addr = dest_ipaddr.toString();
		printMessageLog(proto, dest_addr, dest_port, msg.getLength(), msg,
				"sent");
		return conn_id;
	}

	/**
	 * Sends the message <i>msg</i>.
	 * <p>
	 * The destination for the request is computed as follows: <br> - if
	 * <i>outbound_addr</i> is set, <i>outbound_addr</i> and <i>outbound_port</i>
	 * are used, otherwise <br> - if message has Route header with lr option
	 * parameter (i.e. RFC3261 compliant), the first Route address is used,
	 * otherwise <br> - the request's Request-URI is considered.
	 * <p>
	 * The destination for the response is computed based on the sent-by
	 * parameter in the Via header field (RFC3261 compliant)
	 * <p>
	 * As transport it is used the protocol specified in the 'via' header field
	 * <p>
	 * In case of connection-oriented transport: <br> - if an already
	 * established connection is found matching the destination end point
	 * (socket), such connection is used, otherwise <br> - a new connection is
	 * established
	 * 
	 * @return Returns a ConnectionIdentifier in case of connection-oriented
	 *         delivery (e.g. TCP) or null in case of connection-less delivery
	 *         (e.g. UDP)
	 */
	public ConnectionIdentifier sendMessage(Message msg) {
		printLog("Sending message:\r\n" + msg.toString(), LogLevel.LOWER);

		// select the transport protocol
		ViaHeader via = msg.getViaHeader();
		String proto;
		if (via != null)
			proto = via.getProtocol().toLowerCase(Locale.US);
		else
			proto = getDefaultTransport().toLowerCase(Locale.US); // modified
		printLog("using transport " + proto, LogLevel.MEDIUM);

		// select the destination address and port
		String dest_addr = null;
		int dest_port = 0;
		int ttl = 0;

		if (!msg.isResponse()) { // modified
			if (outbound_proxy != null) {
				dest_addr = outbound_proxy.getAddress().toString();
				dest_port = outbound_proxy.getPort();
			} else {
				if (msg.hasRouteHeader()
						&& msg.getRouteHeader().getNameAddress().getAddress()
								.hasLr()) {
					SipURL url = msg.getRouteHeader().getNameAddress()
							.getAddress();
					dest_addr = url.getHost();
					dest_port = url.getPort();
				} else {
					SipURL url = msg.getRequestLine().getAddress();
					dest_addr = url.getHost();
					dest_port = url.getPort();
					if (url.hasMaddr()) {
						dest_addr = url.getMaddr();
						if (url.hasTtl())
							ttl = url.getTtl();
						// update the via header by adding maddr and ttl params
						via.setMaddr(dest_addr);
						if (ttl > 0)
							via.setTtl(ttl);
						msg.removeViaHeader();
						msg.addViaHeader(via);
					}
				}
			}
		} else { // RESPONSES
			SipURL url = via.getSipURL();
			if (via.hasReceived())
				dest_addr = via.getReceived();
			else
				dest_addr = url.getHost();
			if (via.hasRport())
				dest_port = via.getRport();
			if (dest_port <= 0)
				dest_port = url.getPort();
		}

		if (dest_port <= 0)
			dest_port = SipStack.default_port;

		return sendMessage(msg, proto, dest_addr, dest_port, ttl);
	}

	/** Sends the message <i>msg</i> using the specified connection. */
	public ConnectionIdentifier sendMessage(Message msg,
			ConnectionIdentifier conn_id) {
		if (log_all_packets || msg.getLength() > MIN_MESSAGE_LENGTH)
			printLog("Sending message through conn " + conn_id, LogLevel.HIGH);
		printLog("message:\r\n" + msg.toString(), LogLevel.LOWER);

		if (conn_id != null && connections.containsKey(conn_id)) { // connection
			// exists
			printLog("active connection found matching " + conn_id,
					LogLevel.MEDIUM);
			ConnectedTransport conn = (ConnectedTransport) connections
					.get(conn_id);
			try {
				conn.sendMessage(msg);
				// logs
				// String proto=conn.getProtocol();
				String proto = conn.getProtocol();
				String dest_addr = conn.getRemoteAddress().toString();
				int dest_port = conn.getRemotePort();
				printMessageLog(proto, dest_addr, dest_port, msg.getLength(),
						msg, "sent");
				return conn_id;
			} catch (Exception e) {
				printException(e, LogLevel.HIGH);
			}
		}
		// else
		printLog("no active connection found matching " + conn_id,
				LogLevel.MEDIUM);
		return sendMessage(msg);
	}

	/**
	 * Processes the message received. It is called each time a new message is
	 * received by the transport layer, and it performs the actual message
	 * processing.
	 */
	protected void processReceivedMessage(Message msg) {
		try { // logs
			printMessageLog(msg.getTransportProtocol(), msg.getRemoteAddress(),
					msg.getRemotePort(), msg.getLength(), msg, "received");

			// discard too short messages
			if (msg.getLength() <= 2) {
				if (log_all_packets)
					printLog("message too short: discarded\r\n", LogLevel.LOW);
				return;
			}
			// discard non-SIP messages
			String first_line = msg.getFirstLine();
			if (first_line == null
					|| first_line.toUpperCase(Locale.US).indexOf("SIP/2.0") < 0) {
				if (log_all_packets)
					printLog("NOT a SIP message: discarded\r\n", LogLevel.LOW);
				return;
			}
			if (!Sipdroid.release)
				printLog("received new SIP message "+msg.getRequestLine()+" "+msg.getStatusLine(), LogLevel.HIGH); // modified
			printLog("message:\r\n" + msg.toString(), LogLevel.LOWER);

			// if a request, handle "received" and "rport" parameters
			if (msg.isRequest()) {
				ViaHeader vh = msg.getViaHeader();
				boolean via_changed = false;
				String src_addr = msg.getRemoteAddress();
				int src_port = msg.getRemotePort();
				String via_addr = vh.getHost();
				int via_port = vh.getPort();
				if (via_port <= 0)
					via_port = SipStack.default_port;

				if (!via_addr.equals(src_addr)) {
					vh.setReceived(src_addr);
					via_changed = true;
				}

				if (vh.hasRport()) {
					vh.setRport(src_port);
					via_changed = true;
				} else {
					if (force_rport && via_port != src_port) {
						vh.setRport(src_port);
						via_changed = true;
					}
				}

				if (via_changed) {
					msg.removeViaHeader();
					msg.addViaHeader(vh);
				}
			}

			// is there any listeners?
			if (listeners == null || listeners.size() == 0) {
				printLog("no listener found: message discarded.", LogLevel.HIGH);
				return;
			}

			// try to look for a UA in promisque mode
			if (listeners.containsKey(PROMISQUE)) {
				printLog("message passed to uas: " + PROMISQUE, LogLevel.MEDIUM);
				((SipProviderListener) listeners.get(PROMISQUE))
						.onReceivedMessage(this, msg);
			}

			// after the callback check if the message is still valid
			if (!msg.isRequest() && !msg.isResponse()) {
				printLog("No valid SIP message: message discarded.",
						LogLevel.HIGH);
				return;
			}

			// this was the promisque listener; now keep on looking for a
			// tighter listener..

			// try to look for a transaction
			Identifier key = msg.getTransactionId();
			printLog("DEBUG: transaction-id: " + key, LogLevel.MEDIUM);
			if (listeners.containsKey(key)) {
				printLog("message passed to transaction: " + key,
						LogLevel.MEDIUM);
				SipProviderListener sip_listener = (SipProviderListener) listeners.get(key);
				if (sip_listener != null)
				{
					sip_listener.onReceivedMessage(
						this, msg);
				}
				return;
			}
			// try to look for a dialog
			key = msg.getDialogId();
			printLog("DEBUG: dialog-id: " + key, LogLevel.MEDIUM);
			if (listeners.containsKey(key)) {
				printLog("message passed to dialog: " + key, LogLevel.MEDIUM);
				((SipProviderListener) listeners.get(key)).onReceivedMessage(
						this, msg);
				return;
			}
			// try to look for a UAS
			key = msg.getMethodId();
			if (listeners.containsKey(key)) {
				printLog("message passed to uas: " + key, LogLevel.MEDIUM);
				((SipProviderListener) listeners.get(key)).onReceivedMessage(
						this, msg);
				return;
			}
			// try to look for a default UA
			if (listeners.containsKey(ANY)) {
				printLog("message passed to uas: " + ANY, LogLevel.MEDIUM);
				((SipProviderListener) listeners.get(ANY)).onReceivedMessage(
						this, msg);
				return;
			}

			RequestLine rl = msg.getRequestLine();
			if(rl != null) {
				String meth = rl.getMethod();
				if(meth != null && meth.equalsIgnoreCase("INVITE")) {
					// NB: fix/hack: if we're here, no listener has answered this INVITE with a "486" although there's likely a pending call.
					// todo: find a better way to do this via SipProviderListener / don't terminate() the InviteTransactionServer in InviteDialog.cancel().
					printLog("----> sending '486 busy here' because there's propably a pending call. <----", LogLevel.HIGH);
					Message resp = MessageFactory.createResponse(msg, 486, SipResponses.reasonOf(486), null);
					sendMessage(resp);
					return;
				}
			}
			
			// if we are here, no listener_ID matched..
			printLog(
					"No SipListener found matching that message: message DISCARDED",
					LogLevel.HIGH);
			// printLog("Pending SipProviderListeners=
			// "+getListeners().size(),3);
			printLog("Pending SipProviderListeners= " + listeners.size(),
					LogLevel.MEDIUM);
		} catch (Exception e) {
			printWarning("Error handling a new incoming message", LogLevel.HIGH);
			printException(e, LogLevel.MEDIUM);
			if (exception_listeners == null || exception_listeners.size() == 0) {
				System.err.println("Error handling a new incoming message");
                FileLog.w(TAG, e.getMessage(), e);
			} else {
				for (Iterator i = exception_listeners.iterator(); i.hasNext();)
					try {
						((SipProviderExceptionListener) i.next())
								.onMessageException(msg, e);
					} catch (Exception e2) {
						printWarning("Error handling handling the Exception",
								LogLevel.HIGH);
						printException(e2, LogLevel.MEDIUM);
					}
			}
		}
	}

	/** Adds a new Connection */
	private void addConnection(ConnectedTransport conn) {
		ConnectionIdentifier conn_id = new ConnectionIdentifier(conn);
		if (connections.containsKey(conn_id)) { // remove the previous
			// connection
			printLog("trying to add the already established connection "
					+ conn_id, LogLevel.HIGH);
			printLog("connection " + conn_id + " will be replaced",
					LogLevel.HIGH);
			ConnectedTransport old_conn = (ConnectedTransport) connections
					.get(conn_id);
			old_conn.halt();
			connections.remove(conn_id);
		} else if (connections.size() >= nmax_connections) { // remove the
			// older unused
			// connection
			printLog(
					"reached the maximum number of connection: removing the older unused connection",
					LogLevel.HIGH);
			long older_time = System.currentTimeMillis();
			ConnectionIdentifier older_id = null;
			for (Enumeration<ConnectedTransport> e = connections.elements(); e
					.hasMoreElements();) {
				ConnectedTransport co =  e.nextElement();
				if (co.getLastTimeMillis() < older_time)
					older_id = new ConnectionIdentifier(co);
			}
			if (older_id != null)
				removeConnection(older_id);
		}
		connections.put(conn_id, conn);
		conn_id = new ConnectionIdentifier(conn);
		conn = (ConnectedTransport) connections.get(conn_id);
		// DEBUG log:
		printLog("active connenctions:", LogLevel.LOW);
		for (Enumeration<ConnectionIdentifier> e = connections.keys(); e
				.hasMoreElements();) {
			ConnectionIdentifier id = (ConnectionIdentifier) e.nextElement();
			printLog("conn-id=" + id + ": "
					+ ((ConnectedTransport) connections.get(id)).toString(),
					LogLevel.LOW);
		}
	}

	/** Removes a Connection */
	private void removeConnection(ConnectionIdentifier conn_id) {
		if (connections != null && connections.containsKey(conn_id)) { // modified
			ConnectedTransport conn = (ConnectedTransport) connections
					.get(conn_id);
			conn.halt();
			connections.remove(conn_id);
			// DEBUG log:
			printLog("active connenctions:", LogLevel.LOW);
			for (Enumeration<ConnectedTransport> e = connections.elements(); e
					.hasMoreElements();) {
				ConnectedTransport co = (ConnectedTransport) e.nextElement();
				printLog("conn " + co.toString(), LogLevel.LOW);
			}
		}
	}

	// ************************* Callback methods *************************
	PowerManager pm;
	PowerManager.WakeLock wl;
	
	/** When a new SIP message is received. */
	public void onReceivedMessage(Transport transport, Message msg) {
		if (pm == null) {
			pm = (PowerManager) Receiver.mContext.getSystemService(Context.POWER_SERVICE);
			wl = pm.newWakeLock(PowerManager.PARTIAL_WAKE_LOCK, "Sipdroid.SipProvider");
		}
		wl.acquire(); // modified
		try
		{
			processReceivedMessage(msg);
		}
		finally
		{
			wl.release();
		}
	}

	/** When Transport terminates. */
	public void onTransportTerminated(Transport transport, Exception error) {
		printLog("transport " + transport + " terminated", LogLevel.MEDIUM);
		if (transport.getProtocol().equals(PROTO_TCP)) {
			ConnectionIdentifier conn_id = new ConnectionIdentifier(
					(ConnectedTransport) transport);
			removeConnection(conn_id);
			if (Sipdroid.on(Receiver.mContext))
				Receiver.engine(Receiver.mContext).register(); // modified
		}
		if (error != null)
			printException(error, LogLevel.HIGH);
	}

	/** When a new incoming Connection is established */
	public void onIncomingConnection(TcpServer tcp_server, TcpSocket socket) {
		printLog("incoming connection from " + socket.getAddress() + ":"
				+ socket.getPort(), LogLevel.MEDIUM);
		ConnectedTransport conn = new TcpTransport(socket, this);
		printLog("tcp connection " + conn + " opened", LogLevel.MEDIUM);
		addConnection(conn);
	}

	/** When TcpServer terminates. */
	public void onServerTerminated(TcpServer tcp_server, Exception error) {
		printLog("tcp server " + tcp_server + " terminated", LogLevel.MEDIUM);
	}

	// ************************** Other methods ***************************

	/**
	 * Picks a fresh branch value. The branch ID MUST be unique across space and
	 * time for all requests sent by the UA. The branch ID always begin with the
	 * characters "z9hG4bK". These 7 characters are used by RFC 3261 as a magic
	 * cookie.
	 */
	public static String pickBranch() { // String
		// str=Long.toString(Math.abs(Random.nextLong()),16);
		// if (str.length()<5) str+="00000";
		// return "z9hG4bK"+str.substring(0,5);
		return "z9hG4bK" + Random.nextNumString(5);
	}

	/**
	 * Picks an unique branch value based on a SIP message. This value could
	 * also be used as transaction ID
	 */
	public String pickBranch(Message msg) {
		StringBuffer sb = new StringBuffer();
		sb.append(msg.getRequestLine().getAddress().toString());
		sb.append(getViaAddress() + getPort());
		ViaHeader top_via = msg.getViaHeader();
		if (top_via.hasBranch())
			sb.append(top_via.getBranch());
		else {
			sb.append(top_via.getHost() + top_via.getPort());
			sb.append(msg.getCSeqHeader().getSequenceNumber());
			sb.append(msg.getCallIdHeader().getCallId());
			sb.append(msg.getFromHeader().getTag());
			sb.append(msg.getToHeader().getTag());
		}
		// return "z9hG4bK"+(new MD5(unique_str)).asHex().substring(0,9);
		return "z9hG4bK" + (new SimpleDigest(5, sb.toString())).asHex();
	}

	/**
	 * Picks a new tag. A tag MUST be globally unique and cryptographically
	 * random with at least 32 bits of randomness. A property of this selection
	 * requirement is that a UA will place a different tag into the From header
	 * of an INVITE than it would place into the To header of the response to
	 * the same INVITE. This is needed in order for a UA to invite itself to a
	 * session.
	 */
	public static String pickTag() { // String
		// str=Long.toString(Math.abs(Random.nextLong()),16);
		// if (str.length()<8) str+="00000000";
		// return str.substring(0,8);
		return "z9hG4bK" + Random.nextNumString(8);
	}

	/**
	 * Picks a new tag. The tag is generated uniquely based on message <i>req</i>.
	 * This tag can be generated for responses in a stateless manner - in a
	 * manner that will generate the same tag for the same request consistently.
	 */
	public static String pickTag(Message req) { // return
		// String.valueOf(tag_generator++);
		// return (new MD5(request.toString())).asHex().substring(0,8);
		return (new SimpleDigest(8, req.toString())).asHex();
	}

	/**
	 * Picks a new call-id. The call-id is a globally unique identifier over
	 * space and time. It is implemented in the form "localid@host". Call-id
	 * must be considered case-sensitive and is compared byte-by-byte.
	 */
	public String pickCallId() { // String
		// str=Long.toString(Math.abs(Random.nextLong()),16);
		// if (str.length()<12) str+="000000000000";
		// return str.substring(0,12)+"@"+getViaAddress();
		return Random.nextNumString(12) + "@" + getViaAddress();
	}

	/** picks an initial CSeq */
	public static int pickInitialCSeq() {
		return 1;
	}

	/**
	 * (<b>Deprecated</b>) Constructs a NameAddress based on an input string.
	 * The input string can be a: <br> - <i>user</i> name, <br> -
	 * <i>user@address</i> url, <br> - <i>"Name" &lt;sip:user@address&gt;</i>
	 * address,
	 * <p>
	 * In the former case, a SIP URL is costructed using the outbound proxy as
	 * host address if present, otherwise the local via address is used.
	 */
	public NameAddress completeNameAddress(String str) {
		if (str.indexOf("<sip:") >= 0)
			return new NameAddress(str);
		else {
			SipURL url = completeSipURL(str);
			return new NameAddress(url);
		}
	}

	/** Constructs a SipURL based on an input string. */
	private SipURL completeSipURL(String str) { // in case it is passed only the
		// 'user' field, add
		// '@'<outbound_proxy>[':'<outbound_port>]
		if (!str.startsWith("sip:") && str.indexOf("@") < 0
				&& str.indexOf(".") < 0 && str.indexOf(":") < 0) { // may be it
			// is just
			// the user
			// name..
			String url = "sip:" + str + "@";
			if (outbound_proxy != null) {
				url += outbound_proxy.getAddress().toString();
				int port = outbound_proxy.getPort();
				if (port > 0 && port != SipStack.default_port)
					url += ":" + port;
			} else {
				url += getViaAddress();
				if (host_port > 0 && host_port != SipStack.default_port)
					url += ":" + host_port;
			}
			return new SipURL(url);
		} else
			return new SipURL(str);
	}

	/**
	 * Constructs a SipURL for the given <i>username</i> on the local SIP UA.
	 * If <i>username</i> is null, only host address and port are used.
	 */
	/*
	 * public SipURL getSipURL(String user_name) { return new
	 * SipURL(user_name,via_addr,(host_port!=SipStack.default_port)?host_port:-1); }
	 */

	// ******************************* Logs *******************************
	/** Gets a String value for this object */
	public String toString() {
		if (host_ipaddr == null)
			return host_port + "/" + transportProtocolsToString();
		else
			return host_ipaddr.toString() + ":" + host_port + "/"
					+ transportProtocolsToString();
	}

	/** Adds a new string to the default Log */
	private final void printLog(String str, int level) {
		if (!Sipdroid.release)
		{
			if (event_log != null) {
				String provider_id = (host_ipaddr == null) ? Integer
						.toString(host_port) : host_ipaddr.toString() + ":"
						+ host_port;
				event_log.println(TAG + "-" + provider_id + ": " + str, level
						+ SipStack.LOG_LEVEL_TRANSPORT);
			}
			if (level <= LogLevel.HIGH) System.out.println(TAG + ": " + str); // modified
		}
	}

	/** Adds a WARNING to the default Log */
	private final void printWarning(String str, int level) {
		printLog("WARNING: " + str, level);
	}

	/** Adds the Exception message to the default Log */
	private final void printException(Exception e, int level) {
		if (event_log != null)
			event_log.printException(e, level + SipStack.LOG_LEVEL_TRANSPORT);
	}

	/** Adds the SIP message to the messageslog */
	private final void printMessageLog(String proto, String addr, int port,
			int len, Message msg, String str) {
		if (log_all_packets || len >= MIN_MESSAGE_LENGTH) {
			if (message_log != null) {
				message_log.printPacketTimestamp(proto, addr, port, len, str
						+ "\r\n" + msg.toString()
						+ "-----End-of-message-----\r\n", 1);
			}
			if (event_log != null) {
				String first_line = msg.getFirstLine();
				if (first_line != null)
					first_line = first_line.trim();
				else
					first_line = "NOT a SIP message";
				event_log.print("\r\n");
				event_log.printPacketTimestamp(proto, addr, port, len,
						first_line + ", " + str, 1);
				event_log.print("\r\n");
			}
		}
	}

}
