/**
 * Copyright 2006-2010 Daniel Henninger.  All rights reserved.
 *
 * This software is published under the terms of the GNU Public License (GPL),
 * a copy of which is included in this distribution.
 */

package net.sf.kraken.protocols.oscar;

import java.util.ArrayList;
import java.util.HashSet;
import java.util.List;
import java.util.Set;

import net.kano.joscar.ByteBlock;
import net.kano.joscar.OscarTools;
import net.kano.joscar.SeqNum;
import net.kano.joscar.flapcmd.SnacCommand;
import net.kano.joscar.net.ConnDescriptor;
import net.kano.joscar.snac.SnacRequest;
import net.kano.joscar.snac.SnacRequestListener;
import net.kano.joscar.snaccmd.CapabilityBlock;
import net.kano.joscar.snaccmd.ExtraInfoBlock;
import net.kano.joscar.snaccmd.ExtraInfoData;
import net.kano.joscar.snaccmd.InfoData;
import net.kano.joscar.snaccmd.conn.ServiceRequest;
import net.kano.joscar.snaccmd.conn.SetExtraInfoCmd;
import net.kano.joscar.snaccmd.icbm.SendImIcbm;
import net.kano.joscar.snaccmd.icbm.SendTypingNotification;
import net.kano.joscar.snaccmd.icq.MetaShortInfoRequest;
import net.kano.joscar.snaccmd.icq.OfflineMsgIcqRequest;
import net.kano.joscar.snaccmd.loc.SetInfoCmd;
import net.kano.joscar.snaccmd.mailcheck.MailCheckCmd;
import net.kano.joscar.snaccmd.ssi.AuthFutureCmd;
import net.kano.joscar.snaccmd.ssi.DeleteItemsCmd;
import net.kano.joscar.ssiitem.BuddyItem;
import net.kano.joscar.ssiitem.VisibilityItem;
import net.sf.kraken.registration.Registration;
import net.sf.kraken.roster.TransportBuddy;
import net.sf.kraken.roster.TransportBuddyManager;
import net.sf.kraken.session.TransportSession;
import net.sf.kraken.type.ChatStateType;
import net.sf.kraken.type.PresenceType;
import net.sf.kraken.type.SupportedFeature;
import net.sf.kraken.type.TransportType;

import org.apache.log4j.Logger;
import org.jivesoftware.openfire.roster.RosterItem;
import org.jivesoftware.openfire.user.UserNotFoundException;
import org.jivesoftware.util.JiveGlobals;
import org.jivesoftware.util.LocaleUtils;
import org.jivesoftware.util.NotFoundException;
import org.xmpp.packet.JID;

/**
 * Represents an OSCAR session.
 *
 * This is the interface with which the base transport functionality will
 * communicate with OSCAR (AIM/ICQ).
 *
 * Yeesh, this is the one I'm most familiar with and yet it's the ugliest.
 * This needs some housecleaning.
 * 
 * @author Daniel Henninger
 */
public class OSCARSession extends TransportSession<OSCARBuddy> {

    static Logger Log = Logger.getLogger(OSCARSession.class);

    private BOSConnection bosConn = null;
    private LoginConnection loginConn = null;
    private final Set<ServiceConnection> services = new HashSet<ServiceConnection>();
    private String propertyPrefix;
    private final SeqNum icqSeqNum = new SeqNum(0, Integer.MAX_VALUE);

    /**
     * Representation of the Server Sided Item hierarchy of this session. 
     */
    private final SSIHierarchy ssiHierarchy;

