/*
 * 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.roster;

import net.sf.kraken.avatars.Avatar;
import net.sf.kraken.type.NameSpace;
import net.sf.kraken.type.PresenceType;

import org.apache.log4j.Logger;
import org.dom4j.DocumentHelper;
import org.dom4j.Element;
import org.dom4j.QName;
import org.jivesoftware.openfire.roster.RosterItem;
import org.jivesoftware.util.JiveGlobals;
import org.jivesoftware.util.NotFoundException;
import org.xmpp.packet.JID;
import org.xmpp.packet.Presence;

import java.lang.ref.WeakReference;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Date;
import java.util.List;

/**
 * Transport Buddy.
 *
 * This class is intended to be extended and includes all necessary pieces for
 * syncing with a roster.  It also handles keeping current statuses and such for
 * easy retrieval and only sends presence changes upon status changes.  So that
 * the base transport can manage this list too sometimes, it is very important
 * that the specific transport implementation take into account that there may
 * buddy instances that do not know anything about any 'custom' fields the
 * transport may be implementing.
 *
 * @author Daniel Henninger
 */
public abstract class TransportBuddy {

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

    /**
     * Default constructor, nothing set up.
     */
    public TransportBuddy() {
        // Nothing
    }

    /**
     * Creates a TransportBuddy instance.
     *
     * @param manager Transport buddy manager we are associated with.
     * @param contactname The legacy contact name.
     * @param nickname The legacy nickname (can be null).
     * @param groups The list of groups the legacy contact is in (can be null).
     */
    public TransportBuddy(TransportBuddyManager manager, String contactname, String nickname, Collection<String> groups) {
        this.managerRef = new WeakReference<TransportBuddyManager>(manager);
        this.jid = manager.getSession().getTransport().convertIDToJID(contactname);
        this.contactname = manager.getSession().getTransport().convertJIDToID(this.jid);
        if (nickname != null) {
            this.nickname = nickname;
        }
        else {
            this.nickname = this.contactname;
        }
        if (groups != null && !groups.isEmpty()) {
            this.groups = groups;
        }
        if (JiveGlobals.getBooleanProperty("plugin.gateway."+getManager().getSession().getTransport().getType()+".avatars", true)) {
            try {
                this.avatar = new Avatar(this.jid);
                this.avatarSet = true;
            }
            catch (NotFoundException e) {
                // Ok then, no avatar, no worries.
            }
        }
        lastActivityTimestamp = new Date().getTime();
    }

    /**
     * The transport buddy manager we are attached to.
     */
    private WeakReference<TransportBuddyManager> managerRef = null;

    public TransportBuddyManager getManager() {
        return managerRef.get();
    }

    /**
     * ID, Screenname, name, whatever the contact name is on the legacy system
     */
    public String contactname = null;

    /**
     * Converted JID for the contact, for caching purposes
     */
    public JID jid = null;

    /**
     * A nickname associated with this contact, if it exists.
     */
    public String nickname = null;

    /**
     * A group associated with this contact, if it exists.
     */
    public Collection<String> groups = new ArrayList<String>();

    /**
     * Specific requested subscription status, if desired.
     */
    public RosterItem.SubType subtype = RosterItem.SUB_TO;

    /**
     * Specific requested ask status, if desired.
     */
    public RosterItem.AskType asktype = null;

    /**
     * Current presence status.
     */
    public PresenceType presence = PresenceType.unavailable;

    /**
     * Current verbose status.
     */
    public String verboseStatus = "";

    /**
     * Avatar instance associated with this contact.
     */
    public Avatar avatar = null;

    /**
     * Has the avatar been set?
     */
    public Boolean avatarSet = false;
    
    /**
     * Timestamp of last activity
     */
    public Long lastActivityTimestamp = null;
    
    /**
     * Retrieves timestamp of last activity.
     * 
     * @return Timestamp in milliseconds since the epoch.
     */
    public Long getLastActivityTimestamp() {
        return lastActivityTimestamp;
    }

    /**
     * Retrieves text event of last activity or null if no event text.
     * 
     * @return Text of last event.
     */
    public String getLastActivityEvent() {
        return lastActivityEvent;
    }

    /**
     * Text of last activity
     */
    public String lastActivityEvent = null;

