/*
 * Jitsi, the OpenSource Java VoIP and Instant Messaging client.
 *
 * Copyright @ 2015 Atlassian Pty Ltd
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
package net.java.sip.communicator.slick.protocol.icq;

import java.beans.*;
import java.io.*;
import java.util.*;

import net.java.sip.communicator.service.protocol.icqconstants.*;
import net.java.sip.communicator.util.*;
import net.kano.joscar.*;
import net.kano.joscar.flapcmd.*;
import net.kano.joscar.snac.*;
import net.kano.joscar.snaccmd.*;
import net.kano.joscar.snaccmd.conn.*;
import net.kano.joscar.snaccmd.error.*;
import net.kano.joscar.snaccmd.icbm.*;
import net.kano.joscar.snaccmd.loc.*;
import net.kano.joscar.snaccmd.ssi.*;
import net.kano.joscar.tlv.*;
import net.kano.joustsim.*;
import net.kano.joustsim.oscar.*;
import net.kano.joustsim.oscar.oscar.service.bos.*;
import net.kano.joustsim.oscar.oscar.service.buddy.*;
import net.kano.joustsim.oscar.oscar.service.icbm.*;
import net.kano.joustsim.oscar.oscar.service.ssi.*;

/**
 * An utility that we use to test AIM/ICQ implementations of the
 * ProtocolProviderService. This class implements functionality such as
 * verifying whether a particular user is currently on-line, single message
 * reception and single message sending, and other features that help us verify
 * that icq implementations behave properly.
 *
 * @author Emil Ivov
 * @author Damian Minkov
 */
public class IcqTesterAgent
{
    private static final Logger logger =
        Logger.getLogger(IcqTesterAgent.class);
    /**
     * We use this field to determine whether registration has gone ok.
     */
    private IcbmService icbmService = null;

    /**
     * The AimConnection that the IcqEchoTest user has established with the icq
     * service.
     */
    private AimConnection conn = null;

    /**
     * We use it to wait for registration completion.
     */
    private Object connectionLock = new Object();

    /**
     * This one should actually be in joscar. But since it isn't we might as
     * well define it here.
     */
    public static final long ICQ_ONLINE_MASK = 0x01000000L;

    /**
     * Indicates whether the tester agent is registered (signed on) the icq
     * service.
     */
    private boolean registered = false;

    /**
     * The icqUIN (or AIM screenname) that the tester agent should use to log
     * onto the aim service.
     */
    private Screenname icqUIN = null;

    private AuthCmdFactory authCmdFactory = new AuthCmdFactory();

    /**
     * The IcqTesterAgent constructor that would create a tester agent instance,
     * prepared to sign on line with the specified icq uin.
     *
     * @param icqUinString the icq uin that the tester agent should use when
     * signing online.
     */
    IcqTesterAgent(String icqUinString)
    {
        this.icqUIN = new Screenname(icqUinString);
    }

    /**
     * Registers the echo test user on icq so that it could receive test
     * messages from icq/aim tested stacks.
     *
     * @param password the password corresponding to the icq uin specified in
     * the constructor.
     * @return true if registration was successful and false otherwise.
     */
    public boolean register(String password)
    {
        if(registered
            || IcqSlickFixture.onlineTestingDisabled)
            return true;

        DefaultAppSession session = new DefaultAppSession();

        AimSession aimSession =
            session.openAimSession(icqUIN);
        aimSession.openConnection(
            new AimConnectionProperties(
                icqUIN, password));

        conn = aimSession.getConnection();

        conn.addStateListener(new AimConnStateListener());
        conn.getBuddyInfoManager().addGlobalBuddyInfoListener(new GlobalBuddyListener());

        conn.connect();

        synchronized(connectionLock){
            try{connectionLock.wait(10000);}catch(InterruptedException ex){}
        }

        if (icbmService == null){
            //maybe throw an exception here
            return (registered = false);
        }

        //conn.getSsiService()
        //    .getBuddyList().addRetroactiveLayoutListener(new RetroListener());
        conn.getBuddyService().addBuddyListener(new BuddyListener());

        return (registered = true );
    }

    /**
     * The method would delete all existing buddies on its current server stored
     * contact list and fill it in again as specified by the listContents
     * hashtable. IMPORTANT - Note that this method would completely ERASE
     * any existing contacts in the account currently used by the tester agent
     * to log in.
     *
     * @param listContents a Hashtable that must contain a description of the
     * contact list such as the caller would like it to be (groupnNawhere copy
     * of the initialized contact list would be stored, mapping group names to
     * lists of contact identifiers
     * (screennames).
     */
    public void initializeBuddyList(Hashtable<String, List<String>> listContents)
    {
        logger.debug("Will Create the following contact list:\n"+ listContents);
        MutableBuddyList joustSimBuddyList
            = conn.getSsiService().getBuddyList();

        //First empty the existing contact list.
        List<? extends Group> groups = joustSimBuddyList.getGroups();

        Iterator<? extends Group> groupsIter = groups.iterator();
        while (groupsIter.hasNext())
        {
            Group group = groupsIter.next();
            joustSimBuddyList.deleteGroupAndBuddies(group);
        }

        //Now insert all items from the listContents hashtable if they're not
        //already there.
        Enumeration<String> newGroupsEnum = listContents.keys();

        LayoutEventCollector evtCollector = new LayoutEventCollector();

        //go over all groups in the contactsToAdd table
        while (newGroupsEnum.hasMoreElements())
        {
            String groupName = newGroupsEnum.nextElement();
            logger.debug("Will add group " + groupName);

            //first clear any previously registered groups and then add the
            //layout listenet to the buddy list.
            evtCollector.addedGroups.removeAllElements();
            joustSimBuddyList.addLayoutListener(evtCollector);

            joustSimBuddyList.addGroup(groupName);

            //wait for a notification from the aim server that the group has
            //been added
            evtCollector.waitForANewGroup(10000);
            joustSimBuddyList.removeLayoutListener(evtCollector);

            //now see if it all worked ok and if yes get a ref to the newly
            //added group.
            MutableGroup newlyCreatedGroup = null;

            if (evtCollector.addedGroups.size() == 0
                || (newlyCreatedGroup
                        = (MutableGroup)evtCollector.addedGroups.get(0))
                            == null)
                throw new NullPointerException(
                    "Couldn't create group " + groupName);

            Iterator<String> contactsToAddToThisGroup
                = listContents.get(groupName).iterator();
            while (contactsToAddToThisGroup.hasNext())
            {
                String screenname = contactsToAddToThisGroup.next();

                //remove all buddies captured by the event collector so far
                //then register it as a listener
                evtCollector.addedBuddies.removeAllElements();
                joustSimBuddyList.addLayoutListener(evtCollector);


                logger.debug("Will add buddy " + screenname);
                newlyCreatedGroup.addBuddy( screenname);

                //wait for a notification from the aim server that the buddy has
                //been added
                evtCollector.waitForANewBuddy(10000);
                joustSimBuddyList.removeLayoutListener(evtCollector);

                //now see if it all worked ok and if yes get a ref to the newly
                //added group.
                if (evtCollector.addedBuddies.size() == 0
                    || evtCollector.addedBuddies.get(0) == null)
                {
                    //We didn't get an event ... let's see that the new buddy
                    //is really not there and if that is the case throw an exs
                    if(findBuddyInBuddyList(joustSimBuddyList, screenname) == null)
                        throw new NullPointerException(
                            "Couldn't add buddy " + screenname);
                }
            }
        }
    }

