/*
 * Copyright 2010-2015 Institut Pasteur.
 * 
 * This file is part of Icy.
 * 
 * Icy 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.
 * 
 * Icy 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 Icy. If not, see <http://www.gnu.org/licenses/>.
 */
package icy.update;

import java.io.File;
import java.util.ArrayList;

import org.w3c.dom.Document;
import org.w3c.dom.Element;
import org.w3c.dom.Node;

import icy.file.FileUtil;
import icy.update.ElementDescriptor.ElementFile;
import icy.util.StringUtil;
import icy.util.XMLUtil;
import icy.util.ZipUtil;

public class Updater
{
    public static final String ICYKERNEL_NAME = "ICY Kernel";
    public static final String ICYUPDATER_NAME = "ICY Updater";

    public static final String UPDATE_DIRECTORY = FileUtil.APPLICATION_DIRECTORY + FileUtil.separator + "update";
    public static final String BACKUP_DIRECTORY = FileUtil.APPLICATION_DIRECTORY + FileUtil.separator + "backup";
    public static final String UPDATE_BASE_NAME = "update";
    public static final String UPDATE_EXT_NAME = ".xml";
    public static final String UPDATE_NAME = UPDATE_BASE_NAME + UPDATE_EXT_NAME;
    public static final String VERSION_NAME = "version.xml";
    public static final String UPDATER_NAME = "updater.jar";

    public static final String ARG_NOSTART = "-nostart";
    public static final String ARG_UPDATE = "-update";

    private static final String ID_ELEMENTS = "elements";
    private static final String ID_ELEMENT = "element";
    private static final String ID_OBSOLETES = "obsoletes";
    private static final String ID_LOCALPATH = "localpath";

    // /**
    // * Get update elements.<br>
    // * Compare local elements with online element and return a list of element<br>
    // * which need to be updated.
    // */
    // public static ArrayList<ElementDescriptor> getUpdateElements()
    // {
    // return getUpdateElements(getLocalElements());
    // }

    // /**
    // * Update the local version.xml file so it contains only present elements with correct
    // * modification date.
    // */
    // public static boolean validateLocalElementsXML()
    // {
    // // get local elements
    // final ArrayList<ElementDescriptor> localElements = getLocalElements();
    // // validate them
    // validateLocalElements(localElements);
    // // and save to local XML file
    // return saveElementsToXML(localElements, VERSION_NAME, false);
    // }

    // public static boolean updateXML()
    // {
    // final ArrayList<ElementDescriptor> localElements = getLocalElements();
    // final ArrayList<ElementDescriptor> onlineElements = getOnlineElements();
    //
    // // update local list
    // for (ElementDescriptor onlineElement : onlineElements)
    // {
    // final ElementDescriptor localElement = findElement(onlineElement.getName(), localElements);
    // // local element absent or outdated ?
    // if (localElement == null)
    // localElements.add(onlineElement);
    // else if (onlineElement.getVersion().isGreater(localElement.getVersion()))
    // // set new version
    // localElement.setVersion(onlineElement.getVersion());
    // }
    //
    // // save new version XML file return
    // return saveElementsToXML(localElements, VERSION_NAME, false);
    // }

    /**
     * Validate the specified list of elements against local files.<br>
     * This actually remove missing files and update the file modification date.
     */
    public static void validateElements(ArrayList<ElementDescriptor> elements)
    {
        // validate elements against local files
        for (int i = elements.size() - 1; i >= 0; i--)
        {
            final ElementDescriptor element = elements.get(i);

            // validate element
            element.validate();

            // no more valid file ? --> remove element
            if (element.getFilesNumber() == 0)
                elements.remove(i);
        }
    }

    /**
     * Get the list of local elements.<br>
     * Elements are fetched from local version.xml file then validated with local files.
     */
    public static ArrayList<ElementDescriptor> getLocalElements()
    {
        // get local elements from XML file
        final ArrayList<ElementDescriptor> result = loadElementsFromXML(
                FileUtil.APPLICATION_DIRECTORY + FileUtil.separator + VERSION_NAME);

        // validate elements
        validateElements(result);

        return result;
    }

    /**
     * Get the list of online elements (online update.xml file)
     */
    public static ArrayList<ElementDescriptor> getOnlineElements()
    {
        return loadElementsFromXML(UPDATE_DIRECTORY + FileUtil.separator + UPDATE_NAME);
    }

