/*
 * Jitsi Videobridge, OpenSource video conferencing.
 *
 * Distributable under LGPL license.
 * See terms of license at gnu.org.
 */
package org.jitsi.jigasi.openfire;

import java.io.*;
import java.lang.reflect.*;
import java.net.*;
import java.util.*;
import java.util.jar.*;
import java.util.concurrent.*;
import java.security.cert.Certificate;

import javax.media.*;
import javax.media.protocol.*;
import javax.media.format.*;

import org.jivesoftware.util.*;
import org.jivesoftware.openfire.container.Plugin;
import org.jivesoftware.openfire.container.PluginManager;
import org.jivesoftware.openfire.SessionManager;
import org.jivesoftware.openfire.XMPPServer;
import org.jivesoftware.openfire.muc.*;
import org.jivesoftware.openfire.session.*;
import org.jivesoftware.database.DbConnectionManager;
import java.sql.*;

import org.slf4j.*;
import org.slf4j.Logger;

import net.java.sip.communicator.impl.protocol.jabber.extensions.rayo.*;
import net.java.sip.communicator.util.*;
import org.dom4j.*;
import org.jitsi.jigasi.*;
import org.osgi.framework.*;

import org.xmpp.component.*;
import org.xmpp.packet.*;

import org.jitsi.jigasi.xmpp.*;
import org.jitsi.videobridge.xmpp.*;
import org.jitsi.videobridge.*;
import org.jitsi.impl.neomedia.*;
import org.jitsi.impl.neomedia.format.*;
import org.jitsi.impl.neomedia.device.*;
import org.jitsi.impl.neomedia.conference.*;
import org.jitsi.impl.neomedia.jmfext.media.renderer.audio.*;
import org.jitsi.service.neomedia.*;
import org.jitsi.service.neomedia.device.*;
import org.jitsi.service.neomedia.event.*;
import org.jitsi.service.neomedia.format.*;
import org.jitsi.service.libjitsi.*;
import org.jitsi.util.*;
import org.jitsi.videobridge.openfire.PluginImpl;

import org.ifsoft.*;
import org.ifsoft.sip.*;
import net.sf.fmj.media.rtp.*;
import org.ifsoft.rtp.*;
import uk.nominet.DDDS.*;

public class CallControlComponent extends AbstractComponent
{
    private static final Logger Log = LoggerFactory.getLogger(JigasiPlugin.class);
	public SSRCFactoryImpl ssrcFactory = new SSRCFactoryImpl();
    private long initialLocalSSRC = ssrcFactory.doGenerateSSRC() & 0xFFFFFFFFL;
	public ConcurrentHashMap<String, CallSession> callSessions = new ConcurrentHashMap<String, CallSession>();
	public ConcurrentHashMap<String, String> conferences = new ConcurrentHashMap<String, String>();
	public ConcurrentHashMap<String, String> registrations = new ConcurrentHashMap<String, String>();
	public static CallControlComponent self;
	private SipService sipService = null;
	private MultiUserChatManager mucManager = XMPPServer.getInstance().getMultiUserChatManager();

	private Videobridge getVideobridge()
	{
		return PluginImpl.component.getVideobridge();
	}

	public CallControlComponent(File pluginDirectory)
	{
		Log.info("CallControlComponent " + pluginDirectory);
		self = this;

		Properties properties = new Properties();
		String hostName = JiveGlobals.getProperty("org.jitsi.videobridge.nat.harvester.public.address", XMPPServer.getInstance().getServerInfo().getXMPPDomain());
		String logDir = pluginDirectory.getAbsolutePath() + File.separator + ".." + File.separator + ".." + File.separator + "logs" + File.separator;
		String port = JiveGlobals.getProperty("org.jitsi.videobridge.sip.port.number", "5060");

		properties.setProperty("com.voxbone.kelpie.hostname", hostName);
		properties.setProperty("com.voxbone.kelpie.ip", hostName);
		properties.setProperty("com.voxbone.kelpie.sip_port", port);

		properties.setProperty("javax.sip.IP_ADDRESS", hostName);

		properties.setProperty("gov.nist.javax.sip.TRACE_LEVEL", "99");
		properties.setProperty("gov.nist.javax.sip.SERVER_LOG", logDir + "sip_server.log");
		properties.setProperty("gov.nist.javax.sip.DEBUG_LOG", logDir + "sip_debug.log");

		if (JiveGlobals.getBooleanProperty("org.jitsi.videobridge.ofmeet.sip.enabled", true))
		{
			Log.info("CallControlComponent - enabling SIP gateway ");

			sipService = new SipService(properties);

		} else {
			Log.info("CallControlComponent -enabling SIP gateway");
		}
	}

