/*
 * 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 icy.common.Version;
import icy.file.FileUtil;
import icy.file.xml.XMLPersistent;
import icy.util.StringUtil;
import icy.util.XMLUtil;

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

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

/**
 * @author stephane
 */
public class ElementDescriptor implements XMLPersistent
{
    private static final String ID_NAME = "name";
    private static final String ID_VERSION = "version";
    private static final String ID_FILES = "files";
    private static final String ID_FILE = "file";
    private static final String ID_LINK = "link";
    private static final String ID_EXECUTE = "execute";
    private static final String ID_WRITE = "write";
    private static final String ID_DIRECTORY = "directory";
    private static final String ID_FILENUMBER = "fileNumber";
    private static final String ID_DATEMODIF = "datemodif";
    private static final String ID_LOCALPATH = "localpath";
    private static final String ID_ONLINEPATH = "onlinepath";
    private static final String ID_CHANGESLOG = "changeslog";

    public class ElementFile implements XMLPersistent
    {
        private String localPath;
        private String onlinePath;

        /**
         * symbolic link element, onlinePath define the target of the link file
         */
        private boolean link;

        /**
         * need execute permission
         */
        private boolean executable;

        /**
         * need write permission
         */
        private boolean writable;

        /**
         * directory file.
         */
        private boolean directory;

        /**
         * date of modification
         */
        private long dateModif;

        /**
         * number of file (for directory only, -1 = don't check file number)
         */
        private int fileNumber;

        /**
         * 
         */
        public ElementFile(Node node)
        {
            super();

            loadFromXML(node);
        }

        /**
         * Create a new element file using specified element informations
         */
        public ElementFile(ElementFile elementFile)
        {
            super();

            localPath = elementFile.localPath;
            onlinePath = elementFile.onlinePath;
            dateModif = elementFile.dateModif;
            link = elementFile.link;
            executable = elementFile.executable;
            writable = elementFile.writable;
            directory = elementFile.directory;
            fileNumber = elementFile.fileNumber;
        }

        @Override
        public boolean loadFromXML(Node node)
        {
            if (node == null)
                return false;

            localPath = XMLUtil.getElementValue(node, ID_LOCALPATH, "");
            onlinePath = XMLUtil.getElementValue(node, ID_ONLINEPATH, "");
            dateModif = XMLUtil.getElementLongValue(node, ID_DATEMODIF, 0L);
            link = XMLUtil.getElementBooleanValue(node, ID_LINK, false);
            executable = XMLUtil.getElementBooleanValue(node, ID_EXECUTE, false);
            writable = XMLUtil.getElementBooleanValue(node, ID_WRITE, false);
            directory = XMLUtil.getElementBooleanValue(node, ID_DIRECTORY, false);
            fileNumber = XMLUtil.getElementIntValue(node, ID_FILENUMBER, 1);

            return true;
        }

        @Override
        public boolean saveToXML(Node node)
        {
            return saveToNode(node, true);
        }

        boolean saveToNode(Node node, boolean onlineSave)
        {
            if (node == null)
                return false;

            XMLUtil.addElement(node, ID_LOCALPATH, localPath);

            if (onlineSave)
            {
                XMLUtil.addElement(node, ID_ONLINEPATH, onlinePath);
                XMLUtil.addElement(node, ID_DATEMODIF, Long.toString(dateModif));
                if (link)
                    XMLUtil.addElement(node, ID_LINK, Boolean.toString(link));
                if (executable)
                    XMLUtil.addElement(node, ID_EXECUTE, Boolean.toString(executable));
                if (writable)
                    XMLUtil.addElement(node, ID_WRITE, Boolean.toString(writable));
                if (directory)
                {
                    XMLUtil.addElement(node, ID_DIRECTORY, Boolean.toString(directory));
                    XMLUtil.addElement(node, ID_FILENUMBER, Integer.toString(fileNumber));
                }
            }

            return true;
        }

        public boolean isEmpty()
        {
            return StringUtil.isEmpty(localPath) && StringUtil.isEmpty(onlinePath);
        }

        public boolean exists()
        {
            return FileUtil.exists(localPath);
        }

        /**
         * @return the localPath
         */
        public String getLocalPath()
        {
            return localPath;
        }

        /**
         * @return the onlinePath
         */
        public String getOnlinePath()
        {
            return onlinePath;
        }

        /**
         * @return the dateModif
         */
        public long getDateModif()
        {
            return dateModif;
        }

        /**
         * @return the link
         */
        public boolean isLink()
        {
            return link;
        }

        /**
         * @return the executable
         */
        public boolean isExecutable()
        {
            return executable;
        }

        /**
         * @return the writable
         */
        public boolean isWritable()
        {
            return writable;
        }

        /**
         * @return the directory
         */
        public boolean isDirectory()
        {
            return directory;
        }