    /**
     * Get update elements.<br>
     * Compare specified local elements with online element and return a list of element<br>
     * which need to be updated.
     */
    public static ArrayList<ElementDescriptor> getUpdateElements(ArrayList<ElementDescriptor> localElements)
    {
        final ArrayList<ElementDescriptor> result = new ArrayList<ElementDescriptor>();
        final ArrayList<ElementDescriptor> onlineElements = getOnlineElements();

        // build update list
        for (ElementDescriptor onlineElement : onlineElements)
        {
            final ElementDescriptor localElement = findElement(onlineElement.getName(), localElements);
            // get update element (differences between online and local element)
            final ElementDescriptor updateElement = ElementDescriptor.getUpdateElement(localElement, onlineElement);

            if (updateElement != null)
                // add the element to update list
                result.add(updateElement);
        }

        return result;
    }

    /**
     * Get the list of obsoletes files
     */
    public static ArrayList<String> getObsoletes()
    {
        final ArrayList<String> result = new ArrayList<String>();

        final Document document = XMLUtil.loadDocument(UPDATE_DIRECTORY + FileUtil.separator + UPDATE_NAME, false);

        if (document != null)
        {
            // TODO: check if we can really remove that
            // document.normalizeDocument();

            // get obsoletes node
            final Node obsoletes = XMLUtil.getElement(document.getDocumentElement(), ID_OBSOLETES);

            // get all local path
            final ArrayList<Node> nodesLocalpath = XMLUtil.getChildren(obsoletes, ID_LOCALPATH);
            if (nodesLocalpath != null)
            {
                for (Node n : nodesLocalpath)
                {
                    final String value = XMLUtil.getValue((Element) n, "");
                    if (!StringUtil.isEmpty(value, true))
                        result.add(value);
                }
            }
        }

        return result;
    }

    public static ArrayList<ElementDescriptor> loadElementsFromXML(String path)
    {
        final ArrayList<ElementDescriptor> result = new ArrayList<ElementDescriptor>();

        final Document document = XMLUtil.loadDocument(path, true);

        if (document != null)
        {
            // TODO: check if we can really remove that
            // document.normalizeDocument();

            // get elements node
            final Node elements = XMLUtil.getElement(document.getDocumentElement(), ID_ELEMENTS);

            // get elements
            final ArrayList<Node> nodesElement = XMLUtil.getChildren(elements, ID_ELEMENT);
            if (nodesElement != null)
            {
                for (Node n : nodesElement)
                    result.add(new ElementDescriptor(n));
            }
        }

        return result;
    }

    /**
     * Save the specified elements to the specified filename
     */
    public static boolean saveElementsToXML(ArrayList<ElementDescriptor> elements, String path, boolean onlineSave)
    {
        final Document document = XMLUtil.createDocument(true);

        final Element elementsNode = XMLUtil.addElement(document.getDocumentElement(), ID_ELEMENTS);

        // set elements
        for (ElementDescriptor element : elements)
            element.saveToNode(XMLUtil.addElement(elementsNode, ID_ELEMENT), onlineSave);

        return XMLUtil.saveDocument(document, path);
    }

    /**
     * Find an element in the specified list
     */
    public static ElementDescriptor findElement(String name, ArrayList<ElementDescriptor> list)
    {
        for (ElementDescriptor element : list)
            if (name.equals(element.getName()))
                return element;

        return null;
    }

    /**
     * Find an element from his local path in the specified list
     */
    // private static ElementDescriptor findElementFromLocalPath(String path,
    // ArrayList<ElementDescriptor> list)
    // {
    // for (ElementDescriptor element : list)
    // if (element.hasLocalPath(path))
    // return element;
    //
    // return null;
    // }

    /**
     * Return true if some update files are present in the update directory
     */
    public static boolean hasUpdateFiles()
    {
        final String[] paths = FileUtil.getFiles(UPDATE_DIRECTORY, null, true, false, false);

        for (String path : paths)
        {
            final String filename = FileUtil.getFileName(path);

            // check if we have others files other than updater and XML definitions
            if ((!filename.equals(UPDATER_NAME)) && (!filename.equals(UPDATE_NAME)))
                return true;
        }

        return false;
    }

    /**
     * Update the specified "update" element (move files from update to application directory)<br>
     * then modify local elements list according to changes made.
     * 
     * @return true if update succeed, false otherwise
     */
    public static boolean udpateElement(ElementDescriptor updateElement, ArrayList<ElementDescriptor> localElements)
    {
        // update all element files
        if (Updater.updateFiles(updateElement.getFiles()))
        {
            // then modify local elements list
            updateElementInfos(updateElement, localElements);
            return true;
        }

        return false;
    }

    /**
     * Update local elements according to changes presents in updateElement
     */
    public static void clearElementInfos(ElementDescriptor updateElement, ArrayList<ElementDescriptor> localElements)
    {
        // find corresponding current local element
        final ElementDescriptor localElement = Updater.findElement(updateElement.getName(), localElements);

        // remove it
        localElements.remove(localElement);
    }