    /**
     * Initialize a new session object for OSCAR
     * 
     * @param registration The registration information to use during login.
     * @param jid The JID associated with this session.
     * @param transport The transport that created this session.
     * @param priority Priority of this session.
     */
    public OSCARSession(Registration registration, JID jid, OSCARTransport transport, Integer priority) {
        super(registration, jid, transport, priority);
        setSupportedFeature(SupportedFeature.chatstates);

        ssiHierarchy = new SSIHierarchy(this);
        this.propertyPrefix = "plugin.gateway."+transport.getType().toString();
        OscarTools.setDefaultCharset(JiveGlobals.getProperty(this.propertyPrefix+".encoding", "ISO8859-1"));
        if (JiveGlobals.getBooleanProperty("plugin.gateway."+transport.getType()+".crosschat", true)) {
            MY_CAPS.add(CapabilityBlock.BLOCK_ICQCOMPATIBLE);
        }
        if (transport.getType().equals(TransportType.icq)) {
            MY_CAPS.add(CapabilityBlock.BLOCK_ICQ_UTF8);
        }
    }
    
    /**
     * Returns the Server Sided Item hierarchy of this session.
     * 
     * @return Server Sided Item hierarchy
     */
    public SSIHierarchy getSsiHierarchy() {
        return ssiHierarchy;
    }
    
    private final List<CapabilityBlock> MY_CAPS = new ArrayList<CapabilityBlock>();

    public List<CapabilityBlock> getCapabilities() {
        return MY_CAPS;
    }

    /**
     * @see net.sf.kraken.session.TransportSession#logIn(net.sf.kraken.type.PresenceType, String)
     */
    @Override
    public synchronized void logIn(PresenceType presenceType, String verboseStatus) {
        setPendingPresenceAndStatus(presenceType, verboseStatus);
        if (!isLoggedIn()) {
            loginConn = new LoginConnection(new ConnDescriptor(
                    JiveGlobals.getProperty(propertyPrefix+".connecthost", (this.getTransport().getType().equals(TransportType.icq) ? "login.icq.com" : "login.oscar.aol.com")),
                    JiveGlobals.getIntProperty(propertyPrefix+".connectport", 5190)),
                    this);
            loginConn.connect();
        }
    }

    /**
     * @see net.sf.kraken.session.TransportSession#logOut()
     */
    @Override
    public synchronized void logOut() {
        cleanUp();
        sessionDisconnectedNoReconnect(null);
    }

    /**
     * @see net.sf.kraken.session.TransportSession#cleanUp()
     */
    @Override
    public synchronized void cleanUp() {
        if (loginConn != null) {
            try {
                loginConn.stopKeepAlive();
            }
            catch (Exception e) {
                // Ignore.
            }
            try {
                loginConn.disconnect();
            }
            catch (Exception e) {
                // Ignore.
            }
            loginConn = null;
        }
        if (bosConn != null) {
            try {
                bosConn.stopKeepAlive();
            }
            catch (Exception e) {
                // Ignore.
            }
            try {
                bosConn.disconnect();
            }
            catch (Exception e) {
                // Ignore.
            }
            bosConn = null;
        }
        for (ServiceConnection conn : getServiceConnections()) {
            try {
                conn.stopKeepAlive();
            }
            catch (Exception e) {
                // Ignore.
            }
            try {
                conn.disconnect();
            }
            catch (Exception e) {
                // Ignore.
            }
            try {
                services.remove(conn);
            }
            catch (Exception e) {
                // Ignore.
            }
            try {
                snacMgr.unregister(conn);
            }
            catch (Exception e) {
                // Ignore.
            }
        }
    }


    /**
     * @see net.sf.kraken.session.TransportSession#addContact(org.xmpp.packet.JID, String, java.util.ArrayList)
     */
    @Override
    public void addContact(JID jid, String nickname, ArrayList<String> groups) {
        String legacyId = getTransport().convertJIDToID(jid);
        if (nickname == null || nickname.equals("")) {
            nickname = legacyId;
        }

        if (getTransport().getType().equals(TransportType.icq)) {
            request(new AuthFutureCmd(legacyId, null));
        }

        // Syncing takes care of all the dirty work.
        ssiHierarchy.syncContactGroupsAndNickname(legacyId, nickname, groups);
    }