        /**
         * @return the fileNumber
         */
        public int getFileNumber()
        {
            return fileNumber;
        }

        /**
         * @param dateModif
         *        the dateModif to set
         */
        public void setDateModif(long dateModif)
        {
            this.dateModif = dateModif;
        }

        /**
         * @param link
         *        the link to set
         */
        public void setLink(boolean link)
        {
            this.link = link;
        }

        /**
         * @param executable
         *        the executable to set
         */
        public void setExecutable(boolean executable)
        {
            this.executable = executable;
        }

        /**
         * @param writable
         *        the writable to set
         */
        public void setWritable(boolean writable)
        {
            this.writable = writable;
        }

        /**
         * @param directory
         *        the directory to set
         */
        public void setDirectory(boolean directory)
        {
            this.directory = directory;
        }

        /**
         * @param fileNumber
         *        the fileNumber to set
         */
        public void setFileNumber(int fileNumber)
        {
            this.fileNumber = fileNumber;
        }

        /**
         * Return true if the specified ElementFile is the same than current one.<br>
         * 
         * @param elementFile
         *        the element file to compare
         * @param compareOnlinePath
         *        specify if we compare online path information
         * @param compareValidDateOnly
         *        true if we do compare only valid date (!= 0)
         */
        public boolean isSame(ElementFile elementFile, boolean compareOnlinePath, boolean compareValidDateOnly)
        {
            if (elementFile == null)
                return false;

            if (!StringUtil.equals(elementFile.localPath, localPath))
                return false;
            if (compareOnlinePath && (!StringUtil.equals(elementFile.onlinePath, onlinePath)))
                return false;
            // -1 means we don't check file number
            if ((elementFile.fileNumber != -1) && (fileNumber != -1))
            {
                if (elementFile.fileNumber != fileNumber)
                    return false;
            }

            if ((elementFile.dateModif == 0) || (dateModif == 0))
            {
                // don't compare dates if one is invalid
                if (compareValidDateOnly)
                    return true;

                // one of the date is not valid --> can't compare
                return false;
            }

            return (elementFile.dateModif == dateModif);
        }

        @Override
        public String toString()
        {
            return FileUtil.getFileName(localPath);
        }

    }

    private String name;
    private Version version;
    private final ArrayList<ElementFile> files;
    private String changelog;

    /**
     * 
     */
    public ElementDescriptor(Node node)
    {
        super();

        files = new ArrayList<ElementFile>();

        loadFromXML(node);
    }

    /**
     * Create a new element descriptor using specified element informations
     */
    public ElementDescriptor(ElementDescriptor element)
    {
        super();

        name = element.name;
        version = new Version(element.version.toString());
        changelog = element.changelog;

        files = new ArrayList<ElementFile>();

        for (ElementFile f : element.files)
            files.add(new ElementFile(f));
    }

    @Override
    public boolean loadFromXML(Node node)
    {
        if (node == null)
            return false;

        name = XMLUtil.getElementValue(node, ID_NAME, "");
        version = new Version(XMLUtil.getElementValue(node, ID_VERSION, ""));
        changelog = XMLUtil.getElementValue(node, ID_CHANGESLOG, "");

        final ArrayList<Node> nodesFile = XMLUtil.getChildren(XMLUtil.getElement(node, ID_FILES), ID_FILE);
        if (nodesFile != null)
        {
            for (Node n : nodesFile)
            {
                final ElementFile elementFile = new ElementFile(n);

                if (!elementFile.isEmpty())
                    files.add(elementFile);
            }
        }

        return true;
    }

    @Override
    public boolean saveToXML(Node node)
    {
        return saveToNode(node, true);
    }

    public boolean saveToNode(Node node, boolean onlineSave)
    {
        if (node == null)
            return false;

        XMLUtil.addElement(node, ID_NAME, name);
        XMLUtil.addElement(node, ID_VERSION, version.toString());

        // some informations aren't needed for local version
        if (onlineSave)
            XMLUtil.addElement(node, ID_CHANGESLOG, changelog);

        final Element filesNode = XMLUtil.addElement(node, ID_FILES);
        for (ElementFile elementFile : files)
            elementFile.saveToNode(XMLUtil.addElement(filesNode, ID_FILE), onlineSave);

        return true;
    }

    /**
     * return ElementFile containing specified local path
     */
    public ElementFile getElementFile(String localPath)
    {
        for (ElementFile file : files)
            if (file.getLocalPath().compareToIgnoreCase(localPath) == 0)
                return file;

        return null;
    }

    /**
     * return true if element contains the specified local path
     */
    public boolean hasLocalPath(String localPath)
    {
        return getElementFile(localPath) != null;
    }

    public boolean addElementFile(ElementFile file)
    {
        return files.add(file);
    }

    public boolean removeElementFile(ElementFile file)
    {
        return files.remove(file);
    }

