/*
 * Licensed to the Apache Software Foundation (ASF) under one or more
 * contributor license agreements.  See the NOTICE file distributed with
 * this work for additional information regarding copyright ownership.
 * The ASF licenses this file to You under the Apache License, Version 2.0
 * (the "License"); you may not use this file except in compliance with
 * the License.  You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
package org.apache.jackrabbit.test.api;

import javax.jcr.Session;
import javax.jcr.Workspace;
import javax.jcr.Item;
import javax.jcr.Node;
import javax.jcr.RepositoryException;
import javax.jcr.NodeIterator;
import javax.jcr.PropertyIterator;
import javax.jcr.Property;
import javax.jcr.PropertyType;
import javax.jcr.Value;
import javax.jcr.PathNotFoundException;
import javax.jcr.nodetype.NodeTypeManager;
import javax.jcr.nodetype.NodeTypeIterator;
import javax.jcr.nodetype.NodeType;
import java.util.Calendar;
import java.io.ByteArrayInputStream;

import junit.framework.Assert;

/**
 * <code>TreeComparator</code> compares two trees. This allows re-use for
 * different tests, and it allows to test a function on any tree, not just a
 * simple example node.
 * <p>
 * TreeComparator also creates an example tree that contains as many features as
 * possible.
 */
class TreeComparator extends Assert {

    public SerializationContext sc;

    public final boolean WORKSPACE = true;
    public final boolean SESSION = false;

    public final int CHECK_EMPTY = -1;
    public final int IGNORE = 0;
    public final int CHECK_SAME = 1;

    public int check = CHECK_SAME;

    private Session session;
    private Workspace workspace;

    public boolean skipBinary = false, noRecurse = false;
    public String sourceFolder, targetFolder;
    public String root;

    public TreeComparator(SerializationContext sc, Session s) throws Exception {
        this.sc = sc;
        session = s;
        workspace = session.getWorkspace();
        init();
    }

    public void setSession(Session session) {
        this.session = session;
    }

    /**
     * Makes sure that the source and target folder exist, and are empty
     */
    private void init() throws RepositoryException {
        root = sc.testroot;
        sourceFolder = root + "/" + sc.sourceFolderName;
        targetFolder = root + "/" + sc.targetFolderName;

        // make sure the node does not have a and target sub node.
        try {
            session.getItem(sourceFolder).remove();
            session.save();
        } catch (PathNotFoundException e) {
            // item does not exist
        }
        try {
            Item tgt = session.getItem(targetFolder);
            tgt.remove();
            session.save();
        } catch (PathNotFoundException e) {
            // item does not exist
        }

        // add the source and target nodes
        Node rootNode = (Node) session.getItem(root);
        rootNode.addNode(sc.sourceFolderName);
        rootNode.addNode(sc.targetFolderName);
        session.save();
    }

    /**
     * Creates an example tree in the workspace.
     */
    public void createExampleTree() {
        createExampleTree(true);
    }

    /**
     * Creates a simple example tree. Use this tree for general repository
     * functions, such as serialization, namespaces and versioning.
     * <p>
     * The beauty of this is that the tree contains exactly the features that
     * are supported by the repository. Any repository exceptions that occur are
     * displayed on "out", but are otherwise ignored.
     *
     * @param save If true, the example tree is saved to the workspace. If
     *             false, it remains in the session.
     */
    public void createExampleTree(boolean save) {
        try {
            Node src = (Node) session.getItem(sourceFolder);
            Node root = src.addNode(sc.rootNodeName);
            root.addNode(sc.nodeName1);
            root.addNode(sc.nodeName2, sc.testNodeType);
            byte[] byteArray = {(byte) 0, (byte) 255, (byte) 167, (byte) 100, (byte) 21, (byte) 6, (byte) 19, (byte) 71, (byte) 221};
            root.setProperty(sc.propertyName1, new ByteArrayInputStream(byteArray));
            root.setProperty(sc.nodeName3, "hello");
        } catch (Exception e) {
            sc.log("Error while creating example tree: " + e.getMessage());
        }
        if (save) {
            try {
                session.save();
            } catch (RepositoryException e) {
                fail("Cannot save the example tree to the repository: " + e);
            }
        }
    }

    /**
     * Creates a complex example tree in the workspace
     */
    public void createComplexTree() {
        createComplexTree(true);
    }

