/*
 *  Copyright (C) 2006  Takashi Kasuya <kasuya@sfc.keio.ac.jp>
 *
 * 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., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301  USA
 */

package jp.ac.naka.ec.sip;

import java.io.IOException;
import java.text.ParseException;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.Map;
import java.util.Properties;
import java.util.Random;
import java.util.TooManyListenersException;

import javax.sdp.SessionDescription;
import javax.sip.ClientTransaction;
import javax.sip.Dialog;
import javax.sip.DialogState;
import javax.sip.InvalidArgumentException;
import javax.sip.ListeningPoint;
import javax.sip.RequestEvent;
import javax.sip.ServerTransaction;
import javax.sip.SipException;
import javax.sip.SipFactory;
import javax.sip.SipListener;
import javax.sip.SipProvider;
import javax.sip.SipStack;
import javax.sip.address.Address;
import javax.sip.address.AddressFactory;
import javax.sip.address.SipURI;
import javax.sip.header.CSeqHeader;
import javax.sip.header.CallIdHeader;
import javax.sip.header.ContactHeader;
import javax.sip.header.ContentTypeHeader;
import javax.sip.header.EventHeader;
import javax.sip.header.ExpiresHeader;
import javax.sip.header.FromHeader;
import javax.sip.header.HeaderFactory;
import javax.sip.header.MaxForwardsHeader;
import javax.sip.header.SubscriptionStateHeader;
import javax.sip.header.ToHeader;
import javax.sip.header.ViaHeader;
import javax.sip.message.MessageFactory;
import javax.sip.message.Request;
import javax.sip.message.Response;

import jp.ac.naka.ec.entity.Entity;
import jp.ac.naka.ec.sip.pidf.PIDFData;

import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;

/**
 * SIPリクエスト、レスポンスの送信を行うクラス。NOTIFYリクエストのの自動送信もになう。
 * 
 * @author Takashi Kasuya
 * 
 */
public class SipCore {

	private SipProvider provider;
	private MessageFactory messageFactory;
	private AddressFactory addressFactory;
	private HeaderFactory headerFactory;

	// private SipURI uri;
	private static SipCore instance = new SipCore();
	public static int DEFAULT_EXPIRES = 6000;
	public static String event_package = "presence";
	private static Map<String, Dialog> dialogTable = new HashMap<String, Dialog>();
	private static final String pathname = "gov.nist";
	public static int PORT = 5060;
	public static String TRANSPORT = "udp";
	private static Log logger = LogFactory.getLog(SipCore.class);

	/**
	 * 
	 * @param name
	 *            Name of EntityContainer
	 * @param hostname
	 * @param sip_port
	 * @param sip_config
	 * @throws SipException
	 * @throws IOException
	 * @throws ParseException
	 * @throws InvalidArgumentException
	 * @throws TooManyListenersException
	 */
	public void initialize(String hostname, int sip_port) throws SipException,
			IOException, ParseException, InvalidArgumentException,
			TooManyListenersException {
		if (provider != null) {
			return;
		}
		PORT = getValidPort(sip_port);
		SipFactory sipFactory = SipFactory.getInstance();
		sipFactory.setPathName(pathname);

		Properties prop = SipConfiguration.getDefaultConfigurationProperties();
		// v1.2で非推奨
		// prop.setProperty("javax.sip.IP_ADDRESS", hostname);
		SipStack sipStack = null;
		sipStack = sipFactory.createSipStack(prop);
		addressFactory = sipFactory.createAddressFactory();

		// uri = addressFactory.createSipURI(name, hostname + ":" + sip_port);
		logger.info("Initiate SipCore at " + hostname + ":" + sip_port);

		ListeningPoint lp = sipStack.createListeningPoint(hostname, sip_port,
				TRANSPORT);
		provider = sipStack.createSipProvider(lp);
		SipListener sip_listener = new DefaultListener();
		provider.addSipListener(sip_listener);
		messageFactory = sipFactory.createMessageFactory();
		headerFactory = sipFactory.createHeaderFactory();
	}

	private int getValidPort(int port) {
		// TODO
		return port;
	}

	/**
	 * It retrieve an Instance of SipCore.
	 * 
	 * @return
	 */
	public static SipCore getInstance() {
		return instance;
	}

	/**
	 * Send a SIP response by stateless.
	 * 
	 * @param num
	 * @param req
	 * @throws ParseException
	 * @throws InvalidArgumentException
	 * @throws SipException
	 */
	public void sendResponse(int num, Request req) throws ParseException,
			InvalidArgumentException, SipException {
		Response res = messageFactory.createResponse(num, req);
		// ToHeader to = (ToHeader) res.getHeader(ToHeader.NAME);
		FromHeader from = (FromHeader) res.getHeader(FromHeader.NAME);
		logger.info("Sending Response (" + num + ") to " + from.getAddress());
		provider.sendResponse(res);
	}

