package railo.loader.engine;

import java.io.BufferedOutputStream;
import java.io.EOFException;
import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.io.PrintWriter;
import java.io.UnsupportedEncodingException;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.net.URL;
import java.net.URLDecoder;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Date;
import java.util.Iterator;
import java.util.List;

import javax.servlet.ServletConfig;
import javax.servlet.ServletException;

import railo.Version;
import railo.loader.TP;
import railo.loader.classloader.RailoClassLoader;
import railo.loader.util.ExtensionFilter;
import railo.loader.util.Util;

import com.intergral.fusiondebug.server.FDControllerFactory;

/**
 * Factory to load CFML Engine
 */
public class CFMLEngineFactory {
	
	 // set to false to disable patch loading, for example in major alpha releases
    private static final boolean PATCH_ENABLED = true;
    
	private static CFMLEngineFactory factory;
    private static File railoServerRoot;
    private static CFMLEngineWrapper engineListener;
    private CFMLEngine engine;
    private ClassLoader mainClassLoader=new TP().getClass().getClassLoader();
    private int version;
    private List<EngineChangeListener> listeners=new ArrayList<EngineChangeListener>();
    private File resourceRoot;

	private PrintWriter out;
    
    
    /**
     * Constructor of the class
     */
    protected CFMLEngineFactory(){
    }

    /**
     * returns instance of this factory (singelton-> always the same instance)
     * do auto update when changes occur
     * @param config 
     * @return Singelton Instance of the Factory
     * @throws ServletException 
     */
    public static CFMLEngine getInstance(ServletConfig config) throws ServletException {
        
        if(engineListener!=null) {
        	if(factory==null) factory=engineListener.getCFMLEngineFactory();
        	return engineListener;
        }
        
        if(factory==null) factory=new CFMLEngineFactory();
        
        
        // read init param from config
        factory.setInitParam(config);
        
        CFMLEngine engine = factory.getEngine();
        engine.addServletConfig(config);
        engineListener = new CFMLEngineWrapper(engine);
        
        // add listener for update
        factory.addListener(engineListener);
        return engineListener;
    }

    /**
     * returns instance of this factory (singelton-> always the same instance)
     * do auto update when changes occur
     * @return Singelton Instance of the Factory
     * @throws RuntimeException 
     */
    public static CFMLEngine getInstance() throws RuntimeException {
        if(engineListener!=null) return engineListener;
        throw new RuntimeException("engine is not initalized, you must first call getInstance(ServletConfig)");
    }

    /**
     * used only for internal usage
     * @param engine
     * @throws RuntimeException
     */
    public static void registerInstance(CFMLEngine engine) throws RuntimeException {
    	if(factory==null) factory=engine.getCFMLEngineFactory();
    	
    	// first update existing listener
    	if(engineListener!=null) {
    		if(engineListener.equalTo(engine, true)) return;
    		engineListener.onUpdate(engine);// perhaps this is still refrenced in the code, because of that we update it
    		factory.removeListener(engineListener);
    	}
    	
    	// now register this
    	if(engine instanceof CFMLEngineWrapper) 
    		engineListener=(CFMLEngineWrapper) engine;
    	else 
    		engineListener = new CFMLEngineWrapper(engine);
    	
    	factory.addListener(engineListener);	
    }
    
    
    /**
     * returns instance of this factory (singelton-> always the same instance)
     * @param config
     * @param listener 
     * @return Singelton Instance of the Factory
     * @throws ServletException 
     */
    public static CFMLEngine getInstance(ServletConfig config, EngineChangeListener listener) throws ServletException {
        getInstance(config);
        
        // add listener for update
        factory.addListener(listener);
        
        // read init param from config
        factory.setInitParam(config);
        
        CFMLEngine e = factory.getEngine();
        e.addServletConfig(config);
        
        // make the FDController visible for the FDClient
        FDControllerFactory.makeVisible();
        
        return e;
    }
    
