package org.jivesoftware.openfire.plugin.gojara.messagefilter;

import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.Timer;
import java.util.TimerTask;

import org.dom4j.Element;
import org.dom4j.Node;
import org.jivesoftware.openfire.PacketRouter;
import org.jivesoftware.openfire.XMPPServer;
import org.jivesoftware.openfire.interceptor.PacketInterceptor;
import org.jivesoftware.openfire.interceptor.PacketRejectedException;
import org.jivesoftware.openfire.plugin.gojara.messagefilter.processors.*;
import org.jivesoftware.openfire.plugin.gojara.sessions.GojaraAdminManager;
import org.jivesoftware.openfire.plugin.gojara.sessions.TransportSessionManager;
import org.jivesoftware.openfire.plugin.gojara.utils.XpathHelper;
import org.jivesoftware.openfire.roster.RosterManager;
import org.jivesoftware.openfire.session.Session;
import org.jivesoftware.util.ConcurrentHashSet;
import org.jivesoftware.util.JiveGlobals;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.xmpp.packet.IQ;
import org.xmpp.packet.JID;
import org.xmpp.packet.Message;
import org.xmpp.packet.Packet;
import org.xmpp.packet.Presence;

/**
 * This is the only Interceptor GoJara uses. Each IQ/Message/Presence is checked if it is of interest to us, so we can
 * process it accordingly. This is done in the specific Processors. The Interceptor keeps a set of currently connected
 * Transports.
 * 
 * @author axel.frederik.brand
 * @author Holger Bergunde
 * 
 */
public class MainInterceptor implements PacketInterceptor {

	private static final Logger Log = LoggerFactory.getLogger(MainInterceptor.class);
	private Set<String> activeTransports = new ConcurrentHashSet<String>();
	/**
	 * For referencing the abstract remote Processors
	 */
	private Map<String, AbstractRemoteRosterProcessor> packetProcessors = new HashMap<String, AbstractRemoteRosterProcessor>();
	private TransportSessionManager tSessionManager = TransportSessionManager.getInstance();
	private GojaraAdminManager gojaraAdminmanager = GojaraAdminManager.getInstance();
	private XMPPServer server;
	private Boolean frozen;

	public MainInterceptor() {
		Log.info("Created MainInterceptor for GoJara Plugin.");
		server = XMPPServer.getInstance();
		RosterManager rosterManager = server.getRosterManager();

		AbstractRemoteRosterProcessor iqRegisteredProcessor = new DiscoIQRegisteredProcessor();
		AbstractRemoteRosterProcessor iqRosterPayloadProcessor = new IQRosterPayloadProcessor(rosterManager);
		AbstractRemoteRosterProcessor nonPersistantProcessor = new NonPersistantRosterProcessor(rosterManager);
		AbstractRemoteRosterProcessor statisticsProcessor = new StatisticsProcessor();
		AbstractRemoteRosterProcessor updateToComponentProcessor = new ClientToComponentUpdateProcessor(activeTransports);
		AbstractRemoteRosterProcessor whitelistProcessor = new WhitelistProcessor(activeTransports);
		AbstractRemoteRosterProcessor mucfilterProcessor = new MucFilterProcessor();
		AbstractRemoteRosterProcessor gojaraAdminProcessor = new GojaraAdminProcessor();
		packetProcessors.put("sparkIQRegistered", iqRegisteredProcessor);
		packetProcessors.put("iqRosterPayload", iqRosterPayloadProcessor);
		packetProcessors.put("handleNonPersistant", nonPersistantProcessor);
		packetProcessors.put("statisticsProcessor", statisticsProcessor);
		packetProcessors.put("clientToComponentUpdate", updateToComponentProcessor);
		packetProcessors.put("whitelistProcessor", whitelistProcessor);
		packetProcessors.put("mucfilterProcessor", mucfilterProcessor);
		packetProcessors.put("gojaraAdminProcessor", gojaraAdminProcessor);
		frozen = false;

	}

	public boolean addTransport(String subDomain) {
		Log.info("Adding " + subDomain + " to watched Transports.");
		boolean retval = this.activeTransports.add(subDomain);
		if (retval) {
			tSessionManager.addTransport(subDomain);
			gojaraAdminmanager.testAdminConfiguration(subDomain);
		}

		return retval;
	}

	public boolean removeTransport(String subDomain) {
		Log.info("Removing " + subDomain + " from watched Transports.");
		tSessionManager.removeTransport(subDomain);
		gojaraAdminmanager.gatewayUnregistered(subDomain);
		return this.activeTransports.remove(subDomain);
	}

	public void freeze() {
		Log.info("Freezing GoJara Maininterceptor.");
		frozen = true;
	}

