/*******************************************************************************
 * 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.security.AccessController;
import java.security.PrivilegedAction;
import java.util.Hashtable;
import java.util.Vector;
import org.eclipse.osgi.util.NLS;
import org.osgi.framework.*;
import org.osgi.service.device.Device;
import org.osgi.service.device.Driver;
import org.osgi.service.log.LogService;
import org.osgi.util.tracker.ServiceTracker;

/**
 * DriverTracker class. This class tracks all Driver services.
 *
 */
public class DriverTracker extends ServiceTracker {
	/** Driver service name */
	protected final static String clazz = "org.osgi.service.device.Driver"; //$NON-NLS-1$

	/** LogService object */
	protected LogTracker log;

	/** Dictionary mapping DRIVER_ID strings <==> Driver ServiceReferences */
	protected Hashtable<Object, Object> drivers;

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

	/** Dictionary mapping Driver ID String =>
	 *  Hashtable (Device ServiceReference => cached Match objects) */
	protected Hashtable<String, Hashtable<ServiceReference, Match>> matches;

	/** Dictionary mapping Driver ID String =>
	 *  Hashtable (Device ServiceReference => cached referral String) */
	protected Hashtable<String, Hashtable<ServiceReference, String>> referrals;

	/**
	 * Create the DriverTracker.
	 *
	 * @param manager DeviceManager object.
	 * @param device DeviceTracker we are working for.
	 */
	public DriverTracker(Activator manager) {
		super(manager.context, clazz, null);

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

		drivers = new Hashtable<>(37);
		matches = new Hashtable<>(37);
		referrals = new Hashtable<>(37);

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

		open();
	}

