/*
 * Licensed to the Apache Software Foundation (ASF) under one
 * or more contributor license agreements.  See the NOTICE file
 * distributed with this work for additional information
 * regarding copyright ownership.  The ASF licenses this file
 * to you under the Apache License, Version 2.0 (the
 * "License"); you may not use this file except in compliance
 * with the License.  You may obtain a copy of the License at
 *
 * http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing,
 * software distributed under the License is distributed on an
 * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
 * KIND, either express or implied.  See the License for the
 * specific language governing permissions and limitations
 * under the License.    
 */
package org.apache.openjpa.sdo;

import java.io.ByteArrayInputStream;
import java.io.File;
import java.io.IOException;
import java.lang.instrument.IllegalClassFormatException;
import java.util.Arrays;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;

import javax.persistence.Entity;
import javax.persistence.OneToMany;
import javax.persistence.OneToOne;

import org.apache.commons.lang.StringUtils;
import org.apache.openjpa.conf.OpenJPAConfiguration;
import org.apache.openjpa.enhance.PCClassFileTransformer;
import org.apache.openjpa.enhance.PCEnhancer;
import org.apache.openjpa.enhance.PersistenceCapable;
import org.apache.openjpa.jdbc.meta.ClassMapping;
import org.apache.openjpa.jdbc.meta.FieldMapping;
import org.apache.openjpa.jdbc.meta.MappingStrategyInstaller;
import org.apache.openjpa.lib.conf.Configuration;
import org.apache.openjpa.lib.conf.Configurations;
import org.apache.openjpa.lib.log.Log;
import org.apache.openjpa.lib.util.Files;
import org.apache.openjpa.lib.util.Localizer;
import org.apache.openjpa.meta.ClassMetaData;
import org.apache.openjpa.meta.FieldMetaData;
import org.apache.openjpa.meta.MetaDataFactory;
import org.apache.openjpa.meta.MetaDataRepository;
import org.apache.openjpa.sdo.mapping.AbstractPropertyMapping;
import org.apache.openjpa.sdo.mapping.PropertyMapping;
import org.apache.openjpa.sdo.mapping.SDOMappingStrategy;
import org.apache.openjpa.sdo.mapping.TypeMapping;
import org.apache.openjpa.util.InternalException;
import org.apache.openjpa.util.UserException;

import serp.bytecode.Annotation;
import serp.bytecode.Annotations;
import serp.bytecode.BCClass;
import serp.bytecode.BCClassLoader;
import serp.bytecode.BCField;
import serp.bytecode.BCMethod;
import serp.bytecode.Code;
import serp.bytecode.Constants;
import serp.bytecode.Project;
import serp.util.Strings;

import commonj.sdo.Property;
import commonj.sdo.Type;
import commonj.sdo.helper.TypeHelper;

/**
 * Maintains mapping information for SDO Types to Java classes.
 * 
 * @author Pinaki Poddar
 * @since 0.2.0
 */
public class SDOMappingRepository {
	private static final Localizer _loc = 
		Localizer.forPackage(SDOMetaDataFactory.class);
	// Mapping for each SDO Type
	private Map<Type, TypeMapping> mappings = new HashMap<Type, TypeMapping>();
	// Associates SDO Type and Java Class
	private BiDiMap<Type, Class> _cache = new BiDiMap<Type, Class>();
	
	private OpenJPAConfiguration conf;
	private Log _log;
	
	private BCClassLoader throwAwayLoader;
	private BCClassLoader enhancedLoader;
	private File  _root;
	
	/**
	 * Supply configuration.
	 * 
	 */
	public SDOMappingRepository(OpenJPAConfiguration conf) {
		super();
		this.conf = conf;
		_log = conf.getLog(OpenJPAConfiguration.LOG_METADATA);
		_root = new File(System.getProperty("user.dir"));
	}
	
	/**
	 * Gets the strategy used for mapping of the given SDO Type.
	 * Constructs if necessary. 
	 * Resolving a mapping if asked.
	 */
	public TypeMapping getMapping(Type type, boolean resolve) {
		TypeMapping mapping = mappings.get(type);
		Class cls = _cache.get(type);
		if (mapping == null) {
			cls = getJavaClass(type, true);
			if (_log.isInfoEnabled())
				_log.info(_loc.get("type-mapping-new", type.getName(), toString(cls)).toString());
			mapping = SDOMappingStrategy.decideStrategy(type, cls);
			mappings.put(type, mapping);
		}
		if (resolve && !mapping.isResolved()) {
			if (_log.isInfoEnabled())
				_log.info(_loc.get("type-mapping-resolve", type.getName(), toString(cls)).toString());
			MetaDataRepository mdr = conf.getMetaDataRepositoryInstance();
			ClassMapping cm = (ClassMapping)mdr.getCachedMetaData(cls);
			if (cm == null)
				throw new RuntimeException(_loc.get("resolve-mapping-failed", toString(cls), mdr).toString());
			mapping.resolve(cm);
		}
		return mapping;
	}
	
