/*
 * JBoss, Home of Professional Open Source
 * Copyright 2005, JBoss Inc., and individual contributors as indicated
 * by the @authors tag. See the copyright.txt in the distribution for a
 * full listing of individual contributors.
 *
 * This is free software; you can redistribute it and/or modify it
 * under the terms of the GNU Lesser General Public License as
 * published by the Free Software Foundation; either version 2.1 of
 * the License, or (at your option) any later version.
 *
 * This software 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 software; if not, write to the Free
 * Software Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA
 * 02110-1301 USA, or see the FSF site: http://www.fsf.org.
 */
package org.hibernate.console;

import java.io.File;
import java.net.URI;
import java.net.URISyntaxException;
import java.net.URL;
import java.security.AccessController;
import java.security.PrivilegedAction;
import java.sql.DriverManager;
import java.sql.SQLException;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.Iterator;
import java.util.Map;

import org.eclipse.osgi.util.NLS;
import org.hibernate.SessionFactory;
import org.hibernate.cfg.Configuration;
import org.hibernate.cfg.Environment;
import org.hibernate.cfg.Settings;
import org.hibernate.console.execution.DefaultExecutionContext;
import org.hibernate.console.execution.ExecutionContext;
import org.hibernate.console.execution.ExecutionContext.Command;
import org.hibernate.console.execution.ExecutionContextHolder;
import org.hibernate.console.ext.HibernateExtension;
import org.hibernate.console.ext.HibernateExtensionDefinition;
import org.hibernate.console.ext.HibernateExtensionManager;
import org.hibernate.console.preferences.ConsoleConfigurationPreferences;
import org.hibernate.console.preferences.PreferencesClassPathUtils;
import org.hibernate.eclipse.libs.FakeDelegatingDriver;
import org.hibernate.tool.hbm2x.StringUtils;

public class ConsoleConfiguration implements ExecutionContextHolder {

	private ExecutionContext executionContext;
	private ConsoleConfigClassLoader classLoader = null;

	private Map<String, FakeDelegatingDriver> fakeDrivers = new HashMap<String, FakeDelegatingDriver>();

	/* TODO: move this out to the actual users of the configuraiton/sf ? */
	private Configuration configuration;
	private SessionFactory sessionFactory;
	
	//****************************** EXTENSION **********************
	private String hibernateVersion = "==<None>=="; //set to some unused value //$NON-NLS-1$
	
	private HibernateExtension extension;
	
	private void loadHibernateExtension(){
		String version = hibernateVersion == null ? "3.5" : hibernateVersion;//3.5 is a default version
		HibernateExtensionDefinition def = HibernateExtensionManager.findHibernateExtensionDefinition(version);
		if (def != null){
			HibernateExtension hibernateExtension = def.createHibernateExtensionInstance();
			hibernateExtension.setConsoleConfigurationPreferences(prefs);
			extension = hibernateExtension;
		} else {
			throw new IllegalArgumentException("Can't find definition for hibernate version " + version);
		}
	}
	
	private void updateHibernateVersion(String hibernateVersion){
		if (!StringUtils.equals(this.hibernateVersion, hibernateVersion)){
			this.hibernateVersion = hibernateVersion;
			loadHibernateExtension();
		}
	}
	
	public HibernateExtension getHibernateExtension(){
		updateHibernateVersion(prefs.getHibernateVersion());//reinit if necessary
		return this.extension;
	}

	//****************************** EXTENSION **********************
	
	public ConsoleConfiguration(ConsoleConfigurationPreferences config) {
		prefs = config;
	}

	/** Unique name for this configuration */
	public String getName() {
		return prefs.getName();
	}

	public synchronized Object execute(Command c) {
		if (executionContext != null) {
			return executionContext.execute(c);
		}
		final String msg = NLS.bind(ConsoleMessages.ConsoleConfiguration_null_execution_context, getName());
		throw new HibernateConsoleRuntimeException(msg);
	}

	public ConsoleConfigurationPreferences prefs = null;

	/**
	 * Reset configuration, session factory, class loader and execution context.
	 *
	 */
	public synchronized boolean reset() {
		boolean resetted = false;
		// resetting state
		if (getHibernateExtension() != null ) {
			getHibernateExtension().reset();
			getHibernateExtension().closeSessionFactory();
		}
		if (configuration != null) {
			configuration = null;
			resetted = true;
		}
		resetted = resetted | closeSessionFactory() | cleanUpClassLoader();

		if (resetted) {
			fireConfigurationReset();
		}
		executionContext = null;
		return resetted;
	}

