/*******************************************************************************
 * Copyright (c) 2000, 2017 IBM Corporation and others.
 *
 * This program and the accompanying materials
 * are made available under the terms of the Eclipse Public License 2.0
 * which accompanies this distribution, and is available at
 * https://www.eclipse.org/legal/epl-2.0/
 *
 * SPDX-License-Identifier: EPL-2.0
 *
 * Contributors:
 *     IBM Corporation - initial API and implementation
 *******************************************************************************/
package org.eclipse.equinox.device;

import java.util.*;
import org.eclipse.osgi.util.NLS;
import org.osgi.framework.*;
import org.osgi.service.device.Device;
import org.osgi.service.log.LogService;
import org.osgi.util.tracker.ServiceTracker;

/**
 * DeviceTracker class. This class has the logic for refining a
 * Device service.
 *
 */
public class DeviceTracker extends ServiceTracker {
	/** OSGi Device class name */
	protected final static String clazz = "org.osgi.service.device.Device"; //$NON-NLS-1$

	/** DeviceManager object. */
	protected Activator manager;

	/** reference to Device service we are attempting to refine */
	protected ServiceReference device;

	/** LogService object */
	protected LogTracker log;

	/** Device services properties */
	protected Dictionary<String, ?> properties;

	/** if false the algorithm must terminate */
	protected volatile boolean running;

	/**
	 * Create a DeviceTracker from a ServiceReference.
	 *
	 * @param manager DeviceManager object
	 * @param device ServiceReference to the Device service.
	 */
	public DeviceTracker(Activator manager, ServiceReference device) {
		super(manager.context, device, null);

		this.manager = manager;
		log = manager.log;

		if (Activator.DEBUG) {
			log.log(device, LogService.LOG_DEBUG, this + " constructor"); //$NON-NLS-1$
		}

		open();
	}

	/**
	 * Close the Device.
	 */

	public void close() {
		if (device != null) {
			if (Activator.DEBUG) {
				log.log(device, LogService.LOG_DEBUG, this + " closing"); //$NON-NLS-1$
			}

			running = false; /* request thread to stop */

			super.close();

			device = null;
		}
	}

	/**
	 * A service is being added to the ServiceTracker.
	 *
	 * <p>This method is called before a service which matched
	 * the search parameters of the ServiceTracker is
	 * added to the ServiceTracker. This method should return the
	 * service object to be tracked for this ServiceReference.
	 * The returned service object is stored in the ServiceTracker
	 * and is available from the getService and getServices
	 * methods.
	 *
	 * @param reference Reference to service being added to the ServiceTracker.
	 * @return The service object to be tracked for the
	 * ServiceReference or <tt>null</tt> if the ServiceReference should not
	 * be tracked.
	 */
	public Object addingService(ServiceReference reference) {
		if (Activator.DEBUG) {
			log.log(reference, LogService.LOG_DEBUG, this + " adding Device service"); //$NON-NLS-1$
		}

		device = reference;

		running = true;

		properties = new Properties(reference);

		return (reference);
	}

	/**
	 * A service tracked by the ServiceTracker has been modified.
	 *
	 * <p>This method is called when a service being tracked
	 * by the ServiceTracker has had it properties modified.
	 *
	 * @param reference Reference to service that has been modified.
	 * @param service The service object for the modified service.
	 */
	public void modifiedService(ServiceReference reference, Object service) {
		properties = new Properties(reference);
	}

	/**
	 * A service tracked by the ServiceTracker is being removed.
	 *
	 * <p>This method is called after a service is no longer being tracked
	 * by the ServiceTracker.
	 *
	 * @param reference Reference to service that has been removed.
	 * @param service The service object for the removed service.
	 */
	public void removedService(ServiceReference reference, Object service) {
		if (running) {
			log.log(reference, LogService.LOG_WARNING, DeviceMsg.Device_service_unregistered);
			running = false; /* request algorithm to stop */
		} else {
			if (Activator.DEBUG) {
				log.log(reference, LogService.LOG_DEBUG, this + " removing Device service"); //$NON-NLS-1$
			}
		}
		super.removedService(reference, service);
	}