    /**
     * Sends to <tt>buddy</tt> a notification that our typing state has now
     * changed to indicated by <tt>notif</tt>.
     * @param buddy the screenname of the budy that we'd like to notify.
     * @param state the typing state that we'd like to send to the specified
     * buddy.
     */
    public void sendTypingNotification(String buddy, TypingState state)
    {
        conn.getIcbmService().getImConversation(new Screenname(buddy))
            .setTypingState(state);
    }

    /**
     * Adds a typing listener that would receive joust sim based on typing
     * notifications received from <tt>buddy</tt>
     * @param buddy the screenname of the buddy that we'd like to receive
     * notifications from.
     * @param l the <tt>ConversationListener</tt> (which also needs to be a
     * TypingListener) that would be registered for typing notifications
     * @throws ClassCastException if <tt>l</tt> is only an instance of
     * <tt>ConversationListener</tt> without implementing
     * <tt>TypingListener</tt>
     */
    public void addTypingStateInfoListenerForBuddy( String buddy,
                                                    ConversationListener l)
        throws ClassCastException
    {
        if (! (l instanceof TypingListener))
            throw new ClassCastException(
                "In order to receive typing notifications a typing listener "
                +"needs to also implement " + TypingListener.class.getName());

        conn.getIcbmService().getImConversation(new Screenname(buddy))
            .addConversationListener(l);
    }

    /**
     * Removes <tt>l</tt> so that it won't receive further typing events for
     * <tt>buddy</tt>.
     * @param buddy the screenname of the buddy that we'd like to stop receiving
     * notifications from.
     * @param l the <tt>ConversationListener</tt> to remove
     */
    public void removeTypingStateInfoListenerForBuddy( String buddy,
                                                       ConversationListener l)
    {
        conn.getIcbmService().getImConversation(new Screenname(buddy))
            .removeConversationListener(l);
    }

    /**
     * Sends <tt>body</tt> to <tt>buddy</tt>  as an instant message
     * @param buddy the screenname of the budy that we'd like to send our msg to.
     * @param body the content of the message to send.
     */
    public void sendMessage(String buddy, String body)
    {
        conn.getIcbmService().getImConversation(new Screenname(buddy))
            .sendMessage(new SimpleMessage(body));

        //the aim server doesn't like fast consecutice messages
        try{Thread.sleep(600);}catch (InterruptedException ex){}
    }

    /**
     * Registers <tt>listener</tt> as an ImConversationListener so that it would
     * receive messages coming from <tt>buddy</tt>.
     * @param buddy the screenname of the buddy that we'd like to listen to.
     * @param listener the <tt>ImConversationListener</tt> to register.
     */
    public void addConversationListener(String               buddy,
                                        ConversationListener listener)
    {
        conn.getIcbmService().getImConversation(new Screenname(buddy))
            .addConversationListener(listener);
    }

    /**
     * Removes <tt>listener</tt> as an ImConversationListener so that it won't
     * receive further messages coming from <tt>buddy</tt>.
     * @param buddy the screenname of the buddy that we'd like to unregister
     * from.
     * @param listener the <tt>ImConversationListener</tt> to remove.
     */
    public void removeConversationListener(String               buddy,
                                           ConversationListener listener)
    {
        conn.getIcbmService().getImConversation(new Screenname(buddy))
            .removeConversationListener(listener);
    }

    /**
     * Tries to find the buddy with screenname screenname in the given buddy
     * list
     * @param list the BuddyList where to look for the buddy
     * @param screenname  the screen name of the buddy we're looking for
     * @return a ref to the Buddy we're looking for or null if no such buddy
     * was found.
     */
    private Buddy findBuddyInBuddyList(BuddyList list, String screenname)
    {
        Iterator<? extends Group> groups = list.getGroups().iterator();

        while (groups.hasNext())
        {
            Group group = groups.next();
            for (Buddy buddy : group.getBuddiesCopy())
            {
                if(buddy.getScreenname().getFormatted().equals(screenname))
                    return buddy;
            }
        }
        return null;
    }
    /**
     * Unregisters from (signs off) the ICQ service.
     */
    public void unregister()
    {
        if(!registered)
            return;
        conn.disconnect(true);
        registered = false;
    }

    /**
     * All this listener does is wait for an event coming from oscar.jar and
     * indicating that registration has been successful.
     */
    private class AimConnStateListener implements StateListener
    {
        public synchronized void handleStateChange(StateEvent event)
        {
            synchronized( connectionLock ) {
                AimConnection conn = event.getAimConnection();
                logger.debug("EchoUser change state from:"
                             + event.getOldState() + " to " + event.getNewState());
                if (event.getNewState() == State.ONLINE)
                {
                    icbmService = conn.getIcbmService();
                    connectionLock.notifyAll();

                    icbmService.getOscarConnection().getSnacProcessor().
                        getCmdFactoryMgr().getDefaultFactoryList().
                        registerAll(authCmdFactory);

                    icbmService.getOscarConnection().getSnacProcessor().
                        getCmdFactoryMgr().getDefaultFactoryList().
                        registerAll(FullUserInfoCmd.getCommandFactory());

                    icbmService.getOscarConnection().getSnacProcessor().
                        addGlobalResponseListener(authCmdFactory);
                }
                else if (event.getNewState() == State.FAILED
                         || event.getNewState() == State.DISCONNECTED)
                {
                    logger.error("AIM Connection DISCONNECTED for "
                                 + getIcqUIN() + "!");
                    connectionLock.notifyAll();
                }
            }
        }
    }