	/**
	 * Send a SIP response. It holds a Dialog.
	 * 
	 * @param num
	 * @param evt
	 * @param sender_uri
	 * @throws ParseException
	 * @throws SipException
	 * @throws InvalidArgumentException
	 */
	public void sendResponse(int num, RequestEvent evt, String sender_uri)
			throws ParseException, SipException, InvalidArgumentException {
		Request req = evt.getRequest();
		Response res = messageFactory.createResponse(num, req);
		// Expires header is mandatory in 2xx response of SUBSCRIBE
		if (req.getMethod().equals(Request.SUBSCRIBE)) {
			prepareResponse(res);
			ExpiresHeader expiresHeader = req.getExpires();
			int expires = expiresHeader.getExpires() - 1;
			res.setHeader(headerFactory.createExpiresHeader(expires));
		}

		ServerTransaction st = evt.getServerTransaction();
		if (st == null) {
			st = provider.getNewServerTransaction(req);
		}
		logger.info("Sending Response (" + num + ") to " + sender_uri);
		st.sendResponse(res);
		
		if (req.getMethod().equals(Request.REGISTER)) 
			return;
		
		Dialog dialog = st.getDialog();
		// Dialogの保持
		if (req.getMethod().equals(Request.SUBSCRIBE)
				|| req.getMethod().equals(Request.NOTIFY)) {
			dialogTable.put(sender_uri, dialog);
		} else if (req.getMethod().equals(Request.BYE)) {
			// Dialog終了
			FromHeader from = (FromHeader) req.getHeader(FromHeader.NAME);
			String target = from.getAddress().getURI().toString();
			dialogTable.remove(target);
		} else if (num == Response.TRYING) {
			dialogTable.put(sender_uri, dialog);
			dialog.setApplicationData(st);
		}
	}

	/**
	 * ANSWER送信
	 * 
	 * @param sdp
	 * @param req
	 * @param uri2
	 */
	public void sendResponse(SessionDescription sdp, Request req,
			SipURI target, SipURI source) {
		Response res = null;
		try {
			res = messageFactory.createResponse(Response.OK, req);
			ContentTypeHeader type = headerFactory.createContentTypeHeader(
					"application", "sdp");
			res.setContent(sdp, type);

			// ContactHeader
			Address contact_addr = addressFactory.createAddress(source);
			contact_addr.setDisplayName(source.getUser());
			ContactHeader contact = headerFactory
					.createContactHeader(contact_addr);
			res.setHeader(contact);

			String uri = target.toString();
			Dialog dialog = dialogTable.get(uri);
			ServerTransaction st = null;
			if (dialog == null) {
				st = provider.getNewServerTransaction(req);
				dialog = st.getDialog();
				dialogTable.put(target.toString(), dialog);
				dialog.setApplicationData(st);
			} else {
				st = (ServerTransaction) dialog.getApplicationData();
			}
			st.sendResponse(res);
			logger.info("Sending Response 200(OK) to " + uri);

		} catch (Exception e1) {
			logger.warn("Error while creating Response", e1);
			return;
		}
	}

	/**
	 * 
	 * @param res
	 * @throws ParseException
	 * @throws InvalidArgumentException
	 */
	private void prepareResponse(Response res) throws ParseException,
			InvalidArgumentException {
		ToHeader to = (ToHeader) res.getHeader(ToHeader.NAME);
		FromHeader from = (FromHeader) res.getHeader(FromHeader.NAME);
		// TODO
		Random rand = new Random();
		to.setTag(rand.nextInt(999999) + "");
		// ContactHeader
		// Address addr = addressFactory.createAddress(uri);
		ContactHeader contact = headerFactory.createContactHeader(from
				.getAddress());
		res.setHeader(contact);
	}

	/**
	 * Send a SIP Message by a specified method. It sends by stateless.
	 * 
	 * @param target
	 * @param method
	 * @throws SipException
	 */
	public void sendRequest(Entity target, Entity source, String method)
			throws SipException {
		Dialog dialog = dialogTable.get(target.getURI().toString());
		Request req;

		if (dialog == null || dialog.getState() == null
				|| dialog.getState().getValue() > DialogState.EARLY.getValue()) {
			req = createRequest(target, source, method);
			ClientTransaction ct = provider.getNewClientTransaction(req);
			ct.sendRequest();
			dialog = ct.getDialog();
			if (dialog != null)
				dialogTable.put(target.getURI().toString(), dialog);

		} else {
			req = dialog.createRequest(method);
			ClientTransaction ct = provider.getNewClientTransaction(req);
			dialog.sendRequest(ct);
		}
		logger.info("Sending " + method + " to " + target.getURI());
	}