	/**
	 * As our Set of Subdomains is a Hash of Strings like icq.domain.tld, if we want to check if a jid CONTAINS a
	 * watched subdomain we need to iterate over the set. We also return the subdomain as a string so we can use it if
	 * we find it.
	 */
	private String searchJIDforSubdomain(String jid) {
		if (jid.length() > 0) {
			for (String subdomain : activeTransports) {
				if (jid.contains(subdomain))
					return subdomain;
			}
		}
		return "";
	}

	/**
	 * This Interceptor tests if GoJara needs to process this package. We decided to do one global Interceptor so we
	 * would'nt redundantly test for cases we already checked in previous Interceptors, also we have only one big ugly
	 * If Structure to maintain instead of several.
	 * 
	 * @see org.jivesoftware.openfire.interceptor.PacketInterceptor#interceptPacket (org.xmpp.packet.Packet,
	 *      org.jivesoftware.openfire.session.Session, boolean, boolean)
	 */
	public void interceptPacket(Packet packet, Session session, boolean incoming, boolean processed) throws PacketRejectedException {
		if (frozen)
			return;

		String from = "";
		String to = "";
		if (!processed || (incoming && processed)) {

			try {
				if (packet.getFrom() != null)
					from = packet.getFrom().toString();
				if (packet.getTo() != null)
					to = packet.getTo().toString();
			} catch (IllegalArgumentException e) {
				Log.debug("There was an illegal JID while intercepting Message for GoJara. Not Intercepting it! " + e.getMessage());
				return;
			}
		}

		if (incoming && !processed) {

			if (packet instanceof IQ) {
				IQ iqPacket = (IQ) packet;
				Element query = iqPacket.getChildElement();
				if (query == null)
					return;
				// Jabber:IQ:roster Indicates Client to Component update or Rosterpush
				else if (query.getNamespaceURI().equals("jabber:iq:roster")) {
					if (!activeTransports.contains(from) && to.length() == 0 && iqPacket.getType().equals(IQ.Type.set))
						packetProcessors.get("clientToComponentUpdate").process(packet, "", to, from);
					else if (from.length() > 0 && activeTransports.contains(from))
						packetProcessors.get("iqRosterPayload").process(packet, from, to, from);
				}
				// SPARK IQ REGISTERED Feature
				else if (query.getNamespaceURI().equals("http://jabber.org/protocol/disco#info") && to.length() > 0
						&& activeTransports.contains(to) && iqPacket.getType().equals(IQ.Type.get)) {
					packetProcessors.get("sparkIQRegistered").process(packet, to, to, from);
				}
				// JABBER:IQ:LAST - Autoresponse Feature
				else if (JiveGlobals.getBooleanProperty("plugin.remoteroster.iqLastFilter", false)
						&& query.getNamespaceURI().equals("jabber:iq:last")) {

					String to_s = searchJIDforSubdomain(to);
					if (to_s.length() > 0 && iqPacket.getType().equals(IQ.Type.get))
						throw new PacketRejectedException();
				}

			}
			// Gojara Admin Manager Feature - Intercept responses to ADHOC commands sent via AdminManager
			else if (packet instanceof Message && activeTransports.contains(from) && to.contains("gojaraadmin")) {
				packetProcessors.get("gojaraAdminProcessor").process(packet, from, to, from);
			}
			// NONPERSISTANT Feature
			else {
				if (!JiveGlobals.getBooleanProperty("plugin.remoteroster.persistent", false)) {
					if (packet instanceof Presence && activeTransports.contains(from))
						packetProcessors.get("handleNonPersistant").process(packet, from, to, from);
				}
			}

		} else if (incoming && processed) {
			// We ignore Pings from S2 to S2 itself.
			// STATISTICS - Feature
			String from_searched = searchJIDforSubdomain(from);
			String to_searched = searchJIDforSubdomain(to);
			String subdomain = from_searched.length() == 0 ? to_searched : from_searched;
			if (!from.equals(to) && subdomain.length() > 0)
				packetProcessors.get("statisticsProcessor").process(packet, subdomain, to, from);
			// TransportSession Feature
			if (packet instanceof Presence && activeTransports.contains(from)) {
				Presence presence_packet = (Presence) packet;
				if (presence_packet.getType() == null) {
					tSessionManager.connectUserTo(from, packet.getTo().getNode().toString());
				} else if (presence_packet.getType() != null && presence_packet.getType().equals(Presence.Type.unavailable)) {
					tSessionManager.disconnectUserFrom(from, packet.getTo().getNode().toString());
				}
			}
			// TransportSession Feature - track Registrations and unregistrations so we can reset unsuccesfull ones
			else if (packet instanceof IQ && activeTransports.contains(to)) {
				IQ iqPacket = (IQ) packet;
				Element query = iqPacket.getChildElement();
				if (query == null)
					return;
				/*
				 * Okay, so now we have a IQ Packet with a query xmlns: jabber:iq:register Spark sends registrations and
				 * unregistrations in such a query, register has a x xmls jabber:iq:gateway:register, unregister is just
				 * a <remove/> element Gajim sends the whole form with x xmlns jabber:x:data type="submit", where a
				 * field var=unregister value of 0 is a registration, and unregister = 1 is unregistration
				 */
				if (query.getNamespaceURI().equals("jabber:iq:register") && iqPacket.getType().equals(IQ.Type.set)) {
					// spark + gajim unregister
					if (query.element("remove") != null)
						tSessionManager.removeRegistrationOfUserFromDB(to, iqPacket.getFrom().getNode().toString());
					else if (query.element("x") != null) {
						Element xElem = query.element("x");
						String xNamespace = xElem.getNamespaceURI();
						// spark register
						if (xNamespace.equals("jabber:iq:gateway:register"))
							tSessionManager.registerUserTo(to, iqPacket.getFrom().getNode().toString());
						// Gajim register + presence push
						else if (xNamespace.equals("jabber:x:data") && xElem.attribute("type").getText().equals("submit")) {
							tSessionManager.registerUserTo(to, iqPacket.getFrom().getNode().toString());
							presencePush(packet.getTo(), packet.getFrom(), 150);
						}
					}
				}
			}

		} else if (!incoming && !processed) {

			if (packet instanceof IQ) {
				IQ iqPacket = (IQ) packet;
				Element query = iqPacket.getChildElement();
				if (query == null)
					return;

				// DISCO#ITEMS - Whitelisting Feature
				if (query.getNamespaceURI().equals("http://jabber.org/protocol/disco#items"))
					packetProcessors.get("whitelistProcessor").process(packet, "", to, from);
				// DISCO#INFO - MUC-Filter-Feature
				else if (JiveGlobals.getBooleanProperty("plugin.remoteroster.mucFilter", false)
						&& query.getNamespaceURI().equals("http://jabber.org/protocol/disco#info") && from.length() > 0
						&& activeTransports.contains(from))
					packetProcessors.get("mucfilterProcessor").process(packet, from, to, from);

				// GAJIM presence push when connecting... probably doesnt really belong here but by now we would have
				// 50processors for every edge case we handle...
				// If roster is persistent and we dont block presences no need to do this since presences are being
				// pushed by OF anyways
				else if (JiveGlobals.getBooleanProperty("plugin.remoteroster.gajimBroadcast", false)) {
					if (to.contains("Gajim") && query.getNamespaceURI().equals("jabber:iq:roster") && iqPacket.getType().equals(IQ.Type.result)) {
						List<Node> nodes = XpathHelper.findNodesInDocument(iqPacket.getElement().getDocument(), "//roster:item");
						for (Node n : nodes) {
							String jid = n.valueOf("@jid");
							if (activeTransports.contains(jid)) {
								JID pushTo = new JID(jid);
								presencePush(pushTo, iqPacket.getTo(), 3000);
							}
						}
					}
				}

			} else if (JiveGlobals.getBooleanProperty("plugin.remoteroster.blockPresences", true) && packet instanceof Presence) {
				/*
				 * We block Presences to users of a subdomain (except transport itself) so OF/S2 wont log you in
				 * automatically if you have a subdomain user in your roster. This prevents some clients from logging
				 * automatically, as the clients dont send a specific presence to the transport. We could do a presence
				 * push if we see them come online, but that would do the same as just not intercepting these presences,
				 * as the problem is really the client
				 */
				String to_s = searchJIDforSubdomain(to);
				if (to_s.length() > 0 && !activeTransports.contains(to))
					throw new PacketRejectedException();
			}
		}
	}

	/**
	 * Just pushes a available presence, we need this for GAJIM Client as it does not push available presence after
	 * registering We also wait a little so after register transport is on users roster. Really didnt wanted this here
	 * but doesnt really belong anywhere else
	 * 
	 * @param to
	 * @param from
	 * @param delay MS until the job should be done
	 */
	private void presencePush(final JID to, final JID from, int delay) {
		TimerTask pushPresenceTask = new TimerTask() {

			@Override
			public void run() {
				PacketRouter router = server.getPacketRouter();
				Packet presence = new Presence();
				presence.setTo(to);
				presence.setFrom(from);
				router.route(presence);
			}
		};

		Timer timer = new Timer();
		timer.schedule(pushPresenceTask, delay);

	}
}
