/*
 * 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.lang.reflect.Method;
import java.util.List;

import org.apache.commons.lang.StringUtils;
import org.apache.openjpa.lib.util.Localizer;
import org.apache.openjpa.meta.ClassMetaData;
import org.apache.openjpa.util.UserException;

import commonj.sdo.DataObject;
import commonj.sdo.Property;
import commonj.sdo.Type;

/**
 * Static helper utilities to create method name, extract field values via 
 * reflection and more importantly process identity and version property in DataObject.
 * 
 * @author Pinaki Poddar
 * @since 0.1.0
 */
public final class ImplHelper {
	private static Localizer _loc = Localizer.forPackage(ImplHelper.class);

	/**
	 * Get the name of a method for a given property name.
	 */
	public static String getMethodName(String prefix, String name) {
		return prefix + StringUtils.capitalize(name);
	}

	/**
	 * Get the name of a method for a given property.
	 * The name of the method is suffixed with <code>List</code> if the 
	 * property is multi-valued.
	 */
	public static String getMethodName(String prefix, Property p) {
		String suffix = (p.isMany()) ? "List" : "";
		return prefix + StringUtils.capitalize(p.getName()) + suffix;
	}
		
	/**
	 * Get the value of the given property from the given persistence capable
	 * instance (not a DataObject). The property is used to determine the
	 * get method that will be invoked reflectively on the persistence capable
	 * instance to obtain the value.
	 */
//	public static Object getFieldValue(Property property, Object pc) {
//		String methodName = getMethodName("get", property);
//		try {
//			Method method = pc.getClass().getMethod(methodName, (Class[])null);
//			return method.invoke(pc, (Object[])null);
//		} catch (Exception e) {
//			throw new RuntimeException(e);
//		} 
//	}
	
	/**
	 * Get the name of Java class given the Type. The Java class name is 
	 * the type name with first letter capitalized. 
	 */
	public static String getClassName(Type type) {
		return StringUtils.capitalize(type.getName());
	}
	
	/**
	 * Gets fully qualified Java class name for a given type.
	 * Fully qualified Java class name is name of the given type prepended by
	 * the package name, if {@link #isEMFImplementaionAvailable() available}.
	 */
	public static String getFullClassName(Type type) {
		String pkg = getPackageName(type);
		String cls = getClassName(type);
		return (pkg==null || pkg.equals("")) ? cls : pkg + "." + cls;
	}
	
	/**
	 * Gets the package name for the given type, if 
	 * {@link #isEMFImplementaionAvailable() possible}.
	 */
	public static String getPackageName(Type type) {
		String result = "";
		if (type != null && "org.apache.tuscany.sdo.impl.ClassImpl".equals(
				type.getClass().getName()) && isEMFImplementaionAvailable()) {
			try {
				Object pkg = getEPackage.invoke(emfClass.cast(type), NULL_PARAMS);
				result = (String)getPackageName.invoke(pkg, NULL_PARAMS);
				if ("null".equals(result)) result = null; 
			} catch (Throwable e) {
				isEMFImplementation = Boolean.FALSE;
			}
		}
		return result;
	}
	
	/**
	 * Get the identity Property of given type.
	 * Identity property is either
	 * <OL>
	 * <LI>a) a property named <code>id</code> with simple xsd data type.
	 * <LI>b) a property with type <code>xsd:ID</code>
	 * </OL>
	 * <br>
	 * Note: Currently, multiple properties with <code>xsd:ID</code> is not
	 * considered
	 * 
	 */
	//TODO: Improve checking for identity property
	public static Property getIdentityProperty(Type type) {
		// If there is property named "id" of simple data type return it  
		Property idProperty = type.getProperty("id");
		if (idProperty != null && idProperty.getType().isDataType())
			return idProperty;
		
		// Otherwise a property with xsd:ID type (only available if EMF Core
		// implementation is available
		if (isEMFImplementaionAvailable()) {
			List properties = type.getProperties();
			for (Object p:properties) {
				Property property = (Property)p;
				if (property.getType().isDataType()) {
					try {
						Object isID = getIDMethod.invoke(
							property.getClass().cast(property), NULL_PARAMS);
						if (Boolean.TRUE.equals(isID))
							idProperty = property;
					} catch (Throwable e) {
						isEMFImplementation = Boolean.FALSE;
					}
				}
			}
		}
		return idProperty;
	}
	
	/**
	 * Affirms if the given property is identity property.
	 */
	public static boolean isIdentity(Property property) {
		if (property == null)
			return false;
		if ("id".equals(property.getName()) && property.getType().isDataType())
				return true;
		if (isEMFImplementaionAvailable() && property.getType().isDataType()) {
			try {
				Object isID = getIDMethod.invoke(
						property.getClass().cast(property), NULL_PARAMS);
				if (Boolean.TRUE.equals(isID))
					return true;
			} catch (Throwable e) {
				isEMFImplementation = Boolean.FALSE;
			}
		}
		return false;
	}
	