    /**
     * Creates a complex example tree that uses as many features of the
     * repository as possible
     *
     * @param save Save the tree to the workspace. If false, the tree remains in
     *             the session.
     */
    public void createComplexTree(boolean save) {
        Node rootNode = null;
        Node nt = null;
        Node pt = null;
        Node mvp = null;
        Node referenceable = null;
        try {
            Node src = (Node) session.getItem(sourceFolder);
            rootNode = src.addNode(sc.rootNodeName);
            nt = rootNode.addNode(sc.nodeTypesTestNode);
            rootNode.addNode(sc.mixinTypeTestNode);
            pt = rootNode.addNode(sc.propertyTypesTestNode);
            rootNode.addNode(sc.sameNameChildrenTestNode);
            mvp = rootNode.addNode(sc.multiValuePropertiesTestNode);
            referenceable = rootNode.addNode(sc.referenceableNodeTestNode);
            rootNode.addNode(sc.orderChildrenTestNode);
            rootNode.addNode(sc.namespaceTestNode);
        } catch (RepositoryException e) {
            sc.log("Error while creating example tree: " + e.getMessage());
        }

        // Add nodes with mixin types
        NodeTypeManager ntmgr = null;
        NodeTypeIterator types = null;
        try {
            ntmgr = workspace.getNodeTypeManager();
            types = ntmgr.getMixinNodeTypes();
        } catch (RepositoryException e) {
            fail("Cannot access NodeType iterator: " + e);
        }
        while (types.hasNext()) {
            NodeType t = (NodeType) types.next();
            String name = t.getName();
            name = "Node_" + name.replaceAll(":", "_");

            Node n = null;
            try {
                n = nt.addNode(name);
                n.addMixin(t.getName());
                // try saving, because some exceptions are thrown only at save time
                session.save();
            } catch (RepositoryException e) {
                sc.log("Cannot create node with mixin node type: " + e);
                // if saving failed for a node, then remove it again (or else the next save will fail on it)
                try {
                    if (n != null) {
                        n.remove();
                    }
                } catch (RepositoryException e1) {
                    sc.log("Could not remove node: " + e);
                }
            }
        }

        // Create all property types
        try {
            // String
            pt.setProperty(sc.stringTestProperty, "This is a string.");
            // Binary
            byte[] byteArray = {(byte) 0, (byte) 255, (byte) 167, (byte) 100, (byte) 21, (byte) 6, (byte) 19, (byte) 71, (byte) 221};
            pt.setProperty(sc.binaryTestProperty, new ByteArrayInputStream(byteArray));
            // Date
            Calendar c = Calendar.getInstance();
            c.set(2005, 6, 21, 13, 30, 5);
            pt.setProperty(sc.dateTestProperty, c);
            // Long
            pt.setProperty(sc.longTestProperty, (long) (1 / 3));
            // Double
            pt.setProperty(sc.doubleTestProperty, (double) Math.PI);
            // Boolean
            pt.setProperty(sc.booleanTestProperty, true);
            // Name
            pt.setProperty(sc.nameTestProperty, session.getValueFactory().createValue(sc.jcrPrimaryType, PropertyType.NAME));
            // Path
            pt.setProperty(sc.pathTestProperty, session.getValueFactory().createValue("paths/dont/have/to/point/anywhere", PropertyType.PATH));
            // Reference: Note that I only check if the node exists. We do not specify what happens with
            // the UUID during serialization.
            sc.ensureMixinType(referenceable, sc.mixReferenceable);
            // some implementations may require a save after addMixin()
            session.save();

            pt.setProperty(sc.referenceTestProperty, referenceable);

            // Create a boolean property on the root node, so I can test noRecurse and skipBinary at the same time
            rootNode.setProperty(sc.binaryTestProperty, new ByteArrayInputStream(byteArray));
            session.save();
        } catch (Exception e) {
            fail("Could not add property: " + e);
        }

        // multi value properties
        String[] s = {"one", "two", "three"};
        try {
            mvp.setProperty(sc.multiValueTestProperty, s);
            session.save();
        } catch (RepositoryException e) {
            sc.log("Could not create multi-value property: " + e);
        }

        // Save to the workspace. Note that export is from session anyway.
        if (save) {
            try {
                session.save();
            } catch (RepositoryException e) {
                fail("Cannot save the example tree to the repository: " + e);
            }
        }
    }

