package com.alvazan.orm.layer0.base;

import java.net.URL;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.List;
import java.util.Map;

import javax.inject.Inject;
import javax.inject.Provider;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import com.alvazan.orm.api.base.Bootstrap;
import com.alvazan.orm.api.base.NoSqlEntityManager;
import com.alvazan.orm.api.base.NoSqlEntityManagerFactory;
import com.alvazan.orm.api.base.anno.NoSqlQueries;
import com.alvazan.orm.api.base.anno.NoSqlQuery;
import com.alvazan.orm.api.z5api.QueryParser;
import com.alvazan.orm.api.z5api.SpiMetaQuery;
import com.alvazan.orm.api.z8spi.conv.Converter;
import com.alvazan.orm.impl.meta.data.MetaAbstractClass;
import com.alvazan.orm.impl.meta.data.MetaClass;
import com.alvazan.orm.impl.meta.data.MetaClassInheritance;
import com.alvazan.orm.impl.meta.data.MetaClassSingle;
import com.alvazan.orm.impl.meta.data.MetaInfo;
import com.alvazan.orm.impl.meta.scan.ScannerForField;
import com.alvazan.orm.layer3.typed.CachedMeta;
import com.impetus.annovention.ClasspathDiscoverer;
import com.impetus.annovention.Filter;

public class BaseEntityManagerFactoryImpl implements NoSqlEntityManagerFactory {

	private static final Logger log = LoggerFactory.getLogger(BaseEntityManagerFactoryImpl.class);
	
	@Inject
	private Provider<BaseEntityManagerImpl> entityMgrProvider;
	@Inject
	private MyClassAnnotationDiscoveryListener listener;
	@Inject
	private ScannerForField inspectorField;
	@Inject
	private QueryParser noSqlSessionFactory;
	@Inject
	private ClasspathDiscoverer discoverer; 
	private boolean isScanned;
	@Inject
	private MetaInfo metaInfo;
	@Inject
	private CachedMeta cache;
	
	private Object injector;

	@Override
	public NoSqlEntityManager createEntityManager() {
		if(!isScanned)
			throw new IllegalStateException("Must call scanForEntities first");
		BaseEntityManagerImpl mgr = entityMgrProvider.get();
		mgr.setup();
		return mgr;
	}

	@SuppressWarnings("rawtypes")
	@Override
	public void rescan(List<Class> classesToScan, ClassLoader cl) {
		List<Class> classes = classesToScan;
		if(classes == null)
			classes = new ArrayList<Class>();
		metaInfo.clearAll();
		
		listener.setClassLoader(cl);
		
		//TODO: Fork annovention, it is a very small library AND then copy from
		//http://code.google.com/p/reflections/source/browse/trunk/reflections/src/main/java/org/reflections/util/ClasspathHelper.java?r=103
		//so that we only scan classes that are in a certain package instead of all classes on the classpath!!!!
		discoverer.setFilter(new OurFilter());
        // Add class annotation listener (optional)
        discoverer.addAnnotationListener(listener);
        // Fire it
        discoverer.discover(cl);
        
        for(Class c : classes) {
			listener.scanClass(c);
			if (c.getSuperclass() != java.lang.Object.class) {
				listener.scanClass(c.getSuperclass());
			}
		}
        
        if(log.isTraceEnabled()) {
        	URL[] resources = discoverer.findResources(cl);
        	for(URL res : resources) {
        		if(log.isTraceEnabled())
        			log.trace("jar="+res);
        	}
        }
        
        Collection<MetaAbstractClass> allEntities = metaInfo.getAllEntities();
        for(MetaAbstractClass meta : allEntities) {
        	setupQueryStuff(meta);
        }
        
        if (log.isInfoEnabled())
			log.info("Finished scanning classes, saving meta data");
        isScanned = true;
        
        BaseEntityManagerImpl tempMgr = (BaseEntityManagerImpl) createEntityManager();
        tempMgr.saveMetaData();
        
        cache.init(this);
        if (log.isInfoEnabled())
			log.info("Finished saving meta data, complelety done initializing");
	}
	