	protected boolean cleanUpClassLoader() {
		boolean resetted = false;
		if (executionContext != null) {
			executionContext.execute(new Command() {
				public Object execute() {
					Iterator<FakeDelegatingDriver> it = fakeDrivers.values().iterator();
					while (it.hasNext()) {
						try {
							DriverManager.deregisterDriver(it.next());
						} catch (SQLException e) {
							// ignore
						}
					}
					return null;
				}
			});
		}
		if (fakeDrivers.size() > 0) {
			fakeDrivers.clear();
			resetted = true;
		}
		ClassLoader classLoaderTmp = classLoader;
		while (classLoaderTmp != null) {
			if (classLoaderTmp instanceof ConsoleConfigClassLoader) {
				((ConsoleConfigClassLoader)classLoaderTmp).close();
				resetted = true;
			}
			classLoaderTmp = classLoaderTmp.getParent();
		}
		if (classLoader != null) {
			classLoader = null;
			resetted = true;
		}
		return resetted;
	}
	
	/**
	 * Create class loader - so it uses the original urls list from preferences. 
	 */
	protected void reinitClassLoader() {
		//the class loader caches user's compiled classes
		//need to rebuild it on every console configuration rebuild to pick up latest versions.
		final URL[] customClassPathURLs = PreferencesClassPathUtils.getCustomClassPathURLs(prefs);
		cleanUpClassLoader();
		classLoader = createClassLoader(customClassPathURLs);
	}

	public void build() {
		reset();
		configuration = buildWith(null, true);
		getHibernateExtension().build();
		fireConfigurationBuilt();
	}
	
	protected ConsoleConfigClassLoader createClassLoader(final URL[] customClassPathURLs) {
		ConsoleConfigClassLoader classLoader = AccessController.doPrivileged(new PrivilegedAction<ConsoleConfigClassLoader>() {
			public ConsoleConfigClassLoader run() {
				return new ConsoleConfigClassLoader(customClassPathURLs, getParentClassLoader()) {
					protected Class<?> findClass(String name) throws ClassNotFoundException {
						try {
							return super.findClass(name);
						} catch (ClassNotFoundException cnfe) {
							throw cnfe;
						}
					}
		
					protected synchronized Class<?> loadClass(String name, boolean resolve) throws ClassNotFoundException {
						try {
							return super.loadClass(name, resolve);
						} catch (ClassNotFoundException cnfe) {
							throw cnfe;
						}
					}
		
					public Class<?> loadClass(String name) throws ClassNotFoundException {
						try {
							return super.loadClass(name);
						} catch (ClassNotFoundException cnfe) {
							throw cnfe;
						}
					}
					
					public URL getResource(String name) {
					      return super.getResource(name);
					}
				};
			}
		});
		return classLoader;
	}

	/**
	 * @return
	 *
	 */
	public Configuration buildWith(final Configuration cfg, final boolean includeMappings) {
		reinitClassLoader();
		executionContext = new DefaultExecutionContext(getName(), classLoader);
		Configuration result = (Configuration)execute(new Command() {
			public Object execute() {
				ConfigurationFactory csf = new ConfigurationFactory(prefs, fakeDrivers);
				return csf.createConfiguration(cfg, includeMappings);
			}
		});
		return result;
	}

	protected ClassLoader getParentClassLoader() {
		ClassLoader cl = Thread.currentThread().getContextClassLoader();
		//ClassLoader cl = ConsoleConfiguration.class.getClassLoader();
		return cl;
	}
	
	public Configuration getConfiguration() {
		return configuration;
	}
	/**
	 * @return
	 */
	public boolean hasConfiguration() {
		return configuration != null;
	}
	
	public void buildMappings(){
		execute(new Command() {
			public Object execute() {
				getConfiguration().buildMappings();
				return null;
			}
		});
		getHibernateExtension().buildMappings();
	}

	public void buildSessionFactory() {
		execute(new Command() {
			public Object execute() {
				if (sessionFactory != null) {
					throw new HibernateConsoleRuntimeException(ConsoleMessages.ConsoleConfiguration_factory_not_closed_before_build_new_factory);
				}
				sessionFactory = getConfiguration().buildSessionFactory();
				fireFactoryBuilt();
				return null;
			}
		});
		getHibernateExtension().buildSessionFactory();
	}

	public SessionFactory getSessionFactory() {
		return sessionFactory;
	}