    public void removeElementFile(String localPath)
    {
        removeElementFile(getElementFile(localPath));
    }

    /**
     * Validate the current element descriptor.<br>
     * It actually remove missing files from the element.<br>
     * Return true if all files are valid.
     */
    public boolean validate()
    {
        boolean result = true;

        for (int i = files.size() - 1; i >= 0; i--)
        {
            final ElementFile elementFile = files.get(i);
            final File file = new File(elementFile.getLocalPath());

            if (file.exists())
            {
                // update modification date
                elementFile.setDateModif(file.lastModified());

                // directory file ?
                if (file.isDirectory())
                {
                    // update directory informations
                    elementFile.setDirectory(true);
                    elementFile.setFileNumber(FileUtil.getFiles(file, null, true, false, false).length);
                }
            }
            else
            {
                // remove missing file
                files.remove(i);
                result = false;
            }
        }

        return result;
    }

    public boolean isValid()
    {
        for (ElementFile file : files)
            if (!file.exists())
                return false;

        return true;
    }

    /**
     * @return the name
     */
    public String getName()
    {
        return name;
    }

    /**
     * @return the version
     */
    public Version getVersion()
    {
        return version;
    }

    /**
     * @return the number of files
     */
    public int getFilesNumber()
    {
        return files.size();
    }

    /**
     * @return the files
     */
    public ArrayList<ElementFile> getFiles()
    {
        return files;
    }

    /**
     * @return the specified file
     */
    public ElementFile getFile(int index)
    {
        return files.get(index);
    }

    /**
     * @return the changelog
     */
    public String getChangelog()
    {
        return changelog;
    }

    /**
     * @param version
     *        the version to set
     */
    public void setVersion(Version version)
    {
        this.version = version;
    }

    /**
     * Return true if the specified ElementDescriptor is the same than current one.<br>
     * 
     * @param element
     *        the element descriptor to compare
     * @param compareFileOnlinePath
     *        specify if we compare file online path information
     */
    public boolean isSame(ElementDescriptor element, boolean compareFileOnlinePath)
    {
        if (element == null)
            return false;

        // different name
        if (!name.equals(element.name))
            return false;
        // different version
        if (!version.equals(element.version))
            return false;
        // different number of files
        if (files.size() != element.files.size())
            return false;

        // compare files
        for (ElementFile file : files)
        {
            final ElementFile elementFile = element.getElementFile(file.getLocalPath());

            // file missing --> different
            if (elementFile == null)
                return false;

            // file different (compare date only if they are valid) --> different
            if (!elementFile.isSame(file, compareFileOnlinePath, true))
                return false;
        }

        // same element
        return true;
    }

    /**
     * Process and return the update the element which contain differences<br>
     * from the specified local and online elements.<br>
     * If local element refers the same item, only missing or different files will remains.<br>
     * If local element refers a different element, online element is returned unchanged.
     * 
     * @return the update element (null if local and online elements are the same)
     */
    public static ElementDescriptor getUpdateElement(ElementDescriptor localElement, ElementDescriptor onlineElement)
    {
        if (onlineElement == null)
            return null;

        // use a copy
        final ElementDescriptor result = new ElementDescriptor(onlineElement);

        if (localElement == null)
            return result;
        // different name
        if (!StringUtil.equals(result.name, localElement.name))
            return result;

        // if same version, compare files on valid date only
        final boolean compareValidDateOnly = result.version.equals(localElement.version);

        // compare files
        for (int i = result.files.size() - 1; i >= 0; i--)
        {
            final ElementFile onlineFile = result.files.get(i);
            final ElementFile localFile = localElement.getElementFile(onlineFile.getLocalPath());

            // same file ? --> remove it (no need to be updated)
            if ((localFile != null) && onlineFile.isSame(localFile, false, compareValidDateOnly))
                result.files.remove(i);
        }

        // no files to update ? --> return null
        if (result.files.isEmpty())
            return null;

        return result;
    }

    /**
     * Update current element with informations from specified element
     */
    public void update(ElementDescriptor updateElement)
    {
        // update version info
        version = updateElement.version;

        // updateElement contains only new or modified files (do not contain unmodified ones)
        // so we have to add or update files but not remove old ones.
        for (ElementFile updateFile : updateElement.files)
        {
            // get corresponding file
            final ElementFile localFile = getElementFile(updateFile.getLocalPath());

            // file missing ? --> add it
            if (localFile == null)
                files.add(updateFile);
            else
            {
                // update file (we don't care about online information)
                localFile.setDateModif(updateFile.getDateModif());
                localFile.setExecutable(updateFile.isExecutable());
                localFile.setLink(updateFile.isLink());
                localFile.setWritable(updateFile.isWritable());
                localFile.setDirectory(updateFile.isDirectory());
            }
        }
    }

    @Override
    public String toString()
    {
        return name + " " + version;
    }

}