    /**
     * Retrieves the name of the contact.
     *
     * @return Name of contact.
     */
    public String getName() {
       return contactname;
    }

    /**
     * Retrieves the JID of the contact.
     *
     *
     * @return JID of contact.
     */
    public JID getJID() {
        return jid;
    }

    /**
     * Sets the name of the contact.
     *
     * @param contactname Username of the contact.
     */
    public void setName(String contactname) {
        this.jid = getManager().getSession().getTransport().convertIDToJID(contactname);
        this.contactname = getManager().getSession().getTransport().convertJIDToID(this.jid);
    }

    /**
     * Retrieves the nickname of the contact.
     *
     * @return Nickname of contact.
     */
    public String getNickname() {
       return nickname;
    }

    /**
     * Sets the nickname of the contact.
     *
     * @param nickname Nickname of contact.
     */
    public void setNickname(String nickname) {
        Boolean changed = false;
        if (nickname != null) {
            if (this.nickname == null || !this.nickname.equals(nickname)) {
                changed = true;
            }
            this.nickname = nickname;
        }
        else {
            if (this.nickname == null || !this.nickname.equals(getName())) {
                changed = true;
            }
            this.nickname = getName();
        }
        if (changed && getManager().isActivated()) {
            Log.debug("TransportBuddy: Triggering contact update for "+this);
            getManager().getSession().updateContact(this);
        }
    }

    /**
     * Retrieves the groups of the contact.
     *
     * @return Groups contact is in.
     */
    public Collection<String> getGroups() {
       return groups;
    }

    /**
     * Sets the list of groups of the contact.
     *
     * @param groups List of groups the contact is in.
     */
    public void setGroups(List<String> groups) {
        Boolean changed = false;
        if (groups != null && !groups.isEmpty()) {
            if (this.groups == null || this.groups.isEmpty() || !groups.containsAll(this.groups) || !this.groups.containsAll(groups)) {
                changed = true;
            }
            this.groups = groups;
        }
        else {
            if (this.groups != null && !this.groups.isEmpty()) {
                changed = true;
            }
            this.groups = null;
        }
        if (changed && getManager().isActivated()) {
            Log.debug("TransportBuddy: Triggering contact update for "+this);
            getManager().getSession().updateContact(this);
        }
    }

    /**
     * Sets the nickname and list of groups of the contact.
     *
     * @param nickname Nickname of contact.
     * @param groups List of groups the contact is in.
     */
    public void setNicknameAndGroups(String nickname, List<String> groups) {
        Boolean changed = false;
        if (nickname != null) {
            if (this.nickname == null || !this.nickname.equals(nickname)) {
                changed = true;
            }
            this.nickname = nickname;
        }
        else {
            if (this.nickname == null || !this.nickname.equals(getName())) {
                changed = true;
            }
            this.nickname = getName();
        }
        if (groups != null && !groups.isEmpty()) {
            if (this.groups == null || this.groups.isEmpty() || !groups.containsAll(this.groups) || !this.groups.containsAll(groups)) {
                changed = true;
            }
            this.groups = groups;
        }
        else {
            if (this.groups != null && !this.groups.isEmpty()) {
                changed = true;
            }
            this.groups = null;
        }
        if (changed && getManager().isActivated()) {
            Log.debug("TransportBuddy: Triggering contact update for "+this);
            getManager().getSession().updateContact(this);
        }
    }

    /**
     * Retrieves the subscription status for the contact.
     *
     * @return SubType if set.
     */
    public RosterItem.SubType getSubType() {
        return subtype;
    }

    /**
     * Sets the subscription status for the contact.
     *
     * @param substatus Subscription status to be set.
     */
    public void setSubType(RosterItem.SubType substatus) {
        subtype = substatus;
    }

    /**
     * Retrieves the ask status for the contact.
     *
     * @return AskType if set.
     */
    public RosterItem.AskType getAskType() {
        return asktype;
    }

    /**
     * Sets the ask status for the contact.
     *
     * @param askstatus Ask status to be set.
     */
    public void setAskType(RosterItem.AskType askstatus) {
        asktype = askstatus;
    }

    /**
     * Retrieves the current status.
     *
     * @return Current status setting.
     */
    public PresenceType getPresence() {
        return presence;
    }

