/*
 * Hibernate, Relational Persistence for Idiomatic Java
 *
 * Copyright (c) 2012, Red Hat, Inc. and/or its affiliates or third-party contributors as
 * indicated by the @author tags or express copyright attribution
 * statements applied by the authors.  All third-party contributions are
 * distributed under license by Red Hat, Inc.
 *
 * This copyrighted material is made available to anyone wishing to use, modify,
 * copy, or redistribute it subject to the terms and conditions of the GNU
 * Lesser General Public License, as published by the Free Software Foundation.
 *
 * 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 Lesser General Public License
 * for more details.
 *
 * You should have received a copy of the GNU Lesser General Public License
 * along with this distribution; if not, write to:
 * Free Software Foundation, Inc.
 * 51 Franklin Street, Fifth Floor
 * Boston, MA  02110-1301  USA
 */
package org.hibernate.search.engine.impl;

import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.net.URL;
import java.util.Enumeration;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Map;
import java.util.Properties;
import java.util.concurrent.ConcurrentHashMap;

import org.hibernate.annotations.common.AssertionFailure;
import org.hibernate.search.SearchException;
import org.hibernate.search.cfg.spi.SearchConfiguration;
import org.hibernate.search.engine.ServiceManager;
import org.hibernate.search.spi.BuildContext;
import org.hibernate.search.spi.ServiceProvider;
import org.hibernate.search.util.impl.ClassLoaderHelper;
import org.hibernate.search.util.logging.impl.Log;
import org.hibernate.search.util.logging.impl.LoggerFactory;

/**
 * @author Emmanuel Bernard
 * @author Sanne Grinovero
 */
public class StandardServiceManager implements ServiceManager {
	private static final String SERVICES_FILE = "META-INF/services/" + ServiceProvider.class.getName();
	private static final Log log = LoggerFactory.make();

	//barrier protected by the Hibernate Search instantiation
	private final HashSet<Class<?>> availableProviders = new HashSet<Class<?>>();
	private final ConcurrentHashMap<Class<?>, ServiceProviderWrapper<?>> managedProviders = new ConcurrentHashMap<Class<?>, ServiceProviderWrapper<?>>();
	private final Map<Class<? extends ServiceProvider<?>>, Object> providedProviders = new HashMap<Class<? extends ServiceProvider<?>>, Object>();
	private final Properties properties;

	public StandardServiceManager(SearchConfiguration cfg) {
		this.properties = cfg.getProperties();
		this.providedProviders.putAll( cfg.getProvidedServices() );
		listAndInstantiateServiceProviders();
	}

	private void listAndInstantiateServiceProviders() {
		//get list of services available
		final Enumeration<URL> resources = ClassLoaderHelper.getResources( SERVICES_FILE, StandardServiceManager.class );
		String name;
		try {
			while ( resources.hasMoreElements() ) {
				URL url = resources.nextElement();
				InputStream stream = url.openStream();
				try {
					BufferedReader reader = new BufferedReader( new InputStreamReader( stream ), 100 );
					name = reader.readLine();
					while ( name != null ) {
						name = name.trim();
						if ( !name.startsWith( "#" ) ) {
							final Class<?> serviceProviderClass =
									ClassLoaderHelper.classForName( name, StandardServiceManager.class.getClassLoader(), "service provider" );
							availableProviders.add( serviceProviderClass );
						}
						name = reader.readLine();
					}
				}
				finally {
					stream.close();
				}
			}
		}
		catch ( IOException e ) {
			throw new SearchException( "Unable to read " + SERVICES_FILE, e );
		}
	}