	/**
	 * Cache the given SDO Type and PersistenceCapable Java class in 
	 * a bi-directional map.
	 */
	void cache(Type type, Class cls) {
//		if (!PersistenceCapable.class.isAssignableFrom(cls))
//			throw new RuntimeException(_loc.get("cache-unenhanced", cls, cls.getClassLoader()).toString());
		if (_cache.containsKey(type)) {
			Class oldClass = _cache.get(type);
			if (oldClass != cls) {
				if (_log.isTraceEnabled())
					_log.trace(_loc.get("cache-replace-sdo-type", 
						type.getName(), toString(cls), toString(oldClass)).toString());
			}
		} else {
			if (_log.isTraceEnabled())
				_log.trace(_loc.get("cache-sdo-type", type.getName(), toString(cls)).toString());
			_cache.put(type, cls);
		}
	}

	/**
	 * Get the Java Class corresponding to the given SDO type.
	 */
	public Class getJavaClass(Type type, boolean mustExist) {
		Class cls = _cache.get(type);
		if (cls != null)
			return cls;
		if (mustExist) {
			String fullName = ImplHelper.getFullClassName(type);
			try {
				cls = Class.forName(fullName, false, Thread.currentThread().getContextClassLoader());
				cache(type, cls);
			} catch (ClassNotFoundException ex) {
				cls = generateEnhancedClass(type);
				cache(type, cls);
			} finally {
//				ClassMetaData cm = conf.getMetaDataRepositoryInstance().getCachedMetaData(cls);
//				cm.setIdentityType(ImplHelper.getIdentityProperty(type)==null ? 
//						ClassMetaData.ID_DATASTORE : ClassMetaData.ID_APPLICATION);
			}
		}
		return cls;
	}
	
	/**
	 * Gets SDO Type corresponding to the given Java class.
	 * Will trigger parsing configured XML 
	 * Schema definitions if the named Type has not been cached.
	 */
	public Type getSDOType(Class cls, boolean mustExist) {
		Type type = _cache.getReverse(cls);
		if (type == null && mustExist) {
			MetaDataFactory mdf = conf.getMetaDataRepositoryInstance().getMetaDataFactory();
			mdf.getPersistentTypeNames(true, Thread.currentThread().getContextClassLoader());
			type = _cache.getReverse(cls);
			if (type == null)
				throw new RuntimeException(_loc.get("no-type-for-class",
					toString(cls)).toString());
		}
		return type;
	}
	
	/**
	 * Get the SDO Type of given name. Will trigger parsing configured XML 
	 * Schema definitions if the named Type has not been cached.
	 */
	public Type getType(String name) throws TypeNotFoundException {
		for (Type t:_cache.keySet())
			if (t.getName().equals(name))
				return t;
		
		MetaDataFactory mdf = conf.getMetaDataRepositoryInstance().getMetaDataFactory();
		mdf.getPersistentTypeNames(true, Thread.currentThread().getContextClassLoader());
		
		for (Type t:_cache.keySet())
				if (t.getName().equals(name))
					return t;
		
		throw new TypeNotFoundException(_loc.get("no-type-for-name", name)
			.toString());
	}
	
	private void initializeDynamicClassConstructionEnvironment() {
		if (throwAwayLoader == null) {
			if (_log.isInfoEnabled())
				_log.info(_loc.get("dynamic-enhancement").toString());
			throwAwayLoader = new BCClassLoader(new Project());
			enhancedLoader = new BCClassLoader(new Project());
			MetaDataRepository repos = conf.getMetaDataRepositoryInstance();
			repos.setValidate(MetaDataRepository.VALIDATE_NONE);
			repos.setSourceMode(MetaDataRepository.MODE_META);
		}
	}
	