	@Override protected String[] discoInfoFeatureNamespaces()
	{
		return
			new String[]
				{
					"http://jitsi.org/protocol/jigasi",
					"urn:xmpp:rayo:0"
				};
	}

	@Override public String getDescription()
	{
		return "Call control component";
	}

	@Override public String getName()
	{
		return "Call control";
	}

	public void stop()
	{
		for (CallSession callSession : callSessions.values())
		{
			callSession.mediaStream.stop();
			callSession.mediaStream.close();
		}

		callSessions.clear();

		if (sipService != null) sipService.stop();
	}

	public void recordCall(Conference conference, String token, String state)
	{
		String focusJid = conference.getFocus();
		String domain = XMPPServer.getInstance().getServerInfo().getXMPPDomain();

		Log.info("CallControlComponent - recordCall " + token + " " + state + " " + focusJid);

		IQ iq = new IQ(IQ.Type.set);
		iq.setTo("ofmeet-jitsi-videobridge."+domain);
		iq.setFrom(focusJid);

		Element colibri = iq.setChildElement("conference", "http://jitsi.org/protocol/colibri");
		colibri.addAttribute("id", conference.getID());
		colibri.addElement("recording").addAttribute("state", state).addAttribute("token", token);

		sendPacket(iq);
	}

	private void makeCall(Conference conference, String confJid, String to, String callId, String username, long startTimestamp)
	{
		Log.info("CallControlComponent - makeCall " + confJid + " " + to + " " + callId);

		try {
			String hostname = XMPPServer.getInstance().getServerInfo().getXMPPDomain();
			String callerId = (new JID(confJid)).getNode();
			String focusJid = conference.getFocus();

			MediaService mediaService = LibJitsi.getMediaService();
			MediaStream mediaStream = mediaService.createMediaStream(null, org.jitsi.service.neomedia.MediaType.AUDIO, mediaService.createSrtpControl(SrtpControlType.MIKEY));
			mediaStream.setName("rayo-" + System.currentTimeMillis());

			Content content = conference.getOrCreateContent("audio");
			boolean audioMixer = "true".equals(JiveGlobals.getProperty("org.jitsi.videobridge.ofmeet.audio.mixer", "false"));

			if (audioMixer)
			{
				mediaStream.setSSRCFactory(ssrcFactory);
				mediaStream.setDevice(content.getMixer());
			} else {
				mediaStream.setRTPTranslator(content.getRTPTranslator());
			}

			content.createRtpChannel(null, null, null);

			CallSession cs = new CallSession(mediaStream, hostname, this, callId, focusJid, confJid);
			callSessions.put(callId, cs);

			boolean toSip = to.indexOf("sip:") == 0 ;
			boolean toPhone = to.indexOf("tel:") == 0;
			String from = null;

			if (!toSip && !toPhone)
			{
				String sipUri = null;

				if (to.length() == 8)
				{
					toSip = true;
					to = "sip:8835100" + to + "@81.201.82.25";

				} else {

					if (to.indexOf("+") != 0) to = "+" + to;

					Log.info("CallControlComponent - makeCall looking up "  + to);

					ENUM mEnum = new ENUM("e164.arpa");
					Rule[] rules = mEnum.lookup(to);

					for (Rule rule: rules)
					{
						String temp = rule.evaluate();
						Log.info("CallControlComponent - makeCall found "  + temp);
						if (temp.indexOf("sip:") == 0) sipUri = temp;
					}

					if (sipUri != null)
					{
						toSip = true;
						to = sipUri;

					} else {
						to = "tel:" + to;
						toPhone = true;
					}
				}
			}

			if (toSip)
			{
				from = "sip:" + callerId + "@" + hostname;

				Log.info("CallControlComponent - makeCall with direct sip "  + to + " " + from);

			} else {

				to = to.substring(4);

				if (registrations.containsKey(to))
				{
					to = registrations.get(to);
					from = "sip:" + callerId + "@" + hostname;

					Log.info("CallControlComponent - makeCall with registration "  + to + " " + from);

				} else {

					String outboundProxy = JiveGlobals.getProperty("voicebridge.default.proxy.outboundproxy", null);
					String sipUsername = JiveGlobals.getProperty("voicebridge.default.proxy.sipauthuser", null);

					if (outboundProxy != null && sipUsername != null && !"".equals(outboundProxy.trim()) && !"".equals(sipUsername.trim()))
					{
						to = "sip:" + to + "@" + outboundProxy;
						from = "sip:" + sipUsername + "@" + outboundProxy;

						Log.info("CallControlComponent - makeCall with outbound proxy "  + to + " " + from);

					} else {
						Log.error("SIP proxy not setup with voicebridge.default.proxy.outboundproxy and voicebridge.default.proxy.sipauthuser");
						return;
					}
				}
			}

			cs.jabberRemote = to;
			cs.jabberLocal = from;
			cs.username = username;
			cs.startTimestamp = startTimestamp;

			SipService.sendInvite(cs);

		} catch (Exception e) {

			Log.error("CallControlComponent makeCall", e);
		}
	}

