package vermilion;

//TODO: Improve the mechanism for registering serialisable types in this class,
//including deregistration of types. (Simple code work)

import java.io.*;
import java.util.ArrayList;
import java.util.StringTokenizer;
import java.util.TreeMap;
import java.util.zip.ZipEntry;
import java.util.zip.ZipInputStream;
import java.util.zip.ZipOutputStream;

/**
 * A searchable structure for storing preferences or a user's document. The
 * purpose of the class is to provide the searchable structure and allow
 * storage and retrieval of complex data types at runtime, but also to provide
 * versioned (de)serialisation of the data, ensuring compatibility between
 * versions of the program and of the runtime objects being stored. There are
 * other niceties such as storing the file's path, keeping track of whether the
 * data has been saved, and allowing automatic saving after any update.
 *
 * The file allows simple path access to nodes, e.g. 'headnode/subnode' which
 * allows organisation of data within the file. Paths to nodes are created
 * as necessary when setting values. If a node or part of the path to it is
 * not found when getting a value, the value returned will be null.
 *
 * To allow versioned (de)serialisation there is a static list of known types
 * or UsefulFileObjects. These are registered at runtime to provide the ability
 * to deserialise subtypes. UsefulFileObjects extend a base class that provides
 * the basic serialisation functionality, and the subclasses provide the actual
 * string building and interpreting that store the object. In this way, the
 * UsefulFile can return the correct object type that was stored in a file and
 * the subclass simply does the work of interpreting its string representation.
 * This also leaves the code monkey free to use the UsefulFileObject as either
 * runtime data or simply a representation of it and only worry about writing
 * the serialisation code once. The catch is he must remember to call setValue
 * on the UsefulFile with his data so it can be serialised ready for saving.
 */
public class UsefulFile
{
    // The head of the document, an empty node where we can attach paths.
    // There is no need to directly reference the head node.
    private UsefulFileNode head = new UsefulFileNode("$HEAD$");

    // Indicates that the file should be saved every time an update is made.
    // This is useful for things like preferences where we don't want to have
    // to remember to write them directly all the time.
    private boolean saveOnUpdate = false;

    /**
     * This list contains subclasses of UsefulFileObject so when we want to
     * deserialise a node we can look up the class that wrote it. You must
     * place an (empty) instance of your subclass into this list before you
     * can interpret data written via that subclass.
     */
    private static TreeMap<String, UsefulFileObject> registeredTypes = new TreeMap<String, UsefulFileObject>();
    
    /**
     * The filename used on the last load or saveAs operation. If we call
     * save (or do an update on an auto saving file) the file will be written
     * to this filename.
     */
    public String currentFilename = "";

    /**
     * Indicates that the 'file' has been updated since the last time it was
     * saved. This allows us to check if the user wants to save their document
     * before exiting, etc.
     */
    public boolean needsSaving = false;

    /**
     * Constructor does nothing, providing an empty file. The boolean indicates
     * whether we want a file that saves automatically when it is changed. A
     * head node is created but it is only used as a place to start looking for
     * nodes when decoding a path.
     * @param saveOnUpdate If true, the file saves automatically when changed.
     */
    public UsefulFile(boolean saveOnUpdate)
    {
        this.saveOnUpdate = saveOnUpdate;
        head = new UsefulFileNode("$HEAD$");
    }

    public static void registerType(UsefulFileObject objType)
    {
        if (registeredTypes.get(objType.getType()) != null)
        {
            System.out.println("* WARNING *   The type '" + objType.getType() + "' is already registered.");
        }
        registeredTypes.put(objType.getType(), objType);
    }

    public static UsefulFileObject getRegisteredType(String typeStr)
    {
        return registeredTypes.get(typeStr);
    }


    /**
     * Gets whatever is stored in the file at a given location. The data is
     * deserialised and returned as the UsefulFileObject subclass that it was
     * wrapped in when written into the document, providing of course that that
     * subclass has been registered as a decoder.
     * @param location The path to the needed data, separated with '/'.
     * @return The value found at the location provided, or null if it was not
     * found.
     */
    public UsefulFileObject getValue(String location)
    {
        UsefulFileNode curr = findNode(location);

        if (curr != null) 
        {
            UsefulFileObject obj = UsefulFileObject.load(curr.nodeValue);
            if (obj == null)
            {
                String nodeStr = curr.nodeValue;
                if (nodeStr.length() > 100) nodeStr = nodeStr.substring(0, 97) + "...";
                System.out.println("failed: " + nodeStr + " at " + location);
            }
            return obj;
        }
        return null;
    }

    // Helper method to locate nodes using location strings.
    private UsefulFileNode findNode(String location)
    {
        StringTokenizer toke = new StringTokenizer(location, "/");
        UsefulFileNode curr = head;
        while (toke.hasMoreTokens() && curr != null) {
            String path = toke.nextToken();
            curr = curr.getChild(path);
        }
        return curr;
    }

    /**
     * Overwrites whatever is stored at the given location, replacing it
     * with obj. If the location is not found it will be created. If the
     * object was created with saveOnUpdate = true the file will be saved.
     * The data must be passed in as a UsefulFileObject so that it can
     * serialise itself into the actual document data.
     * @param location The path to the target data, separated with '/'.
     * @param obj The Object to place at the target location.
     */
    public void setValue(String location, UsefulFileObject obj)
    {
        StringTokenizer toke = new StringTokenizer(location, "/");
        UsefulFileNode curr = head;

        while(toke.hasMoreTokens())
        {
            String path = toke.nextToken();
            curr = curr.getOrAddChild(path);
        }

        curr.nodeValue = obj.save();
        if (needsSaving == false) System.out.println("setting " + location + " caused save");
        needsSaving = true;
        
        if (saveOnUpdate) save();
    }