    /**
     * Returns the on-line status of the user with the specified screenname.
     * @param screenname the screenname of the user whose status we're
     *        interested in.
     * @return a PresenceStatus (one of the IcqStatusEnum static fields)
     * indicating the the status of the specified buddy.
     *
     * @throws java.lang.IllegalStateException if the method is called before
     *         the IcqTesterAgent has been registered with (signed on) the
     *         IcqService.
     */
    public IcqStatusEnum getBuddyStatus(String screenname)
        throws IllegalStateException
    {
        if ( !registered )
            throw new IllegalStateException(
                "You need to register before querying a buddy's status");

        StatusResponseRetriever responseRetriever =
            new StatusResponseRetriever();

        GetInfoCmd getInfoCmd =
            new GetInfoCmd(GetInfoCmd.CMD_NEW_GET_INFO | GetInfoCmd.FLAG_AWAYMSG | GetInfoCmd.FLAG_INFO,
                           new Screenname(screenname).getFormatted());


        conn.getInfoService().getOscarConnection()
            .sendSnacRequest(getInfoCmd, responseRetriever);

        synchronized(responseRetriever.waitingForResponseLock)
        {
            try{
                logger.debug("waiting to receive status for " + screenname);
                responseRetriever.waitingForResponseLock.wait(100000);
            }
            catch (InterruptedException ex){
                logger.debug("Couldn't wait upon a response retriver", ex);
            }
        }

        logger.debug("Done. we'll return status "  + responseRetriever.status);

        return responseRetriever.status == null
                    ? IcqStatusEnum. OFFLINE
                    : responseRetriever.status;
    }

    /**
     * Converts the specified icqstatus to on of the ICQ_STATUS string fields
     * of this class
     * @param icqStatus the icqStatus as retured in FullUserInfo by the joscar
     *        stack
     * @param returnOnMinus1 specifies the value that should be returned if
     * icqStatus is on its default joust sim value "-1".
     * @return the IcqStatusEnum instance that best corresponds to the "long"
     * icqStatus parameter.
     */
    private static IcqStatusEnum icqStatusLongToString(long icqStatus,
                                                IcqStatusEnum returnOnMinus1)
    {
        if (icqStatus == -1 )
        {
            return returnOnMinus1;
        }
        else if ( (icqStatus & FullUserInfo.ICQSTATUS_AWAY ) != 0)
        {
            return IcqStatusEnum.AWAY;
        }
        else if ( (icqStatus & FullUserInfo.ICQSTATUS_DND ) != 0)
        {
            return IcqStatusEnum.DO_NOT_DISTURB;
        }
        else if ( (icqStatus & FullUserInfo.ICQSTATUS_FFC ) != 0)
        {
            return IcqStatusEnum.FREE_FOR_CHAT;
        }
        else if ( (icqStatus & FullUserInfo.ICQSTATUS_INVISIBLE ) != 0)
        {
            return IcqStatusEnum.INVISIBLE;
        }
        else if ( (icqStatus & FullUserInfo.ICQSTATUS_NA ) != 0)
        {
            return IcqStatusEnum.NOT_AVAILABLE;
        }
        else if ( (icqStatus & FullUserInfo.ICQSTATUS_OCCUPIED ) != 0)
        {
            return IcqStatusEnum.OCCUPIED;
        }
        else if ((icqStatus & ICQ_ONLINE_MASK) == 0 )
        {
            return IcqStatusEnum.OFFLINE;
        }

        return IcqStatusEnum.ONLINE;
    }

    /**
     * The StatusResponseRetriever is used as a one time handler for responses
     * to requests sent through the sendSnacRequest method of one of joustsim's
     * Services. The StatusResponseRetriever would ignore everything apart from
     * the first response, which will be stored in the status field. In the
     * case of a timeout, the status would remain null. Both a response and
     * a timeout would make the StatusResponseRetriever call its notifyAll
     * method so that those that are waiting on it be notified.
     */
    private static class StatusResponseRetriever extends SnacRequestAdapter
    {
            private boolean ran = false;
            private IcqStatusEnum status = null;
            public Object waitingForResponseLock = new Object();


            @Override
            public void handleResponse(SnacResponseEvent e) {
                SnacCommand snac = e.getSnacCommand();
                logger.debug("Received a response to our status request: " + snac);

                synchronized(this) {
                    if (ran) return;
                    ran = true;
                }

                if (snac instanceof UserInfoCmd)
                {
                    UserInfoCmd uic = (UserInfoCmd) snac;

                    FullUserInfo userInfo = uic.getUserInfo();
                    if (userInfo != null)
                    {
                        //if we got a UserInfoCmd and not a SnacError then we
                        //are certain the user is not offline and we specify
                        //the second (defaultReturn) param accordingly.
                        this.status =
                            icqStatusLongToString(userInfo.getIcqStatus(),
                                                  IcqStatusEnum.ONLINE);

                        logger.debug("status is " + status +"="
                                     + userInfo.getIcqStatus());

                        List<ExtraInfoBlock> eInfoBlocks
                            = userInfo.getExtraInfoBlocks();
                        if(eInfoBlocks != null){
                            System.out.println("printing extra info blocks ("
                                               + eInfoBlocks.size() + ")");

                            for (ExtraInfoBlock block : eInfoBlocks)
                            {
                                System.out.println(
                                    "block.toString()=" + block.toString());
                            }
                        }
                        else
                            System.out.println("no extra info.");
                        synchronized(waitingForResponseLock){
                            waitingForResponseLock.notifyAll();
                        }
                    }
                }
                else if( snac instanceof SnacError)
                {
                    //this is most probably a CODE_USER_UNAVAILABLE, but
                    //whatever it is it means that to us the buddy in question
                    //is as good as offline so leave status at -1 and notify.
                    this.status = IcqStatusEnum.OFFLINE;
                    logger.debug("status is" + status);
                    synchronized(waitingForResponseLock){
                        waitingForResponseLock.notifyAll();
                    }
                }

            }