    void setInitParam(ServletConfig config) {
        if(railoServerRoot!=null) return;
        
        String initParam=config.getInitParameter("railo-server-directory");
        if(Util.isEmpty(initParam))initParam=config.getInitParameter("railo-server-root");
        if(Util.isEmpty(initParam))initParam=config.getInitParameter("railo-server-dir");
        if(Util.isEmpty(initParam))initParam=config.getInitParameter("railo-server");
        initParam=Util.parsePlaceHolder(Util.removeQuotes(initParam,true));
        
        try {
            if(!Util.isEmpty(initParam)) {
                File root=new File(initParam);
                if(!root.exists()) {
                    if(root.mkdirs()) {
                        railoServerRoot=root.getCanonicalFile();
                        return;
                    }
                }
                else if(root.canWrite()) {
                    railoServerRoot=root.getCanonicalFile();
                    return;
                }
            }
        }
        catch(IOException ioe){}
    }
    

	/**
     * adds a listener to the factory that will be informed when a new engine will be loaded.
     * @param listener
     */
    private void addListener(EngineChangeListener listener) {
       if(!listeners.contains(listener)) {
           listeners.add(listener);
       }
    }
    
    private void removeListener(EngineChangeListener listener) {
    	listeners.remove(listener);
     }

    /**
     * @return CFML Engine
     * @throws ServletException
     */
    private CFMLEngine getEngine() throws ServletException {
        if(engine==null)initEngine();
        return engine;
    }

    private void initEngine() throws ServletException {
        
        int coreVersion=Version.getIntVersion();
        long coreCreated=Version.getCreateTime();
        
        
        // get newest railo version as file
        File patcheDir=null;
        try {
            patcheDir = getPatchDirectory();
            log("railo-server-root:"+patcheDir.getParent());
        } 
        catch (IOException e) {
           throw new ServletException(e);
        }
        
        File[] patches=PATCH_ENABLED?patcheDir.listFiles(new ExtensionFilter(new String[]{"."+getCoreExtension()})):null;
        File railo=null;
        if(patches!=null) {
            for(int i=0;i<patches.length;i++) {
                if(patches[i].getName().startsWith("tmp.rc")) {
                    patches[i].delete();
                }
                else if(patches[i].lastModified()<coreCreated) {
                    patches[i].delete();
                }
                else if(railo==null || isNewerThan(Util.toInVersion(patches[i].getName()),Util.toInVersion(railo.getName()))) {
                    railo=patches[i];
                }
            }
        }
        if(railo!=null && isNewerThan(coreVersion,Util.toInVersion(railo.getName())))railo=null;
        
        // Load Railo
        //URL url=null;
        try {
            // Load core version when no patch available
            if(railo==null) {
            	tlog("Load Build in Core");
                // 
                String coreExt=getCoreExtension();
                engine=getCore(coreExt);
            	
                
                railo=new File(patcheDir,engine.getVersion()+"."+coreExt);
               if(PATCH_ENABLED) {
	                InputStream bis = new TP().getClass().getResourceAsStream("/core/core."+coreExt);
	                OutputStream bos=new BufferedOutputStream(new FileOutputStream(railo));
	                Util.copy(bis,bos);
	                Util.closeEL(bis,bos);
                }
            }
            else {
            	try {
            		engine=getEngine(new RailoClassLoader(railo,mainClassLoader));
            	}
            	catch(EOFException e) {
            		System.err.println("Railo patch file "+railo+" is invalid, please delete it");
            		engine=getCore(getCoreExtension());
            	}
            }
            version=Util.toInVersion(engine.getVersion());
            
            tlog("Loaded Railo Version "+engine.getVersion());
        }
        catch(InvocationTargetException e) {
            e.getTargetException().printStackTrace();
            throw new ServletException(e.getTargetException());
        }
        catch(Exception e) {
            e.printStackTrace();
            throw new ServletException(e);
        }
        
        //check updates
        String updateType=engine.getUpdateType();
        if(updateType==null || updateType.length()==0)updateType="manuell";
        
        if(updateType.equalsIgnoreCase("auto")) {
            new UpdateChecker(this).start();
        }
        
    }
    

    private String getCoreExtension() throws ServletException {
    	URL res = new TP().getClass().getResource("/core/core.rcs");
        if(res!=null) return "rcs";
        
        res = new TP().getClass().getResource("/core/core.rc");
        if(res!=null) return "rc";
        
        throw new ServletException("missing core file");
	}

