package railo.commons.lang;

import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.lang.reflect.Constructor;
import java.lang.reflect.Field;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.net.URL;
import java.net.URLClassLoader;
import java.net.URLDecoder;
import java.util.Map;
import java.util.Set;

import railo.commons.collection.MapFactory;
import railo.commons.io.FileUtil;
import railo.commons.io.IOUtil;
import railo.commons.io.SystemUtil;
import railo.runtime.PageContextImpl;
import railo.runtime.config.Config;
import railo.runtime.engine.ThreadLocalPageContext;
import railo.runtime.exp.PageException;
import railo.runtime.op.Caster;
import railo.runtime.type.Array;
import railo.runtime.type.util.ListUtil;


public final class ClassUtil {

	/**
	 * @param className
	 * @return
	 * @throws ClassException 
	 * @throws PageException
	 */
	public static Class toClass(String className) throws ClassException {
		return ClassUtil.loadClass(className);
	}
	
	private static Class checkPrimaryTypes(String className, Class defaultValue) {
		String lcClassName=className.toLowerCase();
		boolean isRef=false;
		
		if(lcClassName.startsWith("java.lang.")){
			lcClassName=lcClassName.substring(10);
			isRef=true;
		}

		if(lcClassName.equals("boolean") || className.equals("[Z"))	{ 
			if(isRef) return Boolean.class;
			return boolean.class; 
		}
		if(lcClassName.equals("byte") || className.equals("[B"))	{
			if(isRef) return Byte.class;
			return byte.class; 
		}
		if(lcClassName.equals("int") || className.equals("[I"))	{
			return int.class; 
		}
		if(lcClassName.equals("long") || className.equals("[J"))	{
			if(isRef) return Long.class;
			return long.class; 
		}
		if(lcClassName.equals("float") || className.equals("[F"))	{
			if(isRef) return Float.class;
			return float.class; 
		}
		if(lcClassName.equals("double") || className.equals("[D"))	{
			if(isRef) return Double.class;
			return double.class; 
		}
		if(lcClassName.equals("char") || className.equals("[C"))	{
			return char.class; 
		}
		if(lcClassName.equals("short") || className.equals("[S"))	{
			if(isRef) return Short.class;
			return short.class; 
		}
		
		if(lcClassName.equals("integer"))	return Integer.class; 
		if(lcClassName.equals("character"))	return Character.class; 
		if(lcClassName.equals("object"))	return Object.class; 
		if(lcClassName.equals("string"))	return String.class; 
		if(lcClassName.equals("null"))		return Object.class; 
		if(lcClassName.equals("numeric"))	return Double.class; 
		
		return defaultValue;
	}
	
	
	
	/**
	 * loads a class from a String classname
	 * @param className
	 * @param defaultValue 
	 * @return matching Class
	 */
	public static Class loadClass(String className, Class defaultValue) {
		return loadClass(null,className,defaultValue);
	}

	/**
	 * loads a class from a String classname
	 * @param className
	 * @return matching Class
	 * @throws ClassException 
	 */
	public static Class loadClass(String className) throws ClassException {
		Config config = ThreadLocalPageContext.getConfig();
		Class clazz = loadClass(config==null?null:config.getClassLoader(),className,null);
		if(clazz!=null) return clazz;
		throw new ClassException("cannot load class through its string name, because no definition for the class with the specified name ["+className+"] could be found");
	}
	