    /**
     * @see net.sf.kraken.session.TransportSession#removeContact(net.sf.kraken.roster.TransportBuddy)
     */
    @Override
    public void removeContact(OSCARBuddy oscarBuddy) {
        String legacyId = getTransport().convertJIDToID(oscarBuddy.getJID());
        for (BuddyItem i : oscarBuddy.getBuddyItems()) {
            if (i.getScreenname().equalsIgnoreCase(legacyId)) {
                request(new DeleteItemsCmd(i.toSsiItem()));
            }
        }
    }

    /**
     * @see net.sf.kraken.session.TransportSession#updateContact(net.sf.kraken.roster.TransportBuddy)
     */
    @Override
    public void updateContact(OSCARBuddy contact) {
        String legacyId = getTransport().convertJIDToID(contact.getJID());
        String nickname = contact.getNickname();
        if (nickname == null || nickname.equals("")) {
            nickname = legacyId;
        }

        // Syncing takes care of all of the dirty work.
        ssiHierarchy.syncContactGroupsAndNickname(legacyId, nickname, (List<String>)contact.getGroups());
    }
    
    /**
     * @see net.sf.kraken.session.TransportSession#acceptAddContact(JID)
     */
    @Override
    public void acceptAddContact(JID jid) {
        final String userID = getTransport().convertJIDToID(jid);
        Log.debug("OSCAR: accept-adding is currently not implemented."
                + " Cannot accept-add: " + userID);
    }

    /**
     * @see net.sf.kraken.session.TransportSession#sendMessage(org.xmpp.packet.JID, String)
     */
    @Override
    public void sendMessage(JID jid, String message) {
        SendImIcbm icbm = new SendImIcbm(getTransport().convertJIDToID(jid), message);
        // TODO: Should we consider checking to see if they really are offline?
        if (getTransport().getType().equals(TransportType.icq)) {
            icbm.setOffline(true);
        }
        request(icbm);
    }

    /**
     * @see net.sf.kraken.session.TransportSession#sendChatState(org.xmpp.packet.JID,net.sf.kraken.type.ChatStateType)
     */
    @Override
    public void sendChatState(JID jid, ChatStateType chatState) {
        if (chatState.equals(ChatStateType.composing)) {
            request(new SendTypingNotification(
                    getTransport().convertJIDToID(jid),
                    SendTypingNotification.STATE_TYPING
            ));
        }
        else if (chatState.equals(ChatStateType.paused)) {
            request(new SendTypingNotification(
                    getTransport().convertJIDToID(jid),
                    SendTypingNotification.STATE_PAUSED
            ));
        }
        else if (chatState.equals(ChatStateType.inactive)) {
            request(new SendTypingNotification(
                    getTransport().convertJIDToID(jid),
                    SendTypingNotification.STATE_NO_TEXT
            ));
        }
    }
    
    /**
     * @see net.sf.kraken.session.TransportSession#sendBuzzNotification(org.xmpp.packet.JID, String)
     */
    @Override
    public void sendBuzzNotification(JID jid, String message) {
    }

    /**
     * @see net.sf.kraken.session.TransportSession#updateLegacyAvatar(String, byte[])
     */
    @Override
    public void updateLegacyAvatar(String type, byte[] data) {
        ssiHierarchy.setIcon(type, data);
    }

    /**
     * Opens/creates a new BOS connection to a specific server and port, given a cookie.
     *
     * @param server Server to connect to.
     * @param port Port to connect to.
     * @param cookie Auth cookie.
     */
    synchronized void startBosConn(String server, int port, ByteBlock cookie) {
        bosConn = new BOSConnection(new ConnDescriptor(server, port), this, cookie);
        bosConn.connect();
    }

    /**
     * Registers the set of SNAC families that the given connection supports.
     *
     * @param conn FLAP connection to be registered.
     */
    void registerSnacFamilies(BasicFlapConnection conn) {
        snacMgr.register(conn);
    }

    protected SnacManager snacMgr = new SnacManager(new PendingSnacListener() {
        public void dequeueSnacs(List<SnacRequest> pending) {
            for (SnacRequest request : pending) {
                handleRequest(request);
            }
        }
    });