	int execcount;
	ArrayList<ConsoleConfigurationListener> consoleCfgListeners = new ArrayList<ConsoleConfigurationListener>();

	public QueryPage executeHQLQuery(final String hql) {
		return executeHQLQuery(hql, new QueryInputModel());
	}

	public QueryPage executeHQLQuery(final String hql, final QueryInputModel queryParameters) {
		QueryPage qp = getHibernateExtension().executeHQLQuery(hql, queryParameters);
		qp.setId(++execcount);
		fireQueryPageCreated(qp);
		return qp;
	}

	public QueryPage executeBSHQuery(final String queryString, final QueryInputModel model) {
		QueryPage qp = getHibernateExtension().executeCriteriaQuery(queryString, model);
		qp.setId(++execcount);
		fireQueryPageCreated(qp);
		return qp;
	}
	
	@SuppressWarnings("unchecked")
	// clone listeners to thread safe iterate over array
	private ArrayList<ConsoleConfigurationListener> cloneConsoleCfgListeners() {
		return (ArrayList<ConsoleConfigurationListener>)consoleCfgListeners.clone();
	}

	private void fireConfigurationBuilt() {
		for (ConsoleConfigurationListener view : cloneConsoleCfgListeners()) {
			view.configurationBuilt(this);
		}
	}	

	private void fireConfigurationReset() {
		for (ConsoleConfigurationListener view : cloneConsoleCfgListeners()) {
			view.configurationReset(this);
		}
	}	

	private void fireQueryPageCreated(QueryPage qp) {
		for (ConsoleConfigurationListener view : cloneConsoleCfgListeners()) {
			view.queryPageCreated(qp);
		}
	}

	private void fireFactoryBuilt() {
		for (ConsoleConfigurationListener view : cloneConsoleCfgListeners()) {
			view.sessionFactoryBuilt(this, sessionFactory);
		}
	}

	private void fireFactoryClosing(SessionFactory sessionFactory2) {
		for (ConsoleConfigurationListener view : cloneConsoleCfgListeners()) {
			view.sessionFactoryClosing(this, sessionFactory2);
		}
	}

	public void addConsoleConfigurationListener(ConsoleConfigurationListener v) {
		consoleCfgListeners.add(v);
	}

	public void removeConsoleConfigurationListener(ConsoleConfigurationListener sfListener) {
		consoleCfgListeners.remove(sfListener);
	}

	public ConsoleConfigurationListener[] getConsoleConfigurationListeners() {
		return consoleCfgListeners.toArray(new ConsoleConfigurationListener[consoleCfgListeners.size()]);
	}


	public boolean isSessionFactoryCreated() {
		return sessionFactory != null;
	}

	public ConsoleConfigurationPreferences getPreferences() {
		return prefs;
	}
	
	public File getConfigXMLFile() {
		File configXMLFile = null;
		if (prefs != null) {
			configXMLFile = prefs.getConfigXMLFile();
		}
		if (configXMLFile == null) {
			URL url = null;
			//reinitClassLoader();
			if (classLoader != null) {
				url = classLoader.findResource("hibernate.cfg.xml"); //$NON-NLS-1$
			}
			if (url != null) {
				URI uri = null;
				try {
					uri = url.toURI();
					configXMLFile = new File(uri);
				} catch (URISyntaxException e) {
					// ignore
				}
			}
		}
		if (configXMLFile == null) {
			URL url = Environment.class.getClassLoader().getResource("hibernate.cfg.xml"); //$NON-NLS-1$
			if (url != null) {
				URI uri = null;
				try {
					uri = url.toURI();
					configXMLFile = new File(uri);
				} catch (URISyntaxException e) {
					// ignore
				}
			}
		}
		return configXMLFile;
	}

	public String toString() {
		return getClass().getName() + ":" + getName(); //$NON-NLS-1$
	}

	public ExecutionContext getExecutionContext() {
		return executionContext;
	}

	public boolean closeSessionFactory() {
		boolean resetted = false;
		if (sessionFactory != null) {
			fireFactoryClosing(sessionFactory);
			sessionFactory.close();
			sessionFactory = null;
			resetted = true;
		}
		if (getHibernateExtension() != null){
			getHibernateExtension().closeSessionFactory();
		}
		return resetted;
	}

	public Settings getSettings(final Configuration cfg) {
		return (Settings) execute(new Command() {
				public Object execute() {
					return cfg.buildSettings();
				}
			}
		);
	}
}
