/*
 *   Copyright (C) 2014  Alfons Wirtz
 *   website www.freerouting.net
 *
 *   Copyright (C) 2017 Michael Hoffer <info@michaelhoffer.de>
 *   Website www.freerouting.mihosoft.eu
*
 *   This program 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.
 *
 *   This program 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 at <http://www.gnu.org/licenses/> 
 *   for more details.
 *
 * UndoableObjects.java
 *
 * Created on 24. August 2003, 06:41
 */
package eu.mihosoft.freerouting.datastructures;

import eu.mihosoft.freerouting.logger.FRLogger;

import java.util.Collection;
import java.util.Iterator;
import java.util.LinkedList;
import java.util.concurrent.ConcurrentSkipListMap;
import java.util.concurrent.ConcurrentMap;
import java.util.Vector;

/**
 * Database of objects, for which Undo and Redo operations are made possible.
 * The algorithm works only for objects containing no references.
 *
 * @author Alfons Wirtz
 */
public class UndoableObjects implements java.io.Serializable
{

    /** Creates a new instance of UndoableObjectsList */
    public UndoableObjects()
    {
        stack_level = 0;
        objects = new ConcurrentSkipListMap<Storable, UndoableObjectNode>();
        deleted_objects_stack = new Vector<Collection<UndoableObjectNode>>();
    }

    /**
     * Returns an iterator for sequential reading of the object list.
     * Use it together with this.read_object().
     */
    public Iterator<UndoableObjectNode> start_read_object()
    {
        Collection<UndoableObjectNode> object_list = objects.values();
        return object_list.iterator();
    }

    /**
     * Reads the next object in this list.
     * Returns null, if the list is exhausted.
     * p_it must be created by start_read_object.
     */
    public UndoableObjects.Storable read_object(Iterator<UndoableObjectNode> p_it)
    {
        while (p_it.hasNext())
        {
            UndoableObjectNode curr_node = p_it.next();
            // skip objects getting alive only by redo
            if (curr_node != null && curr_node.level <= this.stack_level)
            {
                return (curr_node.object);
            }
        }
        return null;
    }

    /**
     * Adds p_object to the UndoableObjectsList.
     */
    public void insert(UndoableObjects.Storable p_object)
    {
        disable_redo();
        UndoableObjectNode curr_undoable_object = new UndoableObjectNode(p_object, stack_level);
        objects.put(p_object, curr_undoable_object);
    }

    /**
     * Removes p_object from the top level of the UndoableObjectsList.
     * Returns false, if p_object was not found in the list.
     */
    public boolean delete(UndoableObjects.Storable p_object)
    {
        disable_redo();
        Collection<UndoableObjectNode> curr_delete_list;
        if (deleted_objects_stack.isEmpty())
        {
            // stack_level 0
            curr_delete_list = null;
        }
        else
        {
            curr_delete_list = deleted_objects_stack.lastElement();
        }
        // search p_object in the list
        UndoableObjectNode object_node = objects.get(p_object);
        if (object_node == null)
        {
            return false;
        }
        if (object_node.object != p_object)
        {
            FRLogger.warn("UndoableObjectList.delete: Object inconsistent");
            return false;
        }

        if (curr_delete_list != null)
        {
            if (object_node.level < this.stack_level)
            {
                // add curr_ob to the current delete list to make Undo possible.
                curr_delete_list.add(object_node);
            }
            else if (object_node.undo_object != null)
            {
                // add curr_ob.undo_object to the current delete list to make Undo possible.

                curr_delete_list.add(object_node.undo_object);
            }
        }
        objects.remove(p_object);
        return true;
    }

    /**
     * Makes the current state of the list restorable by Undo.
     */
    public void generate_snapshot()
    {
        disable_redo();
        Collection<UndoableObjectNode> curr_deleted_objects_list = new LinkedList<UndoableObjectNode>();
        deleted_objects_stack.add(curr_deleted_objects_list);
        ++stack_level;
    }

