/**
 *
 * Copyright 2003-2007 Jive Software.
 *
 * All rights reserved. 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 org.jivesoftware.smack;

import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
import java.util.HashSet;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import java.util.Set;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.CopyOnWriteArrayList;

import org.jivesoftware.smack.filter.PacketFilter;
import org.jivesoftware.smack.filter.PacketIDFilter;
import org.jivesoftware.smack.filter.PacketTypeFilter;
import org.jivesoftware.smack.util.StringUtils;
import org.xmpp.packet.IQ;
import org.xmpp.packet.JID;
import org.xmpp.packet.Packet;
import org.xmpp.packet.Presence;
import org.xmpp.packet.Roster;
import org.xmpp.packet.Roster.Item;
import org.xmpp.packet.Roster.Subscription;

/**
 * Represents a user's roster, which is the collection of users a person
 * receives presence updates for. Roster items are categorized into groups for
 * easier management.
 *

 *

 * Others users may attempt to subscribe to this user using a subscription
 * request. Three modes are supported for handling these requests:
 *

 *
{@link SubscriptionMode#accept_all accept_all} -- accept all subscription
 * requests.

 *
{@link SubscriptionMode#reject_all reject_all} -- reject all subscription
 * requests.

 *
{@link SubscriptionMode#manual manual} -- manually process all
 * subscription requests.

 *

 *
 * @author Matt Tucker
 * @see Connection#getRoster()
 */
public class UserRoster {

    /**
     * The default subscription processing mode to use when a Roster is created.
     * By default all subscription requests are automatically accepted.
     */
    private static SubscriptionMode defaultSubscriptionMode = SubscriptionMode.accept_all;

    private final Connection connection;
    private final RosterStore rosterStore;
    private final Map<String, RosterGroup> groups;
    private final Map<String, RosterEntry> entries;
    private final List<RosterEntry> unfiledEntries;
    private final List<RosterListener> rosterListeners;
    private Map<String, Map<String, Presence>> presenceMap;
    // The roster is marked as initialized when at least a single roster packet
    // has been received and processed.
    boolean rosterInitialized = false;
    private PresencePacketListener presencePacketListener;

    private SubscriptionMode subscriptionMode = getDefaultSubscriptionMode();

    /**
     * Returns the default subscription processing mode to use when a new Roster
     * is created. The subscription processing mode dictates what action Smack
     * will take when subscription requests from other users are made. The
     * default subscription mode is {@link SubscriptionMode#accept_all}.
     *
     * @return the default subscription mode to use for new Rosters
     */
    public static SubscriptionMode getDefaultSubscriptionMode() {
        return defaultSubscriptionMode;
    }

    /**
     * Sets the default subscription processing mode to use when a new Roster is
     * created. The subscription processing mode dictates what action Smack will
     * take when subscription requests from other users are made. The default
     * subscription mode is {@link SubscriptionMode#accept_all}.
     *
     * @param subscriptionMode
     *            the default subscription mode to use for new Rosters.
     */
    public static void setDefaultSubscriptionMode(
            SubscriptionMode subscriptionMode) {
        defaultSubscriptionMode = subscriptionMode;
    }

    /**
     * Creates a new roster.
     *
     * @param connection
     *            an XMPP connection.
     */
    UserRoster(final Connection connection) {
        this.connection = connection;
        rosterStore = connection.getConfiguration().getRosterStore();
        groups = new ConcurrentHashMap<String, RosterGroup>();
        unfiledEntries = new CopyOnWriteArrayList<RosterEntry>();
        entries = new ConcurrentHashMap<String, RosterEntry>();
        rosterListeners = new CopyOnWriteArrayList<RosterListener>();
        presenceMap = new ConcurrentHashMap<String, Map<String, Presence>>();
        // Listen for any roster packets.
        PacketFilter rosterFilter = new PacketTypeFilter(Roster.class);
        connection.addPacketListener(new RosterPacketListener(), rosterFilter);
        // Listen for any presence packets.
        PacketFilter presenceFilter = new PacketTypeFilter(Presence.class);
        presencePacketListener = new PresencePacketListener();
        connection.addPacketListener(presencePacketListener, presenceFilter);

        // Listen for connection events
        final ConnectionListener connectionListener = new AbstractConnectionListener() {

            public void connectionClosed() {
                // Changes the presence available contacts to unavailable
                setOfflinePresences();
            }

            public void connectionClosedOnError(Exception e) {
                // Changes the presence available contacts to unavailable
                setOfflinePresences();
            }

        };

        // if not connected add listener after successful login
        if (!this.connection.isConnected()) {
            Connection
                    .addConnectionCreationListener(new ConnectionCreationListener() {

                        public void connectionCreated(Connection connection) {
                            if (connection.equals(UserRoster.this.connection)) {
                                UserRoster.this.connection
                                        .addConnectionListener(connectionListener);
                            }

                        }
                    });
        } else {
            connection.addConnectionListener(connectionListener);
        }
    }

    /**
     * Returns the subscription processing mode, which dictates what action
     * Smack will take when subscription requests from other users are made. The
     * default subscription mode is {@link SubscriptionMode#accept_all}.
     *

     *

     * If using the manual mode, a PacketListener should be registered that
     * listens for Presence packets that have a type of
     * {@link org.jivesoftware.smack.packet.Presence.Type#subscribe}.
     *
     * @return the subscription mode.
     */
    public SubscriptionMode getSubscriptionMode() {
        return subscriptionMode;
    }

    /**
     * Sets the subscription processing mode, which dictates what action Smack
     * will take when subscription requests from other users are made. The
     * default subscription mode is {@link SubscriptionMode#accept_all}.
     *

     *

     * If using the manual mode, a PacketListener should be registered that
     * listens for Presence packets that have a type of
     * {@link org.jivesoftware.smack.packet.Presence.Type#subscribe}.
     *
     * @param subscriptionMode
     *            the subscription mode.
     */
    public void setSubscriptionMode(SubscriptionMode subscriptionMode) {
        this.subscriptionMode = subscriptionMode;
    }

    /**
     * Reloads the entire roster from the server. This is an asynchronous
     * operation, which means the method will return immediately, and the roster
     * will be reloaded at a later point when the server responds to the reload
     * request.
     *
     * @throws IllegalStateException
     *             if connection is not logged in or logged in anonymously
     */
    public void reload() {
        if (!connection.isAuthenticated()) {
            throw new IllegalStateException("Not logged in to server.");
        }
        if (connection.isAnonymous()) {
            throw new IllegalStateException(
                    "Anonymous users can't have a roster.");
        }

        Roster packet = new Roster();
        if (rosterStore != null && connection.isRosterVersioningSupported()) {
            packet.getElement().element("query")
                    .addAttribute("ver", rosterStore.getRosterVersion());
            PacketFilter filter = new PacketIDFilter(packet.getID());
            connection.addPacketListener(new RosterResultListener(), filter);
        }
        connection.sendPacket(packet);
    }

    /**
     * Adds a listener to this roster. The listener will be fired anytime one or
     * more changes to the roster are pushed from the server.
     *
     * @param rosterListener
     *            a roster listener.
     */
    public void addRosterListener(RosterListener rosterListener) {
        if (!rosterListeners.contains(rosterListener)) {
            rosterListeners.add(rosterListener);
        }
    }

    /**
     * Removes a listener from this roster. The listener will be fired anytime
     * one or more changes to the roster are pushed from the server.
     *
     * @param rosterListener
     *            a roster listener.
     */
    public void removeRosterListener(RosterListener rosterListener) {
        rosterListeners.remove(rosterListener);
    }

    /**
     * Creates a new group.
     *

     *

     * Note: you must add at least one entry to the group for the group to be
     * kept after a logout/login. This is due to the way that XMPP stores group
     * information.
     *
     * @param name
     *            the name of the group.
     * @return a new group.
     * @throws IllegalStateException
     *             if connection is not logged in or logged in anonymously
     */
    public RosterGroup createGroup(String name) {
        if (!connection.isAuthenticated()) {
            throw new IllegalStateException("Not logged in to server.");
        }
        if (connection.isAnonymous()) {
            throw new IllegalStateException(
                    "Anonymous users can't have a roster.");
        }
        if (groups.containsKey(name)) {
            throw new IllegalArgumentException("Group with name " + name
                    + " alread exists.");
        }

        RosterGroup group = new RosterGroup(name, connection);
        groups.put(name, group);
        return group;
    }