    /**
     * Sets the current status.
     *
     * @param newpresence New presence to set to.
     */
    public void setPresence(PresenceType newpresence) {
        if (newpresence == null) {
            newpresence = PresenceType.unknown;
        }
        if (newpresence.equals(PresenceType.unavailable)) {
            verboseStatus = "";
        }
        if (!presence.equals(newpresence) && newpresence != PresenceType.unknown) {
            Presence p = new Presence();
            p.setTo(getManager().getSession().getJID());
            p.setFrom(jid);
            getManager().getSession().getTransport().setUpPresencePacket(p, newpresence);
            if (!verboseStatus.equals("")) {
                p.setStatus(verboseStatus);
            }
            if (avatarSet && avatar != null) {
                Element vcard = p.addChildElement("x", NameSpace.VCARD_TEMP_X_UPDATE);
                vcard.addElement("photo").addCDATA(avatar.getXmppHash());
                vcard.addElement("hash").addCDATA(avatar.getXmppHash());
            }
            getManager().sendPacket(p);
        }
        presence = newpresence;
        lastActivityTimestamp = new Date().getTime();
    }

    /**
     * Retrieves the current verbose status.
     *
     * @return Current verbose status.
     */
    public String getVerboseStatus() {
        return verboseStatus;
    }

    /**
     * Sets the current verbose status.
     *
     * @param newstatus New verbose status.
     */
    public void setVerboseStatus(String newstatus) {
        if (newstatus == null) {
            newstatus = "";
        }
        if (!verboseStatus.equals(newstatus)) {
            Presence p = new Presence();
            p.setTo(getManager().getSession().getJID());
            p.setFrom(jid);
            getManager().getSession().getTransport().setUpPresencePacket(p, presence);
            if (!newstatus.equals("")) {
                p.setStatus(newstatus);
            }
            if (avatarSet && avatar != null) {
                Element vcard = p.addChildElement("x", NameSpace.VCARD_TEMP_X_UPDATE);
                vcard.addElement("photo").addCDATA(avatar.getXmppHash());
                vcard.addElement("hash").addCDATA(avatar.getXmppHash());
            }
            getManager().sendPacket(p);
        }
        verboseStatus = newstatus;
        lastActivityTimestamp = new Date().getTime();
        lastActivityEvent = verboseStatus;
    }

    /**
     * Convenience routine to set both presence and verbose status at the same time.
     *
     * @param newpresence New presence to set to.
     * @param newstatus New verbose status.
     */
    public void setPresenceAndStatus(PresenceType newpresence, String newstatus) {
        Log.debug("Updating status ["+newpresence+","+newstatus+"] for "+this);
        if (newpresence == null) {
            newpresence = PresenceType.unknown;
        }
        if (newstatus == null) {
            newstatus = "";
        }
        if (newpresence.equals(PresenceType.unavailable)) {
            newstatus = "";
        }
        if ((!presence.equals(newpresence) && newpresence != PresenceType.unknown) || !verboseStatus.equals(newstatus)) {
            Presence p = new Presence();
            p.setTo(getManager().getSession().getJID());
            p.setFrom(jid);
            getManager().getSession().getTransport().setUpPresencePacket(p, newpresence);
            if (!newstatus.equals("")) {
                p.setStatus(newstatus);
            }
            if (avatarSet && avatar != null) {
                Element vcard = p.addChildElement("x", NameSpace.VCARD_TEMP_X_UPDATE);
                vcard.addElement("photo").addCDATA(avatar.getXmppHash());
                vcard.addElement("hash").addCDATA(avatar.getXmppHash());
            }
            getManager().sendPacket(p);
        }
        presence = newpresence;
        verboseStatus = newstatus;
        lastActivityTimestamp = new Date().getTime();
        lastActivityEvent = verboseStatus;
    }

    /**
     * Sends the current presence to the session user.
     *
     * @param to JID to send presence updates to.
     */
    public void sendPresence(JID to) {
        // TODO: Should figure out best way to handle unknown here.
        Presence p = new Presence();
        p.setTo(to);
        p.setFrom(jid);
        getManager().getSession().getTransport().setUpPresencePacket(p, presence);
        if (verboseStatus != null && verboseStatus.length() > 0) {
            p.setStatus(verboseStatus);
        }
        if (avatarSet && avatar != null) {
            Element vcard = p.addChildElement("x", NameSpace.VCARD_TEMP_X_UPDATE);
            vcard.addElement("photo").addCDATA(avatar.getXmppHash());
            vcard.addElement("hash").addCDATA(avatar.getXmppHash());
        }
        getManager().sendPacket(p);
    }