	/**
	 * loads a class from a specified Classloader with given classname
	 * @param className
	 * @param cl 
	 * @return matching Class
	 */
	public static Class loadClass(ClassLoader cl,String className, Class defaultValue) {
		className=className.trim();
		
		Class clazz = checkPrimaryTypes(className, null);
		if(clazz!=null) return clazz;
		
		if(cl==null){
			PageContextImpl pci = (PageContextImpl) ThreadLocalPageContext.get();
			if(pci!=null){
				try {
					cl=pci.getClassLoader();
				}
				catch (IOException e) {}
			}
			if(cl==null) {
				Config config = ThreadLocalPageContext.getConfig();
				if(config!=null)cl=config.getClassLoader();
			}
		}
		
		
		
		try {
			if(cl==null)return Class.forName(className.trim());
			return cl.loadClass(className.trim());
			
		}
		catch (ClassNotFoundException e) {
			try {
				return Class.forName(className, false, cl);
			} 
			catch (ClassNotFoundException e1) {
				// array in the format boolean[] or java.lang.String[]
				if(!StringUtil.isEmpty(className) && className.endsWith("[]")) {
					StringBuilder pureCN=new StringBuilder(className);
					int dimensions=0;
					do{
						pureCN.delete(pureCN.length()-2, pureCN.length());
						dimensions++;
					}
					while(pureCN.lastIndexOf("[]")==pureCN.length()-2);
					
					clazz = loadClass(cl,pureCN.toString(),null);
					if(clazz!=null) {
						for(int i=0;i<dimensions;i++)clazz=toArrayClass(clazz);
						return clazz;
					}
				}
				// array in the format [C or [Ljava.lang.String;
				else if(!StringUtil.isEmpty(className) && className.charAt(0)=='[') {
					StringBuilder pureCN=new StringBuilder(className);
					int dimensions=0;
					do{
						pureCN.delete(0, 1);
						dimensions++;
					}
					while(pureCN.charAt(0)=='[');
					
					clazz = loadClass(cl,pureCN.toString(),null);
					if(clazz!=null) {
						for(int i=0;i<dimensions;i++)clazz=toArrayClass(clazz);
						return clazz;
					}
				}
				// class in format Ljava.lang.String;
				else if(!StringUtil.isEmpty(className) && className.charAt(0)=='L' && className.endsWith(";")) {
					className=className.substring(1,className.length()-1).replace('/', '.');
					return loadClass(cl, className,defaultValue);
				}
				
				return defaultValue;
			}
		}
	}

	/**
	 * loads a class from a specified Classloader with given classname
	 * @param className
	 * @param cl 
	 * @return matching Class
	 * @throws ClassException 
	 */
	public static Class loadClass(ClassLoader cl,String className) throws ClassException {
		Class clazz = loadClass(cl,className,null);
		if(clazz!=null) return clazz;
		throw new ClassException("cannot load class through its string name, because no definition for the class with the specified name ["+className+"] could be found");
	}

	/**
	 * loads a class from a String classname
	 * @param clazz class to load
	 * @return matching Class
	 * @throws ClassException 
	 */
	public static Object loadInstance(Class clazz) throws ClassException{
		try {
			return clazz.newInstance();
		}
		catch (InstantiationException e) {
			throw new ClassException("the specified class object ["+clazz.getName()+"()] cannot be instantiated");
		}
		catch (IllegalAccessException e) {
			throw new ClassException("can't load class because the currently executing method does not have access to the definition of the specified class");
		}
	}

	public static Object loadInstance(String className) throws ClassException{
		return loadInstance(loadClass(className));
	}
	public static Object loadInstance(ClassLoader cl, String className) throws ClassException{
		return loadInstance(loadClass(cl,className));
	}
	
	/**
	 * loads a class from a String classname
	 * @param clazz class to load
	 * @return matching Class
	 */
	public static Object loadInstance(Class clazz, Object defaultValue){
		try {
			return clazz.newInstance();
		}
		catch (Throwable t) {
			return defaultValue;
		}
	}
	
	public static Object loadInstance(String className, Object deaultValue){
		Class clazz = loadClass(className,null);
		if(clazz==null) return deaultValue;
		return loadInstance(clazz,deaultValue);
	}
	
	public static Object loadInstance(ClassLoader cl, String className, Object deaultValue) {
		Class clazz = loadClass(cl,className,null);
		if(clazz==null) return deaultValue;
		return loadInstance(clazz,deaultValue);
	}
	
	/**
	 * loads a class from a String classname
	 * @param clazz class to load
	 * @param args 
	 * @return matching Class
	 * @throws ClassException 
	 * @throws ClassException 
	 * @throws InvocationTargetException 
	 */
	public static Object loadInstance(Class clazz, Object[] args) throws ClassException, InvocationTargetException {
		if(args==null || args.length==0) return loadInstance(clazz);
		
		Class[] cArgs=new Class[args.length];
		for(int i=0;i<args.length;i++) {
			cArgs[i]=args[i].getClass();
		}
		
		try {
			Constructor c = clazz.getConstructor(cArgs);
			return c.newInstance(args);
			
		}
		catch (SecurityException e) {
			throw new ClassException("there is a security violation (throwed by security manager)");
		}
		catch (NoSuchMethodException e) {
			
			StringBuilder sb=new StringBuilder(clazz.getName());
			char del='(';
			for(int i=0;i<cArgs.length;i++) {
				sb.append(del);
				sb.append(cArgs[i].getName());
				del=',';
			}
			sb.append(')');
			
			throw new ClassException("there is no constructor with this ["+sb+"] signature for the class ["+clazz.getName()+"]");
		}
		catch (IllegalArgumentException e) {
			throw new ClassException("has been passed an illegal or inappropriate argument");
		}
		catch (InstantiationException e) {
			throw new ClassException("the specified class object ["+clazz.getName()+"] cannot be instantiated because it is an interface or is an abstract class");
		}
		catch (IllegalAccessException e) {
			throw new ClassException("can't load class because the currently executing method does not have access to the definition of the specified class");
		}
	}