            @Override
            public void handleTimeout(SnacRequestTimeoutEvent event) {
                synchronized(this) {
                    if (ran) return;
                    ran = true;
                }

                synchronized(waitingForResponseLock)
                {
                    waitingForResponseLock.notifyAll();
                }
            }
    }

    /**
     * Returns a string representation of the UIN used by the tester agent to
     * signon on the icq network.
     * @return a String containing the icq UIN used by the tester agent to
     * signon on the AIM network.
     */
    public String getIcqUIN()
    {
        return icqUIN.getFormatted();
    }

    /**
     * Causes the tester agent to enter the specified icq status and returns
     * only after receiving a notification from tha AIM server that the new
     * status has been successfully published.
     * @param icqStatus the icq status to enter
     * @return true if the status change has succeeded and the corresponding bos
     * event was received and false otherwise.
     */
    public boolean enterStatus(long icqStatus)
    {
        //first init the guy that'll tell us that it's ok.
        BosEventNotifier bosEventNotifier = new BosEventNotifier();
        conn.getBosService().addMainBosServiceListener(bosEventNotifier);

        //do the state switch
        synchronized(bosEventNotifier.infoLock ){
            conn.getBosService().getOscarConnection().sendSnac(new SetExtraInfoCmd(icqStatus));

            try{bosEventNotifier.infoLock.wait(10000);}
                catch (InterruptedException ex){logger.debug("Strange!");}

            conn.getBosService().removeMainBosServiceListener(bosEventNotifier);

            if(bosEventNotifier.lastUserInfo == null){
                logger.debug("Status change was not confirmed by AIM server.");
                return false;
            }

            return true;
        }
    }

    /**
     * Queries the AIM server for our own status and returns accordingly.
     * @return the IcqStatusEnum instance corresponding to our own status.
     */
    public IcqStatusEnum getPresneceStatus()
    {
        return getBuddyStatus(getIcqUIN());
    }

    public net.java.sip.communicator.slick.protocol.icq.IcqTesterAgent.
        AuthCmdFactory getAuthCmdFactory()
    {
        return authCmdFactory;
    }

    /**
     * A bos listener is the way of receiving notifications of changes in our
     * own state or info. This class allows others to wait() for Bos events.
     * The class defines an extraInfoLock and an infoLock both of which are
     * notifyAll()ed whenever a corresponding event is received
     */
    private static class BosEventNotifier implements MainBosServiceListener
    {
        public Object extraInfoLock = new Object();
        public Object infoLock = new Object();

        public FullUserInfo lastUserInfo = null;

        /**
         * Saves the extraInfos list and calls a notifyAll on the extraInfoLock
         * @param extraInfos the list of extraInfos that the AIM server sent
         */
        public void handleYourExtraInfo(List<ExtraInfoBlock> extraInfos)
        {
            logger.debug("Bosiat.extrainfo=" + extraInfos);
            synchronized(extraInfoLock){
                extraInfoLock.notifyAll();
            }
        }

        /**
         * Saves the full user info and calls a notifyAll on the infoLock
         * @param service the source bos service
         * @param userInfo the FullUserInfo as received from the aim server.
         */
        public void handleYourInfo(MainBosService service,
                                   FullUserInfo userInfo)
        {
            logger.debug("Bosiat.yourinfo=" + userInfo);
            synchronized(infoLock){
                lastUserInfo = userInfo;
                infoLock.notifyAll();
            }
        }
    }