    /**
     * Returns the children of a given location in an arraylist. This is handy
     * for e.g. enumerating the patterns for a machine. We structure
     * machines in the same way for convenience and to lower the overhead of
     * reading or writing a machine from the composition (by updating single
     * nodes rather than one holding all machines).
     * @param location The parent node where we want to find children.
     * @return The children if found, or an empty list if not.
     */
    public ArrayList<UsefulFileObject> getChildren(String location)
    {
        ArrayList<UsefulFileObject> list = new ArrayList<UsefulFileObject>();

        UsefulFileNode parent = findNode(location);
        if (parent == null) return null;

        for (UsefulFileNode node : parent.subNodes)
        {
            list.add(UsefulFileObject.load(node.nodeValue));
        }

        return list;
    }

    /**
     * Loads the structure (via serialization) from a previously saved file.
     * @param fileName The name of the file to open.
     */
    public boolean load(String fileName)
    {
        try
        {
            FileInputStream fis = new FileInputStream(fileName);
            ZipInputStream zis = new ZipInputStream(fis);
            zis.getNextEntry();
            ObjectInputStream in = new ObjectInputStream(zis);

            try
            {
                head = (UsefulFileNode) in.readObject();
            }
            catch (Exception e) { System.out.println(e); return false; }

            in.close();
            zis.close();
            fis.close();

            currentFilename = fileName;
            needsSaving = false;
            
            System.out.println("Loaded " + fileName);
            return true;
        }
        catch (Exception e) { System.out.println(e); return false; }
    }

    /**
     * Saves the structure to a file for later use.
     * @param fileName The name of the file to create or overwrite.
     */
    public boolean saveAs(String fileName)
    {
        try
        {
            FileOutputStream fos = new FileOutputStream(fileName);
            ZipOutputStream zos = new ZipOutputStream(fos);
            zos.putNextEntry(new ZipEntry("OK"));
            ObjectOutputStream out = new ObjectOutputStream(zos);

            try
            {
                out.writeObject(head);
            }
            catch (Exception e) { System.out.println(e); return false; }

            zos.closeEntry();
            out.close();
            zos.close();
            fos.close();

            currentFilename = fileName;
            needsSaving = false;

            System.out.println("Saved " + fileName);
            return true;
        }
        catch (Exception e) { System.out.println(e + ": '" + fileName + "'"); return false; }
    }

    /**
     * Saves the structure to a file for later use, using the last location the
     * file was loaded from or saved to.
     */
    public void save()
    {
        saveAs(currentFilename);
    }

    public void delete(String location)
    {
        StringTokenizer toke = new StringTokenizer(location, "/");
        UsefulFileNode curr = head;
        UsefulFileNode parent = null;
        while (toke.hasMoreTokens() && curr != null) {
            String path = toke.nextToken();
            parent = curr;
            curr = curr.getChild(path);
        }

        if (parent != null && curr != null) 
        {
            parent.removeChild(curr);
            needsSaving = true;
        }
    }
}

/**
 * WARNING! WARNING! DANGER WILL ROBINSON!
 *
 * This class is serialised to UsefulFiles, storing the string encoded versions
 * of UsefulFileObjects and providing methods for finding children etc.
 *
 * If you change this class all documents saved previously will be UNREADABLE.
 * Whatever reason you have for changing anything below these lines, forget it.
 * Figure out another way to implement your half-assed hack Mr. Code Monkey! ;-)
 */
final class UsefulFileNode implements Serializable
{
    /**
     * The name to identify the node with, which becomes the last part of the
     * path to the node.
     */
    public String nodeName = "";

    /**
     * The serialised data that this node stores. This is decoded by the file
     * object to provide the runtime data.
     */
    public String nodeValue = new String();

    /**
     * The list of nodes that exist beneath this node. This is serialised as
     * part of this node by Java's mechanisms.
     */
    public ArrayList<UsefulFileNode> subNodes = new ArrayList<UsefulFileNode>();

    /**
     * Construct a new node with no data attached.
     * @param nodeName The path to this node.
     */
    public UsefulFileNode(String nodeName)
    {
        this.nodeName = nodeName;
    }

    /**
     * Get the child below this node with the specified name.
     * @param path The part of the path identifying the child node.
     * @return The child node, or null if no such child exists.
     */
    public UsefulFileNode getChild(String path)
    {
        for (int x = 0; x < subNodes.size(); x++)
        {
            UsefulFileNode child = subNodes.get(x);
            if (child.nodeName.equals(path)) return child;
        }

        return null;
    }

    /**
     * Get the child below this node with the specified name, or create it if
     * it doesn't exist.
     * @param path The part of the path identifying the child node.
     * @return The child node, whether it was found or created.
     */
    public UsefulFileNode getOrAddChild(String path)
    {
        for (int x = 0; x < subNodes.size(); x++)
        {
            UsefulFileNode child = subNodes.get(x);
            if (child.nodeName.equals(path)) return child;
        }

        UsefulFileNode newNode = new UsefulFileNode(path);
        subNodes.add(newNode);
        return newNode;
    }

    /**
     * Remove the child from this node with this specified name.
     * @param path The name identifying the child.
     */
    public void removeChild(UsefulFileNode child)
    {
        subNodes.remove(child);
    }
}