/*
 * 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.io.UnsupportedEncodingException;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.text.DateFormat;
import java.util.Arrays;
import java.util.Collection;
import java.util.List;

import net.kano.joscar.BinaryTools;
import net.kano.joscar.ByteBlock;
import net.kano.joscar.flap.FlapCommand;
import net.kano.joscar.flap.FlapPacketEvent;
import net.kano.joscar.flapcmd.LoginFlapCmd;
import net.kano.joscar.flapcmd.SnacCommand;
import net.kano.joscar.net.ConnDescriptor;
import net.kano.joscar.ratelim.RateLimitingQueueMgr;
import net.kano.joscar.snac.SnacPacketEvent;
import net.kano.joscar.snac.SnacRequest;
import net.kano.joscar.snac.SnacRequestAdapter;
import net.kano.joscar.snac.SnacRequestListener;
import net.kano.joscar.snac.SnacRequestTimeoutEvent;
import net.kano.joscar.snac.SnacResponseEvent;
import net.kano.joscar.snaccmd.ExtraInfoBlock;
import net.kano.joscar.snaccmd.ExtraInfoData;
import net.kano.joscar.snaccmd.FullUserInfo;
import net.kano.joscar.snaccmd.MiniUserInfo;
import net.kano.joscar.snaccmd.SnacFamilyInfoFactory;
import net.kano.joscar.snaccmd.buddy.BuddyOfflineCmd;
import net.kano.joscar.snaccmd.buddy.BuddyStatusCmd;
import net.kano.joscar.snaccmd.conn.ClientReadyCmd;
import net.kano.joscar.snaccmd.conn.ClientVersionsCmd;
import net.kano.joscar.snaccmd.conn.ExtraInfoAck;
import net.kano.joscar.snaccmd.conn.RateAck;
import net.kano.joscar.snaccmd.conn.RateClassInfo;
import net.kano.joscar.snaccmd.conn.RateInfoCmd;
import net.kano.joscar.snaccmd.conn.RateInfoRequest;
import net.kano.joscar.snaccmd.conn.ServerReadyCmd;
import net.kano.joscar.snaccmd.conn.SnacFamilyInfo;
import net.kano.joscar.snaccmd.conn.WarningNotification;
import net.kano.joscar.snaccmd.error.SnacError;
import net.kano.joscar.snaccmd.icbm.InstantMessage;
import net.kano.joscar.snaccmd.icbm.OldIcbm;
import net.kano.joscar.snaccmd.icbm.RecvImIcbm;
import net.kano.joscar.snaccmd.icbm.TypingCmd;
import net.kano.joscar.snaccmd.icon.IconDataCmd;
import net.kano.joscar.snaccmd.icon.IconRequest;
import net.kano.joscar.snaccmd.icon.UploadIconAck;
import net.kano.joscar.snaccmd.icon.UploadIconCmd;
import net.sf.kraken.avatars.Avatar;
import net.sf.kraken.type.PresenceType;
import net.sf.kraken.type.TransportType;
import net.sf.kraken.util.StringUtils;
import net.sf.kraken.util.chatstate.ChatStateEventSource;

import org.apache.log4j.Logger;
import org.jivesoftware.util.JiveGlobals;
import org.jivesoftware.util.LocaleUtils;
import org.jivesoftware.util.NotFoundException;
import org.xmpp.packet.JID;
import org.xmpp.packet.Message;

/**
 * Handles incoming FLAP packets.
 *
 * @author Daniel Henninger
 * Heavily inspired by joscardemo from the joscar project.
 */
public abstract class BasicFlapConnection extends AbstractFlapConnection {

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

    protected final ByteBlock cookie;
    protected boolean sentClientReady = false;

    protected int[] snacFamilies = null;
    protected Collection<SnacFamilyInfo> snacFamilyInfos;
    protected RateLimitingQueueMgr rateMgr = new RateLimitingQueueMgr();

    public BasicFlapConnection(ConnDescriptor cd, OSCARSession mainSession, ByteBlock cookie) {
        super(cd, mainSession);
        this.cookie = cookie;
        initBasicFlapConnection();
    }

    private void initBasicFlapConnection() {
        sp.setSnacQueueManager(rateMgr);
    }

    protected DateFormat dateFormat
            = DateFormat.getDateTimeInstance(DateFormat.SHORT,
                    DateFormat.SHORT);

    @Override
    protected void handleFlapPacket(FlapPacketEvent e) {
        Log.debug("OSCAR flap packet received: "+e);
        FlapCommand cmd = e.getFlapCommand();

        if (cmd instanceof LoginFlapCmd) {
            getFlapProcessor().sendFlap(new LoginFlapCmd(cookie));
        }
    }