	public static Object loadInstance(String className, Object[] args) throws ClassException, InvocationTargetException{
		return loadInstance(loadClass(className),args);
	}
	
	public static Object loadInstance(ClassLoader cl, String className, Object[] args) throws ClassException, InvocationTargetException{
		return loadInstance(loadClass(cl,className),args);
	}
	
	/**
	 * loads a class from a String classname
	 * @param clazz class to load
	 * @param args 
	 * @return matching Class
	 */
	public static Object loadInstance(Class clazz, Object[] args, Object defaultValue) {
		if(args==null || args.length==0) return loadInstance(clazz,defaultValue);
		try {
			Class[] cArgs=new Class[args.length];
			for(int i=0;i<args.length;i++) {
				if(args[i]==null)cArgs[i]=Object.class;
				else cArgs[i]=args[i].getClass();
			}
			Constructor c = clazz.getConstructor(cArgs);
			return c.newInstance(args);
			
		}
		catch (Throwable t) {//print.printST(t);
			return defaultValue;
		}
		
	}
	
	public static Object loadInstance(String className, Object[] args, Object deaultValue){
		Class clazz = loadClass(className,null);
		if(clazz==null) return deaultValue;
		return loadInstance(clazz,args,deaultValue);
	}
	
	public static Object loadInstance(ClassLoader cl, String className, Object[] args, Object deaultValue) {
		Class clazz = loadClass(cl,className,null);
		if(clazz==null) return deaultValue;
		return loadInstance(clazz,args,deaultValue);
	}
	
	/**
	 * @return returns a string array of all pathes in classpath
	 */
	public static String[] getClassPath(Config config) {

        Map<String,String> pathes=MapFactory.<String,String>getConcurrentMap();
		String pathSeperator=System.getProperty("path.separator");
		if(pathSeperator==null)pathSeperator=";";
			
	// pathes from system properties
		String strPathes=System.getProperty("java.class.path");
		if(strPathes!=null) {
			Array arr=ListUtil.listToArrayRemoveEmpty(strPathes,pathSeperator);
			int len=arr.size();
			for(int i=1;i<=len;i++) {
				File file=FileUtil.toFile(Caster.toString(arr.get(i,""),"").trim());
				if(file.exists())
					try {
						pathes.put(file.getCanonicalPath(),"");
					} catch (IOException e) {}
			}
		}
		
		
	// pathes from url class Loader (dynamic loaded classes)
		getClassPathesFromLoader(new ClassUtil().getClass().getClassLoader(), pathes);
		getClassPathesFromLoader(config.getClassLoader(), pathes);
		
		Set set = pathes.keySet();
		return (String[]) set.toArray(new String[set.size()]);
	}
	
	/**
	 * get class pathes from all url ClassLoaders
	 * @param cl URL Class Loader
	 * @param pathes Hashmap with allpathes
	 */
	private static void getClassPathesFromLoader(ClassLoader cl, Map pathes) {
		if(cl instanceof URLClassLoader) 
			_getClassPathesFromLoader((URLClassLoader) cl, pathes);
	}
		
	
	private static void _getClassPathesFromLoader(URLClassLoader ucl, Map pathes) {
		getClassPathesFromLoader(ucl.getParent(), pathes);
		
		// get all pathes
		URL[] urls=ucl.getURLs();
		
		for(int i=0;i<urls.length;i++) {
			File file=FileUtil.toFile(urls[i].getPath());
			if(file.exists())
				try {
					pathes.put(file.getCanonicalPath(),"");
				} catch (IOException e) {}
		}
	}
	
	// CafeBabe (Java Magic Number)
	private static final int ICA=202;//CA 
    private static final int IFE=254;//FE
    private static final int IBA=186;//BA
    private static final int IBE=190;//BE
    
    // CF33 (Railo Magic Number)
    private static final int ICF=207;//CF 
    private static final int I33=51;//33
    

	private static final byte BCA=(byte)ICA;//CA 
    private static final byte BFE=(byte)IFE;//FE
    private static final byte BBA=(byte)IBA;//BA
    private static final byte BBE=(byte)IBE;//BE
    
