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);
                }
            }

        }
    }

}