	/**
	 * Gets the property that represents version information.
	 * Version property is a property named <code>version</code> and declared
	 * as <code>xsd:int</code> type.
	 */
	public static Property getVersionProperty(Type type) {
		Property version = type.getProperty("version");
		if (!isVersion(version))
			throw new UserException(_loc.get("missing-version", type.getName())
					.toString());
		return version;
	}
	
	/**
	 * Affirms if the property represents version.
	 * Version property is a property named <code>version</code> and declared
	 * as <code>xsd:int</code> type.
	 */
	public static boolean isVersion(Property property) {
		return (property != null && property.getName().equals("version") &&
				property.getType().isDataType() &&
				property.getType().getInstanceClass()==int.class);
	}
	
	/**
	 * Gets the value of the {@link #getIdentityProperty(Type) identity property}
	 * of the given DataObject.
	 */
	public static Object getIdValue(DataObject data) {
		return data.get(getIdentityProperty(data.getType()));
	}
	
	/**
	 * Sets the value of the {@link #getIdentityProperty(Type) identity property}
	 * of the given DataObject.
	 */
	public static void setIdValue(DataObject data, Object idValue) {
		data.set(getIdentityProperty(data.getType()), idValue);
	}
	
	/**
	 * Gets the version of the given DataObject.
	 * @see #getVersionProperty(Type)
	 */
	public static int getVersion(DataObject data) {
		return data.getInt(getVersionProperty(data.getType()));
	}
	
	/**
	 * Sets the version of the given DataObject.
	 * @see #getVersionProperty(Type)
	 */
	public static void setVersion(DataObject data, int version) {
		data.setInt(getVersionProperty(data.getType()), version);
	}
	
	/**
	 * Affirms if this type is <em>Container</em> type. 
	 * A Type is defined as a container if 
	 * <LI>it has one and only one property and 
	 * <LI>that property is many-valued and 
	 * <LI>type of that single property is <em>not</em> a basic data type.
	 */
	public static boolean isContainer(Type type) {
		List properties = type.getProperties();
		if (properties.size()==1) {
			Property p = (Property)properties.get(0);
			return p.isMany();
		}
		return false;
	}
	
	/**
	 * If the given type is a {@link #isContainer(Type) container type} then 
	 * return the type of its one 
	 * and only one property. Otherwise null.
	 */
	public static Type getComponentType(Type type) {
		if (isContainer(type)) {
			return ((Property)type.getProperties().get(0)).getType();
		}
		return null;
	}
	
	/**
	 * If the given type is a {@link #isContainer(Type) container type} then 
	 * return the one and only one property it contains. Otherwise null.
	 */
	public static Property getComponentProperty(Type type) {
		if (isContainer(type)) {
			return (Property)type.getProperties().get(0);
		}
		return null;
	}
	
	/**
	 * Affirms if Eclipse Modeling Framework classes are available
	 * as SDO implementation.
	 * <br>
	 * These implementation classes are used to find specific information that
	 * SDO API hides; for example, whether a type has <code>xsd:ID</code> 
	 * property or package name specified in *.xsd header.
	 * <br>
	 * EMF implementation is not assumed to keep compile-time dependency at
	 * minimum. So EMF implementation is accessed via reflection at runtime.
	 * 
	 */
	public static boolean isEMFImplementaionAvailable() {
		if (isEMFImplementation != null)
			return isEMFImplementation.booleanValue();
		
		try {
			emfClass = Class.forName(EMF_CLASS);
			emfProperty = Class.forName(EMF_PROPERTY);
			getEPackage = emfClass.getMethod("getEPackage", NULL_ARGS);
			getPackageName = getEPackage.getDeclaringClass()
				.getMethod("getName", NULL_ARGS);
			getIDMethod = emfProperty.getMethod("isID", NULL_ARGS);
			isEMFImplementation = getPackageName != null;
		} catch (Exception e) {
			isEMFImplementation = Boolean.FALSE;
		}
		return isEMFImplementation.booleanValue();
	}
	private static Boolean isEMFImplementation = null;
	private static final String EMF_CLASS = 
		"org.apache.tuscany.sdo.impl.ClassImpl";
	private static final String EMF_PROPERTY = 
		"org.apache.tuscany.sdo.impl.AttributeImpl";
	private static Class emfClass = null;
	private static Class emfProperty = null;
	private static Method getEPackage = null;
	private static Method getPackageName = null;
	private static Method getIDMethod = null;
	private static Class[] NULL_ARGS = {};
	private static Object[] NULL_PARAMS = {};
}