	private CFMLEngine getCore(String ext) throws SecurityException, IllegalArgumentException, ClassNotFoundException, NoSuchMethodException, IllegalAccessException, InvocationTargetException, IOException {
    	InputStream is = null;
    	try {
    		is = new TP().getClass().getResourceAsStream("/core/core."+ext);
    		RailoClassLoader classLoader=new RailoClassLoader(is,mainClassLoader,ext.equalsIgnoreCase("rcs"));
    		return getEngine(classLoader);
    	}
    	finally {
    		Util.closeEL(is);
    	}
	}

	/**
     * method to initalize a update of the CFML Engine.
     * checks if there is a new Version and update it whwn a new version is available
     * @param password
     * @return has updated
     * @throws IOException
     * @throws ServletException 
     */
    public boolean update(String password) throws IOException, ServletException {
        if(!engine.can(CFMLEngine.CAN_UPDATE,password))
            throw new IOException("access denied to update CFMLEngine");
        //new RunUpdate(this).start();
        return update();
    }

    /**
     * restart the cfml engine
     * @param password
     * @return has updated
     * @throws IOException 
     * @throws ServletException 
     */
    public boolean restart(String password) throws IOException, ServletException {
        if(!engine.can(CFMLEngine.CAN_RESTART_ALL,password))
            throw new IOException("access denied to restart CFMLEngine");
        
        return _restart();
    }

    /**
     * restart the cfml engine
     * @param password
     * @return has updated
     * @throws IOException 
     * @throws ServletException 
     */
    public boolean restart(String configId, String password) throws IOException, ServletException {
        if(!engine.can(CFMLEngine.CAN_RESTART_CONTEXT,password))// TODO restart single context
            throw new IOException("access denied to restart CFML Context (configId:"+configId+")");
        
        return _restart();
    }
    
    /**
     * restart the cfml engine
     * @param password
     * @return has updated
     * @throws IOException 
     * @throws ServletException 
     */
    private synchronized boolean _restart() throws ServletException {
        engine.reset();
        initEngine();
        registerInstance(engine);
        callListeners(engine);
        System.gc(); 
        System.gc();
        return true;
    }

	/**
     * updates the engine when a update is available
     * @return has updated
     * @throws IOException
     * @throws ServletException
     */
    private boolean update() throws IOException, ServletException {
    	
        URL hostUrl=getEngine().getUpdateLocation();
        if(hostUrl==null)hostUrl=new URL("http://www.getrailo.org");
        URL infoUrl=new URL(hostUrl,"/railo/remote/version/info.cfm?ext="+getCoreExtension()+"&version="+version);// FUTURE replace with Info.cfc or better move the functionality to core if possible. something like engine.getUpdater a class provided by the core and defined (interface) by the loader.
        
        tlog("Check for update at "+hostUrl);
        
        String availableVersion = Util.toString((InputStream)infoUrl.getContent()).trim();
        
        if(availableVersion.length()!=9) throw new IOException("can't get update info from ["+infoUrl+"]");
        if(!isNewerThan(Util.toInVersion(availableVersion),version)) {
            tlog("There is no newer Version available");
            return false;
        }
        
        tlog("Found a newer Version \n - current Version "+Util.toStringVersion(version)+"\n - available Version "+availableVersion);
        
        URL updateUrl=new URL(hostUrl,"/railo/remote/version/update.cfm?ext="+getCoreExtension()+"&version="+availableVersion);
        File patchDir=getPatchDirectory();
        File newRailo=new File(patchDir,availableVersion+("."+getCoreExtension()));//isSecure?".rcs":".rc"
        
        if(newRailo.createNewFile()) {
            Util.copy((InputStream)updateUrl.getContent(),new FileOutputStream(newRailo));  
        }
        else {
            tlog("File for new Version already exists, won't copy new one");
            return false;
        }
        try {
        engine.reset();
        }
        catch(Throwable t) {
        	t.printStackTrace();
        }
        
        // Test new railo version valid
        //FileClassLoader classLoader=new FileClassLoader(newRailo,mainClassLoader);
        RailoClassLoader classLoader=new RailoClassLoader(newRailo,mainClassLoader);
        //URLClassLoader classLoader=new URLClassLoader(new URL[]{newRailo.toURL()},mainClassLoader);
        String v="";
        try {
            CFMLEngine e = getEngine(classLoader);
            if(e==null)throw new IOException("can't load engine");
            v=e.getVersion();
            engine=e;
            version=Util.toInVersion(v);
            //e.reset();
            callListeners(e);
        }
        catch (Exception e) {
            classLoader=null;
            System.gc();
            try {
                newRailo.delete();
            }
            catch(Exception ee){}
            tlog("There was a Problem with the new Version, can't install ("+e+":"+e.getMessage()+")");
            e.printStackTrace();
            return false;
        }
        
        tlog("Version ("+v+")installed");
        return true;
    }
    
    
    /**
     * method to initalize a update of the CFML Engine.
     * checks if there is a new Version and update it whwn a new version is available
     * @param password
     * @return has updated
     * @throws IOException
     * @throws ServletException 
     */
    public boolean removeUpdate(String password) throws IOException, ServletException {
        if(!engine.can(CFMLEngine.CAN_UPDATE,password))
            throw new IOException("access denied to update CFMLEngine");
        return removeUpdate();
    }
    