    @Override
    protected void handleSnacPacket(SnacPacketEvent e) {
        Log.debug("OSCAR snac packet received: "+e);
        SnacCommand cmd = e.getSnacCommand();
        if (cmd instanceof ServerReadyCmd) {
            ServerReadyCmd src = (ServerReadyCmd) cmd;
            setSnacFamilies(src.getSnacFamilies());

            Collection<SnacFamilyInfo> familyInfos = SnacFamilyInfoFactory.getDefaultFamilyInfos(src.getSnacFamilies());
            setSnacFamilyInfos(familyInfos);

            getMainSession().registerSnacFamilies(this);

            request(new ClientVersionsCmd(familyInfos));
            request(new RateInfoRequest());
        }
        else if (cmd instanceof RecvImIcbm) {
            RecvImIcbm icbm = (RecvImIcbm) cmd;

            String sn = icbm.getSenderInfo().getScreenname();
            InstantMessage message = icbm.getMessage();
            String msg = StringUtils.convertFromHtml(message.getMessage());

            getMainSession().getTransport().sendMessage(
                    getMainSession().getJID(),
                    getMainSession().getTransport().convertIDToJID(sn),
                    msg
            );
        }
        else if (cmd instanceof OldIcbm) {
            OldIcbm oicbm = (OldIcbm) cmd;
            if (oicbm.getMessageType() == OldIcbm.MTYPE_PLAIN) {
                String uin = String.valueOf(oicbm.getSender());
                String msg = StringUtils.convertFromHtml(oicbm.getReason());
                Log.debug("Got ICBM message "+uin+" with "+msg+"\n"+oicbm);
//                InstantMessage message = oicbm.getMessage();
//                Log.debug("Got ICBM message "+uin+" with "+message+"\n"+oicbm);
//                String msg = StringUtils.unescapeFromXML(OscarTools.stripHtml(message.getMessage()));

                getMainSession().getTransport().sendMessage(
                        getMainSession().getJID(),
                        getMainSession().getTransport().convertIDToJID(uin),
                        msg
                );
            }
        }
        else if (cmd instanceof WarningNotification) {
            WarningNotification wn = (WarningNotification) cmd;
            MiniUserInfo warner = wn.getWarner();
            if (warner == null) {
                getMainSession().getTransport().sendMessage(
                        getMainSession().getJID(),
                        getMainSession().getTransport().getJID(),
                        LocaleUtils.getLocalizedString("gateway.aim.warninganon", "kraken", Arrays.asList(wn.getNewLevel().toString())),
                        Message.Type.headline
                );
            }
            else {
                Log.debug("*** " + warner.getScreenname()
                        + " warned you up to " + wn.getNewLevel() + "%");
                getMainSession().getTransport().sendMessage(
                        getMainSession().getJID(),
                        getMainSession().getTransport().getJID(),
                        LocaleUtils.getLocalizedString("gateway.aim.warningdirect", "kraken", Arrays.asList(warner.getScreenname(), wn.getNewLevel().toString())),
                        Message.Type.headline
                );
            }
        }
        else if (cmd instanceof ExtraInfoAck) {
            ExtraInfoAck eia = (ExtraInfoAck)cmd;
            List<ExtraInfoBlock> extraInfo = eia.getExtraInfos();
            if (extraInfo != null) {
                for (ExtraInfoBlock i : extraInfo) {
                    ExtraInfoData data = i.getExtraData();

                    final byte[] pendingAvatar = getMainSession().getSsiHierarchy().getPendingAvatarData();
                    if (JiveGlobals.getBooleanProperty("plugin.gateway."+getMainSession().getTransport().getType()+".avatars", true) && (data.getFlags() & ExtraInfoData.FLAG_UPLOAD_ICON) != 0 && pendingAvatar != null) {
                        Log.debug("OSCAR: Server has indicated that it wants our icon.");
                        request(new UploadIconCmd(ByteBlock.wrap(pendingAvatar)), new SnacRequestAdapter() {
                            @Override
                            public void handleResponse(SnacResponseEvent e) {
                                SnacCommand cmd = e.getSnacCommand();
                                if (cmd instanceof UploadIconAck && pendingAvatar != null) {
                                    UploadIconAck iconAck = (UploadIconAck) cmd;
                                    if (iconAck.getCode() == UploadIconAck.CODE_DEFAULT || iconAck.getCode() == UploadIconAck.CODE_SUCCESS) {
                                        ExtraInfoBlock iconInfo = iconAck.getIconInfo();
                                        if (iconInfo == null) {
                                          Log.debug("OSCAR: Got icon ack with no iconInfo: " + iconAck);
                                        }
                                        Log.debug("OSCAR: Successfully set icon.");
                                        try {
                                            MessageDigest md = MessageDigest.getInstance("MD5");
                                            md.update(pendingAvatar);
                                            getMainSession().getAvatar().setLegacyIdentifier(org.jivesoftware.util.StringUtils.encodeHex(md.digest()));
                                        }
                                        catch (NoSuchAlgorithmException ee) {
                                            Log.error("No algorithm found for MD5!", ee);
                                        }
                                    }
                                    else if (iconAck.getCode() == UploadIconAck.CODE_BAD_FORMAT) {
                                        Log.debug("OSCAR: Uploaded icon was not in an unaccepted format.");
                                    }
                                    else if (iconAck.getCode() == UploadIconAck.CODE_TOO_LARGE) {
                                        Log.debug("OSCAR: Uploaded icon was too large to be accepted.");
                                    }
                                    else {
                                        Log.debug("OSCAR: Got unknown code from UploadIconAck: " + iconAck.getCode());
                                    }
                                }
                                else if (cmd instanceof SnacError) {
                                    Log.debug("Got SnacError while setting icon: " + cmd);
                                }
                                
                                // Clear the pending binary data from Krakens memory.
                                getMainSession().getSsiHierarchy().clearPendingAvatar();
                            }
                        });
                    }
                }
            }
        }
        else if (cmd instanceof BuddyStatusCmd) {
            BuddyStatusCmd bsc = (BuddyStatusCmd)cmd;
            FullUserInfo info = bsc.getUserInfo();
            PresenceType pType = PresenceType.available;
            String vStatus = "";
            if (info.getAwayStatus()) {
                pType = PresenceType.away;
            }
            if ((info.getFlags() & FullUserInfo.MASK_WIRELESS) != 0) {
                pType = PresenceType.xa;
                vStatus = "Mobile: ";
            }

            if (getMainSession().getTransport().getType().equals(TransportType.icq) && info.getScreenname().matches("/^\\d+$/")) {
                pType = ((OSCARTransport)getMainSession().getTransport()).convertICQStatusToXMPP(info.getIcqStatus());
            }

            List<ExtraInfoBlock> extraInfo = info.getExtraInfoBlocks();
            if (extraInfo != null) {
                for (ExtraInfoBlock i : extraInfo) {
                    ExtraInfoData data = i.getExtraData();

                    if (i.getType() == ExtraInfoBlock.TYPE_AVAILMSG) {
                        ByteBlock msgBlock = data.getData();
                        int len = BinaryTools.getUShort(msgBlock, 0);
                        if (len >= 0) {
                            byte[] msgBytes = msgBlock.subBlock(2, len).toByteArray();
                            String msg;
                            try {
                                msg = new String(msgBytes, "UTF-8");
                            }
                            catch (UnsupportedEncodingException e1) {
                                continue;
                            }
                            if (msg.length() > 0) {
                                vStatus = vStatus + msg;
                            }
                        }
                    }
                    else if (i.getType() == ExtraInfoBlock.TYPE_ICONHASH && JiveGlobals.getBooleanProperty("plugin.gateway."+getMainSession().getTransport().getType()+".avatars", true)) {
                        try {
                            OSCARBuddy oscarBuddy = getMainSession().getBuddyManager().getBuddy(getMainSession().getTransport().convertIDToJID(info.getScreenname()));
                            Avatar curAvatar = oscarBuddy.getAvatar();
                            if (curAvatar == null || !curAvatar.getLegacyIdentifier().equals(org.jivesoftware.util.StringUtils.encodeHex(i.getExtraData().getData().toByteArray()))) {
                                IconRequest req = new IconRequest(info.getScreenname(), i.getExtraData());
                                request(req, new SnacRequestAdapter() {
                                    @Override
                                    public void handleResponse(SnacResponseEvent e) {
                                        SnacCommand cmd = e.getSnacCommand();
                                        if (cmd instanceof IconDataCmd) {
                                            IconDataCmd idc = (IconDataCmd)cmd;
                                            if (idc.getIconData().getLength() > 0 && idc.getIconData().getLength() != 90) {
                                                Log.debug("Got icon data: "+idc);
                                                if (getMainSession().getBuddyManager().isActivated()) {
                                                    try {
                                                        OSCARBuddy oscarBuddy = getMainSession().getBuddyManager().getBuddy(getMainSession().getTransport().convertIDToJID(idc.getScreenname()));
                                                        oscarBuddy.setAvatar(new Avatar(getMainSession().getTransport().convertIDToJID(idc.getScreenname()), org.jivesoftware.util.StringUtils.encodeHex(idc.getIconInfo().getExtraData().getData().toByteArray()), idc.getIconData().toByteArray()));
                                                    }
                                                    catch (NotFoundException ee) {
                                                        // Apparently we don't care about this contact.
                                                    }
                                                    catch (IllegalArgumentException ee) {
                                                        Log.debug("OSCAR: Got null avatar, ignoring.");
                                                    }
                                                }
                                            }
                                        }
                                    }

                                    @Override
                                    public void handleTimeout(SnacRequestTimeoutEvent e) {
                                        Log.debug("Time out while waiting for icon data.");
                                    }
                                });
                            }
                        }
                        catch (NotFoundException ee) {
                            // Apparently we don't care about this contact.
                        }
                    }
                }
            }
            if (getMainSession().getBuddyManager().isActivated()) {
                try {
                    OSCARBuddy oscarBuddy = getMainSession().getBuddyManager().getBuddy(getMainSession().getTransport().convertIDToJID(info.getScreenname()));
                    oscarBuddy.setPresenceAndStatus(pType, vStatus);
                }
                catch (NotFoundException ee) {
                    // Apparently we don't care about this contact.
                    Log.debug("OSCAR: Received presense notification for contact we don't care about: "+info.getScreenname());
                }
            }
            else {
                getMainSession().getBuddyManager().storePendingStatus(getMainSession().getTransport().convertIDToJID(info.getScreenname()), pType, vStatus);
            }
        }
        else if (cmd instanceof BuddyOfflineCmd) {
            BuddyOfflineCmd boc = (BuddyOfflineCmd)cmd;
            if (getMainSession().getBuddyManager().isActivated()) {
                try {
                    OSCARBuddy oscarBuddy = getMainSession().getBuddyManager().getBuddy(getMainSession().getTransport().convertIDToJID(boc.getScreenname()));
                    oscarBuddy.setPresence(PresenceType.unavailable);
                }
                catch (NotFoundException ee) {
                    // Apparently we don't care about this contact.
                }
            }
            else {
                getMainSession().getBuddyManager().storePendingStatus(getMainSession().getTransport().convertIDToJID(boc.getScreenname()), PresenceType.unavailable, null);
            }
        }
        else if (cmd instanceof TypingCmd) {
            TypingCmd tc = (TypingCmd) cmd;
            String sn = tc.getScreenname();

            final ChatStateEventSource chatStateEventSource = getMainSession().getTransport().getChatStateEventSource();
            final JID receiver = getMainSession().getJID();
            final JID sender = getMainSession().getTransport().convertIDToJID(sn);
            
            if (tc.getTypingState() == TypingCmd.STATE_TYPING) {
                chatStateEventSource.isComposing(sender, receiver);
            }
            else if (tc.getTypingState() == TypingCmd.STATE_PAUSED) {
                chatStateEventSource.sendIsPaused(sender, receiver);
            }
            else if (tc.getTypingState() == TypingCmd.STATE_NO_TEXT) {
                chatStateEventSource.isInactive(sender, receiver);
            }
        }
    }