    /**
     * We use this class to collect and/or wait for events generated upon
     * modification of a server stored contact list.
     */
    private class LayoutEventCollector
        implements BuddyListLayoutListener
    {
        public Vector<Group> addedGroups  = new Vector<Group>();
        public Vector<Buddy> addedBuddies = new Vector<Buddy>();
        public Vector<Buddy> removedBuddies = new Vector<Buddy>();

        /**
         * The method would wait until at least one new buddy is collected by
         * this collector or the specified number of milliseconds have passed.
         * @param milliseconds the maximum number of millisseconds to wait for
         * a new buddy before simply bailing out.
         */
        public void waitForANewBuddy(int milliseconds)
        {
            synchronized (this.addedBuddies){
                if ( !addedBuddies.isEmpty()){
                    return;
                }
                try{
                    this.addedBuddies.wait(milliseconds);
                }
                catch (InterruptedException ex){
                    logger.warn("A strange thing happened while waiting", ex);
                }
            }
        }

        /**
         * The method would wait until at least one new group is collected by
         * this collector or the specified number of milliseconds have passed.
         * @param milliseconds the maximum number of millisseconds to wait for
         * a new group before simply bailing out.
         */
        public void waitForANewGroup(int milliseconds)
        {
            synchronized (this.addedGroups){
                if ( !addedGroups.isEmpty()){
                    return;
                }
                try{
                    this.addedGroups.wait(milliseconds);
                }
                catch (InterruptedException ex){
                    logger.warn("A strange thing happened while waiting", ex);
                }
            }
        }

        public void waitForRemovedBuddy(int milliseconds)
        {
            synchronized (this.removedBuddies)
            {
                if (!removedBuddies.isEmpty())
                {
                    return;
                }
                try
                {
                    this.removedBuddies.wait(milliseconds);
                }
                catch (InterruptedException ex)
                {
                    logger.warn("A strange thing happened while waiting", ex);
                }
            }
        }


        /**
         * Registers a reference to the group that has just been created and
         * call a notifyAll() on this.
         * @param list the BuddyList where this is happening (unused).
         * @param oldItems List (unused)
         * @param newItems List (unused)
         * @param group a reference to the Group that has just been created.
         * @param buddies a List of the buddies created by this group (unused).
         */
        public void groupAdded(BuddyList list,
                               List<? extends Group> oldItems,
                               List<? extends Group> newItems,
                               Group group,
                               List<? extends Buddy> buddies)
        {
            logger.debug("A group was added gname is=" + group.getName());
            synchronized(this.addedGroups){
                this.addedGroups.add(group);
                this.addedGroups.notifyAll();
            }
        }

        /**
         * Registers a reference to the newly added buddy and calls a
         * notifyAll() on this.
         * @param list the BuddyList where this is happening (unused).
         * @param group the Group where the buddy was added (unused).
         * @param oldItems List (unused)
         * @param newItems List (unused)
         * @param buddy a reference to the newly added Buddy.
         */
        public void buddyAdded(BuddyList list,
                               Group group,
                               List<? extends Buddy> oldItems,
                               List<? extends Buddy> newItems,
                               Buddy buddy)
        {
            logger.debug("A buddy ("+buddy.getScreenname()
                         +")was added to group " + group.getName());
            synchronized(this.addedBuddies){
                this.addedBuddies.add(buddy);
                this.addedBuddies.notifyAll();
            }
        }

        //we don't use this one so far.
        public void groupsReordered(
            BuddyList list,
            List<? extends Group> oldOrder,
            List<? extends Group> newOrder)
        {
            logger.debug("groupsReordered");
        }

        //we don't use this one so far.
        public void groupRemoved(
            BuddyList list,
            List<? extends Group> oldItems,
            List<? extends Group> newItems,
            Group group)
        {
            logger.debug("removedGroup="+group.getName());
        }

        // we don't use this one so far.
        public void buddyRemoved(BuddyList list, Group group,
                                 List<? extends Buddy> oldItems,
                                 List<? extends Buddy> newItems,
                                 Buddy buddy)
        {
            logger.debug("removed buddy=" + buddy);
        }

        //we don't use this one
        public void buddiesReordered(BuddyList list, Group group,
                                     List<? extends Buddy> oldBuddies,
                                     List<? extends Buddy> newBuddies)
        {
            logger.debug("buddiesReordered in group " + group.getName());
        }
    }

//------------- other utility stuff that is not really very used ---------------
    private class BuddyListener implements BuddyServiceListener{
        public void gotBuddyStatus(BuddyService service, Screenname buddy,
                            FullUserInfo info)
        {
            System.out.println("BuddyListener.gotBuddyStatus " + buddy.toString()
                               +" and status is : " + info.getIcqStatus());
            List<ExtraInfoBlock> eInfoBlocks = info.getExtraInfoBlocks();
            if (eInfoBlocks != null)
            {
                System.out.println("printing extra info blocks ("
                                   + eInfoBlocks.size() + ")");

                for (ExtraInfoBlock block : eInfoBlocks)
                    System.out.println("block.toString()=" + block);
            }
            else
                logger.trace("no extra info.");
        }

        public void buddyOffline(BuddyService service, Screenname buddy)
        {
            System.out.println("BuddyListener.buddyOffline " + buddy.toString());
        }
    }

    private class GlobalBuddyListener implements GlobalBuddyInfoListener
    {
        public void buddyInfoChanged(BuddyInfoManager manager, Screenname buddy,
                                     BuddyInfo info, PropertyChangeEvent event)
        {
            System.out.println("GlobalBuddyListener.buddyInfoChanged: "
                               + "propN= " + event.getPropertyName()
                               + " for buddy: " + buddy.toString()
                               + " info.isOnline()= " + info.isOnline()
                               + " info.statusMessage=" + info.getStatusMessage()
                                + " info.statusMessage=" + info.getAwayMessage() );
            System.out.println("info=" + info);
        }

        public void newBuddyInfo(BuddyInfoManager manager, Screenname buddy,
                                 BuddyInfo info)
        {
            System.out.println( "GlobalBuddyListener.newBuddyInfo: "
                               + buddy.toString()
                               + " info.isOnline()= " + info.isOnline()
                               + " info.statusMessage=" + info.getStatusMessage()
                                + " info.statusMessage=" + info.getAwayMessage() );
            System.out.println("info=" + info);
        }

        public void receivedStatusUpdate(BuddyInfoManager manager,
                                         Screenname buddy, BuddyInfo info)
        {
            System.out.println("GlobalBuddyListener.receivedStatusUpdate "
                               + buddy.toString()
                               + " info.isOnline()= " + info.isOnline()
                               + " info.statusMessage=" + info.getStatusMessage()
                                + " info.statusMessage=" + info.getAwayMessage() );
            System.out.println("info=" + info);
        }

    }

//    private class ServiceListener
//        implements OpenedServiceListener
//    {
//        public void closedServices(AimConnection conn, Collection services)
//        {
//        }
//
//        public void openedServices(AimConnection conn, Collection services)
//        {
//            conn.getBuddyInfoManager()
//                .addGlobalBuddyInfoListener(new GlobalBuddyListener());
//            conn.getBuddyService().addBuddyListener(new BuddyListener());
//        }
//    }

