/*
 * 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.preferences;

import icy.util.ClassUtil;
import icy.util.StringUtil;
import icy.util.XMLUtil;

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

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

/**
 * @author Stephane
 */
public class XMLPreferences
{
    public static class XMLPreferencesRoot
    {
        private final String filename;
        private Document doc;

        // cached
        Element element;
        XMLPreferences preferences;

        public XMLPreferencesRoot(String filename)
        {
            this.filename = filename;

            load();
        }

        /**
         * Load preferences from file
         */
        public void load()
        {
            load(filename);
        }

        /**
         * Load preferences from file
         */
        public void load(String filename)
        {
            try
            {
                // get document
                doc = XMLUtil.loadDocument(new File(filename));
            }
            catch (Throwable t)
            {
                System.err.println("Error: " + filename + " preferences file is corrupted, cannot recover settings.");
                // corrupted XML file
                doc = null;
            }

            // create it if not existing
            if (doc == null)
                doc = XMLUtil.createDocument(false);

            // create root element
            element = XMLUtil.createRootElement(doc);
            // create our root XMLPreference object
            preferences = new XMLPreferences(this, element);
            preferences.clean();
        }

        /**
         * Save preferences to file
         */
        public void save()
        {
            save(filename);
        }

        /**
         * Save preferences to file
         */
        public void save(String filename)
        {
            if (doc != null)
                XMLUtil.saveDocument(doc, new File(filename));
        }

        /**
         * @return the element
         */
        public Element getElement()
        {
            return element;
        }

        /**
         * @return the preferences
         */
        public XMLPreferences getPreferences()
        {
            return preferences;
        }
    }

    private final static String TYPE_SECTION = "section";
    private final static String TYPE_KEY = "key";

    private final XMLPreferencesRoot root;
    private final Element currentElement;

    /**
     * Use:<br>
     * <code>new XMLPreferencesRoot(filename).getPreferences()</code><br>
     * to load preferences from file.
     */
    XMLPreferences(XMLPreferencesRoot root, Element element)
    {
        super();

        this.root = root;
        currentElement = element;
    }

    public String absolutePath()
    {
        String result = "/" + name();

        synchronized (root)
        {
            Element parent = XMLUtil.getParentElement(currentElement);
            while ((parent != null) && (parent != root.element))
            {
                result = "/" + XMLUtil.getGenericElementName(parent) + result;
                parent = XMLUtil.getParentElement(parent);
            }
        }

        return result;
    }

    public String name()
    {
        synchronized (root)
        {
            return XMLUtil.getGenericElementName(currentElement);
        }
    }

    public XMLPreferences getParent()
    {
        final Element parent;

        synchronized (root)
        {
            parent = XMLUtil.getParentElement(currentElement);
        }

        if (parent != null)
            return new XMLPreferences(root, parent);

        return null;
    }

    public ArrayList<XMLPreferences> getChildren()
    {
        final ArrayList<XMLPreferences> result = new ArrayList<XMLPreferences>();
        final List<Element> elements;

        synchronized (root)
        {
            elements = XMLUtil.getGenericElements(currentElement, TYPE_SECTION);
        }

        for (Element element : elements)
            result.add(new XMLPreferences(root, element));

        return result;
    }

    public ArrayList<String> childrenNames()
    {
        final ArrayList<String> result = new ArrayList<String>();

        synchronized (root)
        {
            for (Element element : XMLUtil.getGenericElements(currentElement, TYPE_SECTION))
                result.add(XMLUtil.getGenericElementName(element));
        }

        return result;
    }

    private Element getSection(String name)
    {
        if (StringUtil.isEmpty(name))
            return currentElement;

        Element element;

        // absolute path
        if (name.startsWith("/"))
            element = root.element;
        else
        {
            // we test first current node is still existing
            if (!exists())
                return null;

            element = currentElement;
        }

        synchronized (root)
        {
            for (String subName : name.split("/"))
                if (!subName.isEmpty())
                    element = XMLUtil.getGenericElement(element, TYPE_SECTION, subName);
        }

        return element;
    }

    private Element setSection(String name)
    {
        if (StringUtil.isEmpty(name))
            return currentElement;

        Element element;

        // absolute path
        if (name.startsWith("/"))
            element = root.element;
        else
        {
            // we test first current node is still existing
            if (!exists())
                return null;

            element = currentElement;
        }

        synchronized (root)
        {
            for (String subName : name.split("/"))
                if (!subName.isEmpty())
                    element = XMLUtil.setGenericElement(element, TYPE_SECTION, subName);
        }

        return element;
    }

    /**
     * Return XMLPreferences of specified node.<br>
     */
    public XMLPreferences node(String name)
    {
        final Element element = setSection(name);

        if (element != null)
            return new XMLPreferences(root, element);

        return null;
    }

    /**
     * Return XMLPreferences of specified node using class name of specified object.<br>
     * <code>nodeForClass(object) == node(object.getClass().getName())</code><br>
     * Ex : <code>nodeForClass("text") == node("java.lang.String")</code>
     */
    public XMLPreferences nodeForClass(Object object)
    {
        if (object != null)
            return node(ClassUtil.getPathFromQualifiedName(object.getClass().getName()));

        return null;
    }

    /**
     * Return the {@link XMLPreferences} node as an XML node.
     */
    public Element getXMLNode()
    {
        return currentElement;
    }