    /**
     * Creates a new roster entry and presence subscription. The server will
     * asynchronously update the roster with the subscription status.
     *
     * @param user
     *            the user. (e.g. johndoe@jabber.org)
     * @param name
     *            the nickname of the user.
     * @param groups
     *            the list of group names the entry will belong to, or
     *            <tt>null</tt> if the the roster entry won't belong to a group.
     * @throws XMPPException
     *             if an XMPP exception occurs.
     * @throws IllegalStateException
     *             if connection is not logged in or logged in anonymously
     */
    public void createEntry(String user, String name, String[] groups)
            throws XMPPException {
        if (!connection.isAuthenticated()) {
            throw new IllegalStateException("Not logged in to server.");
        }
        if (connection.isAnonymous()) {
            throw new IllegalStateException(
                    "Anonymous users can't have a roster.");
        }

        // Create and send roster entry creation packet.
        Roster rosterPacket = new Roster();
        rosterPacket.setType(IQ.Type.set);
        rosterPacket.addItem(new JID(user), name, null, null,
                Arrays.asList(groups));
        // Wait up to a certain number of seconds for a reply from the server.
        PacketCollector collector = connection
                .createPacketCollector(new PacketIDFilter(rosterPacket.getID()));
        connection.sendPacket(rosterPacket);
        IQ response = (IQ) collector.nextResult(SmackConfiguration
                .getPacketReplyTimeout());
        collector.cancel();
        if (response == null) {
            throw new XMPPException("No response from the server.");
        }
        // If the server replied with an error, throw an exception.
        else if (response.getType() == IQ.Type.error) {
            throw new XMPPException(response.getError());
        }

        // Create a presence subscription packet and send.
        Presence presencePacket = new Presence(Presence.Type.subscribe);
        presencePacket.setTo(user);
        connection.sendPacket(presencePacket);
    }

    /**
     * Removes a roster entry from the roster. The roster entry will also be
     * removed from the unfiled entries or from any roster group where it could
     * belong and will no longer be part of the roster. Note that this is an
     * asynchronous call -- Smack must wait for the server to send an updated
     * subscription status.
     *
     * @param entry
     *            a roster entry.
     * @throws XMPPException
     *             if an XMPP error occurs.
     * @throws IllegalStateException
     *             if connection is not logged in or logged in anonymously
     */
    public void removeEntry(RosterEntry entry) throws XMPPException {
        if (!connection.isAuthenticated()) {
            throw new IllegalStateException("Not logged in to server.");
        }
        if (connection.isAnonymous()) {
            throw new IllegalStateException(
                    "Anonymous users can't have a roster.");
        }

        // Only remove the entry if it's in the entry list.
        // The actual removal logic takes place in
        // RosterPacketListenerprocess>>Packet(Packet)
        if (!entries.containsKey(entry.getUser())) {
            return;
        }
        Roster packet = new Roster();
        packet.setType(IQ.Type.set);
        packet.addItem(new JID(entry.getUser()), entry.getName(),
                entry.getAsk(), Subscription.remove, entry.getGroupNames());
        PacketCollector collector = connection
                .createPacketCollector(new PacketIDFilter(packet.getID()));
        connection.sendPacket(packet);
        IQ response = (IQ) collector.nextResult(SmackConfiguration
                .getPacketReplyTimeout());
        collector.cancel();
        if (response == null) {
            throw new XMPPException("No response from the server.");
        }
        // If the server replied with an error, throw an exception.
        else if (response.getType() == IQ.Type.error) {
            throw new XMPPException(response.getError());
        }
    }

    /**
     * Returns a count of the entries in the roster.
     *
     * @return the number of entries in the roster.
     */
    public int getEntryCount() {
        return getEntries().size();
    }

    /**
     * Returns an unmodifiable collection of all entries in the roster,
     * including entries that don't belong to any groups.
     *
     * @return all entries in the roster.
     */
    public Collection<RosterEntry> getEntries() {
        Set<RosterEntry> allEntries = new HashSet<RosterEntry>();
        // Loop through all roster groups and add their entries to the answer
        for (RosterGroup rosterGroup : getGroups()) {
            allEntries.addAll(rosterGroup.getEntries());
        }
        // Add the roster unfiled entries to the answer
        allEntries.addAll(unfiledEntries);

        return Collections.unmodifiableCollection(allEntries);
    }

    /**
     * Returns a count of the unfiled entries in the roster. An unfiled entry is
     * an entry that doesn't belong to any groups.
     *
     * @return the number of unfiled entries in the roster.
     */
    public int getUnfiledEntryCount() {
        return unfiledEntries.size();
    }

    /**
     * Returns an unmodifiable collection for the unfiled roster entries. An
     * unfiled entry is an entry that doesn't belong to any groups.
     *
     * @return the unfiled roster entries.
     */
    public Collection<RosterEntry> getUnfiledEntries() {
        return Collections.unmodifiableList(unfiledEntries);
    }

    /**
     * Returns the roster entry associated with the given XMPP address or
     * <tt>null</tt> if the user is not an entry in the roster.
     *
     * @param user
     *            the XMPP address of the user (eg "jsmith@example.com"). The
     *            address could be in any valid format (e.g. "domain/resource",
     *            "user@domain" or "user@domain/resource").
     * @return the roster entry or <tt>null</tt> if it does not exist.
     */
    public RosterEntry getEntry(String user) {
        if (user == null) {
            return null;
        }
        return entries.get(user.toLowerCase());
    }

    /**
     * Returns true if the specified XMPP address is an entry in the roster.
     *
     * @param user
     *            the XMPP address of the user (eg "jsmith@example.com"). The
     *            address could be in any valid format (e.g. "domain/resource",
     *            "user@domain" or "user@domain/resource").
     * @return true if the XMPP address is an entry in the roster.
     */
    public boolean contains(String user) {
        return getEntry(user) != null;
    }

    /**
     * Returns the roster group with the specified name, or <tt>null</tt> if the
     * group doesn't exist.
     *
     * @param name
     *            the name of the group.
     * @return the roster group with the specified name.
     */
    public RosterGroup getGroup(String name) {
        return groups.get(name);
    }

    /**
     * Returns the number of the groups in the roster.
     *
     * @return the number of groups in the roster.
     */
    public int getGroupCount() {
        return groups.size();
    }

    /**
     * Returns an unmodifiable collections of all the roster groups.
     *
     * @return an iterator for all roster groups.
     */
    public Collection<RosterGroup> getGroups() {
        return Collections.unmodifiableCollection(groups.values());
    }

    /**
     * Returns the presence info for a particular user. If the user is offline,
     * or if no presence data is available (such as when you are not subscribed
     * to the user's presence updates), unavailable presence will be returned.
     *

     *

     * If the user has several presences (one for each resource), then the
     * presence with highest priority will be returned. If multiple presences
     * have the same priority, the one with the "most available" presence mode
     * will be returned. In order, that's
     * {@link org.jivesoftware.smack.packet.Presence.Mode#chat free to chat},
     * {@link org.jivesoftware.smack.packet.Presence.Mode#available available},
     * {@link org.jivesoftware.smack.packet.Presence.Mode#away away},
     * {@link org.jivesoftware.smack.packet.Presence.Mode#xa extended away}, and
     * {@link org.jivesoftware.smack.packet.Presence.Mode#dnd do not disturb}.
     *

     *

     * Note that presence information is received asynchronously. So, just after
     * logging in to the server, presence values for users in the roster may be
     * unavailable even if they are actually online. In other words, the value
     * returned by this method should only be treated as a snapshot in time, and
     * may not accurately reflect other user's presence instant by instant. If
     * you need to track presence over time, such as when showing a visual
     * representation of the roster, consider using a {@link RosterListener}.
     *
     * @param user
     *            an XMPP ID. The address could be in any valid format (e.g.
     *            "domain/resource", "user@domain" or "user@domain/resource").
     *            Any resource information that's part of the ID will be
     *            discarded.
     * @return the user's current presence, or unavailable presence if the user
     *         is offline or if no presence information is available..
     */
    public Presence getPresence(String user) {
        String key = getPresenceMapKey(StringUtils.parseBareAddress(user));
        Map<String, Presence> userPresences = presenceMap.get(key);
        if (userPresences == null) {
            Presence presence = new Presence(Presence.Type.unavailable);
            presence.setFrom(user);
            return presence;
        } else {
            // Find the resource with the highest priority
            // Might be changed to use the resource with the highest
            // availability instead.
            Presence presence = null;

            for (String resource : userPresences.keySet()) {
                Presence p = userPresences.get(resource);
                if (!p.isAvailable()) {
                    continue;
                }
                // Chose presence with highest priority first.
                if (presence == null
                        || p.getPriority() > presence.getPriority()) {
                    presence = p;
                }
                // If equal priority, choose "most available" by the mode value.
                else if (p.getPriority() == presence.getPriority()) {
                    Presence.Show pMode = p.getShow();
                    // Default to presence mode of available.
                    if (pMode == null) {
                        pMode = Presence.Show.chat;
                    }
                    Presence.Show presenceMode = presence.getShow();
                    // Default to presence mode of available.
                    if (presenceMode == null) {
                        presenceMode = Presence.Show.chat;
                    }
                    if (pMode.compareTo(presenceMode) < 0) {
                        presence = p;
                    }
                }
            }
            if (presence == null) {
                presence = new Presence(Presence.Type.unavailable);
                presence.setFrom(user);
                return presence;
            } else {
                return presence;
            }
        }
    }

    /**
     * Returns the presence info for a particular user's resource, or
     * unavailable presence if the user is offline or if no presence information
     * is available, such as when you are not subscribed to the user's presence
     * updates.
     *
     * @param userWithResource
     *            a fully qualified XMPP ID including a resource
     *            (user@domain/resource).
     * @return the user's current presence, or unavailable presence if the user
     *         is offline or if no presence information is available.
     */
    public Presence getPresenceResource(String userWithResource) {
        String key = getPresenceMapKey(userWithResource);
        String resource = StringUtils.parseResource(userWithResource);
        Map<String, Presence> userPresences = presenceMap.get(key);
        if (userPresences == null) {
            Presence presence = new Presence(Presence.Type.unavailable);
            presence.setFrom(userWithResource);
            return presence;
        } else {
            Presence presence = userPresences.get(resource);
            if (presence == null) {
                presence = new Presence(Presence.Type.unavailable);
                presence.setFrom(userWithResource);
                return presence;
            } else {
                return presence;
            }
        }
    }