    private class RetroListener
        implements BuddyListLayoutListener, GroupListener
    {
        public void groupsReordered(BuddyList list,
                                    List<? extends Group> oldOrder,
                                    List<? extends Group> newOrder)
        {
            System.out.println("        RetroListener.groupReordered");
        }

        public void groupAdded(BuddyList list,
                               List<? extends Group> oldItems,
                               List<? extends Group> newItems,
                               Group group,
                               List<? extends Buddy> buddies)
        {
            System.out.println("RetroListener.groupAdded");
            System.out.println("    group.name is="+group.getName());
            System.out.println("index="+newItems.indexOf(group));
            for (int i = 0; i < buddies.size(); i++){

                System.out.println("        buddy is="
                             + buddies.get(i)
                                .getScreenname().getFormatted());
                Buddy b = buddies.get(i);
                conn.getBuddyInfoTracker().addTracker(b.getScreenname(),
                    new BuddyInfoTrackerListener(){});
            }

            group.addGroupListener(this);
        }

        public void groupRemoved(BuddyList list,
                                 List<? extends Group> oldItems,
                                 List<? extends Group> newItems,
                                 Group group)
        {
            System.out.println("        RetroListener.groupRemoved");
        }

        public void buddyAdded(BuddyList list,
                               Group group,
                               List<? extends Buddy> oldItems,
                               List<? extends Buddy> newItems,
                               Buddy buddy)
        {
            System.out.println("        RetroListener.buddyAdded="+buddy);
        }

        public void buddyRemoved(BuddyList list,
                                 Group group,
                                 List<? extends Buddy> oldItems,
                                 List<? extends Buddy> newItems,
                                 Buddy buddy)
        {
            System.out.println("        RetroListener.buddyRemoved"+buddy);
        }

        public void buddiesReordered(BuddyList list,
                                     Group group,
                                     List<? extends Buddy> oldBuddies,
                                     List<? extends Buddy> newBuddies)
        {
            System.out.println("        RetroListener.buddiesReordered");
        }

        public void groupNameChanged(Group group,
                                     String oldName,
                                     String newName)
        {
            System.out.println(
                "        RetroListener.GroupListener.groupNameChanged. old is="
                + oldName
                + " new is="
                + newName
                + " group is="+group.getName());
            System.out.println("GroupContains="+group.getBuddiesCopy());
        }
    }

////////////////////////// ugly unused testing code //////////////////////////
    private RetroListener rl = new RetroListener();
    public static void main(String[] args) throws Throwable
    {
        java.util.logging.Logger.getLogger("net.kano")
            .setLevel(java.util.logging.Level.FINEST);

        IcqTesterAgent icqtests = new IcqTesterAgent("319305099");
        if (!icqtests.register("6pC0mmtt"))
        {
            System.out.println("registration failed"); ;
            return;
        }
        Thread.sleep(1000);
        icqtests.conn.getSsiService()
            .getBuddyList().addRetroactiveLayoutListener(icqtests.rl);
        Thread.sleep(1000);
        System.out.println("\n\nr u ready?");
        Thread.sleep(3000);

        java.util.logging.Logger.getLogger("net.kano").setLevel(java.util.logging.Level.FINEST);

        MutableBuddyList list = icqtests.conn.getSsiService().getBuddyList();

        MutableGroup grpGroup = null;
        Buddy buddyToMove = null;

        for (Group group : list.getGroups())
        {
            if (group.getName().equals("grp"))
                grpGroup = (MutableGroup) group;
            List<? extends Buddy> buddies = group.getBuddiesCopy();
            System.out.println("Printing buddies for group " + group.getName());
            Thread.sleep(1000);
            for (Buddy buddy : buddies)
            {
                System.out.println(buddy.getScreenname());
                if (buddy.getScreenname().getFormatted().equals("201345337"))
                    buddyToMove = buddy;
            }
        }

        System.out.println();System.out.println();System.out.println();System.out.println();System.out.println();System.out.println();System.out.println();System.out.println();System.out.println();
        System.out.println("will move buddyyyyyyyyyy");
        Thread.sleep(5000);
        List<Buddy> listToMove = new ArrayList<Buddy>();
        listToMove.add(buddyToMove);
        list.moveBuddies(listToMove, grpGroup);
        System.out.println("MOved i sega triabva da doidat eventi ");
        Thread.sleep(50000);

        //find the buddy again.
        Buddy movedBuddy = null;
        for (Group group : list.getGroups())
        {
            List<? extends Buddy> buddies = group.getBuddiesCopy();
            for (Buddy buddy : buddies)
            {
                if (buddy.getScreenname().getFormatted().equals("201345337"))
                    movedBuddy = buddy;
            }
        }

        if (buddyToMove == movedBuddy)
            System.out.println("hahaha");
    }

    public void deleteBuddy(String screenname)
    {
        logger.debug("Will delete buddy : " + screenname);
        MutableBuddyList joustSimBuddyList
            = conn.getSsiService().getBuddyList();

        LayoutEventCollector evtCollector = new LayoutEventCollector();
        joustSimBuddyList.addLayoutListener(evtCollector);

        List<? extends Group> grList = joustSimBuddyList.getGroups();
        boolean isDeleted = false;
        Iterator<? extends Group> iter = grList.iterator();
        while (iter.hasNext())
        {
            MutableGroup item = (MutableGroup) iter.next();

            List<? extends Buddy> bs = item.getBuddiesCopy();
            Iterator<? extends Buddy> iter1 = bs.iterator();
            while (iter1.hasNext())
            {
                Buddy b = iter1.next();
                if(b.getScreenname().getFormatted().equals(screenname))
                {
                    item.deleteBuddy(b);
                    isDeleted = true;
                }
            }

            if(isDeleted)
                break;
        }

        if(isDeleted)
            evtCollector.waitForRemovedBuddy(10000);

        joustSimBuddyList.removeLayoutListener(evtCollector);
    }

    public void addBuddy(String screenname)
    {
        logger.debug("Will add buddy : " + screenname);
        MutableBuddyList joustSimBuddyList
            = conn.getSsiService().getBuddyList();

        List<? extends Group> grList = joustSimBuddyList.getGroups();

        Iterator<? extends Group> iter = grList.iterator();
        while (iter.hasNext())
        {
            MutableGroup item = (MutableGroup) iter.next();
            logger.debug("group : " + item);
            List<? extends Buddy> bs = item.getBuddiesCopy();
            Iterator<? extends Buddy> iter1 = bs.iterator();
            while (iter1.hasNext())
            {
                Object b = iter1.next();
                logger.debug("buddy : " + b);
            }
        }

        MutableGroup targetGroup = null;

        if(grList.size() < 1)
        {
            logger.debug("No groups! Will stop now");

            LayoutEventCollector evtCollector = new LayoutEventCollector();

            String groupName = "test-group";
            logger.debug("Will add group " + groupName);

            joustSimBuddyList.addLayoutListener(evtCollector);

            joustSimBuddyList.addGroup(groupName);

            //wait for a notification from the aim server that the group has
            //been added
            evtCollector.waitForANewGroup(10000);
            joustSimBuddyList.removeLayoutListener(evtCollector);

            //now see if it all worked ok and if yes get a ref to the newly
            //added group.
            if (evtCollector.addedGroups.size() == 0
                || (targetGroup = (MutableGroup)evtCollector.addedGroups.get(0))
                == null)
                throw new NullPointerException("Couldn't create group " + groupName);
        }
        else
        {
            targetGroup = (MutableGroup)grList.get(0);
        }

        targetGroup.addBuddy(screenname);

        Object lock = new Object();
        synchronized(lock){
            try{
                lock.wait(5000);
            }
            catch (Exception ex){}
        }
    }