    /**
     * Update local elements according to changes presents in updateElement
     */
    public static void updateElementInfos(ElementDescriptor updateElement, ArrayList<ElementDescriptor> localElements)
    {
        // find corresponding current local element
        final ElementDescriptor localElement = Updater.findElement(updateElement.getName(), localElements);

        // local element doesn't exist
        if (localElement == null)
            // add it
            localElements.add(updateElement);
        else
            // just update local element with update element info
            localElement.update(updateElement);
    }

    /**
     * Update the specified files
     */
    public static boolean updateFiles(ArrayList<ElementFile> files)
    {
        for (ElementFile file : files)
            // if update fails --> exit
            if (!updateFile(file))
                return false;

        return true;
    }

    /**
     * Update the specified local file
     */
    public static boolean updateFile(ElementFile file)
    {
        final String localPath = file.getLocalPath();

        // directory type file --> extract it
        if (file.isDirectory())
        {
            final String dirName = UPDATE_DIRECTORY + FileUtil.separator + localPath;
            final String zipName = dirName + ".zip";

            // rename directory type file (no extension) to zip file
            if (!FileUtil.rename(dirName, zipName, true))
                return false;
            // extract zip file
            if (!ZipUtil.extract(zipName))
                return false;
        }

        if (updateFile(localPath, file.getDateModif()))
        {
            final File dest = new File(FileUtil.APPLICATION_DIRECTORY + FileUtil.separator + localPath);

            // there is no reason the file doesn't exists but anyway...
            if (dest.exists())
            {
                if (file.isExecutable())
                    dest.setExecutable(true, false);
                if (file.isWritable())
                    if (!dest.setWritable(true, false))
                        dest.setWritable(true, true);

                return true;
            }
        }

        return false;
    }

    /**
     * Backup the specified local file
     */
    public static boolean backup(String localPath)
    {
        final String src = FileUtil.APPLICATION_DIRECTORY + FileUtil.separator + localPath;
        // file exist ? backup it
        if (FileUtil.exists(src))
        {
            final String dest = BACKUP_DIRECTORY + FileUtil.separator + localPath;

            if (!FileUtil.copy(src, dest, true, true))
                return false;

            // verify that backup file exist
            return FileUtil.exists(dest);
        }

        return true;
    }

    /**
     * Update the specified local file
     */
    public static boolean updateFile(String localPath, long dateModif)
    {
        // no update needed
        if (!needUpdate(localPath, dateModif))
            return true;

        // backup file
        if (!backup(localPath))
        {
            // backup failed
            System.err.println("Updater.udpateFile(" + localPath + ") failed :");
            // System.err.println("Cannot backup file to '" + BACKUP_DIRECTORY + FileUtil.separator
            // + localPath);
            return false;
        }

        // move file
        if (!FileUtil.rename(UPDATE_DIRECTORY + FileUtil.separator + localPath,
                FileUtil.APPLICATION_DIRECTORY + FileUtil.separator + localPath, true))
        {
            // move failed
            System.err.println("Updater.udpateFile('" + localPath + "') failed !");
            // System.err.println("Cannot rename file from '" + UPDATE_DIRECTORY +
            // FileUtil.separator + localPath
            // + "' to '" + localPath + "'");
            return false;
        }

        return true;
    }

    /**
     * Return true if specified file is different from the update file (in Update directory)
     */
    public static boolean needUpdate(String localPath, long dateModif)
    {
        final File localFile = new File(FileUtil.APPLICATION_DIRECTORY + FileUtil.separator + localPath);

        return (!localFile.exists()) || (dateModif == 0L) || (localFile.lastModified() != dateModif);
    }

    /**
     * Process to restoration (in case the update failed)
     */
    public static boolean restore()
    {
        final int len = BACKUP_DIRECTORY.length();
        // get files only (no directory)
        final String[] paths = FileUtil.getFiles(BACKUP_DIRECTORY, null, true, false, false);
        boolean result = true;

        for (String backupPath : paths)
        {
            final String finalPath = backupPath.substring(len + 1);

            // don't restore updater
            if (finalPath.equals(UPDATER_NAME))
                continue;

            if (!FileUtil.rename(backupPath, finalPath, true))
            {
                // rename failed (FileUtil.rename is already displaying error messages if needed)
                System.err.println("Updater.restore() cannot restore '" + finalPath + "', you should do it manually.");
                result = false;
            }
        }

        return result;
    }

    /**
     * Delete obsoletes files
     */
    public static void deleteObsoletes()
    {
        // delete obsolete files
        for (String obsolete : getObsoletes())
            FileUtil.delete(obsolete, false);
    }
}