/*
 * This file is part of muCommander, http://www.mucommander.com
 *
 * muCommander is free software; you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation; either version 3 of the License, or
 * (at your option) any later version.
 *
 * muCommander is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with this program.  If not, see <http://www.gnu.org/licenses/>.
 */

package com.mucommander.job.impl;

import java.io.IOException;

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

import com.mucommander.Application;
import com.mucommander.commons.file.AbstractFile;
import com.mucommander.commons.file.FileFactory;
import com.mucommander.commons.file.filter.AttributeFileFilter;
import com.mucommander.commons.file.filter.AttributeFileFilter.FileAttribute;
import com.mucommander.commons.file.filter.EqualsFilenameFilter;
import com.mucommander.commons.file.filter.ExtensionFilenameFilter;
import com.mucommander.commons.file.filter.OrFileFilter;
import com.mucommander.commons.file.util.FileSet;
import com.mucommander.commons.file.util.ResourceLoader;
import com.mucommander.commons.runtime.OsFamily;
import com.mucommander.desktop.DesktopManager;
import com.mucommander.process.ProcessRunner;
import com.mucommander.text.Translator;
import com.mucommander.ui.dialog.file.FileCollisionDialog;
import com.mucommander.ui.dialog.file.ProgressDialog;
import com.mucommander.ui.main.MainFrame;

/**
 * This job self-updates the muCommander with a new JAR file that is fetched from a specified remote file.
 * The update process boils down to the following steps:
 * <ul>
 *  <li>First, all classes of the existing JAR files are loaded in memory, to ensure that the shutdown sequence has all
 * the classes it needs, including those that haven't been used yet.</li>
 *  <li>The new JAR file is downloaded (using {@link CopyJob}) to a temporary location</li>
 *  <li>The new JAR file is moved from the temporary location to the main application JAR file, overwriting the previous
 * JAR file.
 *  <li>muCommander is restarted: this involves starting a new application instance and shutting down the current one.
 *  The specifics of this step are platform-dependant.</li>
 * </ul>
 *
 * @author Maxence Bernard
 */
public class SelfUpdateJob extends CopyJob {
	private static final Logger LOGGER = LoggerFactory.getLogger(SelfUpdateJob.class);
	
    /** The JAR file to be updated */
    private AbstractFile destJar;

    /** The temporary file where the remote JAR file is copied, before being moved to its final location */
    private AbstractFile tempDestJar;

    /** The ClassLoader to use for loading all classes from the JAR file */
    private ClassLoader classLoader;

    /** Filters directories and class files, used for loading classes from the JAR file */
    private OrFileFilter directoryOrClassFileFilter;

    /** True if classes haven't been loaded yet */ 
    private boolean loadingClasses =  true;


    public SelfUpdateJob(ProgressDialog progressDialog, MainFrame mainFrame, AbstractFile remoteJarFile) {
        this(progressDialog, mainFrame, new FileSet(remoteJarFile.getParent(), remoteJarFile), getDestJarFile());
    }

    private SelfUpdateJob(ProgressDialog progressDialog, MainFrame mainFrame, FileSet files, AbstractFile destJar) {
        this(progressDialog, mainFrame, files, destJar, getTempDestJar(destJar));
    }

    private SelfUpdateJob(ProgressDialog progressDialog, MainFrame mainFrame, FileSet files, AbstractFile destJar, AbstractFile tempDestJar) {
        super(progressDialog, mainFrame, files, tempDestJar.getParent(), tempDestJar.getName(), TransferMode.DOWNLOAD, FileCollisionDialog.OVERWRITE_ACTION);

        this.destJar = destJar;
        this.tempDestJar = tempDestJar;
        this.classLoader = getClass().getClassLoader();

        directoryOrClassFileFilter = new OrFileFilter(
            new AttributeFileFilter(FileAttribute.DIRECTORY),
            new ExtensionFilenameFilter(".class")
        );
    }

    private static AbstractFile getTempDestJar(AbstractFile destJar) {
        try {
            return createTemporaryFolder().getChild(destJar.getName());
        }
        catch(IOException e) {
            return destJar;
        }
    }

    private static AbstractFile createTemporaryFolder() {
        AbstractFile tempFolder;
        try {
            tempFolder = FileFactory.getTemporaryFile("mucomander-self-update", true);
            tempFolder.mkdir();
        }
        catch(IOException e) {
            tempFolder = FileFactory.getTemporaryFolder();
        }

        return tempFolder;
    }