	/**
	 * 
	 * @param message
	 * @param target
	 * @param source
	 * @param method
	 * @throws SipException
	 * @throws ParseException
	 */
	public void sendRequest(String message, Entity target, Entity source,
			String method) throws SipException, ParseException {

		String name = target.getURI().toString();
		Request req;
		/*
		 * if (method.equals(Request.INVITE)) { req = createRequest(target,
		 * source, method);
		 * 
		 * Address contact_addr = addressFactory
		 * .createAddress(source.getURI()); ContactHeader contact =
		 * headerFactory .createContactHeader(contact_addr);
		 * req.setHeader(contact); // SDP body req.setContent(message,
		 * headerFactory.createContentTypeHeader( "application", "sdp"));
		 * ClientTransaction ct = provider.getNewClientTransaction(req);
		 * ct.sendRequest(); dialogTable.put(name, ct.getDialog()); } else
		 */
		if (method.equals(Request.MESSAGE)) {
			req = createRequest(target, source, method);
			FromHeader from = (FromHeader) req.getHeader(FromHeader.NAME);
			Address addr = addressFactory.createAddress(source.getURI());
			addr.setDisplayName(source.getName());
			from.setAddress(addr);
			req.setContent(message, headerFactory.createContentTypeHeader(
					"text", "plain"));
			// TODO Authentication

			// ステートレス送信
			provider.sendRequest(req);
			logger.info("Sending MESSAGE Request to " + target.getURI());
		} else if (method.equals(Request.NOTIFY)) {
			// NOTIFYの場合
			SubscriptionStateHeader state = headerFactory
					.createSubscriptionStateHeader(SubscriptionStateHeader.ACTIVE);
			EventHeader event = headerFactory.createEventHeader(event_package);
			Dialog dialog = dialogTable.get(name);
			req = dialog.createRequest(method);
			ContentTypeHeader contentType = headerFactory
					.createContentTypeHeader(PIDFData.CONTENT_TYPE,
							PIDFData.CONTENT_SUBTYPE);
			req.setContent(message, contentType);
			req.setHeader(state);
			req.setHeader(event);
			ClientTransaction ct = provider.getNewClientTransaction(req);
			dialog.sendRequest(ct);
		} else {
			logger.warn("Invalid Operation :" + method);
		}

	}

	/**
	 * INVITE送信専用メソッド
	 * 
	 * @param sdp
	 * @param target
	 * @param source
	 * @throws SipException
	 * @throws ParseException
	 */
	public void sendInvite(SessionDescription sdp, Entity target, Entity source)
			throws SipException, ParseException {
		Dialog dialog = dialogTable.get(target.getURI().toString());
		Request req;
		Address contact_addr = addressFactory.createAddress(source.getURI());
		ContactHeader contact = headerFactory.createContactHeader(contact_addr);
		if (dialog == null || dialog.getState() == null
				|| dialog.getState().getValue() > DialogState.EARLY.getValue()) {
			String name = target.getURI().toString();
			req = createRequest(target, source, Request.INVITE);
			req.setHeader(contact);
			// SDP body
			if (sdp != null)
				req.setContent(sdp, headerFactory.createContentTypeHeader(
						"application", "sdp"));
			else
				req.setContent("", headerFactory.createContentTypeHeader(
						"application", "sdp"));
			ClientTransaction ct = provider.getNewClientTransaction(req);
			ct.sendRequest();
			dialogTable.put(name, ct.getDialog());
		} else {
			req = dialog.createRequest(Request.INVITE);
			req.setHeader(contact);
			// SDP body
			if (sdp != null)
				req.setContent(sdp, headerFactory.createContentTypeHeader(
						"application", "sdp"));
			else
				req.setContent("", headerFactory.createContentTypeHeader(
						"application", "sdp"));
			ClientTransaction ct = provider.getNewClientTransaction(req);
			dialog.sendRequest(ct);
		}
		logger.info("Sending INVITE Request to " + target.getURI());
	}

