/*******************************************************************************
 * Copyright (c) 2006, 2018 IBM Corporation and others.
 *
 * This program and the accompanying materials are made
 * available under the terms of the Eclipse Public License 2.0
 * which is available at https://www.eclipse.org/legal/epl-2.0/
 *
 * SPDX-License-Identifier: EPL-2.0
 *
 * Contributors:
 *     IBM Corporation - Jeff Briggs, Henry Hughes, Ryan Morse
 *******************************************************************************/

package org.eclipse.linuxtools.internal.systemtap.ui.ide.structures;

import java.io.File;
import java.io.FileReader;
import java.io.FileWriter;
import java.io.IOException;
import java.util.Calendar;

import org.eclipse.core.runtime.IPath;
import org.eclipse.core.runtime.Path;
import org.eclipse.linuxtools.internal.systemtap.ui.ide.structures.nodedata.StapTreeDataFactory;
import org.eclipse.linuxtools.systemtap.structures.TreeDefinitionNode;
import org.eclipse.linuxtools.systemtap.structures.TreeNode;
import org.eclipse.ui.IMemento;
import org.eclipse.ui.WorkbenchException;
import org.eclipse.ui.XMLMemento;


/**
 * Handles access to the cached stap tapset library tree, including reading the cache back from disk
 * on startup, writing the cache to disk when the cache is initially generated, checking to make sure
 * that the cache is up-to-date, and providing accessor methods to the rest of the IDE that allow other
 * classes to use the cached tree data.
 */
public final class TreeSettings {
    private static final String FILE_NAME = "TreeSettings"; //$NON-NLS-1$
    private static final String FILE_DIRECTORY = ".systemtapgui"; //$NON-NLS-1$

    private static final String M_DISP = "display"; //$NON-NLS-1$
    private static final String M_DATA = "data"; //$NON-NLS-1$
    private static final String M_DATATYPE = "datatype"; //$NON-NLS-1$
    private static final String M_DEFINITON = "definition"; //$NON-NLS-1$
    private static final String M_CLICKABLE = "clickable"; //$NON-NLS-1$
    private static final String M_NULL = "<null>"; //$NON-NLS-1$
    private static final String M_ITEM = "item"; //$NON-NLS-1$

    private static final String T_FUNCTIONS = "functionTree"; //$NON-NLS-1$
    private static final String T_PROBES = "probeTree"; //$NON-NLS-1$
    private static final String T_DATE = "modifiedDate"; //$NON-NLS-1$
    private static final String T_VERSION = "version"; //$NON-NLS-1$
    private static final String VERSION_NUMBER = "3.0"; //$NON-NLS-1$

    private static TreeNode cachedFunctions;
    private static TreeNode cachedProbes;
    private static File settingsFile = null;

    private TreeSettings() {}

    /**
     * Deletes the Function and Probe Alias trees that have been saved to the filesystem
     * as an {@link IMemento}.
     * @return <code>true</code> if the delete attempt succeeded, <code>false</code> otherwise.
     */
    static boolean deleteTrees() {
        boolean deleted;
        try {
            deleted = settingsFile.delete();
        } catch (SecurityException e) {
            deleted = false;
        }
        if (deleted) {
            clearCachedTrees();
        }
        return deleted;
    }

    /**
     * Saves the provided Function and Probe Alias trees into an {@link IMemento} on
     * the filesystem. <p>
     * Note: Both trees must be saved at the same time to better ensure that they
     * are both obtained from the same tapset state.
     * @param functions The Function tree to store in cache.
     * @param probes The Probe Alias tree to store in cache.
     * @return <code>true</code> if the caching is successful.
     */
    public static synchronized boolean setTrees(TreeNode functions, TreeNode probes) {
        if (functions == null || probes == null
                || functions == cachedFunctions || probes == cachedProbes
                || !isTreeFileAvailable()) {
            return false;
        }

        XMLMemento data = XMLMemento.createWriteRoot(FILE_NAME);
        writeTree(data, T_FUNCTIONS, functions);
        writeTree(data, T_PROBES, probes);

        data.createChild(T_DATE)
            .putTextData((Long.valueOf(Calendar.getInstance().getTimeInMillis())).toString());

        data.createChild(T_VERSION).putTextData(VERSION_NUMBER);

        try (FileWriter writer = new FileWriter(settingsFile)) {
            data.save(writer);
        } catch (IOException e) {
            return false;
        }

        clearCachedTrees();
        return true;
    }