	private Class generateEnhancedClass(Type type) {
		initializeDynamicClassConstructionEnvironment();
		String javaClassName = ImplHelper.getFullClassName(type);
		if (enhancedLoader.getProject().containsClass(javaClassName)) {
			return enhancedLoader.getProject().loadClass(javaClassName).getType();
		}
		_log.info(_loc.get("generate-dynamic-class", javaClassName));
		BCClass bcls = throwAwayLoader.getProject().loadClass(javaClassName,throwAwayLoader);
		TypeMapping throwAwayMapping = SDOMappingStrategy.decideStrategy(type, null);
		
		// clear the class
		bcls.clearAttributes();
		bcls.clearDeclaredFields();
		bcls.clearDeclaredInterfaces();
		bcls.clearDeclaredMethods();
		
		// annotate the class as @Entity
		bcls.makePublic();
		Annotations classAnns = bcls.getDeclaredRuntimeAnnotations(true);
		Annotation entityAnno = classAnns.addAnnotation(Entity.class);
		entityAnno.addProperty("name").setValue(type.getName());
		
		// set it for Java 5
		bcls.setMajorVersion(49); // 49 bytecode spec = 1.5 source spec
		bcls.setMinorVersion(0);
		bcls.addDefaultConstructor();
		
		// add private field with bean-style get/set methods for each property
		List properties = type.getProperties();
		for (Object p : properties) {
			Property property = (Property) p;
			PropertyMapping pm = throwAwayMapping.getMapping(property);
			BCField field = addField(bcls, pm.getFieldName(), pm.getFieldType(this));
			pm.annotate(field);
		}
		Class enhancedClass = enhanceByteCode(bcls);
		if (!isPersistenceCapable(enhancedClass))
			throw new InternalException(_loc.get("enhancement-failed", toString(enhancedClass)).toString());
		cache(type, enhancedClass);
		return enhancedClass;
	}

	Class enhanceByteCode(BCClass bcls) {
		_log.info(_loc.get("enhance-dynamic-class", bcls.getName()));
		byte[] originalBytes = bcls.toByteArray();
		BCClass ebcls = enhancedLoader.getProject().loadClass(new ByteArrayInputStream(originalBytes), 
				enhancedLoader);
		MetaDataRepository repos = conf.getMetaDataRepositoryInstance();
		PCEnhancer enhancer = new PCEnhancer(conf, ebcls, repos, enhancedLoader);
		enhancer.run();
		byte[] enhancedBytes = enhancer.getPCBytecode().toByteArray();
		enhancedLoader.getProject().removeClass(ebcls);
		ebcls = enhancedLoader.getProject().loadClass(new ByteArrayInputStream(enhancedBytes), 
				enhancedLoader);
		return reloadClass(ebcls);
	}

	Class reloadClass(BCClass bcls) {
		write(bcls);
		try {
			return Class.forName(bcls.getName());
		} catch (ClassNotFoundException e) {
			e.printStackTrace();
		}
		return null;
	}
	
	void write(BCClass bcls) {
		File classFile = getClassFile(bcls);
		classFile.deleteOnExit();
		if (_log.isInfoEnabled()) {
			_log.info(_loc.get("write-class", classFile.getAbsolutePath()).toString());
		}
		try {
			bcls.write(classFile);
		} catch (IOException e) {
			throw new UserException(e);
		}
	}

	File getClassFile(BCClass bcls) {
		File pkgdir = Files.getPackageFile(_root, bcls.getPackageName(), true);
		return new File(pkgdir, bcls.getClassName() + ".class");
	}
	
	void setRootDirectory(String rootPath) {
		File file = new File(rootPath);
		File rootDir = (file.isAbsolute()) ? file : new File(_root, rootPath);
		if (rootDir.exists() && rootDir.isDirectory()) {
				_root = rootDir;
		} else {
			_log.warn(_loc.get("wrong-root-dir", rootPath, _root.getAbsolutePath()));
		}
	}
	
    /**
     * Add bean getters and setters.
     */
    private BCField addField (BCClass bc, String fieldName, Class fieldType) {
		if (_log.isTraceEnabled())
			_log.trace(_loc.get("dynamic-method", fieldName,fieldType).toString());
        BCField field = bc.declareField(fieldName, fieldType);
        field.setAccessFlags(Constants.ACCESS_PRIVATE);

        // getter
        String getterName = "get" + StringUtils.capitalize(fieldName);
        BCMethod meth = bc.declareMethod(getterName, fieldType, null);
        meth.makePublic();
        Code code = meth.getCode(true);
        code.aload().setThis();
        code.getfield().setField(field);
        code.xreturn().setType(fieldType);
        code.calculateMaxStack();
        code.calculateMaxLocals();

        // setter
        String setterName = "set" + StringUtils.capitalize(fieldName);
        meth = bc.declareMethod(setterName, void.class, new Class[]{fieldType});
        meth.makePublic();
        code = meth.getCode(true);
        code.aload().setThis();
        code.xload().setParam(0).setType(fieldType);
        code.putfield().setField(field);
        code.vreturn();
        code.calculateMaxStack();
        code.calculateMaxLocals();
        
        return field;
    }
    
    String toString(Class cls) {
    	return (cls == null) ? "null" : cls + " loaded by " + cls.getClassLoader(); 
    }
    
    boolean isPersistenceCapable(Class cls) {
    	return PersistenceCapable.class.isAssignableFrom(cls);
    }
}