	public void hangupCall(String callId)
	{
		Log.info("hangupCall " + callId);

		CallSession cs = callSessions.remove(callId);

		if (cs != null)
		{
			SipService.sendBye(cs);
			updateCallRecord(cs.startTimestamp, (int)(System.currentTimeMillis() - cs.startTimestamp));

		} else {
			Log.error("CallControlComponent hangup. cannot fine callid " + callId);
		}
	}

	public CallSession findCreateSession(String from, String to, String destination)
	{
		Log.info("CallControlComponent - findCreateSession " + from + " " + to + " " + destination);

		CallSession session = null;
		String hostname = XMPPServer.getInstance().getServerInfo().getXMPPDomain();
		String callerId = to;
		String confJID = null;
		Conference conference =  null;

		boolean allowDirectSIP = "true".equals(JiveGlobals.getProperty("org.jitsi.videobridge.ofmeet.allow.direct.sip", "false"));

		if (!allowDirectSIP || !registrations.containsKey(from))
		{
			Log.warn("CallControlComponent - call rejected from " + from + " " + to);
			return null;	// only accept calls from registered SIP user endpoint
		}

		if (callerId.indexOf("+") == 0) callerId = callerId.substring(1);

		for (MultiUserChatService service : mucManager.getMultiUserChatServices())
		{
			for (MUCRoom room : service.getChatRooms())
			{
				if (!room.isPasswordProtected() && (room.getDescription().indexOf(callerId) > -1 || room.getJID().getNode().equals(callerId)))
				{
					confJID = room.getJID().toString();					// description has telephone no or room name is sip url target
					break;												// and not password protected
				}
			}

			if (confJID != null) break;
		}

		if (confJID != null && conferences.containsKey(confJID))
		{
			String confId = conferences.get(confJID);

			Log.info("CallControlComponent - findCreateSession conference id " + confJID + " " + confId);

			for (Conference conf : getVideobridge().getConferences())
			{
				if (conf.getID().equals(confId))
				{
					conference = conf;
					break;
				}
			}
		}

		if (conference != null)
		{
			Log.info("CallControlComponent - findCreateSession conference " + conference.getFocus());

			try
			{
				MediaService mediaService = LibJitsi.getMediaService();
				MediaStream mediaStream = mediaService.createMediaStream(null, org.jitsi.service.neomedia.MediaType.AUDIO, mediaService.createSrtpControl(SrtpControlType.MIKEY));
				mediaStream.setName("rayo-" + System.currentTimeMillis());

				Content content = conference.getOrCreateContent("audio");
				boolean audioMixer = "true".equals(JiveGlobals.getProperty("org.jitsi.videobridge.ofmeet.audio.mixer", "false"));

				if (audioMixer)
				{
					mediaStream.setSSRCFactory(ssrcFactory);
					mediaStream.setDevice(content.getMixer());
				} else {
					mediaStream.setRTPTranslator(content.getRTPTranslator());
				}

				content.createRtpChannel(null, null, null);

				String callId = Long.toHexString(System.currentTimeMillis());
				session = new CallSession(mediaStream, hostname, this, callId, conference.getFocus(), confJID);

				session.jabberRemote = from;
				session.jabberLocal = to;

				callSessions.put(callId, session);

			}
			catch (Exception e)
			{
				Log.error("CallControlComponent findCreateSession", e);
			}
		}

		if (session != null)
		{
			long startTimestamp = System.currentTimeMillis();
			session.startTimestamp = startTimestamp;
			createCallRecord("admin", from, confJID, startTimestamp, 0, "received");
		}
		return session;
	}