    /**
     * Sends <tt>body</tt> to <tt>buddy</tt>  as an offline instant message
     * @param buddy the screenname of the budy that we'd like to send our msg to.
     * @param body the content of the message to send.
     */
    public void sendOfflineMessage(String buddy, String body)
    {
        conn.sendSnac(new OfflineSnacCmd(buddy, body));
    }

    void sendAuthorizationReplay(String uin, String reasonStr, boolean isAccpeted)
    {
        conn.sendSnac(new AuthReplyCmd(uin, reasonStr, isAccpeted));
    }

    private class OfflineSnacCmd  extends SendImIcbm
    {
        private static final int TYPE_OFFLINE = 0x0006;

        protected OfflineSnacCmd(String sn, String message)
        {
            super(sn, message);
        }

        @Override
        protected void writeChannelData(OutputStream out)
                throws IOException
        {
                super.writeChannelData(out);
                new Tlv(TYPE_OFFLINE).write(out);
        }
    }

    private class AuthReplyCmd
        extends SsiCommand
    {
        private int FLAG_AUTH_ACCEPTED = 1;
        private int FLAG_AUTH_DECLINED = 0;

        private String uin = null;
        private String reason = null;
        private boolean accepted = false;

        public AuthReplyCmd(SnacPacket packet)
        {
            super(0x001b);

            ByteBlock messageData = packet.getData();
            // parse data
            int offset = 0;
            short uinLen = BinaryTools.getUByte(messageData, offset++);
            uin = OscarTools.getString(messageData.subBlock(offset, uinLen), "US-ASCII");
            offset += uinLen;

            accepted = BinaryTools.getUByte(messageData, offset++) == 1;

            int reasonLen = BinaryTools.getUShort(messageData, offset);
            offset += 2;
            reason = OscarTools.getString(messageData.subBlock(offset, reasonLen), "US-ASCII");
        }


        public AuthReplyCmd(String uin, String reason, boolean accepted)
        {
            super(0x001a);

            this.uin = uin;
            this.reason = reason;
            this.accepted = accepted;
        }

        @Override
        public void writeData(OutputStream out)
            throws IOException
        {
            byte[] uinBytes = BinaryTools.getAsciiBytes(uin);
            BinaryTools.writeUByte(out, uinBytes.length);
            out.write(uinBytes);

            if(accepted)
            {
                BinaryTools.writeUByte(out, FLAG_AUTH_ACCEPTED);
            }
            else
            {
                BinaryTools.writeUByte(out, FLAG_AUTH_DECLINED);
            }

            if(reason == null)
                reason = "";

            byte[] reasonBytes = BinaryTools.getAsciiBytes(reason);
            BinaryTools.writeUShort(out, reasonBytes.length);
            out.write(reasonBytes);
        }
    }

    public class AuthCmdFactory
        extends ServerSsiCmdFactory
        implements SnacResponseListener
    {
        List<CmdType> SUPPORTED_TYPES = null;

        public String responseReasonStr = null;
        public String requestReasonStr = null;
        public boolean ACCEPT = false;

        public boolean isErrorAddingReceived = false;
        public boolean isRequestAccepted = false;

        public AuthCmdFactory()
        {
            List<CmdType> types = super.getSupportedTypes();
            ArrayList<CmdType> tempTypes = new ArrayList<CmdType>(types);
            tempTypes.add(new CmdType(SsiCommand.FAMILY_SSI, 0x001b)); // 1b auth request reply
            tempTypes.add(new CmdType(SsiCommand.FAMILY_SSI, 0x0019)); // 19 auth request

            this.SUPPORTED_TYPES = DefensiveTools.getUnmodifiable(tempTypes);
        }

        @Override
        public List<CmdType> getSupportedTypes()
        {return SUPPORTED_TYPES;}

        @Override
        public SnacCommand genSnacCommand(SnacPacket packet)
        {
            int command = packet.getCommand();

            // auth request
            if (command == 25)
            {
                RequestAuthCmd cmd = new RequestAuthCmd(packet);
                requestReasonStr = cmd.reason;

                // will wait as a normal user
                Object lock = new Object();
                synchronized(lock){
                    try{
                        lock.wait(2000);
                    }
                    catch (Exception ex){}
                }

                logger.trace("sending authorization " + ACCEPT);

                sendAuthorizationReplay(
                    String.valueOf(cmd.uin),
                    responseReasonStr,
                    ACCEPT);

                return cmd;
            }
            else if (command == 27) // auth reply
            {
                AuthReplyCmd cmd = new AuthReplyCmd(packet);

                isRequestAccepted = cmd.accepted;
                responseReasonStr = cmd.reason;

                return cmd;
            }

            return super.genSnacCommand(packet);
        }

        public void handleResponse(SnacResponseEvent e)
        {
            if (e.getSnacCommand() instanceof SsiDataModResponse)
            {
                SsiDataModResponse dataModResponse =
                    (SsiDataModResponse) e.getSnacCommand();

                int[] results = dataModResponse.getResults();
                List<SsiItem> items = ( (ItemsCmd) e.getRequest().getCommand()).
                    getItems();
                items = new LinkedList<SsiItem>(items);

                for (int i = 0; i < results.length; i++)
                {
                    int result = results[i];
                    if (result ==
                        SsiDataModResponse.RESULT_ICQ_AUTH_REQUIRED)
                    {
                        isErrorAddingReceived = true;

                        // authorisation required for user
                        SsiItem buddyItem = items.get(i);

                        String uinToAskForAuth = buddyItem.getName();

                        Vector<SsiItem> buddiesToBeAdded = new Vector<SsiItem>();

                        BuddyAwaitingAuth newBuddy = new BuddyAwaitingAuth(
                            buddyItem);
                        buddiesToBeAdded.add(newBuddy);

                        CreateItemsCmd addCMD = new CreateItemsCmd(buddiesToBeAdded);

                        logger.trace("Adding buddy as awaiting authorization " + uinToAskForAuth);

                        MutableBuddyList joustSimBuddyList
                            = conn.getSsiService().getBuddyList();

                        LayoutEventCollector evtCollector = new LayoutEventCollector();
                        joustSimBuddyList.addLayoutListener(evtCollector);

                        conn.getSsiService().getOscarConnection().sendSnac(addCMD);

                        evtCollector.waitForANewBuddy(20000);
                        joustSimBuddyList.removeLayoutListener(evtCollector);

                        logger.trace("Finished - Adding buddy as awaiting authorization");

                        //SNAC(13,18)     send authorization request
                        conn.getSsiService().getOscarConnection().sendSnac(
                            new RequestAuthCmd(
                                uinToAskForAuth,
                                requestReasonStr));
                    }
                }
            }

        }
    }