	@SuppressWarnings("unchecked")
	public <T> T requestService(Class<? extends ServiceProvider<T>> serviceProviderClass, BuildContext context) {
		//provided services have priority over managed services
		if ( providedProviders.containsKey( serviceProviderClass ) ) {
			//we use containsKey as the service itself might be null
			//TODO be safer and throw a cleaner exception
			return (T) providedProviders.get( serviceProviderClass );
		}

		ServiceProviderWrapper<T> wrapper = (ServiceProviderWrapper<T>) managedProviders.get( serviceProviderClass );
		if ( wrapper == null ) {
			if ( availableProviders.contains( serviceProviderClass ) ) {
				ServiceProvider<T> serviceProvider = ClassLoaderHelper.instanceFromClass(
						ServiceProvider.class,
						serviceProviderClass,
						"service provider"
				);
				wrapper = new ServiceProviderWrapper<T>( serviceProvider, context, serviceProviderClass );
				managedProviders.putIfAbsent( serviceProviderClass, wrapper );
			}
			else {
				throw new SearchException( "Unable to find service related to " + serviceProviderClass );
			}
		}
		wrapper = (ServiceProviderWrapper<T>) managedProviders.get( serviceProviderClass );
		wrapper.startVirtual();
		return wrapper.getService();
	}

	public void releaseService(Class<? extends ServiceProvider<?>> serviceProviderClass) {
		//provided services have priority over managed services
		if ( providedProviders.containsKey( serviceProviderClass ) ) {
			return;
		}

		final ServiceProviderWrapper wrapper = managedProviders.get( serviceProviderClass );
		if ( wrapper == null ) {
			throw new AssertionFailure( "Unable to find service related to " + serviceProviderClass);
		}

		wrapper.stopVirtual();
	}

	public void stopServices() {
		for ( ServiceProviderWrapper wrapper :  managedProviders.values() ) {
			wrapper.ensureStopped();
		}
	}

	private class ServiceProviderWrapper<S> {

		private final ServiceProvider<S> serviceProvider;
		private final BuildContext context;
		private final Class<? extends ServiceProvider<S>> serviceProviderClass;

		private int userCounter = 0;
		private ServiceStatus status = ServiceStatus.STOPPED;

		ServiceProviderWrapper(ServiceProvider<S> serviceProvider, BuildContext context, Class<? extends ServiceProvider<S>> serviceProviderClass) {
			this.serviceProvider = serviceProvider;
			this.context = context;
			this.serviceProviderClass = serviceProviderClass;
		}

		synchronized S getService() {
			if ( status != ServiceStatus.RUNNING ) {
				stateExpectedFailure();
			}
			return serviceProvider.getService();
		}

		synchronized void startVirtual() {
			int previousValue = userCounter;
			userCounter++;
			if ( previousValue == 0 ) {
				if ( status != ServiceStatus.STOPPED ) {
					stateExpectedFailure();
				}
				status = ServiceStatus.STARTING;
				serviceProvider.start( properties, context );
				status = ServiceStatus.RUNNING;
			}
			if ( status != ServiceStatus.RUNNING ) {
				//Could happen on circular dependencies
				stateExpectedFailure();
			}
		}

		synchronized void stopVirtual() {
			userCounter--;
			if ( userCounter == 0 ) {
				if ( status != ServiceStatus.RUNNING ) {
					stateExpectedFailure();
				}
				status = ServiceStatus.STOPPING;
				forceStop();
				status = ServiceStatus.STOPPED;
				managedProviders.remove( serviceProviderClass );
			}
			else if ( status != ServiceStatus.RUNNING ) {
				//Could happen on circular dependencies
				stateExpectedFailure();
			}
		}

		synchronized void ensureStopped() {
			if ( status != ServiceStatus.STOPPED ) {
				log.serviceProviderNotReleased( serviceProviderClass );
				forceStop();
			}
		}

		private void forceStop() {
			try {
				serviceProvider.stop();
			}
			catch ( Exception e ) {
				log.stopServiceFailed( serviceProviderClass, e );
			}
		}

		private void stateExpectedFailure() {
			throw new AssertionFailure( "Unexpected status '" + status + "' for serviceProvider '" + serviceProvider + "'." +
					" Check for circular dependencies or unreleased resources in your services." );
		}
	}

	private enum ServiceStatus {
		RUNNING, STOPPED, STARTING, STOPPING
	}
}