    /**
     * method to initalize a update of the CFML Engine.
     * checks if there is a new Version and update it whwn a new version is available
     * @param password
     * @return has updated
     * @throws IOException
     * @throws ServletException 
     */
    public boolean removeLatestUpdate(String password) throws IOException, ServletException {
        if(!engine.can(CFMLEngine.CAN_UPDATE,password))
            throw new IOException("access denied to update CFMLEngine");
        return removeLatestUpdate();
    }
    
    
    
    /**
     * updates the engine when a update is available
     * @return has updated
     * @throws IOException
     * @throws ServletException
     */
    private boolean removeUpdate() throws IOException, ServletException {
        File patchDir=getPatchDirectory();
        File[] patches=patchDir.listFiles(new ExtensionFilter(new String[]{"railo","rc","rcs"}));
        
        for(int i=0;i<patches.length;i++) {
        	if(!patches[i].delete())patches[i].deleteOnExit();
        }
        _restart();
        return true;
    }
    

    private boolean removeLatestUpdate() throws IOException, ServletException {
        File patchDir=getPatchDirectory();
        File[] patches=patchDir.listFiles(new ExtensionFilter(new String[]{"."+getCoreExtension()}));
        File patch=null;
        for(int i=0;i<patches.length;i++) {
        	 if(patch==null || isNewerThan(Util.toInVersion(patches[i].getName()),Util.toInVersion(patch.getName()))) {
                 patch=patches[i];
             }
        }
    	if(patch!=null && !patch.delete())patch.deleteOnExit();
        
        _restart();
        return true;
    }
    

	public String[] getInstalledPatches() throws ServletException, IOException {
		File patchDir=getPatchDirectory();
        File[] patches=patchDir.listFiles(new ExtensionFilter(new String[]{"."+getCoreExtension()}));
        
        List<String> list=new ArrayList<String>();
        String name;
        int extLen=getCoreExtension().length()+1;
        for(int i=0;i<patches.length;i++) {
        	name=patches[i].getName();
        	name=name.substring(0, name.length()-extLen);
        	 list.add(name);
        }
        String[] arr = list.toArray(new String[list.size()]);
    	Arrays.sort(arr);
        return arr;
	}
    

    /**
     * call all registred listener for update of the engine
     * @param engine
     */
    private void callListeners(CFMLEngine engine) {
        Iterator<EngineChangeListener> it = listeners.iterator();
        while(it.hasNext()) {
            it.next().onUpdate(engine);
        }
    }
    

    private File getPatchDirectory() throws IOException {
        File pd = new File(getResourceRoot(),"patches");
        if(!pd.exists())pd.mkdirs();
        return pd;
    }

    /**
     * return directory to railo resource root
     * @return railo root directory
     * @throws IOException
     */
    public File getResourceRoot() throws IOException {
        if(resourceRoot==null) {
            resourceRoot=new File(getRuningContextRoot(),"railo-server");
            if(!resourceRoot.exists()) resourceRoot.mkdirs();
        }
        return resourceRoot;
    }
    