    private static final byte BCF=(byte)ICF;//CF 
    private static final byte B33=(byte)I33;//33
    
    
    /** 
     * check if given stream is a bytecode stream, if yes remove bytecode mark 
     * @param is 
     * @return is bytecode stream 
     * @throws IOException 
     */ 
    public static boolean isBytecode(InputStream is) throws IOException { 
            if(!is.markSupported()) 
                    throw new IOException("can only read input streams that support mark/reset"); 
            is.mark(-1); 
            //print(bytes);
            int first=is.read();
            int second=is.read();
             boolean rtn=(first==ICF && second==I33) || (first==ICA && second==IFE && is.read()==IBA && is.read()==IBE);
            
        is.reset(); 
        return rtn; 
    }
    

    public static boolean isBytecode(byte[] barr){ 
        if(barr.length<4) return false;
        return (barr[0]==BCF && barr[1]==B33) || (barr[0]==BCA && barr[1]==BFE && barr[2]==BBA && barr[3]==BBE); 
    }
    public static boolean isRawBytecode(byte[] barr){ 
        if(barr.length<4) return false;
        return (barr[0]==BCA && barr[1]==BFE && barr[2]==BBA && barr[3]==BBE); 
    }
    
    public static boolean hasCF33Prefix(byte[] barr) { 
        if(barr.length<4) return false;
        return (barr[0]==BCF && barr[1]==B33); 
    }
    
	public static byte[] removeCF33Prefix(byte[] barr) {
		if(!hasCF33Prefix(barr)) return barr;
    	
		byte[] dest = new byte[barr.length-10];
		System.arraycopy(barr, 10, dest, 0, 10);
		return dest;
	}

	public static String getName(Class clazz) {
		if(clazz.isArray()){
			return getName(clazz.getComponentType())+"[]";
		}
		
		return clazz.getName();
	}

	public static Method getMethodIgnoreCase(Class clazz, String methodName, Class[] args) throws ClassException {
		Method[] methods = clazz.getMethods();
		Method method;
		Class[] params;
		outer:for(int i=0;i<methods.length;i++){
			method=methods[i];
			if(method.getName().equalsIgnoreCase(methodName)){
				params = method.getParameterTypes();
				if(params.length==args.length){
					for(int y=0;y<params.length;y++){
						if(!params[y].equals(args[y])){
							continue outer;
						}
					}
					return method;
				}
			}
		}
		
		throw new ClassException("class "+clazz.getName()+" has no method with name "+methodName);
	}

	
	/**
	 * return all field names as String array
	 * @param clazz class to get field names from
	 * @return field names
	 */
	public static String[] getFieldNames(Class clazz) {
		Field[] fields = clazz.getFields();
		String[] names=new String[fields.length];
		for(int i=0;i<names.length;i++){
			names[i]=fields[i].getName();
		}
		return names;
	}

	public static byte[] toBytes(Class clazz) throws IOException {
		return IOUtil.toBytes(clazz.getClassLoader().getResourceAsStream(clazz.getName().replace('.','/')+".class"),true);
	}

	/**
	 * return a array class based on the given class (opposite from Class.getComponentType())
	 * @param clazz
	 * @return
	 */
	public static Class toArrayClass(Class clazz) {
		return java.lang.reflect.Array.newInstance(clazz, 0).getClass();
	}



	public static Class<?> toComponentType(Class<?> clazz) {
		Class<?> tmp;
		while(true){
			tmp=clazz.getComponentType();
			if(tmp==null) break;
			clazz=tmp;
		}
		return clazz;
	}


	/**
	 * returns the path to the directory or jar file that the class was loaded from
	 *
	 * @param clazz - the Class object to check, for a live object pass obj.getClass();
	 * @param defaultValue - a value to return in case the source could not be determined
	 * @return
	 */
	public static String getSourcePathForClass(Class clazz, String defaultValue) {

		try {

			String result = clazz.getProtectionDomain().getCodeSource().getLocation().getPath();
			result = URLDecoder.decode(result, Charset.UTF8);
			result = SystemUtil.fixWindowsPath(result);
			return result;
		}
		catch (Throwable t) {}

		return defaultValue;
	}


	/**
	 * tries to load the class and returns the path that it was loaded from
	 *
	 * @param className - the name of the class to check
	 * @param defaultValue - a value to return in case the source could not be determined
	 * @return
	 */
	public static String getSourcePathForClass(String className, String defaultValue) {

		try {

			return  getSourcePathForClass(ClassUtil.loadClass(className), defaultValue);
		}
		catch (Throwable t) {}

		return defaultValue;
	}


}