    /**
     * Returns an iterator (of Presence objects) for all of a user's current
     * presences or an unavailable presence if the user is unavailable (offline)
     * or if no presence information is available, such as when you are not
     * subscribed to the user's presence updates.
     *
     * @param user
     *            a XMPP ID, e.g. jdoe@example.com.
     * @return an iterator (of Presence objects) for all the user's current
     *         presences, or an unavailable presence if the user is offline or
     *         if no presence information is available.
     */
    public Iterator<Presence> getPresences(String user) {
        String key = getPresenceMapKey(user);
        Map<String, Presence> userPresences = presenceMap.get(key);
        if (userPresences == null) {
            Presence presence = new Presence(Presence.Type.unavailable);
            presence.setFrom(user);
            return Arrays.asList(presence).iterator();
        } else {
            Collection<Presence> answer = new ArrayList<Presence>();
            for (Presence presence : userPresences.values()) {
                if (presence.isAvailable()) {
                    answer.add(presence);
                }
            }
            if (!answer.isEmpty()) {
                return answer.iterator();
            } else {
                Presence presence = new Presence(Presence.Type.unavailable);
                presence.setFrom(user);
                return Arrays.asList(presence).iterator();
            }
        }
    }

    /**
     * Returns the key to use in the presenceMap for a fully qualified XMPP ID.
     * The roster can contain any valid address format such us
     * "domain/resource", "user@domain" or "user@domain/resource". If the roster
     * contains an entry associated with the fully qualified XMPP ID then use
     * the fully qualified XMPP ID as the key in presenceMap, otherwise use the
     * bare address. Note: When the key in presenceMap is a fully qualified XMPP
     * ID, the userPresences is useless since it will always contain one entry
     * for the user.
     *
     * @param user
     *            the bare or fully qualified XMPP ID, e.g. jdoe@example.com or
     *            jdoe@example.com/Work.
     * @return the key to use in the presenceMap for the fully qualified XMPP
     *         ID.
     */
    private String getPresenceMapKey(String user) {
        if (user == null) {
            return null;
        }
        String key = user;
        if (!contains(user)) {
            key = StringUtils.parseBareAddress(user);
        }
        return key.toLowerCase();
    }

    /**
     * Changes the presence of available contacts offline by simulating an
     * unavailable presence sent from the server. After a disconnection, every
     * Presence is set to offline.
     */
    private void setOfflinePresences() {
        Presence packetUnavailable;
        for (String user : presenceMap.keySet()) {
            Map<String, Presence> resources = presenceMap.get(user);
            if (resources != null) {
                for (String resource : resources.keySet()) {
                    packetUnavailable = new Presence(Presence.Type.unavailable);
                    packetUnavailable.setFrom(user + "/" + resource);
                    presencePacketListener.processPacket(packetUnavailable);
                }
            }
        }
    }

    /**
     * Fires roster changed event to roster listeners indicating that the
     * specified collections of contacts have been added, updated or deleted
     * from the roster.
     *
     * @param addedEntries
     *            the collection of address of the added contacts.
     * @param updatedEntries
     *            the collection of address of the updated contacts.
     * @param deletedEntries
     *            the collection of address of the deleted contacts.
     */
    private void fireRosterChangedEvent(Collection<String> addedEntries,
            Collection<String> updatedEntries, Collection<String> deletedEntries) {
        for (RosterListener listener : rosterListeners) {
            if (!addedEntries.isEmpty()) {
                listener.entriesAdded(addedEntries);
            }
            if (!updatedEntries.isEmpty()) {
                listener.entriesUpdated(updatedEntries);
            }
            if (!deletedEntries.isEmpty()) {
                listener.entriesDeleted(deletedEntries);
            }
        }
    }

    /**
     * Fires roster presence changed event to roster listeners.
     *
     * @param presence
     *            the presence change.
     */
    private void fireRosterPresenceEvent(Presence presence) {
        for (RosterListener listener : rosterListeners) {
            listener.presenceChanged(presence);
        }
    }

    private void addUpdateEntry(Collection<String> addedEntries,
            Collection<String> updatedEntries, Roster.Item item,
            RosterEntry entry) {
        RosterEntry oldEntry = entries.put(item.getJID().toBareJID(), entry);
        if (oldEntry == null) {
            addedEntries.add(item.getJID().toBareJID());
        } else {
            if (!oldEntry.equalsDeep(entry)
                    || !item.getGroups().equals(oldEntry.getGroupNames())) {
                updatedEntries.add(item.getJID().toBareJID());
            }
        }

        // Mark the entry as unfiled if it does not belong to any groups.
        if (item.getGroups().isEmpty()) {
            if (!unfiledEntries.contains(entry)) {
                unfiledEntries.add(entry);
            }
        } else {
            unfiledEntries.remove(entry);
        }

        // Add the user to the new groups

        // Add the entry to the groups
        List<String> newGroupNames = new ArrayList<String>();
        for (String groupName : item.getGroups()) {
            // Add the group name to the list.
            newGroupNames.add(groupName);

            // Add the entry to the group.
            RosterGroup group = getGroup(groupName);
            if (group == null) {
                group = createGroup(groupName);
                groups.put(groupName, group);
            }
            // Add the entry.
            group.addEntryLocal(entry);
        }

        // Remove user from the remaining groups.
        List<String> oldGroupNames = new ArrayList<String>();
        for (RosterGroup group : getGroups()) {
            oldGroupNames.add(group.getName());
        }
        oldGroupNames.removeAll(newGroupNames);

        for (String groupName : oldGroupNames) {
            RosterGroup group = getGroup(groupName);
            group.removeEntryLocal(entry);
            if (group.getEntryCount() == 0) {
                groups.remove(groupName);
            }
        }
    }

    private void deleteEntry(Collection<String> deletedEntries,
            RosterEntry entry) {
        String user = entry.getUser();
        entries.remove(user);
        unfiledEntries.remove(entry);
        presenceMap.remove(StringUtils.parseBareAddress(user));
        deletedEntries.add(user);

        for (Entry<String, RosterGroup> e : groups.entrySet()) {
            RosterGroup group = e.getValue();
            group.removeEntryLocal(entry);
            if (group.getEntryCount() == 0) {
                groups.remove(e.getKey());
            }
        }
    }

    /**
     * An enumeration for the subscription mode options.
     */
    public enum SubscriptionMode {

        /**
         * Automatically accept all subscription and unsubscription requests.
         * This is the default mode and is suitable for simple client. More
         * complex client will likely wish to handle subscription requests
         * manually.
         */
        accept_all,

        /**
         * Automatically reject all subscription requests.
         */
        reject_all,

        /**
         * Subscription requests are ignored, which means they must be manually
         * processed by registering a listener for presence packets and then
         * looking for any presence requests that have the type
         * Presence.Type.SUBSCRIBE or Presence.Type.UNSUBSCRIBE.
         */
        manual
    }

    /**
     * Listens for all presence packets and processes them.
     */
    private class PresencePacketListener implements PacketListener {