	public void inviteEvent(boolean accepted, String callId)
	{
		Log.info("CallControlComponent - inviteEvent " + accepted + " " + callId);

		if (callSessions.containsKey(callId))
		{
			CallSession session = callSessions.get(callId);

			try {
				JID confJID = new JID(session.roomJID);
				String conferenceId = confJID.getNode();

				MultiUserChatService service = mucManager.getMultiUserChatService(confJID);

				if (service != null)
				{
					if (service.hasChatRoom(conferenceId))
					{
						MUCRoom room = service.getChatRoom(conferenceId);

						if (room != null)
						{
							for (MUCRole role : room.getOccupants())
							{
								Presence presence = new Presence();
								presence.setFrom(callId + "@" + getJID());
								presence.setTo(role.getUserAddress());

								if (accepted)
								{
									Element answered = presence.addChildElement("answered", "urn:xmpp:rayo:1");
									answered.addElement("header").addAttribute("name", "caller_id").addAttribute("value", session.jabberRemote);
									answered.addElement("header").addAttribute("name", "called_id").addAttribute("value", session.jabberLocal);

								} else {
									Element hangup = presence.addChildElement("hangup", "urn:xmpp:rayo:1");
									hangup.addElement("header").addAttribute("name", "caller_id").addAttribute("value", session.jabberRemote);
									hangup.addElement("header").addAttribute("name", "called_id").addAttribute("value", session.jabberLocal);

									callSessions.remove(callId);
								}

								sendPacket(presence);
							}

							return;
						}
					}
				}

				// no valid muc service or room. send events to requestor

				String username = (new JID(session.focusJID)).getNode();

				Collection<ClientSession> sessions = SessionManager.getInstance().getSessions(username);

				for (ClientSession clientSession : sessions)
				{
					Presence presence = new Presence();
					presence.setFrom(callId + "@" + getJID());
					presence.setTo(clientSession.getAddress());

					if (accepted)
					{
						Element answered = presence.addChildElement("answered", "urn:xmpp:rayo:1");
						answered.addElement("header").addAttribute("name", "caller_id").addAttribute("value", session.jabberRemote);
						answered.addElement("header").addAttribute("name", "called_id").addAttribute("value", session.jabberLocal);

					} else {
						Element hangup = presence.addChildElement("hangup", "urn:xmpp:rayo:1");
						hangup.addElement("header").addAttribute("name", "caller_id").addAttribute("value", session.jabberRemote);
						hangup.addElement("header").addAttribute("name", "called_id").addAttribute("value", session.jabberLocal);

						CallSession cs = callSessions.remove(callId);

						if (cs != null)
						{
							updateCallRecord(cs.startTimestamp, (int)(System.currentTimeMillis() - cs.startTimestamp));
						}
					}

					sendPacket(presence);
				}

			} catch (Exception e) {
				Log.error("CallControlComponent inviteEvent. error" + session.roomJID, e);
			}

		} else {
			Log.error("CallControlComponent inviteEvent. cannot find callid " + callId);
		}
	}