	/**
	 * SUBSCRIBE送信用
	 * 
	 * @param target
	 * @param source
	 * @param subscribe
	 * @param default_expires2
	 * @throws SipException
	 */
	public void sendSubscribe(Entity target, Entity source, int expire)
			throws SipException {
		Dialog dialog = dialogTable.get(target.getURI().toString());
		Request req;
		if (dialog == null || dialog.getState() == null
				|| dialog.getState().getValue() > DialogState.EARLY.getValue()) {
			req = createRequest(target, source, Request.SUBSCRIBE);
			try {
				// Expires
				ExpiresHeader expires = headerFactory
						.createExpiresHeader(expire);
				req.setExpires(expires);

			} catch (Exception e) {
				throw new SipException(e.getMessage());
			}

			ClientTransaction ct = provider.getNewClientTransaction(req);
			ct.sendRequest();

			dialog = ct.getDialog();
			if (dialog != null)
				dialogTable.put(target.getURI().toString(), dialog);
		} else {
			req = dialog.createRequest(Request.SUBSCRIBE);
			try {
				// Expires
				ExpiresHeader expires = headerFactory
						.createExpiresHeader(expire);
				req.setExpires(expires);
				// EventHeader
				EventHeader event = headerFactory
						.createEventHeader(event_package);
				req.setHeader(event);
				Address contact_addr = addressFactory.createAddress(source
						.getURI());
				contact_addr.setDisplayName(source.getName());
				ContactHeader contact = headerFactory
						.createContactHeader(contact_addr);
				req.setHeader(contact);
			} catch (Exception e) {
				throw new SipException(e.getMessage());
			}
			ClientTransaction ct = provider.getNewClientTransaction(req);
			dialog.sendRequest(ct);
			if (expire == 0) {
				dialogTable.remove(target.getURI().toString());
			}
		}
		logger.info("Sending SUBSCRIBE Request to " + target.getURI());
	}

	public void sendBye(Entity target) throws SipException {
		Dialog dialog = dialogTable.get(target.getURI().toString());
		Request req;

		if (dialog == null) {
			throw new NullPointerException("Dialog of " + target.toString()
					+ " is null!");
		} else {
			try {
				req = dialog.createRequest(Request.BYE);
				ClientTransaction ct = provider.getNewClientTransaction(req);
				dialog.sendRequest(ct);
				dialogTable.remove(target.getURI().toString());
			} catch (Exception e) {
				logger.warn(e.getMessage(), e);
				return;
			}
		}
		logger.info("Sending BYE to " + target.getURI());
	}

	public void sendAck(Entity target) {
		Dialog dialog = dialogTable.get(target.getURI().toString());
		Request ack;
		try {
			ack = dialog.createAck(dialog.getLocalSeqNumber());
			logger.info("Send ACK to " + dialog.getRemoteTarget());
			dialog.sendAck(ack);
		} catch (Exception e1) {
			logger.warn("Error while sending ACK", e1);
		}
	}

	Random rand = new Random();

	/**
	 * 
	 * @param target
	 * @param source
	 * @param method
	 * @return
	 * @throws SipException
	 */
	private Request createRequest(Entity target, Entity source, String method)
			throws SipException {
		Address toAddress;
		if (target.getContactURI() == null)
			toAddress = addressFactory.createAddress(target.getURI());
		else 
			toAddress = addressFactory.createAddress(target.getContactURI());
		Request req;
		String branch = null;
		String tag = "" + rand.nextInt(100000000);
		try {
			ToHeader to = headerFactory.createToHeader(toAddress, null);
			Address fromAddress = addressFactory.createAddress(source.getURI());
			fromAddress.setDisplayName(source.getName());
			FromHeader from = headerFactory.createFromHeader(fromAddress, tag);

			// ToHeader to = headerFactory.createToHeader(toAddress, null);
			CSeqHeader cSeq = headerFactory.createCSeqHeader(1L, method);
			CallIdHeader callId = provider.getNewCallId();
			MaxForwardsHeader maxForwards = headerFactory
					.createMaxForwardsHeader(70);
			ArrayList<ViaHeader> viaHeaders = new ArrayList<ViaHeader>();
			String ipAddr = provider.getListeningPoint(TRANSPORT)
					.getIPAddress();

			int port = provider.getListeningPoint(TRANSPORT).getPort();
			ViaHeader viaHeader = headerFactory.createViaHeader(ipAddr, port,
					TRANSPORT, branch);
			viaHeaders.add(viaHeader);

			req = messageFactory.createRequest(to.getAddress().getURI(),
					method, callId, cSeq, from, to, viaHeaders, maxForwards);

			if (method.equals(Request.SUBSCRIBE)) {

				// EventHeader
				EventHeader event = headerFactory
						.createEventHeader(event_package);
				req.setHeader(event);
				Address contact_addr = addressFactory.createAddress(source
						.getURI());
				contact_addr.setDisplayName(source.getName());
				ContactHeader contact = headerFactory
						.createContactHeader(contact_addr);
				req.setHeader(contact);
			}
		} catch (Exception e) {
			e.printStackTrace();
			throw new SipException(e.getMessage());
		}

		return req;
	}

}