    /**
     * Writes the tree passed in to the {@link IMemento} argument.
     * @param data The {@link IMemento} to store the tree to.
     * @param name The name to give to the <code>parent</code> node.
     * @param tree The {@link TreeNode} to store.
     */
    private static void writeTree(IMemento data, String name, TreeNode tree) {
        IMemento child = data.createChild(name);
        child.putString(M_DISP, tree.toString());
        Object treeData = tree.getData();
        if (treeData != null) {
            child.putString(M_DATA, treeData.toString());
            child.putString(M_DATATYPE, StapTreeDataFactory.getDataObjectID(treeData));
        }
        if (tree instanceof TreeDefinitionNode) {
            child.putString(M_DEFINITON,
                    getStringFromValue(((TreeDefinitionNode) tree).getDefinition()));
        }
        child.putBoolean(M_CLICKABLE, tree.isClickable());
        for (int i = 0, n = tree.getChildCount(); i < n; i++) {
            writeTree(child, M_ITEM, tree.getChildAt(i));
        }
    }

    private static void clearCachedTrees() {
        cachedFunctions = null;
        cachedProbes = null;
    }

    /**
     * Allows access to the Tapset Function tree, which contains information about all
     * functions stored in the tapset library.
     * @return The {@link TreeNode} root of the Function tree.
     * @since 2.0
     */
    public static synchronized TreeNode getFunctionTree() {
        if (cachedFunctions == null) {
            cachedFunctions = readData(T_FUNCTIONS);
        }
        return cachedFunctions;
    }

    /**
     * Allows access to the Tapset Probe Alias tree, which contains a list of all probe aliases
     * in the tapset library.
     * @return The {@link TreeNode} root of the Probe Alias tree.
     * @since 2.0
     */
    public synchronized static TreeNode getProbeTree() {
        if (cachedProbes == null) {
            cachedProbes = readData(T_PROBES);
        }
        return cachedProbes;
    }

    /**
     * Reads the contents of the cached memento to recreate the stored trees.
     * @return True if the read is successful.
     */
    private static TreeNode readData(String section) {
        IMemento data = getTreeFileMemento();
        if (data == null) {
            return null;
        }
        return readTree(data.getChild(section));
    }

    /**
     * Opposite action as writeTree. Reconstruct a tree from a previously-saved {@link IMemento}.
     * @param data The {@link IMemento} to read the tree out of.
     * @return The reconstructed {@link TreeNode}.
     */
    private static TreeNode readTree(IMemento data) {
        String disp = data.getString(M_DISP);
        String def = data.getString(M_DEFINITON);
        boolean c = data.getBoolean(M_CLICKABLE);
        Object d = StapTreeDataFactory.createObjectFromString(data.getString(M_DATA), data.getString(M_DATATYPE));

        TreeNode parent;
        if (def == null) {
            parent = new TreeNode(d, disp, c);
        } else {
            parent = new TreeDefinitionNode(d, disp, getValueFromString(def), c);
        }
        for (IMemento child : data.getChildren()) {
            parent.add(readTree(child));
        }
        return parent;
    }

    /**
     * Returns the modification date for the tree file.
     * Use this to make sure that the cache is not out of date.
     * @return The datestamp for the Tree file.
     */
    public synchronized static long getTreeFileDate() {
        IMemento data = getTreeFileMemento();
        if (data != null) {
            IMemento child = data.getChild(T_DATE);
            try {
                return Long.parseLong(child.getTextData());
            } catch (NumberFormatException e) {}
        }
        return -1;
    }

    private static IMemento getTreeFileMemento() {
        if (!isTreeFileAvailable()) {
            return null;
        }

        try (FileReader reader = new FileReader(settingsFile)) {
            IMemento data = XMLMemento.createReadRoot(reader, FILE_NAME);
            IMemento versionChild = data.getChild(T_VERSION);
            if (versionChild != null && versionChild.getTextData().equals(VERSION_NUMBER)) {
                return data;
            }
            return null;
        } catch (IOException | WorkbenchException fnfe) {
            return null;
        }
    }

    private static boolean isTreeFileAvailable() {
        if (settingsFile != null) {
            return true;
        }

        IPath path = new Path(System.getenv("HOME")). //$NON-NLS-1$
                append(FILE_DIRECTORY).append(FILE_NAME).
                addFileExtension("xml"); //$NON-NLS-1$
        settingsFile = path.toFile();

        try {
            if (!settingsFile.exists()){
                // Create a new settings file-and its parent
                // directories- if one does not exist.
                settingsFile.getParentFile().mkdirs();
                settingsFile.createNewFile();
            }
        } catch (IOException ioe) {
            return false;
        }

        return true;
    }

    private static String getStringFromValue(String val) {
        return val == null ? M_NULL : val;
    }

    private static String getValueFromString(String string) {
        return M_NULL.equals(string) ? null : string;
    }
}