        public void processPacket(Packet packet) {
            Presence presence = (Presence) packet;
            String from = presence.getFrom().toBareJID();
            String key = getPresenceMapKey(from);

            // If an "available" presence, add it to the presence map. Each
            // presence
            // map will hold for a particular user a map with the presence
            // packets saved for each resource.
            if (presence.getType() == Presence.Type.subscribed) {
                Map<String, Presence> userPresences;
                // Get the user presence map
                if (presenceMap.get(key) == null) {
                    userPresences = new ConcurrentHashMap<String, Presence>();
                    presenceMap.put(key, userPresences);
                } else {
                    userPresences = presenceMap.get(key);
                }
                // See if an offline presence was being stored in the map. If
                // so, remove
                // it since we now have an online presence.
                userPresences.remove("");
                // Add the new presence, using the resources as a key.
                userPresences.put(StringUtils.parseResource(from), presence);
                // If the user is in the roster, fire an event.
                RosterEntry entry = entries.get(key);
                if (entry != null) {
                    fireRosterPresenceEvent(presence);
                }
            }
            // If an "unavailable" packet.
            else if (presence.getType() == Presence.Type.unavailable) {
                // If no resource, this is likely an offline presence as part of
                // a roster presence flood. In that case, we store it.
                if ("".equals(StringUtils.parseResource(from))) {
                    Map<String, Presence> userPresences;
                    // Get the user presence map
                    if (presenceMap.get(key) == null) {
                        userPresences = new ConcurrentHashMap<String, Presence>();
                        presenceMap.put(key, userPresences);
                    } else {
                        userPresences = presenceMap.get(key);
                    }
                    userPresences.put("", presence);
                }
                // Otherwise, this is a normal offline presence.
                else if (presenceMap.get(key) != null) {
                    Map<String, Presence> userPresences = presenceMap.get(key);
                    // Store the offline presence, as it may include extra
                    // information
                    // such as the user being on vacation.
                    userPresences
                            .put(StringUtils.parseResource(from), presence);
                }
                // If the user is in the roster, fire an event.
                RosterEntry entry = entries.get(key);
                if (entry != null) {
                    fireRosterPresenceEvent(presence);
                }
            } else if (presence.getType() == Presence.Type.subscribe) {
                if (subscriptionMode == SubscriptionMode.accept_all) {
                    // Accept all subscription requests.
                    Presence response = new Presence(Presence.Type.subscribed);
                    response.setTo(presence.getFrom());
                    connection.sendPacket(response);
                } else if (subscriptionMode == SubscriptionMode.reject_all) {
                    // Reject all subscription requests.
                    Presence response = new Presence(Presence.Type.unsubscribed);
                    response.setTo(presence.getFrom());
                    connection.sendPacket(response);
                }
                // Otherwise, in manual mode so ignore.
            } else if (presence.getType() == Presence.Type.unsubscribe) {
                if (subscriptionMode != SubscriptionMode.manual) {
                    // Acknowledge and accept unsubscription notification so
                    // that the
                    // server will stop sending notifications saying that the
                    // contact
                    // has unsubscribed to our presence.
                    Presence response = new Presence(Presence.Type.unsubscribed);
                    response.setTo(presence.getFrom());
                    connection.sendPacket(response);
                }
                // Otherwise, in manual mode so ignore.
            }
            // Error presence packets from a bare JID mean we invalidate all
            // existing
            // presence info for the user.
            else if (presence.getType() == Presence.Type.error
                    && "".equals(StringUtils.parseResource(from))) {
                Map<String, Presence> userPresences;
                if (!presenceMap.containsKey(key)) {
                    userPresences = new ConcurrentHashMap<String, Presence>();
                    presenceMap.put(key, userPresences);
                } else {
                    userPresences = presenceMap.get(key);
                    // Any other presence data is invalidated by the error
                    // packet.
                    userPresences.clear();
                }
                // Set the new presence using the empty resource as a key.
                userPresences.put("", presence);
                // If the user is in the roster, fire an event.
                RosterEntry entry = entries.get(key);
                if (entry != null) {
                    fireRosterPresenceEvent(presence);
                }
            }
        }
    }

    /**
     * Handles the case of the empty IQ-result for roster versioning.
     *
     * Intended to listen for a concrete roster result and deregisters itself
     * after a processed packet.
     */
    private class RosterResultListener implements PacketListener {

        @Override
        public void processPacket(Packet packet) {
            connection.removePacketListener(this);
            if (packet instanceof Roster) {
                // Non-empty roster results are processed by the
                // RosterPacketListener class
                return;
            }
            if (!(packet instanceof IQ)) {
                return;
            }
            IQ result = (IQ) packet;
            if (result.getType().equals(IQ.Type.result)) {
                Collection<String> addedEntries = new ArrayList<String>();
                Collection<String> updatedEntries = new ArrayList<String>();
                for (Roster.Item item : rosterStore.getEntries()) {
                    RosterEntry entry = new RosterEntry(item.getJID()
                            .toBareJID(), item.getName(),
                            item.getSubscription(), item.getAsk(),
                            UserRoster.this, connection);
                    addUpdateEntry(addedEntries, updatedEntries, item, entry);
                }

                synchronized (UserRoster.this) {
                    rosterInitialized = true;
                    UserRoster.this.notifyAll();
                }
                fireRosterChangedEvent(addedEntries, updatedEntries,
                        Collections.<String> emptyList());
            }
        }

    }

    /**
     * Listens for all roster packets and processes them.
     */
    private class RosterPacketListener implements PacketListener {

        public void processPacket(Packet packet) {
            Roster rosterPacket = (Roster) packet;
            Collection<String> addedEntries = new ArrayList<String>();
            Collection<String> updatedEntries = new ArrayList<String>();
            Collection<String> deletedEntries = new ArrayList<String>();

            String version = rosterPacket.getElement().element("query")
                    .attributeValue("ver");

            if (rosterPacket.getType().equals(IQ.Type.set)) {
                // Roster push (RFC 6121, 2.1.6)
                // A roster push with a non-empty from not matching our address
                // MUST be ignored
                String jid = StringUtils.parseBareAddress(connection.getUser());
                if (rosterPacket.getFrom() != null
                        && !rosterPacket.getFrom().equals(jid)) {
                    return;
                }

                // A roster push must contain exactly one entry
                Collection<Item> items = rosterPacket.getItems();
                if (items.size() != 1) {
                    return;
                }
                Item item = items.iterator().next();
                processPushItem(addedEntries, updatedEntries, deletedEntries,
                        version, item);

                connection.sendPacket(IQ.createResultIQ(rosterPacket));
            } else {
                // Roster result (RFC 6121, 2.1.4)

                // Ignore items without valid subscription type
                ArrayList<Item> validItems = new ArrayList<Roster.Item>();
                for (Roster.Item item : rosterPacket.getItems()) {
                    if (hasValidSubscriptionType(item)) {
                        validItems.add(item);
                    }
                }

                processResult(addedEntries, updatedEntries, deletedEntries,
                        version, validItems);
            }

            // Remove all the groups with no entries. We have to do this because
            // RosterGroup.removeEntry removes the entry immediately (locally)
            // and the
            // group could remain empty.
            // TODO Check the performance/logic for rosters with large number of
            // groups
            for (RosterGroup group : getGroups()) {
                if (group.getEntryCount() == 0) {
                    groups.remove(group.getName());
                }
            }

            // Mark the roster as initialized.
            synchronized (UserRoster.this) {
                rosterInitialized = true;
                UserRoster.this.notifyAll();
            }

            // Fire event for roster listeners.
            fireRosterChangedEvent(addedEntries, updatedEntries, deletedEntries);
        }

        private void processPushItem(Collection<String> addedEntries,
                Collection<String> updatedEntries,
                Collection<String> deletedEntries, String version, Item item) {
            RosterEntry entry = new RosterEntry(item.getJID().toBareJID(),
                    item.getName(), item.getSubscription(), item.getAsk(),
                    UserRoster.this, connection);

            if (item.getSubscription().equals(Roster.Subscription.remove)) {
                deleteEntry(deletedEntries, entry);
                if (rosterStore != null) {
                    rosterStore.removeEntry(entry.getUser(), version);
                }
            } else if (hasValidSubscriptionType(item)) {
                addUpdateEntry(addedEntries, updatedEntries, item, entry);
                if (rosterStore != null) {
                    rosterStore.addEntry(item, version);
                }
            }
        }

        private void processResult(Collection<String> addedEntries,
                Collection<String> updatedEntries,
                Collection<String> deletedEntries, String version,
                Collection<Item> items) {
            for (Roster.Item item : items) {
                RosterEntry entry = new RosterEntry(item.getJID().toBareJID(),
                        item.getName(), item.getSubscription(), item.getAsk(),
                        UserRoster.this, connection);
                addUpdateEntry(addedEntries, updatedEntries, item, entry);
            }
            List<String> toDelete = new ArrayList<String>();

            // Delete all entries which where not added or updated
            for (RosterEntry entry : entries.values()) {
                toDelete.add(entry.getUser());
            }
            toDelete.removeAll(addedEntries);
            toDelete.removeAll(updatedEntries);
            for (String user : toDelete) {
                deleteEntry(deletedEntries, entries.get(user));
            }

            if (rosterStore != null) {
                rosterStore.resetEntries(items, version);
            }
        }

        /* Ignore ItemTypes as of RFC 6121, 2.1.2.5. */
        private boolean hasValidSubscriptionType(Roster.Item item) {
            return item.getSubscription().equals(Roster.Subscription.none)
                    || item.getSubscription().equals(Roster.Subscription.from)
                    || item.getSubscription().equals(Roster.Subscription.to)
                    || item.getSubscription().equals(Roster.Subscription.both);
        }

    }

}


/**
 *
 * Copyright 2003-2007 Jive Software.
 *
 * All rights reserved. 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 org.jivesoftware.smack.packet;

import org.dom4j.Element;
import org.jivesoftware.smack.util.PacketParserUtils;
import org.jivesoftware.smack.util.StringUtils;
import org.xmpp.packet.IQ;

/**
 * Authentication packet, which can be used to login to a XMPP server as well as
 * discover login information from the server.
 */
public class Authentication extends IQ {

    private Element queryEl;

    /**
     * Create a new authentication packet. By default, the packet will be in
     * "set" mode in order to perform an actual authentication with the server.
     * In order to send a "get" request to get the available authentication
     * modes back from the server, change the type of the IQ packet to "get":
     *

     *

     * <tt>setType(IQ.Type.GET);</tt>
     */
    public Authentication() {
        super();
        setType(IQ.Type.set);
        queryEl = element.addElement("query", "jabber:iq:auth");
        queryEl.addElement("username");
        queryEl.addElement("digest");
        queryEl.addElement("password");
        queryEl.addElement("resource");
    }

    public Authentication(Element el) {
        super(el);
        queryEl = element.element("query");
    }

    /**
     * Returns the username, or <tt>null</tt> if the username hasn't been sent.
     *
     * @return the username.
     */
    public String getUsername() {
        return queryEl.elementText("username");
    }

    /**
     * Sets the username.
     *
     * @param username
     *            the username.
     */
    public void setUsername(String username) {
        PacketParserUtils.updateText(queryEl, "username", username);
    }

