package org.jivesoftware.smackx;

import java.util.HashMap;
import java.util.Iterator;
import java.util.List;
import java.util.Map;

import org.jivesoftware.smack.Connection;
import org.jivesoftware.smack.PacketCollector;
import org.jivesoftware.smack.PacketListener;
import org.jivesoftware.smack.Roster;
import org.jivesoftware.smack.RosterEntry;
import org.jivesoftware.smack.SmackConfiguration;
import org.jivesoftware.smack.XMPPException;
import org.jivesoftware.smack.filter.PacketIDFilter;
import org.jivesoftware.smack.filter.PacketTypeFilter;
import org.jivesoftware.smack.packet.IQ;
import org.jivesoftware.smack.packet.Packet;
import org.jivesoftware.smack.packet.Presence;
import org.jivesoftware.smack.packet.Registration;
import org.jivesoftware.smack.util.StringUtils;
import org.jivesoftware.smackx.packet.DiscoverInfo;
import org.jivesoftware.smackx.packet.DiscoverInfo.Identity;

/**
 * This class provides an abstract view to gateways/transports. This class handles all
 * actions regarding gateways and transports.
 * @author Till Klocke
 *
 */
public class Gateway {
	
	private Connection connection;
	private ServiceDiscoveryManager sdManager;
	private Roster roster;
	private String entityJID;
	private Registration registerInfo;
	private Identity identity;
	private DiscoverInfo info;
	
	Gateway(Connection connection, String entityJID){
		this.connection = connection;
		this.roster = connection.getRoster();
		this.sdManager = ServiceDiscoveryManager.getInstanceFor(connection);
		this.entityJID = entityJID;
	}
	
	Gateway(Connection connection, String entityJID, DiscoverInfo info, Identity identity){
		this(connection, entityJID);
		this.info = info;
		this.identity = identity;
	}
	
	private void discoverInfo() throws XMPPException{
		info = sdManager.discoverInfo(entityJID);
		Iterator<Identity> iterator = info.getIdentities();
		while(iterator.hasNext()){
			Identity temp = iterator.next();
			if(temp.getCategory().equalsIgnoreCase("gateway")){
				this.identity = temp;
				break;
			}
		}
	}
	
	private Identity getIdentity() throws XMPPException{
		if(identity==null){
			discoverInfo();
		}
		return identity;
	}
	
	private Registration getRegisterInfo(){
		if(registerInfo==null){
			refreshRegisterInfo();
		}
		return registerInfo;
	}
	
	private void refreshRegisterInfo(){
		Registration packet = new Registration();
		packet.setFrom(connection.getUser());
		packet.setType(IQ.Type.GET);
		packet.setTo(entityJID);
		PacketCollector collector = 
			connection.createPacketCollector(new PacketIDFilter(packet.getPacketID()));
		connection.sendPacket(packet);
		Packet result = collector.nextResult(SmackConfiguration.getPacketReplyTimeout());
		collector.cancel();
		if(result instanceof Registration && result.getError()==null){ 
			Registration register = (Registration)result;
			this.registerInfo = register;
		}
	}
	
	/**
	 * Checks if this gateway supports In-Band registration
	 * @return true if In-Band registration is supported
	 * @throws XMPPException
	 */
	public boolean canRegister() throws XMPPException{
		if(info==null){
			discoverInfo();
		}
		return info.containsFeature("jabber:iq:register");
	}
	
	/**
	 * Returns all fields that are required to register to this gateway
	 * @return a list of required fields
	 */
	public List<String> getRequiredFields(){
		return getRegisterInfo().getRequiredFields();
	}
	
	/**
	 * Returns the name as proposed in this gateways identity discovered via service
	 * discovery
	 * @return a String of its name
	 * @throws XMPPException
	 */
	public String getName() throws XMPPException{
		if(identity==null){
			discoverInfo();
		}
		return identity.getName();
	}
	
	/**
	 * Returns the type as proposed in this gateways identity discovered via service
	 * discovery. See {@link http://xmpp.org/registrar/disco-categories.html} for 
	 * possible types
	 * @return a String describing the type
	 * @throws XMPPException
	 */
	public String getType() throws XMPPException{
		if(identity==null){
			discoverInfo();
		}
		return identity.getType();
	}
	
	/**
	 * Returns true if the registration informations indicates that you are already 
	 * registered with this gateway
	 * @return true if already registered
	 * @throws XMPPException
	 */
	public boolean isRegistered() throws XMPPException{
		return getRegisterInfo().isRegistered();
	}
	
	/**
	 * Returns the value of specific field of the registration information. Can be used
	 * to retrieve for example to retrieve username/password used on an already registered
	 * gateway.
	 * @param fieldName name of the field
	 * @return a String containing the value of the field or null
	 */
	public String getField(String fieldName){
		return getRegisterInfo().getField(fieldName);
	}
	
	/**
	 * Returns a List of Strings of all field names which contain values.
	 * @return a List of field names
	 */
	public List<String> getFieldNames(){
		return getRegisterInfo().getFieldNames();
	}
	