    @Override
    protected void handleSnacResponse(SnacResponseEvent e) {
        Log.debug("OSCAR snac packet response: "+e);
        SnacCommand cmd = e.getSnacCommand();

        if (cmd instanceof RateInfoCmd) {
            RateInfoCmd ric = (RateInfoCmd) cmd;
            List <RateClassInfo> rateClasses = ric.getRateClassInfos();

            int[] classes = new int[rateClasses.size()];
            for (int i = 0; i < rateClasses.size(); i++) {
                classes[i] = rateClasses.get(i).getRateClass();
            }

            request(new RateAck(classes));
        }
    }

    public int[] getSnacFamilies() { return snacFamilies; }

    protected void setSnacFamilies(int[] families) {
        this.snacFamilies = families.clone();
        Arrays.sort(snacFamilies);
    }

    protected void setSnacFamilyInfos(Collection<SnacFamilyInfo> infos) {
        snacFamilyInfos = infos;
    }

    protected boolean supportsFamily(int family) {
        return Arrays.binarySearch(snacFamilies, family) >= 0;
    }

    protected void clientReady() {
        if (!sentClientReady) {
            sentClientReady = true;
            request(new ClientReadyCmd(snacFamilyInfos));
        }
    }

    protected SnacRequest dispatchRequest(SnacCommand cmd) {
        return dispatchRequest(cmd, null);
    }

    protected SnacRequest dispatchRequest(SnacCommand cmd,
            SnacRequestListener listener) {
        SnacRequest req = new SnacRequest(cmd, listener);
        dispatchRequest(req);
        return req;
    }

    protected void dispatchRequest(SnacRequest req) {
        getMainSession().handleRequest(req);
    }

    @Override
    protected SnacRequest request(SnacCommand cmd,
            SnacRequestListener listener) {
        SnacRequest req = new SnacRequest(cmd, listener);

        handleReq(req);

        return req;
    }

    private void handleReq(SnacRequest request) {
        int family = request.getCommand().getFamily();
        if (snacFamilies == null || supportsFamily(family)) {
            // this connection supports this snac, so we'll send it here
            sendRequest(request);
        }
        else {
            getMainSession().handleRequest(request);
        }
    }

}