    /**
     * Returns the plain text password or <tt>null</tt> if the password hasn't
     * been set.
     *
     * @return the password.
     */
    public String getPassword() {
        return queryEl.elementText("password");
    }

    /**
     * Sets the plain text password.
     *
     * @param password
     *            the password.
     */
    public void setPassword(String password) {
        PacketParserUtils.updateText(queryEl, "password", password);
    }

    /**
     * Returns the password digest or <tt>null</tt> if the digest hasn't been
     * set. Password digests offer a more secure alternative for authentication
     * compared to plain text. The digest is the hex-encoded SHA-1 hash of the
     * connection ID plus the user's password. If the digest and password are
     * set, digest authentication will be used. If only one value is set, the
     * respective authentication mode will be used.
     *
     * @return the digest of the user's password.
     */
    public String getDigest() {
        return queryEl.elementText("digest");
    }

    /**
     * Sets the digest value using a connection ID and password. Password
     * digests offer a more secure alternative for authentication compared to
     * plain text. The digest is the hex-encoded SHA-1 hash of the connection ID
     * plus the user's password. If the digest and password are set, digest
     * authentication will be used. If only one value is set, the respective
     * authentication mode will be used.
     *
     * @param connectionID
     *            the connection ID.
     * @param password
     *            the password.
     * @see org.jivesoftware.smack.Connection#getConnectionID()
     */
    public void setDigest(String connectionID, String password) {
        setDigest(StringUtils.hash(connectionID + password));
    }

    /**
     * Sets the digest value directly. Password digests offer a more secure
     * alternative for authentication compared to plain text. The digest is the
     * hex-encoded SHA-1 hash of the connection ID plus the user's password. If
     * the digest and password are set, digest authentication will be used. If
     * only one value is set, the respective authentication mode will be used.
     *
     * @param digest
     *            the digest, which is the SHA-1 hash of the connection ID the
     *            user's password, encoded as hex.
     * @see org.jivesoftware.smack.Connection#getConnectionID()
     */
    public void setDigest(String digest) {
        PacketParserUtils.updateText(queryEl, "digest", digest);
    }

    /**
     * Returns the resource or <tt>null</tt> if the resource hasn't been set.
     *
     * @return the resource.
     */
    public String getResource() {
        return queryEl.elementText("resource");
    }

    /**
     * Sets the resource.
     *
     * @param resource
     *            the resource.
     */
    public void setResource(String resource) {
        PacketParserUtils.updateText(queryEl, "resource", resource);
    }
}


package org.intellimate.izou.sdk.frameworks.music.player.template;

import org.intellimate.izou.events.EventModel;
import org.intellimate.izou.identification.IdentificationManagerM;
import org.intellimate.izou.resource.ResourceModel;
import org.intellimate.izou.sdk.Context;
import org.intellimate.izou.sdk.frameworks.music.Capabilities;
import org.intellimate.izou.sdk.frameworks.music.player.*;
import org.intellimate.izou.sdk.frameworks.music.resources.BroadcasterAvailablePlaylists;
import org.intellimate.izou.sdk.frameworks.music.resources.BroadcasterPlaylist;
import org.intellimate.izou.sdk.util.AddOnModule;

import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Optional;

/**
 * this class is responsible for communication information on request.
 * Essentially it is responsible for answering the the resource-requests.
 * The code for answering lies in the Interface (it is implemented this way to allow better composition if you don't want
 * to use the templates)
 * @author LeanderK
 * @version 1.0
 */
public class InformationProvider extends AddOnModule implements MusicResourceGenerator {
    private final MusicProvider musicProvider;
    private final CommandHandler commandHandler;

    /**
     * initializes the Module
     *
     * @param context the current Context
     * @param musicProvider the musicProvider to get the data from
     * @param ID      the ID
     * @param commandHandler the commandHandler
     */
    public InformationProvider(Context context, String ID, MusicProvider musicProvider, CommandHandler commandHandler) {
        super(context, ID+".InformationProvider");
        this.musicProvider = musicProvider;
        this.commandHandler = commandHandler;
    }

    @Override
    public List<? extends ResourceModel> announceResources() {
        List<ResourceModel> list = new ArrayList<>();
        IdentificationManagerM.getInstance().getIdentification(this)
                .ifPresent(id -> {
                    list.add(new BroadcasterAvailablePlaylists(id));
                    list.add(new BroadcasterPlaylist(id));
                });
        list.addAll(MusicResourceGenerator.super.announceResources());
        return list;
    }

    /**
     * generates the resources
     *
     * @param resourceModel the resourceModel
     * @param event         the Event
     * @return a Stream containing the resourceModel(s)
     */
    @Override
    public Optional<? extends ResourceModel> generateResource(ResourceModel resourceModel, Optional<EventModel> event) {
        switch (resourceModel.getResourceID()) {
            case BroadcasterAvailablePlaylists.RESOURCE_ID: return createBroadcasterAvailablePlaylists();
            case BroadcasterPlaylist.RESOURCE_ID : return createBroadcasterPlaylist(resourceModel);
            default: return MusicResourceGenerator.super.generateResource(resourceModel, event);
        }
    }

    private Optional<? extends ResourceModel> createBroadcasterPlaylist(ResourceModel resourceModel) {
        if (!getCapabilities().isBroadcasting()) {
            getContext().getLogger().error(PROVIDE_RESOURCE_ERROR_NOT_CAPABLE + "BroadcasterPlaylist");
            return Optional.empty();
        } else {
            Object resource = resourceModel.getResource();
            try {
                //noinspection unchecked
                HashMap<String, Object> hashMap = (HashMap<String, Object>) resource;
                if (hashMap.get(BroadcasterPlaylist.RESOURCE_ID) == null) {
                    getContext().getLogger().error("playlist name not specified");
                    return Optional.empty();
                }
                Playlist playlist = commandHandler.getPlaylistFromName((String) hashMap.get(BroadcasterPlaylist.RESOURCE_ID));
                Optional<BroadcasterPlaylist> BroadcasterPlaylistResource = IdentificationManagerM.getInstance()
                        .getIdentification(this)
                        .map(id -> BroadcasterPlaylist.createPlaylistAnswer(id, playlist));
                if (!BroadcasterPlaylistResource.isPresent()) {
                    getContext().getLogger().error(PROVIDE_RESOURCE_ERROR_GENERATING + "BroadcasterPlaylist");
                }
                return BroadcasterPlaylistResource;
            } catch (ClassCastException e) {
                getContext().getLogger().error("got wrong resource-data");
                return Optional.empty();
            }
        }
    }

    private Optional<? extends ResourceModel> createBroadcasterAvailablePlaylists() {
        if (!getCapabilities().isBroadcasting()) {
            getContext().getLogger().error(PROVIDE_RESOURCE_ERROR_NOT_CAPABLE + "BroadcasterAvailablePlaylists");
            return Optional.empty();
        } else {
            List<String> playlists = commandHandler.getAvailablePlaylists();
            Optional<BroadcasterAvailablePlaylists> BroadcasterPlaylistResource = IdentificationManagerM.getInstance()
                    .getIdentification(this)
                    .map(id -> new BroadcasterAvailablePlaylists(id, playlists));
            if (!BroadcasterPlaylistResource.isPresent()) {
                getContext().getLogger().error(PROVIDE_RESOURCE_ERROR_GENERATING + "BroadcasterAvailablePlaylists");
            }
            return BroadcasterPlaylistResource;
        }
    }

    /**
     * gets the current Playlist
     *
     * @return the current Playlist
     */
    @Override
    public Playlist getCurrentPlaylist() {
        return musicProvider.getCurrentPlaylist();
    }

    /**
     * gets the Volume
     *
     * @return the volume
     */
    @Override
    public Volume getVolume() {
        return musicProvider.getVolume();
    }

    /**
     * gets the Progress
     *
     * @return the Progress
     */
    @Override
    public Progress getCurrentProgress() {
        return musicProvider.getCurrentProgress();
    }

    /**
     * gets the Capabilities of the Player
     *
     * @return and instance of Capabilities
     */
    @Override
    public Capabilities getCapabilities() {
        return musicProvider.getCapabilities();
    }

    /**
     * gets the PlaybackState of the Player
     *
     * @return the PlaybackState
     */
    @Override
    public PlaybackState getPlaybackState() {
        return musicProvider.getPlaybackState();
    }

    /**
     * true if playing and false if not
     *
     * @return tre if playing
     */
    @Override
    public boolean isOutputRunning() {
        return musicProvider.isOutputRunning();
    }

    /**
     * true if playing and false if not
     *
     * @return tre if playing
     */
    @Override
    public boolean isPlaying() {
        return musicProvider.isPlaying();
    }

    /**
     * true if using java, false if not (and for example a C-library)
     *
     * @return true if using java
     */
    @Override
    public boolean isUsingJava() {
        return musicProvider.isUsingJava();
    }
}


// GenericsNote: Converted.
/*
 *  Copyright 2003-2004 The Apache Software Foundation
 *
 *  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 org.jivesoftware.smack.util.collections;

import java.util.Map;

/**
 * Abstract Pair class to assist with creating correct Map Entry
 * implementations.
 *
 * @author James Strachan
 * @author Michael A. Smith
 * @author Neil O'Toole
 * @author Matt Hall, John Watkinson, Stephen Colebourne
 * @version $Revision: 1.1 $ $Date: 2005/10/11 17:05:32 $
 * @since Commons Collections 3.0
 */