	@Override public IQ handleIQSet(IQ iq)	throws Exception
	{
		IQ reply = IQ.createResultIQ(iq);
		String domain = XMPPServer.getInstance().getServerInfo().getXMPPDomain();

		try
		{
			Log.info("CallControlComponent - handleIQSet\n" + iq);

			Element element = iq.getChildElement();
			String namespace = element.getNamespaceURI();
			String request = element.getName();

			String confJid = null;
			String confId = null;

			if ("dial".equals(request) && "urn:xmpp:rayo:1".equals(namespace))
			{
				Log.info("CallControlComponent - Dial");

				String from = element.attributeValue("from");
				String to = element.attributeValue("to");

				for ( Iterator i = element.elementIterator( "header" ); i.hasNext(); )
				{
					Element header = (Element) i.next();
					String name = header.attributeValue("name");
					String value = header.attributeValue("value");

					if ("JvbRoomId".equals(name)) confId = value;
					if ("JvbRoomName".equals(name)) confJid = value;
				}

				if (confJid == null && confId == null)
				{
					reply.setError(PacketError.Condition.item_not_found);
					Log.error("No JvbRoomName or JvbRoomId header found");

				} else {

					if (confId == null)
					{
						if (conferences.containsKey(confJid))
						{
							confId = conferences.get(confJid);
						}
					}

					if (confJid == null)
					{
						confJid = confId + "@conference." + XMPPServer.getInstance().getServerInfo().getXMPPDomain();
					}

					if (confId != null)
					{
						String callId = Long.toHexString(System.currentTimeMillis());

						Log.info("Got dial request " + from + " -> " + to + " confId " + confId + " callId " + callId);

						String callResource = "xmpp:" + callId + "@" + getJID();

						final Element childElement = reply.setChildElement("ref", "urn:xmpp:rayo:1");
						childElement.addAttribute("uri", (String) "xmpp:" + callId + "@" + getJID());
						childElement.addAttribute("id", (String)  callId);

						Conference conference = null;

						for (Conference conf : getVideobridge().getConferences())
						{
							if (conf.getID().equals(confId))
							{
								conference = conf;
								break;
							}
						}

						if (conference != null)
						{
							String username = iq.getFrom().getNode();
							long startTimestamp = System.currentTimeMillis();

							makeCall(conference, confJid, to, callId, username, startTimestamp);
							createCallRecord(username, confJid, to, startTimestamp, 0, "dialed");

						} else {
							Log.error("CallControlComponent - can't find conference " + confId);
							reply.setError(PacketError.Condition.item_not_found);
						}

					} else {
						Log.error("CallControlComponent - focus not ready " + confJid);
						reply.setError(PacketError.Condition.item_not_found);
					}
				}
			}

			else if ("accept".equals(request) && "urn:xmpp:rayo:1".equals(namespace))
			{
				Log.info("CallControlComponent - Accept");

				String confName = null;

				for ( Iterator i = element.elementIterator( "header" ); i.hasNext(); )
				{
					Element header = (Element) i.next();
					String name = header.attributeValue("name");
					String value = header.attributeValue("value");

					if ("JvbRoomId".equals(name)) confId = value;
					if ("JvbRoomName".equals(name)) confName = value;
				}

				if (confId != null && confName != null)
				{
					Log.info("CallControlComponent - Accept register " + confId + " " + confName);
					conferences.put(confName, confId);

				} else {
					reply.setError(PacketError.Condition.item_not_found);
					Log.error("No JvbRoomName or JvbRoomId header found");
				}
			}

			else if ("hangup".equals(request) && "urn:xmpp:rayo:1".equals(namespace))
			{
				Log.info("CallControlComponent - HangUp");
				String callId = iq.getTo().getNode();
				hangupCall(callId);
			}

			else if ("record".equals(request) && "urn:xmpp:rayo:record:1".equals(namespace))
			{
				Log.info("CallControlComponent - Record");

				String token = null;
				String state = null;
				String confName = null;

				for ( Iterator i = element.elementIterator( "hint" ); i.hasNext(); )
				{
					Element hint = (Element) i.next();
					String name = hint.attributeValue("name");
					String value = hint.attributeValue("value");

					if ("JvbToken".equals(name)) token = value;
					if ("JvbState".equals(name)) state = value;
					if ("JvbRoomName".equals(name)) confName = value;
				}

				if (token != null && state != null && confName != null)
				{
					if (conferences.containsKey(confName))
					{
						confId = conferences.get(confName);

						Conference conference = null;

						for (Conference conf : getVideobridge().getConferences())
						{
							if (conf.getID().equals(confId))
							{
								conference = conf;
								break;
							}
						}

						if (conference != null)
						{
							recordCall(conference, token, state);

						} else {
							Log.error("CallControlComponent - can't find conference " + confId);
							reply.setError(PacketError.Condition.item_not_found);
						}
					} else {
						Log.error("CallControlComponent - focus not ready " + confName);
						reply.setError(PacketError.Condition.item_not_found);
					}
				} else {
					reply.setError(PacketError.Condition.item_not_found);
					Log.error("No JvbRoomName, JvbToken or JvbState headers found");
				}
			}

			else
			{
				Log.warn("CallControlComponent - Unknown");
				reply.setError(PacketError.Condition.item_not_found);
			}
		}
		catch (Exception e)
		{
			Log.error("CallControlComponent handleIQSet", e);
			reply.setError(PacketError.Condition.internal_server_error);
		}

		return reply;
	}