    synchronized void handleRequest(SnacRequest request) {
        Log.debug("Handling request "+request);
        int family = request.getCommand().getFamily();
        if (snacMgr.isPending(family)) {
            snacMgr.addRequest(request);
            return;
        }

        BasicFlapConnection conn = snacMgr.getConn(family);

        if (conn != null) {
            conn.sendRequest(request);
        }
        else {
            // it's time to request a service
            if (!(request.getCommand() instanceof ServiceRequest)) {
                snacMgr.setPending(family, true);
                snacMgr.addRequest(request);
                request(new ServiceRequest(family));
            } else {
                // TODO: Why does this occur a lot and yet not cause problems?
                Log.debug("eep! can't find a service redirector server.");
            }
        }
    }

    SnacRequest request(SnacCommand cmd) {
        Log.debug("Sending SNAC command: "+cmd);
        return request(cmd, null);
    }

    private SnacRequest request(SnacCommand cmd, SnacRequestListener listener) {
        Log.debug("Setting up SNAC request and listener: "+cmd+","+listener);
        SnacRequest req = new SnacRequest(cmd, listener);
        handleRequest(req);
        return req;
    }

    void connectToService(int snacFamily, String host, ByteBlock cookie) {
        Log.debug("Connection to service "+snacFamily+" on host "+host);
        ServiceConnection conn;
        if (snacFamily == MailCheckCmd.FAMILY_MAILCHECK) {
            conn = new EmailConnection(new ConnDescriptor(host,
                    JiveGlobals.getIntProperty(propertyPrefix+".connectport", 5190)),
                    this,
                    cookie,
                    snacFamily);
        }
        else {
            conn = new ServiceConnection(new ConnDescriptor(host,
                    JiveGlobals.getIntProperty(propertyPrefix+".connectport", 5190)),
                    this,
                    cookie,
                    snacFamily);
        }

        conn.connect();
    }

    void serviceFailed(ServiceConnection conn) {
        Log.debug("OSCAR service failed: "+conn.toString());
    }

    void serviceConnected(ServiceConnection conn) {
        Log.debug("OSCAR service connected: "+conn.toString());
        services.add(conn);
    }

    public boolean isServiceConnected(ServiceConnection conn) {
        return services.contains(conn);
    }

    public Set<ServiceConnection> getServiceConnections() {
        return services;
    }

    void serviceReady(ServiceConnection conn) {
        Log.debug("OSCAR service ready: "+conn.toString());
        snacMgr.dequeueSnacs(conn);
    }

    void serviceDied(ServiceConnection conn) {
        Log.debug("OSCAR service died: "+conn.toString());
        services.remove(conn);
        snacMgr.unregister(conn);
    }