public abstract class AbstractMapEntry<K, V> extends AbstractKeyValue<K, V>
        implements Map.Entry<K, V> {

    /**
     * Constructs a new entry with the given key and given value.
     *
     * @param key
     *            the key for the entry, may be null
     * @param value
     *            the value for the entry, may be null
     */
    protected AbstractMapEntry(K key, V value) {
        super(key, value);
    }

    // Map.Entry interface
    // -------------------------------------------------------------------------
    /**
     * Sets the value stored in this Map Entry.
     *

     * This Map Entry is not connected to a Map, so only the local data is
     * changed.
     *
     * @param value
     *            the new value
     * @return the previous value
     */
    public V setValue(V value) {
        V answer = this.value;
        this.value = value;
        return answer;
    }

    /**
     * Compares this Map Entry with another Map Entry.
     *

     * Implemented per API documentation of
     * {@link java.util.Map.Entry#equals(Object)}
     *
     * @param obj
     *            the object to compare to
     * @return true if equal key and value
     */
    public boolean equals(Object obj) {
        if (obj == this) {
            return true;
        }
        if (obj instanceof Map.Entry == false) {
            return false;
        }
        Map.Entry other = (Map.Entry) obj;
        return (getKey() == null ? other.getKey() == null : getKey().equals(
                other.getKey()))
                && (getValue() == null ? other.getValue() == null : getValue()
                        .equals(other.getValue()));
    }

    /**
     * Gets a hashCode compatible with the equals method.
     *

     * Implemented per API documentation of
     * {@link java.util.Map.Entry#hashCode()}
     *
     * @return a suitable hash code
     */
    public int hashCode() {
        return (getKey() == null ? 0 : getKey().hashCode())
                ^ (getValue() == null ? 0 : getValue().hashCode());
    }

}


/*
 * Copyright 2010 Workplace Systems PLC (http://www.workplacesystems.com/).
 *
 * 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 com.workplacesystems.utilsj.threadpool;

import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.apache.commons.pool.impl.GenericObjectPool;

/**
 */
public class ThreadPool extends GenericObjectPool {
   /**
    * Logger for this class
    */
   private final static Log log = LogFactory.getLog(ThreadPool.class);

   /**
    * Constructor when there is no configuration available. Please refer
    * commons-pooling for more details on the configuration parameters.
    * @param objFactory - The factory to be used for thread creation.
    */
   public ThreadPool(ThreadObjectFactory objFactory) {
      super(objFactory);
      this.setMaxIdle(2); // maximum idle threads
      this.setMaxActive(4); // maximum active threads.
      this.setMinEvictableIdleTimeMillis(30000); //Evictor runs every 30 secs.
      this.setTestOnBorrow(true); // check if the thread is still valid
      //this.setMaxWait(1000); // 1 second wait when threads are not available.
   }

   /**
    * Constructor to be used when there is a configuration available.
    * @param objFactory -
    *           The factory to be used for thread creation..
    * @param config -
    *           This could be created by loading a properties file.
    */
   public ThreadPool(ThreadObjectFactory objFactory,
         GenericObjectPool.Config config) {
      super(objFactory, config);
   }

   /* (non-Javadoc)
    * @see org.apache.commons.pool.ObjectPool#borrowObject()
    */
   @Override
public Object borrowObject() throws Exception {
      log.debug(" borrowing object..");
      WorkerThread thread = (WorkerThread)super.borrowObject();
      thread.setPool(this);
      return thread;
   }

   /* (non-Javadoc)
    * @see org.apache.commons.pool.ObjectPool#returnObject(java.lang.Object)
    */
   @Override
public void returnObject(Object obj) throws Exception {
      log.debug(" returning object.." + obj);
      super.returnObject(obj);
   }
}


package org.intellimate.izou.sdk.frameworks.presence.events;

import org.intellimate.izou.identification.Identification;
import org.intellimate.izou.sdk.events.CommonEvents;
import org.intellimate.izou.sdk.events.Event;
import org.intellimate.izou.sdk.frameworks.presence.resources.LastEncountered;

import java.util.List;
import java.util.Optional;

/**
 * this event indicates presence
 * @author LeanderK
 * @version 1.0
 */
public class PresenceEvent extends Event {
    /**
     * this event does not mean the user is able to notice anything (can be used for warm-up), it indicates
     * he might be
     */
    public static final String GENERAL_DESCRIPTOR = "izou.presence.general";
    /**
     * it means that the addon can guarantee that the user entered an area near izou
     */
    public static final String STRICT_DESCRIPTOR = "izou.presence.strict";
    /**
     * it means that someone entered an area near izou, but it is not clear if it is the user,
     */
    public static final String UNKNOWN_DESCRIPTOR = "izou.presence.unkownuser";
    /**
     * it means that the user entered an area near izou
     */
    public static final String KNOWN_DESCRIPTOR = "izou.presence.knownuser";
    /**
     * Means that the Addon encountered the user the first time IN THE SPECIFIED MODE (Strict/General) since he left.
     */
    public static final String FIRST_ENCOUNTER_DESCRIPTOR = "izou.presence.firstencounter";
    /**
     * it means that the addon can guarantee that the user entered an area near izou
     */
    public static final String ID = "izou.presence";

    /**
     * Creates a new Event Object
     *
     * @param source      the source of the Event, most likely a this reference.
     * @param descriptors the descriptors to initialize the Event with
     * @throws IllegalArgumentException if one of the Arguments is null or empty
     */
    protected PresenceEvent(Identification source, List<String> descriptors) throws IllegalArgumentException {
        super(CommonEvents.Type.RESPONSE_TYPE, source, descriptors);
    }

    /**
     * creates a new PresenceEvent
     * @param source the caller
     * @param strict whether the addon can guarantee that the user is around
     * @param known true if the addon knows if the person encountered is the (main) user
     * @param firstEncounter if the user was first encountered in the specified mode
     * @param descriptors the descriptors
     * @return the optional PresenceEvent
     */
    public static Optional<PresenceEvent> createPresenceEvent(Identification source, boolean strict, boolean known,
                                                                    boolean firstEncounter, List<String> descriptors) {
        try {
            if (strict) {
                descriptors.add(STRICT_DESCRIPTOR);
            } else {
                descriptors.add(GENERAL_DESCRIPTOR);
            }
            if (known) {
                descriptors.add(KNOWN_DESCRIPTOR);
            } else {
                descriptors.add(UNKNOWN_DESCRIPTOR);
            }
            if (firstEncounter)
                descriptors.add(FIRST_ENCOUNTER_DESCRIPTOR);
            descriptors.add(ID);
            PresenceEvent stopRequest = new PresenceEvent(source, descriptors);
            return Optional.of(stopRequest);
        } catch (IllegalArgumentException e) {
            return Optional.empty();
        }
    }

    /**
     * creates a new PresenceEvent
     * @param source the caller
     * @param strict whether the addon can guarantee that the user is around
     * @param known true if the addon knows if the person encountered is the (main) user
     * @param firstEncounter if the user was first encountered in the specified mode
     * @param descriptors the descriptors
     * @param timePassed the time passed in seconds since the user was last encountered
     * @return the optional PresenceEvent
     */
    public static Optional<PresenceEvent> createPresenceEvent(Identification source, boolean strict, boolean known,
                                                              boolean firstEncounter, List<String> descriptors, Long timePassed) {
        try {
            if (strict) {
                descriptors.add(STRICT_DESCRIPTOR);
            } else {
                descriptors.add(GENERAL_DESCRIPTOR);
            }
            if (known) {
                descriptors.add(KNOWN_DESCRIPTOR);
            } else {
                descriptors.add(UNKNOWN_DESCRIPTOR);
            }
            if (firstEncounter)
                descriptors.add(FIRST_ENCOUNTER_DESCRIPTOR);
            descriptors.add(ID);
            PresenceEvent presenceEvent = new PresenceEvent(source, descriptors);
            presenceEvent.addResource(new LastEncountered(source, timePassed));
            return Optional.of(presenceEvent);
        } catch (IllegalArgumentException e) {
            return Optional.empty();
        }
    }
}


/*
 * Copyright 2010 Workplace Systems PLC (http://www.workplacesystems.com/).
 *
 * 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 com.workplacesystems.utilsj.collections.decorators;

import java.util.Collection;
import java.util.Map;
import java.util.Set;
import com.workplacesystems.utilsj.Callback;
import com.workplacesystems.utilsj.collections.SyncUtils;

/**
 *
 * @author  Dave
 */
public class SynchronizedMap<K,V> implements Map<K,V>, SynchronizedDecorator {
    /** The object to lock on, needed for List/SortedSet views */
    protected final Object lock;

    protected final Map<K,V> map;

    /**
     * Factory method to create a synchronized map.
     *
     * @param map  the map to decorate, must not be null
     * @throws IllegalArgumentException if map is null
     */
    public static <K,V> Map<K,V> decorate(Map<K,V> map) {
        return new SynchronizedMap<K,V>(map);
    }

    /**
     * Constructor that wraps (not copies).
     *
     * @param map  the map to decorate, must not be null
     * @throws IllegalArgumentException if map is null
     */
    protected SynchronizedMap(Map<K,V> map) {
        if (map == null) {
            throw new IllegalArgumentException("Map must not be null");
        }

        this.map = map;
        this.lock = SyncUtils.createMutex(map);
    }

