/*
 * Copyright (C) 2010 Teleal GmbH, Switzerland
 *
 * This program is free software: you can redistribute it and/or modify
 * it under the terms of the GNU Affero General Public License as
 * published by the Free Software Foundation, either version 3 of the
 * License, or (at your option) any later version.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU Affero General Public License for more details.
 *
 * You should have received a copy of the GNU Affero General Public License
 * along with this program.  If not, see <http://www.gnu.org/licenses/>.
 */

package org.teleal.cling.registry;

import org.teleal.cling.model.Resource;
import org.teleal.cling.model.gena.CancelReason;
import org.teleal.cling.model.gena.RemoteGENASubscription;
import org.teleal.cling.model.meta.Device;
import org.teleal.cling.model.meta.Phase;
import org.teleal.cling.model.meta.RemoteDevice;
import org.teleal.cling.model.types.UDN;

import java.util.Collection;
import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Iterator;
import java.util.Map;
import java.util.Set;
import java.util.logging.Level;
import java.util.logging.Logger;


class RemoteItems extends RegistryItems<RemoteDevice, RemoteGENASubscription> {

    private static Logger log = Logger.getLogger(RemoteItems.class.getName());

    RemoteItems(RegistryImpl registry) {
        super(registry);
    }

    /**
     * Adds the given remote device to the registry, or updates the timestamp of an already registered remote device
     * with the same UDN.
     *
     * @param device The device to be registered.
     * @return The given device if it has been registered as new, or the already registered device with the same UDN. A
     *         <tt>null</tt> is returned if a device with the same UDN was already registered but has not been fully
     *         described (probably still waiting for the descriptor retrieval background operation to complete). Also
     *         returns <tt>null</tt> if the remote device has the same UDN as an already registered local device.
     */
    RemoteDevice add(RemoteDevice device) {

        RemoteDevice registeredRemoteDevice = get(device.getIdentity().getUdn());

        if (registeredRemoteDevice != null && registeredRemoteDevice.getPhase().equals(Phase.HYDRATED)) {

            RegistryItem item = new RegistryItem(
                    registeredRemoteDevice.getIdentity().getUdn(),
                    registeredRemoteDevice,
                    device.getIdentity().getMaxAgeSeconds()
            );

            log.fine("Updating expiration timestamp of hydrated remote device: " + registeredRemoteDevice);
            deviceItems.remove(item);
            deviceItems.add(item);

            return registeredRemoteDevice;

        } else if (registeredRemoteDevice != null && registeredRemoteDevice.getPhase().equals(Phase.DISCOVERED)) {

            log.fine("Ignoring registered but undescribed remote device: " + registeredRemoteDevice);
            return null;

        } else if (registry.getDevice(device.getIdentity().getUdn()) != null) {

                log.fine("Ignoring remote device, local or remote device with same UDN already registered");
                return null;

        } else {

            RegistryItem item = new RegistryItem(
                    device.getIdentity().getUdn(),
                    device,
                    device.getIdentity().getMaxAgeSeconds()
            );

            log.fine("Adding remote device to registry with " + item.getMaxAgeSeconds() + " seconds expiration: " + device);
            deviceItems.add(item);

            return device;

        }
    }

    /**
     * Replaces the existing device with the given, notifies any listeners that a fully usable device is now available.
     *
     * @param remoteDevice The device graph with fully initialized state (after descriptor(s) have been retrieved and applied).
     */
    void hydrate(final RemoteDevice remoteDevice) {

        if (!remoteDevice.getPhase().equals(Phase.HYDRATED)) {
            log.warning("Can't register unhydrated remote device: " + remoteDevice.getIdentity());
        }

        RemoteDevice registeredDevice = get(remoteDevice.getIdentity().getUdn());
        if (registeredDevice != null) {

            log.fine("Describing remote device in registry: " + remoteDevice);

            RegistryItem item = new RegistryItem<UDN, RemoteDevice>(
                    remoteDevice.getIdentity().getUdn(),
                    remoteDevice,
                    registeredDevice.getIdentity().getMaxAgeSeconds()
            );

            deviceItems.remove(item);
            deviceItems.add(item);

            if (log.isLoggable(Level.FINER)) {
                StringBuilder sb = new StringBuilder();
                sb.append("\n");
                sb.append("-------------------------- START Registry Namespace -----------------------------------\n");
                for (Resource resource : registry.getResources()) {
                    sb.append(resource).append("\n");
                }
                sb.append("-------------------------- END Registry Namespace -----------------------------------");
                log.finer(sb.toString());
            }

            for (Resource deviceResource : remoteDevice.getResources()) {
                log.finer("Trying to register remote device resource; " + deviceResource);

                if (registry.getResource(deviceResource.getLocalPathQuery()) != null) {
                    throw new RegistrationException("URI namespace conflict with already registered resource: " + deviceResource);
                }

                registry.addResource(deviceResource);
                log.fine("Registered resource: " + deviceResource);

            }

            log.fine("Completely hydrated remote device graph available, calling listeners: " + remoteDevice);

            // Only notify the listeners when the device is fully usable
            for (final RegistryListener listener : registry.getListeners()) {
                registry.getConfiguration().getRegistryListenerExecutor().execute(
                        new Runnable() {
                            public void run() {
                                listener.remoteDeviceAdded(registry, remoteDevice);
                            }
                        }
                );
            }

        } else {
            log.warning("Remote device was removed from registry before it could be described");
        }
    }