    private class RequestAuthCmd
        extends SsiCommand
    {
        String uin;
        String reason;

        public RequestAuthCmd(String uin, String reason)
        {
            super(0x0018);
            this.uin = uin;
            this.reason = reason;
        }

        public RequestAuthCmd(SnacPacket packet)
        {
            super(0x0019);

            ByteBlock messageData = packet.getData();
            // parse data
            int offset = 0;
            short uinLen = BinaryTools.getUByte(messageData, offset);
            offset++;

            uin = OscarTools.getString(messageData.subBlock(offset, uinLen),"US-ASCII");

            offset += uinLen;

            int reasonLen = BinaryTools.getUShort(messageData, offset);
            offset+=2;

            reason =
                OscarTools.getString(messageData.subBlock(offset, reasonLen), "US-ASCII");
        }

        @Override
        public void writeData(OutputStream out) throws IOException
        {
            byte[] uinBytes = BinaryTools.getAsciiBytes(uin);
            BinaryTools.writeUByte(out, uinBytes.length);
            out.write(uinBytes);

            if (reason == null)
            {
                reason = "";
            }

            byte[] reasonBytes = BinaryTools.getAsciiBytes(reason);
            BinaryTools.writeUShort(out, reasonBytes.length);
            out.write(reasonBytes);
        }
    }

    public void setAuthorizationRequired()
    {
        logger.debug("sending auth required");
        FullUserInfoCmd cmd = new FullUserInfoCmd(getIcqUIN());
        cmd.writeOutByte(0x030c, 0); // 0x030C User authorization permissions
        cmd.writeOutByte(0x02F8, 0); // 0x02F8  User 'show web status' permissions
        conn.getSsiService().getOscarConnection().sendSnac(cmd);
    }

    public Hashtable<String, Object> getUserInfo(String uin)
    {
        UserInfoResponse response = new UserInfoResponse();

        conn.getInfoService().getOscarConnection().sendSnacRequest(
            FullUserInfoCmd.getFullInfoRequestCommand(getIcqUIN(), uin),
            response);

        synchronized(response)
        {
            try{response.wait(5000);}
            catch (InterruptedException ex){}
        }

        return response.info;
    }

    public void setUserInfoLastName(String lastName)
    {
        FullUserInfoCmd cmd = new FullUserInfoCmd(getIcqUIN());
        cmd.writeOutString(0x014A, lastName);
        conn.getSsiService().getOscarConnection().sendSnac(cmd);
    }
    public void setUserInfoPhoneNumber(String phone)
    {
        FullUserInfoCmd cmd = new FullUserInfoCmd(getIcqUIN());
        cmd.writeOutString(0x0276, phone);
        conn.getSsiService().getOscarConnection().sendSnac(cmd);
    }
    public void setUserInfoLanguage(int language1, int language2, int language3)
    {
        FullUserInfoCmd cmd = new FullUserInfoCmd(getIcqUIN());
        cmd.writeOutShort(0x0186, language1);
        cmd.writeOutShort(0x0186, language2);
        cmd.writeOutShort(0x0186, language3);
        conn.getSsiService().getOscarConnection().sendSnac(cmd);
    }
    public void setUserInfoHomeCountry(int countryCode)
    {
        FullUserInfoCmd cmd = new FullUserInfoCmd(getIcqUIN());
        cmd.writeOutShort(0x01A4, countryCode);
        conn.getSsiService().getOscarConnection().sendSnac(cmd);
    }


    private class UserInfoResponse
        extends SnacRequestAdapter
    {
        Hashtable<String, Object> info = null;

        @Override
        public void handleResponse(SnacResponseEvent e)
        {
            if(e.getSnacCommand() instanceof FullUserInfoCmd)
            {
                FullUserInfoCmd cmd = (FullUserInfoCmd)e.getSnacCommand();

                if(cmd.lastOfSequences)
                {
                    info = cmd.getInfo();
                    synchronized(this)
                    {notifyAll();}
                }
            }
        }
    }

    private static class BuddyAwaitingAuth
        extends SsiItem
    {
        private SsiItem originalItem = null;
        public BuddyAwaitingAuth(SsiItem originalItem)
        {
            super(
                originalItem.getName(),
                originalItem.getParentId(),
                originalItem.getId(),
                originalItem.getItemType(),
                getSpecTlvData());

            this.originalItem = originalItem;
        }

        @Override
        public void write(OutputStream out) throws IOException
        {
            byte[] namebytes = BinaryTools.getAsciiBytes(originalItem.getName());
            BinaryTools.writeUShort(out, namebytes.length);
            out.write(namebytes);

            BinaryTools.writeUShort(out, originalItem.getParentId());
            BinaryTools.writeUShort(out, originalItem.getId());
            BinaryTools.writeUShort(out, originalItem.getItemType());

            ByteBlock data = getData();
            // here we are nice and let data be null
            int len = data == null ? 0 : data.getLength();
            BinaryTools.writeUShort(out, len);
            if (data != null)
            {
                data.write(out);
            }
        }

        private static ByteBlock getSpecTlvData()
        {
            try
            {
                ByteArrayOutputStream o = new ByteArrayOutputStream();
                new Tlv(0x0066).write(o);

                ByteBlock block = ByteBlock.wrap(o.toByteArray());
                return block;
            }
            catch (IOException ex)
            {
                logger.error("Error creating buddy awaiting auth tlv", ex);
                return null;
            }
        }
    }
}