	/**
	 * A convenience method for retrieving the username of an existing account 
	 * @return String describing the username
	 */
	public String getUsername(){
		return getField("username");
	}
	
	/**
	 * A convenience method for retrieving the password of an existing accoung
	 * @return String describing the password
	 */
	public String getPassword(){
		return getField("password");
	}
	
	/**
	 * Returns instructions for registering with this gateway
	 * @return String containing instructions
	 */
	public String getInstructions(){
		return getRegisterInfo().getInstructions();	
	}
	
	/**
	 * With this method you can register with this gateway or modify an existing registration
	 * @param username String describing the username
	 * @param password String describing the password
	 * @param fields additional fields like email.
	 * @throws XMPPException 
	 */
	public void register(String username, String password, Map<String,String> fields)throws XMPPException{
		if(getRegisterInfo().isRegistered()) {
			throw new IllegalStateException("You are already registered with this gateway");
		}
		Registration register = new Registration();
		register.setFrom(connection.getUser());
		register.setTo(entityJID);
		register.setType(IQ.Type.SET);
		register.setUsername(username);
		register.setPassword(password);
		for(String s : fields.keySet()){
			register.addAttribute(s, fields.get(s));
		}
		PacketCollector resultCollector = 
			connection.createPacketCollector(new PacketIDFilter(register.getPacketID())); 
		connection.sendPacket(register);
		Packet result = 
			resultCollector.nextResult(SmackConfiguration.getPacketReplyTimeout());
		resultCollector.cancel();
		if(result!=null && result instanceof IQ){
			IQ resultIQ = (IQ)result;
			if(resultIQ.getError()!=null){
				throw new XMPPException(resultIQ.getError());
			}
			if(resultIQ.getType()==IQ.Type.ERROR){
				throw new XMPPException(resultIQ.getError());
			}
			connection.addPacketListener(new GatewayPresenceListener(), 
					new PacketTypeFilter(Presence.class));
			roster.createEntry(entityJID, getIdentity().getName(), new String[]{});
		}
		else{
			throw new XMPPException("Packet reply timeout");
		}
	}
	
	/**
	 * A convenience method for registering or modifying an account on this gateway without
	 * additional fields
	 * @param username String describing the username
	 * @param password String describing the password
	 * @throws XMPPException
	 */
	public void register(String username, String password) throws XMPPException{
		register(username, password,new HashMap<String,String>());
	}
	
	/**
	 * This method removes an existing registration from this gateway
	 * @throws XMPPException
	 */
	public void unregister() throws XMPPException{
		Registration register = new Registration();
		register.setFrom(connection.getUser());
		register.setTo(entityJID);
		register.setType(IQ.Type.SET);
		register.setRemove(true);
		PacketCollector resultCollector = 
			connection.createPacketCollector(new PacketIDFilter(register.getPacketID()));
		connection.sendPacket(register);
		Packet result = resultCollector.nextResult(SmackConfiguration.getPacketReplyTimeout());
		resultCollector.cancel();
		if(result!=null && result instanceof IQ){
			IQ resultIQ = (IQ)result;
			if(resultIQ.getError()!=null){
				throw new XMPPException(resultIQ.getError());
			}
			if(resultIQ.getType()==IQ.Type.ERROR){
				throw new XMPPException(resultIQ.getError());
			}
			RosterEntry gatewayEntry = roster.getEntry(entityJID);
			roster.removeEntry(gatewayEntry);
		}
		else{
			throw new XMPPException("Packet reply timeout");
		}
	}
	
	/**
	 * Lets you login manually in this gateway. Normally a gateway logins you when it
	 * receives the first presence broadcasted by your server. But it is possible to
	 * manually login and logout by sending a directed presence. This method sends an
	 * empty available presence direct to the gateway.
	 */
	public void login(){
		Presence presence = new Presence(Presence.Type.available);
		login(presence);
	}
	
	/**
	 * This method lets you send the presence direct to the gateway. Type, To and From
	 * are modified.
	 * @param presence the presence used to login to gateway
	 */
	public void login(Presence presence){
		presence.setType(Presence.Type.available);
		presence.setTo(entityJID);
		presence.setFrom(connection.getUser());
		connection.sendPacket(presence);
	}
	
	/**
	 * This method logs you out from this gateway by sending an unavailable presence
	 * to directly to this gateway.
	 */
	public void logout(){
		Presence presence = new Presence(Presence.Type.unavailable);
		presence.setTo(entityJID);
		presence.setFrom(connection.getUser());
		connection.sendPacket(presence);
	}
	
	private class GatewayPresenceListener implements PacketListener{

		public void processPacket(Packet packet) {
			if(packet instanceof Presence){
				Presence presence = (Presence)packet;
				if(entityJID.equals(presence.getFrom()) && 
						roster.contains(presence.getFrom()) &&
						presence.getType().equals(Presence.Type.subscribe)){
					Presence response = new Presence(Presence.Type.subscribed);
					response.setTo(presence.getFrom());
					response.setFrom(StringUtils.parseBareAddress(connection.getUser()));
					connection.sendPacket(response);
				}
			}
			
		}
	}

}