	/**
	 * Attempt to refine this Device service.
	 *
	 */
	public void refine() {
		if (Activator.DEBUG) {
			log.log(device, LogService.LOG_DEBUG, this + " refining " + device); //$NON-NLS-1$
		}

		if (running && isIdle()) {
			/* List of excluded drivers from this algorithm run */
			DriverTracker drivers = manager.drivers;

			manager.locators.loadDrivers(properties, drivers);

			Vector exclude = new Vector(drivers.size());

			while (running) {
				ServiceReference driver = drivers.match(device, exclude);

				if (driver == null) {
					noDriverFound();
					break;
				}

				if (drivers.attach(driver, device, exclude)) {
					break;
				}
			}
		}

		close();
	}

	/**
	 * Determine if the device service tracked by this object is idle.
	 *
	 * OSGi SP R2 Section 8.2.2 defines in idle device service as:
	 * "A Device service is not used by any other bundle according to the Framework;
	 * it is called an idle Device service."
	 *
	 * This method defines it as:
	 *  A Device service is not used by any DRIVER bundle according to the Framework;
	 *  it is called an idle Device service.
	 *
	 * Thus if a non-driver bundle uses a device service, it is still considered
	 * idle by this method.
	 *
	 * @return true if the device service is idle.
	 */
	public boolean isIdle() {
		if (Activator.DEBUG) {
			log.log(device, LogService.LOG_DEBUG, "Check device service idle: " + device); //$NON-NLS-1$
		}

		Filter filter_ = manager.driverFilter;
		Bundle[] users = device.getUsingBundles();

		int userCount = (users == null) ? 0 : users.length;

		for (int i = 0; i < userCount; i++) {
			ServiceReference[] services = users[i].getRegisteredServices();

			int servicesCount = (services == null) ? 0 : services.length;

			for (int j = 0; j < servicesCount; j++) {
				if (filter_.match(services[j])) {
					if (Activator.DEBUG) {
						log.log(LogService.LOG_DEBUG, "Device " + device + " already in use by bundle " + users[i]); //$NON-NLS-1$ //$NON-NLS-2$
					}

					return (false);
				}
			}
		}

		return (true);
	}

	/**
	 * Called by the device manager after it has failed to attach
	 * any driver to the device.
	 * <p>
	 * If the device can be configured in alternate ways, the driver
	 * may respond by unregistering the device service and registering
	 * a different device service instead.</p>
	 */

	public void noDriverFound() {
		BundleContext contxt = manager.context;

		Object service = contxt.getService(device);

		try {
			//It is possible that this is a Free Format Device that does not
			//implement Device
			if (service instanceof Device) {
				log.log(device, LogService.LOG_INFO, DeviceMsg.Device_noDriverFound_called);

				try {
					((Device) service).noDriverFound();
				} catch (Throwable t) {
					log.log(device, LogService.LOG_ERROR, NLS.bind(DeviceMsg.Device_noDriverFound_error, t));
				}
			}
		} finally {
			contxt.ungetService(device);
		}

	}

	public String toString() {
		return "DeviceTracker"; //$NON-NLS-1$
	}

	/**
	 * Readonly Dictionary for device properties.
	 *
	 */
	static class Properties extends Hashtable<String, Object> {
		private static final long serialVersionUID = -8489170394007899809L;
		/**
		 * keys in original case.
		 */
		protected Vector<String> keys;

		/**
		 * Create a properties object for the service.
		 *
		 * @param device The service to get the properties of.
		 */
		protected Properties(ServiceReference device) {
			super();

			String[] props = device.getPropertyKeys();

			if (props != null) {
				int size = props.length;

				keys = new Vector<>(size);

				for (int i = 0; i < size; i++) {
					String key = props[i];
					Object value = device.getProperty(key);

					if (value != null) {
						keys.addElement(key);

						super.put(key.toLowerCase(), value);
					}
				}
			} else {
				keys = new Vector<>(0);
			}
		}

		/**
		 * Override keys to support case-preserving of keys.
		 */
		public Enumeration<String> keys() {
			return (keys.elements());
		}

		/**
		 * Override get to support case-insensitivity.
		 *
		 * @param key header name.
		 */
		public Object get(String key) {
			return (super.get(key.toLowerCase()));
		}

		/**
		 * Override put to disable it. This Dictionary is readonly once built.
		 *
		 * @param key header name.
		 * @param value header value.
		 * @throws UnsupportedOperationException
		 */
		public Object put(String key, Object value) {
			throw new UnsupportedOperationException();
		}

		/**
		 * Override remove to disable it. This Dictionary is readonly once built.
		 *
		 * @param key header name.
		 * @throws UnsupportedOperationException
		 */
		public Object remove(Object key) {
			throw new UnsupportedOperationException();
		}
	}
}