	/**
	 * 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 service"); //$NON-NLS-1$
		}

		String driver_id = getDriverID(reference);

		if (drivers.get(driver_id) != null) {
			log.log(reference, LogService.LOG_WARNING, NLS.bind(DeviceMsg.Multiple_Driver_services_with_the_same_DRIVER_ID, driver_id));

			return (null); /* don't track this driver */
		}

		drivers.put(driver_id, reference);
		drivers.put(reference, driver_id);

		manager.driverServiceRegistered = true;

		/* OSGi SPR2 Device Access 1.1
		 * Section 8.4.3 - When a new Driver service is registered,
		 * the Device Attachment Algorithm must be applied to all
		 * idle Device services.
		 *
		 * We do not refine idle Devices when the manager has not fully
		 * started or the Driver service is from a bundle just installed
		 * by the devicemanager.
		 */
		Bundle bundle = reference.getBundle();

		if (manager.running && !manager.locators.isUninstallCandidate(bundle)) {
			manager.refineIdleDevices();
		}

		return (context.getService(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) {
		if (Activator.DEBUG) {
			log.log(reference, LogService.LOG_DEBUG, this + " modified service"); //$NON-NLS-1$
		}

		String driver_id = getDriverID(reference);

		String old_id = (String) drivers.get(reference);

		if (!driver_id.equals(old_id)) {
			drivers.put(driver_id, reference);
			drivers.put(reference, driver_id);
			drivers.remove(old_id);
		}
	}

	/**
	 * 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 (Activator.DEBUG) {
			log.log(reference, LogService.LOG_DEBUG, this + " removing service"); //$NON-NLS-1$
		}

		String driver_id = getDriverID(reference);
		drivers.remove(driver_id);
		drivers.remove(reference);

		matches.remove(driver_id);
		referrals.remove(driver_id);

		context.ungetService(reference);

		/* OSGi SPR2 Device Access 1.1
		 * Section 8.4.4 - When a Driver service is unregistered,
		 * the Device Attachment Algorithm must be applied to all
		 * idle Device services.
		 *
		 * We do not refine idle Devices when the manager has not fully
		 * started or the Driver service is from a bundle just installed
		 * by the devicemanager.
		 */

		Bundle bundle = reference.getBundle();

		if (manager.running && !manager.locators.isUninstallCandidate(bundle)) {
			DriverUpdate update = new DriverUpdate(bundle, manager);

			Thread thread = (new SecureAction()).createThread(update, DeviceMsg.DeviceManager_Update_Wait);

			thread.start();
		}
	}

	/**
	 * Return the DRIVER_ID string for a ServiceReference.
	 *
	 * Per Section 8.4.3 of the OSGi SP R2 spec,
	 * "A Driver service registration must have a DRIVER_ID property"
	 *
	 * This method is somewhat more lenient. If no DRIVER_ID property
	 * is set, it will use the Bundle's location instead.
	 *
	 * @param reference Reference to driver service.
	 * @return DRIVER_ID string.
	 */
	public String getDriverID(final ServiceReference reference) {
		String driver_id = (String) reference.getProperty(org.osgi.service.device.Constants.DRIVER_ID);

		if (driver_id == null) {
			log.log(reference, LogService.LOG_WARNING, DeviceMsg.Driver_service_has_no_DRIVER_ID);
			driver_id = AccessController.doPrivileged((PrivilegedAction<String>) () -> reference.getBundle().getLocation());
		}

		return (driver_id);
	}

	/**
	 * Get the ServiceReference for a given DRIVER_ID.
	 *
	 * @param driver_id
	 * @return ServiceReference to a Driver service.
	 */
	public ServiceReference getDriver(String driver_id) {
		return ((ServiceReference) drivers.get(driver_id));
	}

	/**
	 * Search the driver list to find the best match for the device.
	 *
	 * @return ServiceReference to best matched Driver or null of their is no match.
	 */
	public ServiceReference match(ServiceReference device, Vector exclude) {
		if (Activator.DEBUG) {
			log.log(device, LogService.LOG_DEBUG, this + ": Driver match called"); //$NON-NLS-1$
		}

		ServiceReference[] references = getServiceReferences();

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

			Vector<Match> successfulMatches = new Vector<>(size);

			for (int i = 0; i < size; i++) {
				ServiceReference driver = references[i];

				if (exclude.contains(driver)) {
					if (Activator.DEBUG) {
						log.log(driver, LogService.LOG_DEBUG, this + ": Driver match excluded: " + drivers.get(driver)); //$NON-NLS-1$
					}
				} else {
					if (Activator.DEBUG) {
						log.log(driver, LogService.LOG_DEBUG, this + ": Driver match called: " + drivers.get(driver)); //$NON-NLS-1$
					}

					Match match = getMatch(driver, device);

					if (match == null) {
						Driver service = (Driver) getService(driver);

						if (service == null) {
							continue;
						}

						int matchValue = Device.MATCH_NONE;

						try {
							matchValue = service.match(device);
						} catch (Throwable t) {
							log.log(driver, LogService.LOG_ERROR, DeviceMsg.Driver_error_during_match, t);

							continue;
						}

						if (Activator.DEBUG) {
							log.log(driver, LogService.LOG_DEBUG, this + ": Driver match value: " + matchValue); //$NON-NLS-1$
						}

						match = new Match(driver, matchValue);

						storeMatch(driver, device, match);
					}

					if (match.getMatchValue() > Device.MATCH_NONE) {
						successfulMatches.addElement(match);
					}
				}
			}

			size = successfulMatches.size();

			if (size > 0) {
				Match[] matchArray = new Match[size];
				successfulMatches.copyInto(matchArray);

				return manager.selectors.select(device, matchArray);
			}
		}

		return null;
	}

	public Match getMatch(ServiceReference driver, ServiceReference device) {
		String driverid = getDriverID(driver);

		Hashtable<ServiceReference, Match> driverMatches = matches.get(driverid);

		if (driverMatches == null) {
			return null;
		}

		return driverMatches.get(device);
	}

	public void storeMatch(ServiceReference driver, ServiceReference device, Match match) {
		String driverid = getDriverID(driver);

		Hashtable<ServiceReference, Match> driverMatches = matches.get(driverid);

		if (driverMatches == null) {
			driverMatches = new Hashtable<>(37);

			matches.put(driverid, driverMatches);
		}

		driverMatches.put(device, match);
	}

	/**
	 * Attempt to attach the driver to the device. If the driver
	 * refers, add the referred driver to the driver list.
	 *
	 * @param driver Driver to attach
	 * @param device Device to be attached
	 * @return true is the Driver successfully attached.
	 */
	public boolean attach(ServiceReference driver, ServiceReference device, Vector exclude) {
		if (Activator.DEBUG) {
			log.log(driver, LogService.LOG_DEBUG, this + ": Driver attach called: " + drivers.get(driver)); //$NON-NLS-1$
		}

		Driver service = (Driver) getService(driver);

		if (service != null) {
			String referral = getReferral(driver, device);

			if (referral == null) {
				try {
					referral = service.attach(device);
				} catch (Throwable t) {
					log.log(driver, LogService.LOG_ERROR, DeviceMsg.Driver_error_during_attach, t);

					exclude.addElement(driver);

					return (false);
				}

				storeReferral(driver, device, (referral == null) ? "" : referral); //$NON-NLS-1$
			} else {
				if (referral.length() == 0) {
					referral = null;
				}
			}

			if (referral == null) {
				log.log(device, LogService.LOG_INFO, NLS.bind(DeviceMsg.Device_attached_by_DRIVER_ID, drivers.get(driver)));

				manager.locators.usingDriverBundle(driver.getBundle());

				return (true);
			}

			log.log(device, LogService.LOG_INFO, NLS.bind(DeviceMsg.Device_referred_to, referral));
			manager.locators.loadDriver(referral, this);
		}

		exclude.addElement(driver);

		return (false);
	}

	public String getReferral(ServiceReference driver, ServiceReference device) {
		String driverid = getDriverID(driver);

		Hashtable<ServiceReference, String> driverReferrals = referrals.get(driverid);

		if (driverReferrals == null) {
			return null;
		}

		return driverReferrals.get(device);
	}

	public void storeReferral(ServiceReference driver, ServiceReference device, String referral) {
		String driverid = getDriverID(driver);

		Hashtable<ServiceReference, String> driverReferrals = referrals.get(driverid);

		if (driverReferrals == null) {
			driverReferrals = new Hashtable<>(37);

			referrals.put(driverid, driverReferrals);
		}

		driverReferrals.put(device, referral);
	}

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

	public class DriverUpdate implements Runnable, ServiceListener, BundleListener {
		private Activator manager_;
		private Bundle bundle;
		private BundleContext contxt;

		/** if false the thread must terminate */
		private volatile boolean running;

		private long updatewait;

		DriverUpdate(Bundle bundle, Activator manager) {
			this.manager_ = manager;
			this.bundle = bundle;

			contxt = manager_.context;
			updatewait = manager_.updatewait;
			running = true;

			contxt.addBundleListener(this);
			try {
				contxt.addServiceListener(this, manager_.driverFilter.toString());
			} catch (InvalidSyntaxException e) {
				/* this should not happen */
			}
		}

		public void run() {
			// 1. Wait for some time
			// 2. if bundle registers Driver; terminate
			// 3. if bundle uninstalls; cancel wait
			// 4. manager.refineIdleDevices()

			try {
				if (updatewait > 0) {
					synchronized (this) {
						wait(updatewait);
					}
				}
			} catch (InterruptedException e) {
				//do nothing
			}

			contxt.removeServiceListener(this);
			contxt.removeBundleListener(this);

			if (running) {
				manager.refineIdleDevices();
			}
		}

		public void serviceChanged(ServiceEvent event) {
			if ((event.getType() == ServiceEvent.REGISTERED) && bundle.equals(event.getServiceReference().getBundle())) {
				contxt.removeServiceListener(this);

				running = false; /* cancel */

				/* should probably interrupt waiting thread here */
			}
		}

		public void bundleChanged(BundleEvent event) {
			if ((event.getType() == Bundle.UNINSTALLED) && bundle.equals(event.getBundle())) {
				contxt.removeBundleListener(this);

				updatewait = 0; /* avoid wait */

				/* should probably interrupt waiting thread here */
			}
		}
	}
}