    /**
     * Returns the JAR file to update.
     *
     * @return the JAR file to update
     */
    private static AbstractFile getDestJarFile() {
        return ResourceLoader.getRootPackageAsFile(SelfUpdateJob.class);
    }

    /**
     * Loads all the class files contained in the given JAR file recursively.
     *
     * @param file the JAR file from which to load the classes
     * @throws Exception if an error occurred while loading the classes
     */
    private void loadClassRecurse(AbstractFile file) throws Exception {
        if(file.isBrowsable()) {
            AbstractFile[] children = file.ls(directoryOrClassFileFilter);
            for (AbstractFile child : children)
                loadClassRecurse(child);
        }
        else {          // .class file

            String classname = file.getAbsolutePath(false);
            // Strip off the JAR file's path and ".class" extension
            classname = classname.substring(destJar.getAbsolutePath(true).length(), classname.length()-6);
            // Replace separator characters by '.'
            classname = classname.replace(destJar.getSeparator(), ".");
            // We now have a class name, e.g. "com.mucommander.Launcher"

            try {
                classLoader.loadClass(classname);

                LOGGER.trace("Loaded class "+classname);
            }
            catch(java.lang.NoClassDefFoundError e) {
                LOGGER.debug("Caught an error while loading class "+classname, e);
            }
        }
    }


    ////////////////////////
    // Overridden methods //
    ////////////////////////

    @Override
    public String getStatusString() {
        if(loadingClasses) {
            return Translator.get("version_dialog.preparing_for_update");
        }

        return super.getStatusString();
    }

    @Override
    protected void jobStarted() {
        super.jobStarted();

        try {
            // Loads all classes from the JAR file before the new JAR file is installed.
            // This will ensure that the shutdown sequence, which invokes so not-yet-loaded classes goes down smoothly.
            loadClassRecurse(destJar);
            loadingClasses = false;
        }
        catch(Exception e) {
            LOGGER.debug("Caught exception", e);

            // Todo: display an error message
            interrupt();
        }
    }

    @Override
    protected void jobCompleted() {
        try {
            AbstractFile parent;
            // Mac OS X
            if(OsFamily.MAC_OS_X.isCurrent()) {
                parent = destJar.getParent();

                // Look for an .app container that encloses the JAR file
                if(parent.getName().equals("Java")
                &&(parent=parent.getParent())!=null && parent.getName().equals("Resources")
                &&(parent=parent.getParent())!=null && parent.getName().equals("Contents")
                &&(parent=parent.getParent())!=null && "app".equals(parent.getExtension())) {

                    String appPath = parent.getAbsolutePath();

                    LOGGER.debug("Opening "+appPath);

                    // Open -W wait for the current muCommander .app to terminate, before re-opening it
                    ProcessRunner.execute(new String[]{"/bin/sh", "-c", "open -W "+appPath+" && open "+appPath});

                    return;
                }
            }
            else {
                parent = destJar.getParent();
                EqualsFilenameFilter launcherFilter;

                // Windows
                if(OsFamily.WINDOWS.isCurrent()) {
                    // Look for a muCommander.exe launcher located in the same folder as the JAR file
                    launcherFilter = new EqualsFilenameFilter("muCommander.exe", false);

                }
                // Other platforms, possibly Unix/Linux
                else {
                    // Look for a mucommander.sh located in the same folder as the JAR file
                    launcherFilter = new EqualsFilenameFilter("mucommander.sh", false);
                }

                AbstractFile[] launcherFile = parent.ls(launcherFilter);

                // If a launcher file was found, execute it
                if(launcherFile!=null && launcherFile.length==1) {
                    DesktopManager.open(launcherFile[0]);

                    return;
                }
            }

            // No platform-specific launcher found, launch the Jar directly
            ProcessRunner.execute(new String[]{"java", "-jar", destJar.getAbsolutePath()});
        }
        catch(IOException e) {
            LOGGER.debug("Caught exception", e);
            // Todo: we might want to do something about this
        }
        finally {
            Application.initiateShutdown();
        }
    }

    @Override
    protected boolean processFile(AbstractFile file, Object recurseParams) {
        if(!super.processFile(file, recurseParams))
            return false;

        // Move the file from the temporary location to its final destination
        try {
            tempDestJar.moveTo(destJar);
            return true;
        }
        catch(IOException e) {
            return false;
        }
    }
}