    /**
     * Return true if current node is existing
     */
    public boolean exists()
    {
        // root element, always exists
        if (currentElement == root.element)
            return true;

        synchronized (root)
        {
            // try to reach root from current element
            Element parent = XMLUtil.getParentElement(currentElement);
            while (parent != null)
            {
                // we reached root so the element still exist
                if (parent == root.element)
                    return true;

                parent = XMLUtil.getParentElement(parent);
            }
        }

        // can't reach root, element is no more existing
        return false;
    }

    /**
     * Return true if specified node exists
     */
    public boolean nodeExists(String name)
    {
        return getSection(name) != null;
    }

    /**
     * Return true if node for specified object exists.<br>
     * <code>nodeForClassExists(object) == nodeExists(object.getClass().getName())</code><br>
     * Ex : <code>nodeForClassExists("text") == nodeExists("java.lang.String")</code>
     */
    public boolean nodeForClassExists(Object object)
    {
        if (object != null)
            return nodeExists(ClassUtil.getPathFromQualifiedName(object.getClass().getName()));

        return false;
    }

    public ArrayList<String> keys()
    {
        final ArrayList<String> result = new ArrayList<String>();

        synchronized (root)
        {
            for (Element element : XMLUtil.getGenericElements(currentElement, TYPE_KEY))
                result.add(XMLUtil.getGenericElementName(element));
        }

        return result;
    }

    /**
     * Remove all non element nodes
     */
    public void clean()
    {
        synchronized (root)
        {
            final List<Node> nodes = XMLUtil.getChildren(currentElement);

            for (Node node : nodes)
            {
                final String nodeName = node.getNodeName();

                if (!(nodeName.equals(TYPE_KEY) || nodeName.equals(TYPE_SECTION)))
                    XMLUtil.removeNode(currentElement, node);
            }
        }
    }

    /**
     * Remove all direct children of this node
     */
    public void clear()
    {
        synchronized (root)
        {
            XMLUtil.removeChildren(currentElement, TYPE_KEY);
        }
    }

    /**
     * Remove specified element
     */
    private void remove(Element element)
    {
        if (element != null)
        {
            synchronized (root)
            {
                final Element parent = XMLUtil.getParentElement(element);

                if (parent != null)
                    XMLUtil.removeNode(parent, element);
            }
        }
    }

    /**
     * Remove current section
     */
    public void remove()
    {
        remove(currentElement);
    }

    /**
     * Remove specified section
     */
    public void remove(String name)
    {
        remove(getSection(name));
    }

    /**
     * Remove all sections
     */
    public void removeChildren()
    {
        synchronized (root)
        {
            XMLUtil.removeChildren(currentElement, TYPE_SECTION);
        }
    }

    public String get(String key, String def)
    {
        synchronized (root)
        {
            return XMLUtil.getGenericElementValue(currentElement, TYPE_KEY, key, def);
        }
    }

    public boolean getBoolean(String key, boolean def)
    {
        synchronized (root)
        {
            return XMLUtil.getGenericElementBooleanValue(currentElement, TYPE_KEY, key, def);
        }
    }

    public byte[] getBytes(String key, byte[] def)
    {
        synchronized (root)
        {
            return XMLUtil.getGenericElementBytesValue(currentElement, TYPE_KEY, key, def);
        }
    }

    public double getDouble(String key, double def)
    {
        synchronized (root)
        {
            return XMLUtil.getGenericElementDoubleValue(currentElement, TYPE_KEY, key, def);
        }
    }

    public float getFloat(String key, float def)
    {
        synchronized (root)
        {
            return XMLUtil.getGenericElementFloatValue(currentElement, TYPE_KEY, key, def);
        }
    }

    public int getInt(String key, int def)
    {
        synchronized (root)
        {
            return XMLUtil.getGenericElementIntValue(currentElement, TYPE_KEY, key, def);
        }
    }

    public long getLong(String key, long def)
    {
        synchronized (root)
        {
            return XMLUtil.getGenericElementLongValue(currentElement, TYPE_KEY, key, def);
        }
    }

    public void put(String key, String value)
    {
        synchronized (root)
        {
            XMLUtil.setGenericElementValue(currentElement, TYPE_KEY, key, value);
        }
    }

    public void putBoolean(String key, boolean value)
    {
        synchronized (root)
        {
            XMLUtil.setGenericElementBooleanValue(currentElement, TYPE_KEY, key, value);
        }
    }

    public void putBytes(String key, byte[] value)
    {
        synchronized (root)
        {
            XMLUtil.setGenericElementBytesValue(currentElement, TYPE_KEY, key, value.clone());
        }
    }

    public void putDouble(String key, double value)
    {
        synchronized (root)
        {
            XMLUtil.setGenericElementDoubleValue(currentElement, TYPE_KEY, key, value);
        }
    }

    public void putFloat(String key, float value)
    {
        synchronized (root)
        {
            XMLUtil.setGenericElementFloatValue(currentElement, TYPE_KEY, key, value);
        }
    }

    public void putInt(String key, int value)
    {
        synchronized (root)
        {
            XMLUtil.setGenericElementIntValue(currentElement, TYPE_KEY, key, value);
        }
    }

    public void putLong(String key, long value)
    {
        synchronized (root)
        {
            XMLUtil.setGenericElementLongValue(currentElement, TYPE_KEY, key, value);
        }
    }
}