    /**
     * @return return running context root
     * @throws IOException 
     * @throws IOException 
     */
    private File getRuningContextRoot() throws IOException {
        
        if(railoServerRoot!=null) {
            return railoServerRoot;
        }
        File dir=getClassLoaderRoot(mainClassLoader);
        dir.mkdirs();
        if(dir.exists() && dir.isDirectory()) return dir;
        
            
           
        throw new IOException("can't create/write to directory ["+dir+"], set \"init-param\" \"railo-server-directory\" with path to writable directory");
    }
    /**
     * returns the path where the classloader is located
     * @param cl ClassLoader
     * @return file of the classloader root
     */
    public static File getClassLoaderRoot(ClassLoader cl) {
        String path="railo/loader/engine/CFMLEngine.class";
        URL res = cl.getResource(path);
       
        // get file and remove all after !
        String strFile=null;
		try {
			strFile = URLDecoder.decode(res.getFile().trim(),"iso-8859-1");
		} catch (UnsupportedEncodingException e) {
			
		}
        int index=strFile.indexOf('!');
        if(index!=-1)strFile=strFile.substring(0,index);
        
        // remove path at the end
        index=strFile.lastIndexOf(path);
        if(index!=-1)strFile=strFile.substring(0,index);
        
        // remove "file:" at start and railo.jar at the end
        if(strFile.startsWith("file:"))strFile=strFile.substring(5);
        if(strFile.endsWith("railo.jar")) strFile=strFile.substring(0,strFile.length()-9);
		
        File file=new File(strFile);
        if(file.isFile())file=file.getParentFile();
        
        return file;
    }

    /**
     * check left value against right value
     * @param left
     * @param right
     * @return returns if right is newer than left
     */
    private boolean isNewerThan(int left, int right) {
        return left>right;
    }
  
    /**
     * Load CFMl Engine Implementation (railo.runtime.engine.CFMLEngineImpl) from a Classloader
     * @param classLoader
     * @return loaded CFML Engine
     * @throws ClassNotFoundException 
     * @throws NoSuchMethodException 
     * @throws SecurityException 
     * @throws InvocationTargetException 
     * @throws IllegalAccessException 
     * @throws IllegalArgumentException 
     */
    private CFMLEngine getEngine(ClassLoader classLoader) throws ClassNotFoundException, SecurityException, NoSuchMethodException, IllegalArgumentException, IllegalAccessException, InvocationTargetException {
        Class clazz=classLoader.loadClass("railo.runtime.engine.CFMLEngineImpl");
        Method m = clazz.getMethod("getInstance",new Class[]{CFMLEngineFactory.class});
        return (CFMLEngine) m.invoke(null,new Object[]{this});
        
    }

    /**
     * log info to output
     * @param obj Object to output
     */
    public void tlog(Object obj) {
    	log(new Date()+ " "+obj);
    }
    
    /**
     * log info to output
     * @param obj Object to output
     */
    public void log(Object obj) {
    	if(out==null){
    		boolean isCLI=false;
    		String str=System.getProperty("railo.cli.call");
    		if(!Util.isEmpty(str, true)) {
    			str=str.trim();
    			isCLI="true".equalsIgnoreCase(str) || "yes".equalsIgnoreCase(str);
    			
    		}
    		
    		if(isCLI) {
    			try{
    				File dir = new File(getResourceRoot(),"logs");
    				dir.mkdirs();
    				File file = new File(dir,"out");
        			
    			file.createNewFile();
    			out=new PrintWriter(file);
    			}
    			catch(Throwable t){t.printStackTrace();}
    		}
    		if(out==null)out=new PrintWriter(System.out);
    	}
    	out.write(""+obj+"\n");   
    	out.flush();
    }
    
    private class UpdateChecker extends Thread {
        private CFMLEngineFactory factory;

        private UpdateChecker(CFMLEngineFactory factory) {
            this.factory=factory;
        }
        
        public void run() {
            long time=10000;
            while(true) {
                try {
                    sleep(time);
                    time=1000*60*60*24;
                    factory.update();
                    
                } catch (Exception e) {
                    
                }
            }
        }
    }

}