    /**
     * Compares the trees in the source and target folder.
     *
     * @param skipBinary True if skipbinary is set, so binary properties are not
     *                   in the taget tree.
     * @param noRecurse  True if noRecurse is used, so only the top node and its
     *                   properties are in the target tree.
     */
    public void compare(boolean skipBinary, boolean noRecurse) {
        this.skipBinary = skipBinary;
        this.noRecurse = noRecurse;
        compare();
    }

    /**
     * Compares the source and target tree.
     */
    public void compare() {
        compare(CHECK_SAME);
    }

    /**
     * Compares the source and target tree.
     *
     * @param check CHECK_SAME checks if the two trees have the same nodes and
     *              properties. CHECK_EMPTY checks that the target tree is
     *              empty.
     */
    public void compare(int check) {
        this.check = check;
        compare(sourceFolder + "/" + sc.rootNodeName, 0);
    }

    /**
     * Compares two nodes in the source and target tree
     *
     * @param sourcePath The path of the node in the source tree
     * @param level      The level of depth in the tree
     */
    public void compare(String sourcePath, int level) {
        Node source = null;
        Node target = null;

        // get the source path
        try {
            source = (Node) session.getItem(sourcePath);
        } catch (RepositoryException e) {
            fail("Could not read source node " + sourcePath + ": " + e.getMessage());
        }

        // get the target path
        String targetPath = getTargetPath(sourcePath);
        try {
            session.getItem(targetFolder);
        } catch (RepositoryException e) {
            fail("Target folder not found: " + e);
        }

        // Check noRecurse: After top level, the target tree must be empty
        if (noRecurse && level == 1) {
            check = CHECK_EMPTY;
        }

        // compare source and target
        if (check == CHECK_SAME) {
            try {
                target = (Node) session.getItem(targetPath);
            } catch (RepositoryException e) {
                showTree();
                fail("Could not read target node " + targetPath + ": " + e);
            }
            compareNodes(source, target);
        } else if (check == CHECK_EMPTY) {
            try {
                session.getItem(targetPath);
                fail("The item " + targetPath + " must not be available.");
            } catch (RepositoryException e) {
            }
        }

        // iterate through all child nodes of the source tree
        try {
            NodeIterator ni = source.getNodes();
            while (ni.hasNext()) {
                Node n = (Node) ni.next();
                compare(n.getPath(), level + 1);
            }
        } catch (RepositoryException e) {
            fail("Error while iterating through child nodes: " + e);
        }
    }

    /**
     * Compares two nodes, a and b
     *
     * @param a The node in the source tree
     * @param b The same node in the target tree
     */
    public void compareNodes(Node a, Node b) {
        try {
            sc.log("Comparing " + a.getPath() + " to " + b.getPath());
        } catch (RepositoryException e) {
            fail("Nodes not available: " + e.getMessage());
        }

        // check primary node type
        String primaryTypeA = null, primaryTypeB = null;
        try {
            primaryTypeA = a.getProperty(sc.jcrPrimaryType).getName();
            primaryTypeB = b.getProperty(sc.jcrPrimaryType).getName();
        } catch (RepositoryException e) {
            fail("Primary node type not available: " + e);
        }
        assertEquals("Primary node type has changed.", primaryTypeA, primaryTypeB);

        compareProperties(a, b);
    }

    /**
     * Compares all the properties of the two nodes a and b
     *
     * @param a The node in the source tree.
     * @param b The node in the target tree.
     */
    public void compareProperties(Node a, Node b) {
        PropertyIterator ai = null;
        try {
            ai = a.getProperties();
        } catch (RepositoryException e) {
            fail("Cannot access properties: " + e);
        }
        while (ai.hasNext()) {
            Property pa = (Property) ai.next();
            String pName = null;
            // todo
            String pPath = null;
            try {
                pPath = pa.getPath();
            } catch (RepositoryException e) {

            }

            int pType = 0;

            try {
                pName = pa.getName();
                if (pa.getDefinition().isMultiple()) {
                    pType = -9999;
                } else {
                    pType = pa.getValue().getType();
                }
            } catch (RepositoryException e) {
                fail("Cannot access property information: " + e);
            }

            if (propertyValueMayChange(pName)) {
                continue;
            }

            Property pb = null;
            // avoid skipped properties
            if (!propertySkipped(pName)) {
                try {
                    pb = b.getProperty(pName);
                } catch (RepositoryException e) {
                    //fail if the property is not there but should
                    fail("Property '" + pPath + "' not available: " + e);
                }

                if (!(skipBinary && pType == PropertyType.BINARY)) {
                    // todo
                    // compare source and target value
                    compareProperties(pa, pb);
                }
            }
        }
    }