    /**
     * Restores the situation before the last snapshot.
     * Outputs the cancelled and the restored objects (if != null) to enable
     * the calling function to take additional actions needed for these objects.
     * Returns false, if no more undo is possible
     */
    public boolean undo(Collection<UndoableObjects.Storable> p_cancelled_objects, Collection<UndoableObjects.Storable> p_restored_objects)
    {
        if (stack_level == 0)
        {
            return false; // no more undo possible
        }
        Iterator<UndoableObjectNode> it = objects.values().iterator();
        while (it.hasNext())
        {
            UndoableObjectNode curr_node = it.next();
            if (curr_node.level == stack_level)
            {
                if (curr_node.undo_object != null)
                {
                    // replace the current object by  its previous state.
                    curr_node.undo_object.redo_object = curr_node;
                    objects.put(curr_node.object, curr_node.undo_object);
                    if (p_restored_objects != null)
                    {
                        p_restored_objects.add(curr_node.undo_object.object);
                    }
                }
                if (p_cancelled_objects != null)
                {
                    p_cancelled_objects.add(curr_node.object);
                }
            }
        }
        // restore the deleted objects
        Collection<UndoableObjectNode> curr_delete_list = deleted_objects_stack.elementAt(stack_level - 1);
        Iterator<UndoableObjectNode> it2 = curr_delete_list.iterator();
        while (it2.hasNext())
        {
            UndoableObjectNode curr_deleted_node = it2.next();
            this.objects.put(curr_deleted_node.object, curr_deleted_node);
            if (p_restored_objects != null)
            {
                p_restored_objects.add(curr_deleted_node.object);
            }
        }
        --this.stack_level;
        redo_possible = true;
        return true;
    }

    /**
     * Restores the situation before the last undo.
     * Outputs the cancelled and the restored objects (if != null)  to enable
     * the calling function to take additional actions needed for these objects.
     * Returns false, if no more redo is possible.
     */
    public boolean redo(Collection<UndoableObjects.Storable> p_cancelled_objects, Collection<UndoableObjects.Storable> p_restored_objects)
    {
        if (this.stack_level >= deleted_objects_stack.size())
        {
            return false; // alredy at the top level
        }
        ++this.stack_level;
        Iterator<UndoableObjectNode> it = objects.values().iterator();
        while (it.hasNext())
        {
            UndoableObjectNode curr_node = it.next();
            if (curr_node.redo_object != null && curr_node.redo_object.level == this.stack_level)
            {
                // Object was created on a lower level and changed on the currenzt level,
                // replace the lower level object by the object on the current layer.
                objects.put(curr_node.object, curr_node.redo_object);
                if (p_cancelled_objects != null)
                {
                    p_cancelled_objects.add(curr_node.object);
                }
                if (p_restored_objects != null)
                {
                    p_restored_objects.add(curr_node.redo_object.object);
                // else the redo_object was deleted on the redo level
                }
            }
            else if (curr_node.level == this.stack_level)
            {
                // Object was created on the current level, allow it to be restored.
                p_restored_objects.add(curr_node.object);
            }
        }
        // Delete the objects, which were deleted on the current level, again.
        Collection<UndoableObjectNode> curr_delete_list = deleted_objects_stack.elementAt(stack_level - 1);
        Iterator<UndoableObjectNode> it2 = curr_delete_list.iterator();
        while (it2.hasNext())
        {
            UndoableObjectNode curr_deleted_node = it2.next();
            while (curr_deleted_node.redo_object != null &&
                    curr_deleted_node.redo_object.level <= this.stack_level)
            {
                curr_deleted_node = curr_deleted_node.redo_object;
            }
            if (this.objects.remove(curr_deleted_node.object) == null)
            {
                FRLogger.warn("previous deleted object not found");
            }
            if (p_restored_objects == null || !p_restored_objects.remove(curr_deleted_node.object))
            {
                // the object needs only be cancelled if it is already in the board
                if (p_cancelled_objects != null)
                {
                    p_cancelled_objects.add(curr_deleted_node.object);
                }
            }
        }
        return true;
    }