	@SuppressWarnings({ "rawtypes", "unchecked" })
	public void setup(Map<String, Object> properties, Map<Class, Converter> converters, ClassLoader cl) {
		if(isScanned)
			throw new IllegalStateException("scanForEntities can only be called once");
		else if(properties == null)
			throw new IllegalArgumentException("'properties' parameter must be supplied");
		
		String val = (String) properties.get(Bootstrap.AUTO_CREATE_KEY);
		if(val == null)
			throw new IllegalArgumentException("Must provide property with key NoSqlEntityManagerFactory.AUTO_CREATE_KEY so we know to update or validate existing schema");
		AutoCreateEnum autoCreate = AutoCreateEnum.translate(val);
		if(autoCreate == null)
			throw new IllegalArgumentException("Property NoSqlEntityManagerFactory.AUTO_CREATE_KEY can only have values validate,update, or create");
		
		inspectorField.setCustomConverters(converters);
		
		if (log.isInfoEnabled())
			log.info("Begin scanning for jars with nosql.Persistence.class");
		
        List<Class> classToScan = (List<Class>) properties.get(Bootstrap.LIST_OF_EXTRA_CLASSES_TO_SCAN_KEY);
        
        if(AutoCreateEnum.CREATE_ONLY != autoCreate)
        	throw new UnsupportedOperationException("not implemented yet");
        
		rescan(classToScan, cl);
	}

	@SuppressWarnings({ "rawtypes" })
	public void setupQueryStuff(MetaAbstractClass classMeta) {
		Class<?> clazz = classMeta.getMetaClass();
		NoSqlQuery annotation = clazz.getAnnotation(NoSqlQuery.class);
		NoSqlQueries annotation2 = clazz.getAnnotation(NoSqlQueries.class);
		List<NoSqlQuery> theQueries = new ArrayList<NoSqlQuery>();
		if(annotation2 != null) {
			NoSqlQuery[] queries = annotation2.value();
			List<NoSqlQuery> asList = Arrays.asList(queries);
			theQueries.addAll(asList);
		}
		if(annotation != null)
			theQueries.add(annotation);

		//log.info("Parsing queries for entity="+classMeta.getMetaClass());
		for(NoSqlQuery query : theQueries) {
			if (log.isInfoEnabled())
				log.info("["+classMeta.getMetaClass().getSimpleName()+"]parsing query="+query.name()+" query="+query.query());
			SpiMetaQuery metaQuery = createQueryAndAdd(classMeta, query);
			classMeta.addQuery(query.name(), metaQuery);
		}
		
		if(classMeta instanceof MetaClassInheritance) {
			MetaClassInheritance parentMeta = (MetaClassInheritance) classMeta;
			@SuppressWarnings("unchecked")
			Collection<MetaClassSingle> subMetas = parentMeta.fetchSubclassList();
			for(MetaClassSingle meta : subMetas) {
				setupQueryStuff(meta);
			}
		}
	}

	@SuppressWarnings("rawtypes")
	private SpiMetaQuery createQueryAndAdd(MetaClass classMeta, NoSqlQuery query) {
		// parse and setup this query once here to be used by ALL of the
		// SpiIndexQuery objects.
		// NOTE: This is meta data to be re-used by all threads and all
		// instances of query objects only!!!!

		// We must walk the tree allowing 2 visitors to see it.
		// The first visitor would be ourselves maybe? to get all parameter info
		// The second visitor is the SPI Index so it can create it's "prototype"
		// query (prototype pattern)
		String errorMsg = "Named Query on class "+classMeta.getMetaClass().getName()+" (name=\""+query.name()+"\",query=\""+query.query()+"\")";
		SpiMetaQuery metaQuery = noSqlSessionFactory.parseQueryForOrm(query.query(), classMeta.getColumnFamily(), errorMsg);

		return metaQuery;
	}
	
	private static class OurFilter implements Filter {
		@Override
		public boolean accepts(String filename) {
			if(filename.endsWith(".class"))
				return true;
			return false;
		}
	}

	public Object getInjector() {
		return injector;
	}

	public void setInjector(Object injector) {
		this.injector = injector;
	}

	@Override
	public void close() {
		this.noSqlSessionFactory.close();
	}

}