    /**
     * Apparently we now have the entire list, lets sync.
     */
    void gotCompleteSSI() {
        ArrayList<Integer> nicknameRequests = new ArrayList<Integer>();
        TransportBuddyManager<OSCARBuddy> manager = getBuddyManager();
        for (OSCARBuddy buddy : manager.getBuddies()) {
            String nickname = buddy.getNickname();

            buddy.populateGroupList();

            for (BuddyItem buddyItem : buddy.getBuddyItems()) {
                if (buddyItem.isAwaitingAuth()) {
                    buddy.setAskType(RosterItem.ASK_SUBSCRIBE);
                    buddy.setSubType(RosterItem.SUB_NONE);
                }
                try {
                    if (nickname.equalsIgnoreCase(buddyItem.getScreenname())) {
                        Integer buddyUIN = Integer.parseInt(buddyItem.getScreenname());
                        Log.debug("REQUESTING SHORT INFO FOR "+buddyUIN);
                        nicknameRequests.add(buddyUIN);
                    }
                }
                catch (NumberFormatException e) {
                    // Not an ICQ number then  ;D
                }
            }
        }

        try {
            getTransport().syncLegacyRoster(getJID(), getBuddyManager().getBuddies());
        }
        catch (UserNotFoundException e) {
            Log.debug("Unable to sync oscar contact list for " + getJID(), e);
        }

        getBuddyManager().activate();

        request(new SetInfoCmd(InfoData.forCapabilities(getCapabilities())));

//        if (JiveGlobals.getBooleanProperty("plugin.gateway."+getTransport().getType()+".avatars", true) && getAvatar() != null) {
//            if (storedIconInfo == null || !StringUtils.encodeHex(storedIconInfo.getIconInfo().getData().toByteArray()).equals(getAvatar().getLegacyIdentifier())) {
//                try {
//                    updateLegacyAvatar(getAvatar().getMimeType(), Base64.decode(getAvatar().getImageData()));
//                }
//                catch (NotFoundException e) {
//                    // No avatar found, moving on
//                }
//            }
//        }

        updateStatus(getPresence(), getVerboseStatus());

        ssiHierarchy.setVisibilityFlag(VisibilityItem.MASK_DISABLE_RECENT_BUDDIES);

        if (getTransport().getType().equals(TransportType.icq)) {
            request(new OfflineMsgIcqRequest(getUIN(), (int)nextIcqId()));
        }

        if (JiveGlobals.getBooleanProperty("plugin.gateway."+getTransport().getType()+".mailnotifications", true)) {
            request(new ServiceRequest(MailCheckCmd.FAMILY_MAILCHECK));
        }


        for (Integer uin : nicknameRequests) {
            MetaShortInfoRequest req = new MetaShortInfoRequest(getUIN(), (int)nextIcqId(), uin);
            Log.debug("Doing a MetaShortInfoRequest for "+uin+" as "+req);
            request(req);
        }
    }

    /**
     * @see net.sf.kraken.session.TransportSession#updateStatus(net.sf.kraken.type.PresenceType, String)
     */
    @Override
    public void updateStatus(PresenceType presenceType, String verboseStatus) {
        if (getTransport().getType().equals(TransportType.icq)) {
            request(new SetExtraInfoCmd(((OSCARTransport)getTransport()).convertXMPPStatusToICQ(presenceType)));
        }
        if (presenceType != PresenceType.available && presenceType != PresenceType.chat) {
            String awayMsg = LocaleUtils.getLocalizedString("gateway.oscar.away", "kraken");
            if (verboseStatus != null && verboseStatus.length() > 0) {
                awayMsg = verboseStatus;
            }
            request(new SetInfoCmd(InfoData.forAwayMessage(awayMsg)));
            if (!getTransport().getType().equals(TransportType.icq)) {
                presenceType = PresenceType.away;
            }
        }
        else {
            request(new SetInfoCmd(InfoData.forAwayMessage(InfoData.NOT_AWAY)));
            request(new SetExtraInfoCmd(new ExtraInfoBlock(ExtraInfoBlock.TYPE_AVAILMSG, ExtraInfoData.getAvailableMessageBlock(verboseStatus == null ? "" : verboseStatus))));
        }
        setPresenceAndStatus(presenceType, verboseStatus);
    }

    /**
     * Retrieves the next ICQ id number and increments the counter.
     * @return The next ICQ id number.
     */
    public long nextIcqId() { return icqSeqNum.next(); }

    /**
     * Retrieves a UIN in integer format for the session.
     *
     * @return The UIN in integer format.
     */
    public int getUIN() {
        try {
            return Integer.parseInt(getRegistration().getUsername());
        }
        catch (Exception e) {
            return -1;
        }
    }

    /**
     * Updates roster nickname information about a contact.
     *
     * @param sn Screenname/UIN of contact
     * @param nickname New nickname
     */
    public void updateRosterNickname(String sn, String nickname) {
        try {
            TransportBuddy buddy = getBuddyManager().getBuddy(getTransport().convertIDToJID(sn));
            buddy.setNickname(nickname);
            try {
                getTransport().addOrUpdateRosterItem(getJID(), buddy.getName(), buddy.getNickname(), buddy.getGroups());
            }
            catch (UserNotFoundException e) {
                // Can't update something that's not really in our list.
            }
        }
        catch (NotFoundException e) {
            // Can't update something that's not really in our list.
        }
    }

}
