package de.uni_passau.fim.infosun.prophet.experimentEditor.qTree;

import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.Objects;
import javax.swing.event.TreeModelEvent;
import javax.swing.event.TreeModelListener;
import javax.swing.tree.TreeModel;
import javax.swing.tree.TreePath;

import de.uni_passau.fim.infosun.prophet.util.qTree.QTreeNode;

/**
 * A <code>TreeModel</code> that handles <code>QTreeNode</code> objects.
 */
public class QTreeModel implements TreeModel {

    private List<TreeModelListener> listeners;
    private QTreeNode root;

    /**
     * Constructs a new <code>QTreeModel</code> with the given root for the tree.
     *
     * @param root
     *         the root
     */
    public QTreeModel(QTreeNode root) {
        listeners = new ArrayList<>();
        this.root = root;
    }

    /**
     * Sets the root of the tree.
     *
     * @param newRoot
     *         the new root of the tree
     * @throws NullPointerException if <code>newRoot</code> is <code>null</code>
     */
    public void setRoot(QTreeNode newRoot) {
        Objects.requireNonNull(newRoot, "newRoot must not be null!");

        QTreeNode oldRoot = root;

        root = newRoot;
        fireTreeStructureChanged(oldRoot);
    }

    /**
     * Renames a node to the new name.
     *
     * @param renameNode the node to be renamed
     * @param newName the new name for the node
     */
    public void rename(QTreeNode renameNode, String newName) {
        renameNode.setName(newName);
        fireNodeChanged(renameNode);
    }

    /**
     * Fires a <code>TreeModelEvent</code> indicating that the given node has changed.
     *
     * @param changedNode the node that has changed
     */
    private void fireNodeChanged(QTreeNode changedNode) {
        Object[] path;
        Object[] children = {changedNode};
        int[] childIndices;
        QTreeNode parent = changedNode.getParent();

        if (parent == null) { // construct a NodeChanged event for the root node
            path = new Object[] {changedNode};
            childIndices = null;
        } else {
            path = buildPath(changedNode, false);
            childIndices = new int[] {parent.getIndexOfChild(changedNode)};
        }

        TreeModelEvent event = new TreeModelEvent(this, path, childIndices, children);

        for (TreeModelListener tml : listeners) {
            tml.treeNodesChanged(event);
        }
    }

    /**
     * Removes the given node from its parent thereby deleting it from the tree.
     *
     * @param removeNode the node to be removed
     */
    public void removeFromParent(QTreeNode removeNode) {
        QTreeNode parent = removeNode.getParent();
        int oldIndex = parent.getIndexOfChild(removeNode);

        parent.removeChild(removeNode);
        fireNodeRemoved(removeNode, oldIndex);
    }

    /**
     * Fires a <code>TreeModelEvent</code> indicating that the given node was removed from its parent.
     *
     * @param removedNode the node that was removed
     * @param oldIndex the index of the node in its parent before it was removed
     */
    private void fireNodeRemoved(QTreeNode removedNode, int oldIndex) {
        Object[] path = buildPath(removedNode, false);
        Object[] children = {removedNode};
        int[] childIndices = {oldIndex};
        TreeModelEvent event = new TreeModelEvent(this, path, childIndices, children);

        for (TreeModelListener tml : listeners) {
            tml.treeNodesRemoved(event);
        }
    }

    /**
     * Adds a child node to the given parent.
     *
     * @param parent
     *         the parent of the child
     * @param child
     *         the child node to be added
     */
    public void addChild(QTreeNode parent, QTreeNode child) {
        parent.addChild(child);
        fireNodeInserted(child);
    }

    /**
     * Adds a child node to the given parent at the given index.
     *
     * @param parent
     *         the parent of the child
     * @param child
     *         the child node to be added
     * @param index
     *         the index for the child
     */
    public void addChild(QTreeNode parent, QTreeNode child, int index) {
        parent.addChild(child, index);
        fireNodeInserted(child);
    }

    /**
     * Fires a <code>TreeModelEvent</code> indicating that the given node has been inserted.
     *
     * @param insertedNode
     *         the node that has been inserted
     */
    private void fireNodeInserted(QTreeNode insertedNode) {
        Object[] path = buildPath(insertedNode, false);
        Object[] children = {insertedNode};
        int[] childIndices = {insertedNode.getParent().getIndexOfChild(insertedNode)};
        TreeModelEvent event = new TreeModelEvent(this, path, childIndices, children);

        for (TreeModelListener tml : listeners) {
            tml.treeNodesInserted(event);
        }
    }

    /**
     * Builds an array of <code>QTreeNode</code>s that uniquely identify the path to the given node in the tree.
     * The elements of the array are ordered with the root as the first element of the array.
     *
     * @param from
     *         the node for which the path is to be built
     * @param includeFrom
     *         whether to include the <code>QTreeNode</code> <code>from</code> as the last element of the array
     *
     * @return the path in the specified format
     */
    public static QTreeNode[] buildPath(QTreeNode from, boolean includeFrom) {
        ArrayList<QTreeNode> path = new ArrayList<>();
        QTreeNode node = from.getParent();

        if (includeFrom) {
            path.add(from);
        }

        while (node != null) {
            path.add(node);
            node = node.getParent();
        }
        Collections.reverse(path);

        return path.toArray(new QTreeNode[path.size()]);
    }

    /**
     * Fires a <code>TreeModelEvent</code> indicating that the whole subtree under <code>root</code> has changed.
     * Passing <code>null</code> as <code>root</code> indicates that the tree has a new root node when there was none
     * before.
     *
     * @param root
     *         the root of the subtree
     */
    private void fireTreeStructureChanged(QTreeNode root) {
        TreeModelEvent event;

        if (root != null) {
            event = new TreeModelEvent(this, buildPath(root, true));
        } else {
            event = new TreeModelEvent(this, new Object[] {this.root});
        }

        for (TreeModelListener tml : listeners) {
            tml.treeStructureChanged(event);
        }
    }

    @Override
    public QTreeNode getRoot() {
        return root;
    }

    @Override
    public QTreeNode getChild(Object parent, int index) {
        return ((QTreeNode) parent).getChild(index);
    }

    @Override
    public int getChildCount(Object parent) {
        return ((QTreeNode) parent).getChildCount();
    }

    @Override
    public boolean isLeaf(Object node) {
        return ((QTreeNode) node).isLeaf();
    }

    @Override
    public void valueForPathChanged(TreePath path, Object newValue) {
        // not used by this model
    }

    @Override
    public int getIndexOfChild(Object parent, Object child) {
        return ((QTreeNode) parent).getIndexOfChild((QTreeNode) child);
    }

    @Override
    public void addTreeModelListener(TreeModelListener l) {
        listeners.add(l);
    }

    @Override
    public void removeTreeModelListener(TreeModelListener l) {
        listeners.remove(l);
    }
}