	private long ssrcFactoryGenerateSSRC(String cause, int i)
	{
		if (initialLocalSSRC != -1)
		{
			if (i == 0)
				return (int) initialLocalSSRC;
			else if (cause.equals(GenerateSSRCCause.REMOVE_SEND_STREAM.name()))
				return Long.MAX_VALUE;
		}
		return ssrcFactory.doGenerateSSRC();
	}

	private class SSRCFactoryImpl implements SSRCFactory
	{
		private int i = 0;

		/**
		 * The <tt>Random</tt> instance used by this <tt>SSRCFactory</tt> to
		 * generate new synchronization source (SSRC) identifiers.
		 */
		private final Random random = new Random();

		public int doGenerateSSRC()
		{
			return random.nextInt();
		}

		/**
		 * {@inheritDoc}
		 */
		@Override
		public long generateSSRC(String cause)
		{
			return ssrcFactoryGenerateSSRC(cause, i++);
		}
	}

	private void sendPacket(Packet packet)
	{
		try {
			ComponentManagerFactory.getComponentManager().sendPacket(this, packet);
		} catch (Exception e) {

			Log.error("CallControlComponent sendPacket ", e);
		}
	}

   private void createCallRecord(String username, String addressFrom, String addressTo, long datetime, int duration, String calltype)
   {
		boolean sipPlugin = XMPPServer.getInstance().getPluginManager().getPlugin("sip") != null;

		if (sipPlugin)
		{
			Log.info("createCallRecord " + username + " " + addressFrom + " " + addressTo + " " + datetime);

			String sql = "INSERT INTO ofSipPhoneLog (username, addressFrom, addressTo, datetime, duration, calltype) values  (?, ?, ?, ?, ?, ?)";

			Connection con = null;
			PreparedStatement psmt = null;
			ResultSet rs = null;

			try {
				con = DbConnectionManager.getConnection();
				psmt = con.prepareStatement(sql);
				psmt.setString(1, username);
				psmt.setString(2, addressFrom);
				psmt.setString(3, addressTo);
				psmt.setLong(4, datetime);
				psmt.setInt(5, duration);
				psmt.setString(6, calltype);

				psmt.executeUpdate();

			} catch (SQLException e) {
				Log.error(e.getMessage(), e);
			} finally {
				DbConnectionManager.closeConnection(rs, psmt, con);
			}
		}
    }

	private void updateCallRecord(long datetime, int duration)
	{
		boolean sipPlugin = XMPPServer.getInstance().getPluginManager().getPlugin("sip") != null;

		if (sipPlugin)
		{
			Log.info("updateCallRecord " + datetime + " " + duration);

			String sql = "UPDATE ofSipPhoneLog SET duration = ? WHERE datetime = ?";

			Connection con = null;
			PreparedStatement psmt = null;

			try {

				con = DbConnectionManager.getConnection();
				psmt = con.prepareStatement(sql);

				psmt.setInt(1, duration);
				psmt.setLong(2, (datetime / 1000));
				psmt.executeUpdate();


			} catch (SQLException e) {
				Log.error(e.getMessage(), e);

			} finally {
				DbConnectionManager.closeConnection(psmt, con);
			}
		}
	}
}
