package org.jabref.gui.groups;

import java.util.List;

import org.jabref.gui.undo.AbstractUndoableJabRefEdit;
import org.jabref.logic.l10n.Localization;
import org.jabref.model.groups.GroupTreeNode;

public class UndoableAddOrRemoveGroup extends AbstractUndoableJabRefEdit {

    /// Adding of a single node (group).
    public static final int ADD_NODE = 0;

    /// Removal of a single node. Children, if any, are kept.
    public static final int REMOVE_NODE_KEEP_CHILDREN = 1;

    /// Removal of a node and all of its children.
    public static final int REMOVE_NODE_AND_CHILDREN = 2;

    /// The root of the global groups tree
    private final GroupTreeNodeViewModel m_groupsRootHandle;

    /// The subtree that was added or removed
    private final GroupTreeNode m_subtreeBackup;

    /// In case of removing a node but keeping all of its children, the number of children has to be stored.
    private final int m_subtreeRootChildCount;

    /// The path to the edited subtree's root node
    private final List<Integer> m_pathToNode;

    /// The type of the editing (ADD_NODE, REMOVE_NODE_KEEP_CHILDREN, REMOVE_NODE_AND_CHILDREN)
    private final int m_editType;

    /// Creates an object that can undo/redo an edit event.
    ///
    /// @param groupsRoot The global groups root.
    /// @param editType   The type of editing (ADD_NODE, REMOVE_NODE_KEEP_CHILDREN, REMOVE_NODE_AND_CHILDREN)
    /// @param editedNode The edited node (which was added or will be removed). The node must be a descendant of node **groupsRoot**! This means that, in case of adding, you first have to add it to the tree, then call this constructor. When removing, you first have to call this constructor, then remove the node.
    public UndoableAddOrRemoveGroup(GroupTreeNodeViewModel groupsRoot,
                                    GroupTreeNodeViewModel editedNode, int editType) {
        m_groupsRootHandle = groupsRoot;
        m_editType = editType;
        m_subtreeRootChildCount = editedNode.getChildren().size();
        // storing a backup of the whole subtree is not required when children
        // are kept
        m_subtreeBackup = editType != UndoableAddOrRemoveGroup.REMOVE_NODE_KEEP_CHILDREN ?
                          editedNode.getNode()
                                    .copySubtree()
                                                                                         : GroupTreeNode.fromGroup(editedNode.getNode().getGroup().deepCopy());
        // remember path to edited node. this cannot be stored as a reference,
        // because the reference itself might change. the method below is more
        // robust.
        m_pathToNode = editedNode.getNode().getIndexedPathFromRoot();
    }

    @Override
    public String getPresentationName() {
        return switch (m_editType) {
            case ADD_NODE ->
                    Localization.lang("Add group");
            case REMOVE_NODE_KEEP_CHILDREN ->
                    Localization.lang("Keep subgroups)");
            case REMOVE_NODE_AND_CHILDREN ->
                    Localization.lang("Also remove subgroups");
            default ->
                    "? (" + Localization.lang("unknown edit") + ")";
        };
    }

    @Override
    public void undo() {
        super.undo();
        doOperation(true);
    }

    @Override
    public void redo() {
        super.redo();
        doOperation(false);
    }

    private void doOperation(boolean undo) {
        GroupTreeNode cursor = m_groupsRootHandle.getNode();
        final int childIndex = m_pathToNode.getLast();
        // traverse path up to but last element
        for (int i = 0; i < (m_pathToNode.size() - 1); ++i) {
            cursor = cursor.getChildAt(m_pathToNode.get(i)).get();
        }
        if (undo) {
            switch (m_editType) {
                case ADD_NODE:
                    cursor.removeChild(childIndex);
                    break;
                case REMOVE_NODE_KEEP_CHILDREN:
                    // move all children to newNode, then add newNode
                    GroupTreeNode newNode = m_subtreeBackup.copySubtree();
                    for (int i = childIndex; i < (childIndex
                            + m_subtreeRootChildCount); ++i) {
                        cursor.getChildAt(childIndex).get().moveTo(newNode);
                    }
                    newNode.moveTo(cursor, childIndex);
                    break;
                case REMOVE_NODE_AND_CHILDREN:
                    m_subtreeBackup.copySubtree().moveTo(cursor, childIndex);
                    break;
                default:
                    break;
            }
        } else { // redo
            switch (m_editType) {
                case ADD_NODE:
                    m_subtreeBackup.copySubtree().moveTo(cursor, childIndex);
                    break;
                case REMOVE_NODE_KEEP_CHILDREN:
                    // remove node, then insert all children
                    GroupTreeNode removedNode = cursor
                            .getChildAt(childIndex).get();
                    cursor.removeChild(childIndex);
                    while (removedNode.getNumberOfChildren() > 0) {
                        removedNode.getFirstChild().get().moveTo(cursor, childIndex);
                    }
                    break;
                case REMOVE_NODE_AND_CHILDREN:
                    cursor.removeChild(childIndex);
                    break;
                default:
                    break;
            }
        }
    }
}