    /**
     * Compares two properties a and b.
     *
     * @param a The property in the source tree.
     * @param b The property in the target tree.
     */
    public void compareProperties(Property a, Property b) {
        String nodeName = null, propertyName = null;
        boolean isMultiple = false;
        try {
            nodeName = a.getParent().getName();
            propertyName = a.getName();
            isMultiple = a.getDefinition().isMultiple();
        } catch (RepositoryException e) {
            fail("Cannot access property information: " + e);
        }

        if (!propertyValueMayChange(propertyName)) {
            if (isMultiple) {
                try {
                    compareValues(nodeName, propertyName, a.getValues(), b.getValues());
                } catch (RepositoryException e) {
                    fail("Could not access property values: " + e);
                }
            } else {
                try {
                    compareValue(nodeName, propertyName, a.getValue(), b.getValue());
                } catch (RepositoryException e) {
                    fail("Could not access property value: " + e);
                }
            }
        }
    }

    /**
     * Compares a set of multi-value properties.
     *
     * @param n Name of the node.
     * @param p Name of the property.
     * @param a Value (array) of the property in the source tree.
     * @param b Value (array) of the property in the target tree.
     */
    public void compareValues(String n, String p, Value[] a, Value[] b) {
        assertEquals("Multi-value property '" + p + "' of node '" + n + "' has changed length: ", a.length, b.length);
        for (int t = 0; t < a.length; t++) {
            compareValue(n, p, a[t], b[t]);
        }
    }

    /**
     * Compares the value of two properties
     *
     * @param n Name of the node.
     * @param p Name of the property.
     * @param a Value of the property in the source tree.
     * @param b Value of the property in the target tree.
     */
    public void compareValue(String n, String p, Value a, Value b) {
        if (!propertyValueMayChange(p)) {
            try {
                assertEquals("Properties '" + p + "' of node '" + n + "' have different values.", a.getString(), b.getString());
            } catch (RepositoryException e) {
                fail("Cannot access the content of the property value: " + e);
            }
        }
    }

    /**
     * Returns whether the value of the property may change in serialization.
     * For example, the last changed date of a node may change when the node is
     * re-imported. The values that may change are declared in the config file.
     *
     * @param propertyName The property name you want to check
     * @return True or false to indicate whether the value may or may not
     *         change.
     */
    public boolean propertyValueMayChange(String propertyName) {
        if (sc.propertyValueMayChange.indexOf(" " + propertyName + " ") < 0) {
            return false;
        } else {
            return true;
        }
    }

    /**
     * Returns the path to the source root.
     *
     * @return The path to the source root.
     */
    public String getSourceRootPath() {
        return sourceFolder + "/" + sc.rootNodeName;
    }

    /**
     * Returns the path to the target root.
     *
     * @return The path to the target root.
     */
    private String getTargetPath(String sourcePath) {
        String targetPath = sourcePath.replaceAll(sourceFolder, targetFolder);
        return targetPath;
    }

    /**
     * Displays the current source and target tree for debug/information
     * purpose
     */
    public void showTree() {
        Node n = null;
        try {
            n = (Node) session.getItem(sc.testroot);
            showTree(n, 0);
        } catch (RepositoryException e) {
            sc.log("Cannot display tree diagnostics: " + e);
        }
    }

    /**
     * Recursive display of source and target tree
     */
    public void showTree(Node n, int level) throws RepositoryException {
        StringBuffer sb = new StringBuffer();
        for (int t = 0; t < level; t++) {
            sb.append("-");
        }
        sb.append(n.getName() + " ");
        sb.append(n.getPrimaryNodeType().getName() + " [ ");
        PropertyIterator pi = n.getProperties();
        while (pi.hasNext()) {
            Property p = (Property) pi.next();
            sb.append(p.getName() + " ");
        }
        sb.append("]");
        sc.log(sb.toString());

        NodeIterator ni = n.getNodes();
        while (ni.hasNext()) {
            showTree((Node) ni.next(), level + 1);
        }
    }

    /**
     * Checks if a given property should be skipped during xml import.
     *
     * @param propertyName
     * @return
     */
   public boolean propertySkipped(String propertyName) {
        if (sc.propertySkipped.indexOf(" " + propertyName + " ") < 0) {
            return false;
        } else {
            return true;
        }
    }
}