    /**
     *  Removes the top snapshot from the undo stack, so that its situation cannot be
     *  restored any more.
     *  Returns false, if no more snapshot could be popped.
     */
    public boolean pop_snapshot()
    {
        disable_redo();
        if (stack_level == 0)
        {
            return false;
        }
        Iterator<UndoableObjectNode> it = objects.values().iterator();
        while (it.hasNext())
        {
            UndoableObjectNode curr_node = it.next();
            if (curr_node.level == stack_level - 1)
            {
                if (curr_node.redo_object != null && curr_node.redo_object.level == stack_level)
                {
                    curr_node.redo_object.undo_object = curr_node.undo_object;
                    if (curr_node.undo_object != null)
                    {
                        curr_node.undo_object.redo_object = curr_node.redo_object;

                    }
                }
            }
            else if (curr_node.level >= stack_level)
            {
                --curr_node.level;
            }

        }
        int deleted_objects_stack_size = deleted_objects_stack.size();
        if (deleted_objects_stack_size >= 2)
        {
            // join the top delete list with the delete list of the second top level
            Collection<UndoableObjectNode> from_delete_list = deleted_objects_stack.elementAt(deleted_objects_stack_size - 1);
            Collection<UndoableObjectNode> to_delete_list = deleted_objects_stack.elementAt(deleted_objects_stack_size - 2);
            for (UndoableObjectNode curr_deleted_node : from_delete_list)
            {
                if (curr_deleted_node.level < this.stack_level - 1)
                {
                    to_delete_list.add(curr_deleted_node);
                }
                else if (curr_deleted_node.undo_object != null)
                {
                    to_delete_list.add(curr_deleted_node.undo_object);
                }
            }
        }
        deleted_objects_stack.remove(deleted_objects_stack_size - 1);
        --stack_level;
        return true;
    }

    /**
     * Must be callel before p_object will be modified after a snapshot
     * for the first time, if it may have existed before that snapshot.
     */
    public void save_for_undo(UndoableObjects.Storable p_object)
    {
        disable_redo();
        // search p_object in the map
        UndoableObjectNode curr_node = objects.get(p_object);
        if (curr_node == null)
        {
            FRLogger.warn("UndoableObjects.save_for_undo: object node not found");
            return;
        }
        if (curr_node.level < this.stack_level)
        {

            UndoableObjectNode old_node = new UndoableObjectNode((UndoableObjects.Storable) p_object.clone(), curr_node.level);
            old_node.undo_object = curr_node.undo_object;
            old_node.redo_object = curr_node;
            curr_node.undo_object = old_node;
            curr_node.level = this.stack_level;
            return;
        }
    }

    /**
     * Must be called, if objects are changed for the first time after undo.
     */
    private void disable_redo()
    {
        if (!redo_possible)
        {
            return;
        }
        redo_possible = false;
        // shorten the size of the deleted_objects_stack to this.stack_level
        for (int i = deleted_objects_stack.size() - 1; i >= this.stack_level; --i)
        {
            deleted_objects_stack.remove(i);
        }
        Iterator<UndoableObjectNode> it = objects.values().iterator();
        while (it.hasNext())
        {
            UndoableObjectNode curr_node = it.next();
            if (curr_node.level > this.stack_level)
            {
                it.remove();
            }
            else if (curr_node.level == this.stack_level)
            {
                curr_node.redo_object = null;
            }
        }
    }
    /** The entries of this map are of type UnduableObject, the keys of type UndoableObjects.Storable. */
    private ConcurrentMap<Storable, UndoableObjectNode> objects;
    /**
     * the current undo level
     */
    private int stack_level;
    /**
     * the lists of deleted objects on each undo level, which where already
     * existing before the previous snapshot.
     */
    private Vector<Collection<UndoableObjectNode>> deleted_objects_stack;
    private boolean redo_possible = false;

    /**
     * Conditiom for an Object to be stored in an UndoableObjects database.
     * An object of class UndoableObjects.Storable must not contain any references.
     */
    public interface Storable extends Comparable<Object>
    {

        /**
         * Creates an exact copy of this object
         * Public overwriting of the protected clone method in java.lang.Object,
         */
        Object clone();
    }

    /**
     * Stores informations for correct restoring or cancelling an object
     * in an undo or redo operation.
     * p_level is the level in the Undo stack, where this object was inserted.
     */
    public static class UndoableObjectNode implements java.io.Serializable
    {

        /** Creates a new instance of UndoableObjectNode */
        UndoableObjectNode(Storable p_object, int p_level)
        {
            object = p_object;
            level = p_level;
            undo_object = null;
            redo_object = null;
        }
        final Storable object; // the object in the node
        int level; // the level in the Undo stack, where this node was inserted
        UndoableObjectNode undo_object; // the object to restore in an undo or null.
        UndoableObjectNode redo_object; // the object to restore in a redo or null.
    }
}