    /**
     * Constructor that wraps (not copies).
     *
     * @param map  the map to decorate, must not be null
     * @param lock  the lock object to use, must not be null
     * @throws IllegalArgumentException if map is null
     */
    protected SynchronizedMap(Map<K,V> map, Object lock) {
        if (map == null) {
            throw new IllegalArgumentException("Map must not be null");
        }

        this.map = map;
        this.lock = SyncUtils.createMutex(lock);
    }

    public Object getLockObject() {
        return lock;
    }

    //-----------------------------------------------------------------------
    public void clear() {
        SyncUtils.synchronizeWrite(lock, new Callback<Object>() {
            @Override
            protected void doAction() {
                map.clear();
            }
        });
    }

    public boolean containsKey(final Object key) {
        Boolean ret = SyncUtils.synchronizeRead(lock, new Callback<Boolean>() {
            @Override
            protected void doAction() {
                _return(map.containsKey(key) ? Boolean.TRUE : Boolean.FALSE);
            }
        });
        return ret.booleanValue();
    }

    public boolean containsValue(final Object value) {
        Boolean ret = SyncUtils.synchronizeRead(lock, new Callback<Boolean>() {
            @Override
            protected void doAction() {
                _return(map.containsValue(value) ? Boolean.TRUE : Boolean.FALSE);
            }
        });
        return ret.booleanValue();
    }

    public Set<Map.Entry<K,V>> entrySet() {
        return SyncUtils.synchronizeRead(lock, new Callback<Set<Map.Entry<K,V>>>() {
            @Override
            protected void doAction() {
                Set<Map.Entry<K,V>> _set = map.entrySet();
                _return(new SynchronizedSet<Map.Entry<K,V>>(_set, lock));
            }
        });
    }

    public V get(final Object key) {
        return SyncUtils.synchronizeRead(lock, new Callback<V>() {
            @Override
            protected void doAction() {
                _return(map.get(key));
            }
        });
    }

    public boolean isEmpty() {
        Boolean ret = SyncUtils.synchronizeRead(lock, new Callback<Boolean>() {
            @Override
            protected void doAction() {
                _return(map.isEmpty() ? Boolean.TRUE : Boolean.FALSE);
            }
        });
        return ret.booleanValue();
    }

    public Set<K> keySet() {
        return SyncUtils.synchronizeRead(lock, new Callback<Set<K>>() {
            @Override
            protected void doAction() {
                Set<K> _set = map.keySet();
                _return(new SynchronizedSet<K>(_set, lock));
            }
        });
    }

    public V put(final K key, final V value) {
        return SyncUtils.synchronizeWrite(lock, new Callback<V>() {
            @Override
            protected void doAction() {
                _return(map.put(key, value));
            }
        });
    }

    public void putAll(final Map<? extends K,? extends V> t) {
        SyncUtils.synchronizeWrite(lock, new Callback<Object>() {
            @Override
            protected void doAction() {
                map.putAll(t);
            }
        });
    }

    public V remove(final Object key) {
        return SyncUtils.synchronizeWrite(lock, new Callback<V>() {
            @Override
            protected void doAction() {
                _return(map.remove(key));
            }
        });
    }

    public int size() {
        Integer ret = SyncUtils.synchronizeRead(lock, new Callback<Integer>() {
            @Override
            protected void doAction() {
                _return(new Integer(map.size()));
            }
        });
        return ret.intValue();
    }

    public Collection<V> values() {
        return SyncUtils.synchronizeRead(lock, new Callback<Collection<V>>() {
            @Override
            protected void doAction() {
                Collection<V> _col = map.values();
                _return(new SynchronizedCollection<V>(_col, lock));
            }
        });
    }
}


package org.intellimate.izou.sdk.frameworks.music.user;

import org.intellimate.izou.identification.Identifiable;
import org.intellimate.izou.identification.Identification;
import org.intellimate.izou.identification.IdentificationManagerM;
import org.intellimate.izou.resource.ResourceModel;
import org.intellimate.izou.sdk.Context;
import org.intellimate.izou.sdk.frameworks.music.Capabilities;
import org.intellimate.izou.sdk.frameworks.music.player.Playlist;
import org.intellimate.izou.sdk.frameworks.music.player.TrackInfo;
import org.intellimate.izou.sdk.frameworks.music.player.Volume;
import org.intellimate.izou.sdk.frameworks.music.resources.*;
import org.intellimate.izou.sdk.util.AddOnModule;

import java.util.ArrayList;
import java.util.List;
import java.util.Optional;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.TimeoutException;

/**
 * this is a simple class which should, added as a resource to an Event, request the Player to play the selected
 * track or playlist.
 *

 * To use this class the player has to meet a few criteria:

 *

 *
the player must exist and be support the standard defined through the sdk

 *
the players-capabilities must allow requests from outside

 *
(maybe, depends on the create-method) the players-capabilities must allow a requests with specified a specified playlist/trackInfo

 *

 * an example:

 *

 * {@code
 *
 * Identification playerID;
 * Playlist playlist;
 * List<ResourceModel> resources = PlayerRequest.createPlayerRequest(playlist, player, this) //create a new PlayerRequest
 *          .map(PlayerRequest::resourcesForExisting) //creates the resources needed to add to the event
 *          .orElseGet(ArrayList::new);
 * }
 *

 * @author LeanderK
 * @version 1.0
 */
public class PlayerRequest {
    private final TrackInfo trackInfo;
    private final Playlist playlist;
    private final boolean permanent;
    private final Identification player;
    private final Capabilities capabilities;
    private final Context context;
    private final Identifiable identifiable;
    private Volume volume = null;

    /**
     * internal Constructor
     * @param trackInfo the trackInfo
     * @param playlist the playlist
     * @param permanent whether the Request is permanent
     * @param player the player
     * @param capabilities the capabilities
     * @param context the context
     * @param identifiable the identifiable
     */
    protected PlayerRequest(TrackInfo trackInfo, Playlist playlist, boolean permanent, Identification player,
                            Capabilities capabilities, Context context, Identifiable identifiable) {
        this.trackInfo = trackInfo;
        this.playlist = playlist;
        this.permanent = permanent;
        this.player = player;
        this.capabilities = capabilities;
        this.context = context;
        this.identifiable = identifiable;
    }

    /**
     * internal Constructor
     * @param trackInfo the trackInfo
     * @param playlist the playlist
     * @param permanent whether the Request is permanent
     * @param player the player
     * @param capabilities the capabilities
     * @param context the Context
     * @param identifiable the identifiable
     * @param volume the Volume to set to
     */
    protected PlayerRequest(TrackInfo trackInfo, Playlist playlist, boolean permanent, Identification player,
                            Capabilities capabilities, Context context, Identifiable identifiable, Volume volume) {
        this.trackInfo = trackInfo;
        this.playlist = playlist;
        this.permanent = permanent;
        this.player = player;
        this.capabilities = capabilities;
        this.context = context;
        this.identifiable = identifiable;
        this.volume = volume;
    }

    /**
     * sets the Volume if the Player supports it.
     * @param volume the Volume to set to
     * @return true if the Player supports setting the Volume
     */
    public boolean setVolume(Volume volume) {
        if (capabilities.canChangeVolume()) {
            this.volume = volume;
            return true;
        }
        return false;
    }

    /**
     * tries to set the Volume of the PlayerRequest.
     *

     * if the Player supports the Change of the Volume, it will create a new PlayerRequest and return it, if not it
     * returns this.
     *

     * @param volume the Volume to set to
     * @return a new PlayerRequest or this.
     */
    public PlayerRequest trySetVolume(Volume volume) {
        if (capabilities.canChangeVolume()) {
            return new PlayerRequest(trackInfo, playlist, permanent, player, capabilities, context, identifiable, volume);
        }
        return this;
    }

    /**
     * returns a List of Resources that can be added to an already existing event.
     *

     * This causes the Addon to block the Event in the OutputPlugin lifecycle of the Event.
     * @return a List of Resources
     */
    @SuppressWarnings("unused")
    public List<ResourceModel> resourcesForExisting() {
        List<ResourceModel> resourceModels = new ArrayList<>();
        IdentificationManagerM.getInstance().getIdentification(identifiable)
                .map(id -> new MusicUsageResource(id, true))
                .ifPresent(resourceModels::add);
        if (volume != null) {
            IdentificationManagerM.getInstance().getIdentification(identifiable)
                    .map(id -> new VolumeResource(id, volume))
                    .ifPresent(resourceModels::add);
        }
        if (playlist != null) {
            IdentificationManagerM.getInstance().getIdentification(identifiable)
                    .map(id -> new PlaylistResource(id, playlist))
                    .ifPresent(resourceModels::add);
        }
        if (trackInfo != null) {
            IdentificationManagerM.getInstance().getIdentification(identifiable)
                    .map(id -> new TrackInfoResource(id, trackInfo))
                    .ifPresent(resourceModels::add);
        }
        return resourceModels;
    }