    /**
     * Sends the current presence only if it's not unavailable.
     *
     * @param to JID to send presence updates to.
     */
    public void sendPresenceIfAvailable(JID to) {
       if (!presence.equals(PresenceType.unavailable)) {
           sendPresence(to);
       }
    }

    /**
     * Sends an offline presence for a contact only if it is currently online.
     *
     * In case you are wondering why, this is useful when a resource goes offline and we want to indicate all contacts are offline.
     *
     * @param to JID to send presence updates to.
     */
    public void sendOfflinePresenceIfAvailable(JID to) {
       if (!presence.equals(PresenceType.unavailable)) {
           Presence p = new Presence();
           p.setType(Presence.Type.unavailable);
           p.setTo(to);
           p.setFrom(jid);
           getManager().sendPacket(p);
       }
    }

    /**
     * Retrieves the cached avatar associated with this contact.
     *
     * @return The avatar associated with this contact, or null if no avatar present.
     */
    public Avatar getAvatar() {
        return avatar;
    }

    /**
     * Sets the current avatar for this contact.
     *
     * @param avatar Avatar instance to associate with this contact.
     */
    public void setAvatar(Avatar avatar) {
        boolean triggerUpdate = false;
        if (    (avatar != null && this.avatar == null) ||
                (avatar == null && this.avatar != null) ||
                (avatar != null && !this.avatar.getXmppHash().equals(avatar.getXmppHash()))) {
            triggerUpdate = true;
        }
        this.avatar = avatar;
        this.avatarSet = true;
        if (triggerUpdate) {
            Presence p = new Presence();
            p.setTo(getManager().getSession().getJID());
            p.setFrom(jid);
            getManager().getSession().getTransport().setUpPresencePacket(p, presence);
            if (!verboseStatus.equals("")) {
                p.setStatus(verboseStatus);
            }
            Element vcard = p.addChildElement("x", NameSpace.VCARD_TEMP_X_UPDATE);
            if (avatar != null) {
                vcard.addElement("photo").addCDATA(avatar.getXmppHash());
                vcard.addElement("hash").addCDATA(avatar.getXmppHash());
            }
            getManager().sendPacket(p);
        }
    }

    /**
     * Adds the PHOTO vcard element (representing an avatar) to an existing vcard.
     *
     * This will add the avatar to a vcard if there's one to add.  Otherwise will not add anything.
     * If added, a properly formatted PHOTO element with base64 encoded data in it will be added.
     * 
     * param vcard vcard to add PHOTO element to
     */
    public void addVCardPhoto(Element vcard) {
        if (!avatarSet) {
            Log.debug("TransportBuddy: I've got nothing! (no avatar set)");
            return;
        }
        Element photo = vcard.addElement("PHOTO");
        if (avatar != null) {
            try {
                photo.addElement("TYPE").addCDATA(avatar.getMimeType());
                photo.addElement("BINVAL").addCDATA(avatar.getImageData());
            }
            catch (NotFoundException e) {
                // No problem, leave it empty then.
            }
        }
    }

    /**
     * Returns the entire vcard element for an avatar.
     *
     * This will return a vCard element, filled in as much as possible, regardless of whether we have
     * real data for the user.  It'll return minimal regardless.
     *
     * @return vCard element
     */
    public Element getVCard() {
        Element vcard = DocumentHelper.createElement(QName.get("vCard", NameSpace.VCARD_TEMP));

        vcard.addElement("VERSION").addCDATA("2.0");
        vcard.addElement("JABBERID").addCDATA(getJID().toString());
        vcard.addElement("NICKNAME").addCDATA(getNickname() == null ? getName() : getNickname());

        if (JiveGlobals.getBooleanProperty("plugin.gateway."+getManager().getSession().getTransport().getType()+".avatars", true)) {
            addVCardPhoto(vcard);
        }

        return vcard;
    }

    /**
     * Outputs information about the transport buddy in a pretty format.
     */
    @Override
    public String toString() {
        return "{Buddy: "+this.jid+" (Nickname: "+this.nickname+") (Groups: "+this.groups+")}";
    }

}