    /**
     * @param hydratedOnly Set to <tt>true</tt> if only described and fully usable devices should be returned.
     * @return All fully described and usable registered remote devices, or even undescribed devices.
     */
    Collection<RemoteDevice> get(boolean hydratedOnly) {
        Set<RemoteDevice> c = new HashSet();
        for (RegistryItem<UDN, RemoteDevice> item : deviceItems) {
            if (hydratedOnly && item.getItem().getPhase().equals(Phase.HYDRATED)) {
                c.add(item.getItem());
            } else if (!hydratedOnly) {
                c.add(item.getItem());
            }
        }
        return Collections.unmodifiableCollection(c);
    }

    /**
     * Removes the given device from the registry but only notifies listeners if the device had previously been
     * described and was fully usable.
     *
     * @param remoteDevice The device to remove from the registry.
     * @return <tt>true</tt> if the given device was found and removed from the registry, false if it wasn't registered.
     */
    boolean remove(final RemoteDevice remoteDevice) {
        return remove(remoteDevice, false);
    }

    boolean remove(final RemoteDevice remoteDevice, boolean shuttingDown) throws RegistrationException {
        final RemoteDevice registeredDevice = get(remoteDevice.getIdentity().getUdn());
        if (registeredDevice != null) {

            log.fine("Removing remote device from registry: " + remoteDevice);

            // Resources
            for (Resource deviceResource : registeredDevice.getResources()) {
                if (registry.removeResource(deviceResource)) {
                    log.fine("Unregistered resource: " + deviceResource);
                }
            }

            // Active subscriptions
            Iterator<RegistryItem<String, RemoteGENASubscription>> it = subscriptionItems.iterator();
            while (it.hasNext()) {
                final RegistryItem<String, RemoteGENASubscription> outgoingSubscription = it.next();

                UDN subscriptionForUDN =
                        outgoingSubscription.getItem().getDeviceService().getDevice().getIdentity().getUdn();

                if (subscriptionForUDN.equals(registeredDevice.getIdentity().getUdn())) {
                    log.fine("Removing outgoing subscription: " + outgoingSubscription.getKey());
                    it.remove();
                    if (!shuttingDown) {
                        registry.getConfiguration().getRegistryListenerExecutor().execute(
                                new Runnable() {
                                    public void run() {
                                        outgoingSubscription.getItem().end(CancelReason.DEVICE_WAS_REMOVED, null);
                                    }
                                }
                        );
                    }
                }
            }

            // Only notify the listeners if the device has been fully usable
            if (registeredDevice.getPhase().equals(Phase.HYDRATED) && !shuttingDown) {
                for (final RegistryListener listener : registry.getListeners()) {
                    registry.getConfiguration().getRegistryListenerExecutor().execute(
                            new Runnable() {
                                public void run() {
                                    listener.remoteDeviceRemoved(registry, registeredDevice);
                                }
                            }
                    );
                }
            }

            // Finally, remove the device from the registry
            deviceItems.remove(new RegistryItem(registeredDevice.getIdentity().getUdn()));

            return true;
        }

        return false;
    }

    void removeAll() {
        removeAll(false);
    }

    void removeAll(boolean shuttingDown) {
        RemoteDevice[] allDevices = get().toArray(new RemoteDevice[get().size()]);
        for (RemoteDevice device : allDevices) {
            remove(device, shuttingDown);
        }
    }

    /* ############################################################################################################ */

    void start() {
        // Noop
    }

    void maintain() {

        // Remove expired remote devices
        Map<UDN, RemoteDevice> expiredRemoteDevices = new HashMap();
        for (RegistryItem<UDN, RemoteDevice> remoteItem : deviceItems) {
            if (remoteItem.hasExpired(false)) {
                expiredRemoteDevices.put(remoteItem.getKey(), remoteItem.getItem());
            }
        }
        for (RemoteDevice remoteDevice : expiredRemoteDevices.values()) {
            log.fine("Removing expired: " + remoteDevice);
            remove(remoteDevice);
        }

        // Renew outgoing subscriptions
        Set<RemoteGENASubscription> expiredOutgoingSubscriptions = new HashSet();
        for (RegistryItem<String, RemoteGENASubscription> item : subscriptionItems) {
            if (item.hasExpired(true)) {
                expiredOutgoingSubscriptions.add(item.getItem());
            }
        }
        for (RemoteGENASubscription subscription : expiredOutgoingSubscriptions) {
            log.fine("Renewing outgoing subscription: " + subscription);
            renewOutgoingSubscription(subscription);
        }
    }

    void shutdown() {
        log.fine("Cancelling all outgoing subscriptions to remote devices during shutdown");
        for (RegistryItem<String, RemoteGENASubscription> item : subscriptionItems) {
            // This will remove the active subscription from the registry!
            registry.getProtocolFactory()
                    .createSendingUnsubscribe(item.getItem())
                    .run();
        }

        log.fine("Removing all remote devices from registry during shutdown");
        removeAll(true);
    }

    /* ############################################################################################################ */

    protected void renewOutgoingSubscription(final RemoteGENASubscription subscription) {
        registry.executeAsyncProtocol(
                registry.getProtocolFactory().createSendingRenewal(subscription)
        );
    }
}