    /**
     * helper method for PlaylistSelector
     * @param playlist the playlist selected
     * @param permanent the permanent addon
     * @param player the player
     * @param capabilities the capabilities
     * @param context the context
     * @param identifiable the identifiable
     * @return a PlayerRequest
     */
    static PlayerRequest createPlayerRequest(Playlist playlist, boolean permanent, Identification player, Capabilities capabilities, Context context, Identifiable identifiable) {
        return  new PlayerRequest(null, playlist, permanent, player, capabilities, context, identifiable);
    }

    /**
     * creates a new PlayerRequest.
     *

     * For this method to return a non-empty Optional the following criteria must be met:

     *

     *
the player must exist and be support the standard defined through the sdk

     *
the players-capabilities must allow requests from outside

     *

     * @param permanent true means the player can play indefinitely, but only if no one is currently using audio as
     *                  permanent. It will also not block. false is limited to 10 minutes playback, but will block.
     * @param player the player to target
     * @param source the addOnModule used for Context etc.
     * @return the optional PlayerRequest
     */
    @SuppressWarnings("unused")
    public static Optional<PlayerRequest> createPlayerRequest(boolean permanent, Identification player, AddOnModule source) {
        if (player == null || source == null)
            return Optional.empty();
        try {
            return source.getContext().getResources()
                    .generateResource(new CapabilitiesResource(player))
                    .orElse(CompletableFuture.completedFuture(new ArrayList<>()))
                    .thenApply(list -> list.stream()
                                    .filter(resourceModel -> resourceModel.getProvider().equals(player))
                                    .findAny()
                                    .flatMap(resource -> Capabilities.importFromResource(resource, source.getContext()))
                    ).get(1, TimeUnit.SECONDS)
                    .filter(capabilities -> {
                        if (!capabilities.handlesPlayRequestFromOutside()) {
                            source.getContext().getLogger().error("player does not handle play-request from outside");
                            return false;
                        }
                        return true;
                    })
                    .map(capabilities -> new PlayerRequest(null, null, permanent, player, capabilities, source.getContext(), source));
        } catch (InterruptedException | ExecutionException | TimeoutException e) {
            source.getContext().getLogger().error("unable to obtain capabilities");
            return Optional.empty();
        }
    }

    /**
     * creates a new PlayerRequest.
     *

     * the resulting PlayerRequest is not permanent, which means that it will mute all other sound but is limited to
     * 10 minutes.

     * For this method to return a non-empty Optional the following criteria must be met:

     *

     *
the player must exist and be support the standard defined through the sdk

     *
the players-capabilities must allow requests from outside

     *
the players-capabilities must allow a requests with specified a specified playlist/trackInfo

     *

     * @param trackInfo the trackInfo to pass with the request
     * @param player the player to target
     * @param source the addOnModule used for Context etc.
     * @return the optional PlayerRequest
     */
    @SuppressWarnings("unused")
    public static Optional<PlayerRequest> createPlayerRequest(TrackInfo trackInfo, Identification player, AddOnModule source) {
        return createPlayerRequest(trackInfo, false, player, source);
    }

    /**
     * creates a new PlayerRequest.
     *

     * For this method to return a non-empty Optional the following criteria must be met:

     *

     *
the player must exist and be support the standard defined through the sdk

     *
the players-capabilities must allow requests from outside

     *
the players-capabilities must allow a requests with specified a specified playlist/trackInfo

     *

     * @param trackInfo the trackInfo to pass with the request
     * @param permanent true means the player can play indefinitely, but only if no one is currently using audio as
     *                  permanent. It will also not block. false is limited to 10 minutes playback, but will block.
     * @param player the player to target
     * @param source the addOnModule used for Context etc.
     * @return the optional PlayerRequest
     */
    @SuppressWarnings("unused")
    public static Optional<PlayerRequest> createPlayerRequest(TrackInfo trackInfo, boolean permanent, Identification player, AddOnModule source) {
        if (trackInfo == null ||player == null || source == null)
            return Optional.empty();
        try {
            return source.getContext().getResources()
                    .generateResource(new CapabilitiesResource(player))
                    .orElse(CompletableFuture.completedFuture(new ArrayList<>()))
                    .thenApply(list -> list.stream()
                                    .filter(resourceModel -> resourceModel.getProvider().equals(player))
                                    .findAny()
                                    .flatMap(resource -> Capabilities.importFromResource(resource, source.getContext()))
                    ).get(1, TimeUnit.SECONDS)
                    .filter(capabilities -> {
                        if (!capabilities.handlesPlayRequestFromOutside()) {
                            source.getContext().getLogger().error("player does not handle play-request from outside");
                            return false;
                        }
                        if (!capabilities.hasPlayRequestDetailed()) {
                            source.getContext().getLogger().error("player does not handle trackInfo-request from outside");
                            return false;
                        }
                        return true;
                    })
                    .map(capabilities -> new PlayerRequest(trackInfo, null, permanent, player, capabilities, source.getContext(), source));
        } catch (InterruptedException | ExecutionException | TimeoutException e) {
            source.getContext().getLogger().error("unable to obtain capabilities");
            return Optional.empty();
        }
    }

    /**
     * creates a new PlayerRequest.
     *

     * the resulting PlayerRequest is not permanent, which means that it will mute all other sound but is limited to
     * 10 minutes.

     * For this method to return a non-empty Optional the following criteria must be met:

     *

     *
the player must exist and be support the standard defined through the sdk

     *
the players-capabilities must allow requests from outside

     *
the players-capabilities must allow a requests with specified a specified playlist/trackInfo

     *

     * @param playlist the playlist to pass with the request
     * @param player the player to target
     * @param source the addOnModule used for Context etc.
     * @return the optional PlayerRequest
     */
    @SuppressWarnings("unused")
    public static Optional<PlayerRequest> createPlayerRequest(Playlist playlist, Identification player, AddOnModule source) {
        return createPlayerRequest(playlist, false, player, source);
    }

    /**
     * creates a new PlayerRequest.
     *

     * For this method to return a non-empty Optional the following criteria must be met:

     *

     *
the player must exist and be support the standard defined through the sdk

     *
the players-capabilities must allow requests from outside

     *
the players-capabilities must allow a requests with specified a specified playlist/trackInfo

     *

     * @param playlist the playlist to pass with the request
     * @param permanent true means the player can play indefinitely, but only if no one is currently using audio as
     *                  permanent. It will also not block. false is limited to 10 minutes playback, but will block.
     * @param player the player to target
     * @param source the addOnModule used for Context etc.
     * @return the optional PlayerRequest
     */
    @SuppressWarnings("unused")
    public static Optional<PlayerRequest> createPlayerRequest(Playlist playlist, boolean permanent, Identification player, AddOnModule source) {
        if (playlist == null ||player == null || source == null)
            return Optional.empty();
        try {
            return source.getContext().getResources()
                    .generateResource(new CapabilitiesResource(player))
                    .orElse(CompletableFuture.completedFuture(new ArrayList<>()))
                    .thenApply(list -> list.stream()
                                    .filter(resourceModel -> resourceModel.getProvider().equals(player))
                                    .findAny()
                                    .flatMap(resource -> Capabilities.importFromResource(resource, source.getContext()))
                    ).get(1, TimeUnit.SECONDS)
                    .filter(capabilities -> {
                        if (!capabilities.handlesPlayRequestFromOutside()) {
                            source.getContext().getLogger().error("player does not handle play-request from outside");
                            return false;
                        }
                        if (!capabilities.hasPlayRequestDetailed()) {
                            source.getContext().getLogger().error("player does not handle playlist-request from outside");
                            return false;
                        }
                        if (!playlist.verify(capabilities)) {
                            source.getContext().getLogger().error("player can not handle the playlist, probably illegal PlaybackModes");
                            return false;
                        }
                        return true;
                    })
                    .map(capabilities -> new PlayerRequest(null, playlist, permanent, player, capabilities, source.getContext(), source));
        } catch (InterruptedException | ExecutionException | TimeoutException e) {
            source.getContext().getLogger().debug("unable to obtain capabilities");
            return Optional.empty();
        }
    }
}


package org.intellimate.izou.sdk.frameworks.presence.resources;

import org.intellimate.izou.events.EventModel;
import org.intellimate.izou.identification.Identification;
import org.intellimate.izou.resource.ResourceModel;
import org.intellimate.izou.sdk.resource.Resource;

import java.util.Optional;

/**
 * this Resource holds an integer containing the Seconds passed since the User was last encountered
 * @author LeanderK
 * @version 1.0
 */
public class LastEncountered extends Resource<Long> {
    public static final String ID = "izou.presence.resources.lastencountered";

    /**
     * creates a new Resource.
     *
     * @param provider   the Provider of the Resource
     * @param timePassed the time passed in seconds
     */
    public LastEncountered(Identification provider, Long timePassed) {
        super(ID, provider, timePassed);
    }

    /**
     * returns the time passed if available
     * @param eventModel the event
     * @return the optional long
     */
    @SuppressWarnings("unused")
    public static Optional<Long> getTimePassed(EventModel eventModel) {
        if (eventModel.getListResourceContainer().containsResourcesFromSource(ID)) {
            return eventModel
                    .getListResourceContainer()
                    .provideResource(ID)
                    .stream()
                    .map(ResourceModel::getResource)
                    .filter(ob -> ob instanceof Long)
                    .map(ob -> (Long) ob)
                    .findAny();
        } else {
            return Optional.empty();
        }
    }
}