/*
 * ModeShape (http://www.modeshape.org)
 *
 * Licensed 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.modeshape.test.performance;

import static org.hamcrest.core.Is.is;
import static org.hamcrest.core.IsNull.notNullValue;
import static org.junit.Assert.assertThat;
import java.io.InputStream;
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.TimeUnit;
import javax.jcr.Node;
import javax.jcr.NodeIterator;
import javax.jcr.Repository;
import javax.jcr.RepositoryException;
import javax.jcr.Session;
import org.infinispan.schematic.document.Document;
import org.infinispan.schematic.document.Json;
import org.junit.After;
import org.junit.AfterClass;
import org.junit.Before;
import org.junit.Test;
import org.modeshape.common.FixFor;
import org.modeshape.common.annotation.Performance;
import org.modeshape.common.statistic.Stopwatch;
import org.modeshape.jcr.ModeShapeEngine;
import org.modeshape.jcr.RepositoryConfiguration;

public class InMemoryPerformanceTest {

    private static final String LARGE_STRING_VALUE = "Lorem ipsum dolor sit amet, consectetur adipiscing elit. Sed fermentum iaculis placerat. Mauris condimentum dapibus pretium. Vestibulum gravida sodales tellus vitae porttitor. Nunc dictum, eros vel adipiscing pellentesque, sem mi iaculis dui, a aliquam neque magna non turpis. Maecenas imperdiet est eu lorem placerat mattis. Vestibulum ante ipsum primis in faucibus orci luctus et ultrices posuere cubilia Curae; Vestibulum scelerisque molestie tristique. Mauris nibh diam, vestibulum eu condimentum at, facilisis at nisi. Maecenas vehicula accumsan lacus in venenatis. Nulla nisi eros, fringilla at dapibus mollis, pharetra at urna. Praesent in risus magna, at iaculis sapien. Fusce id velit id dui tempor hendrerit semper a nunc. Nam eget mauris tellus.";
    private static final String SMALL_STRING_VALUE = "The quick brown fox jumped over the moon. What? ";

    private static final Stopwatch STARTUP = new Stopwatch();
    private static final Stopwatch INFINISPAN_STARTUP = new Stopwatch();
    private static final Stopwatch MODESHAPE_STARTUP = new Stopwatch();

    private static final int MANY_NODES_COUNT = 10000;

    private RepositoryConfiguration config;
    protected ModeShapeEngine engine;
    protected Repository repository;
    protected Session session;

    @Before
    public void beforeEach() throws Exception {
        cleanUpFileSystem();

        // Read the configuration file, which will be named the same as the class name ...
        String configFileName = getClass().getSimpleName() + ".json";
        String configFilePath = "config/" + configFileName;
        InputStream configStream = getClass().getClassLoader().getResourceAsStream(configFilePath);
        assertThat("Unable to find configuration file '" + configFilePath, configStream, is(notNullValue()));

        Document configDoc = Json.read(configStream);

        STARTUP.start();
        INFINISPAN_STARTUP.start();
        config = new RepositoryConfiguration(configDoc, configFileName);
        INFINISPAN_STARTUP.stop();

        MODESHAPE_STARTUP.start();
        engine = new ModeShapeEngine();
        engine.start();
        engine.deploy(config);
        repository = engine.startRepository(config.getName()).get();
        session = repository.login();
        MODESHAPE_STARTUP.stop();
        STARTUP.stop();
    }

    @After
    public void afterEach() throws Exception {
        try {
            org.modeshape.jcr.TestingUtil.killEngine(engine);
        } finally {
            engine = null;
            repository = null;
            config = null;
            cleanUpFileSystem();
        }
    }

    @AfterClass
    public static void afterAll() throws Exception {
        System.out.println("Infinispan (CacheManager) startup time: " + INFINISPAN_STARTUP.getSimpleStatistics());
        System.out.println("ModeShape startup time:                 " + MODESHAPE_STARTUP.getSimpleStatistics());
        System.out.println("Total startup time:                     " + STARTUP.getSimpleStatistics());
    }

    protected void cleanUpFileSystem() throws Exception {
        // do nothing by default
    }

    @Test
    public void shouldHaveRootNode() throws Exception {
        Node node = session.getRootNode();
        assertThat(node, is(notNullValue()));
        assertThat(node.getPath(), is("/"));
    }

    @Test
    public void shouldHaveJcrSystemNodeUnderRoot() throws Exception {
        Node node = session.getRootNode();
        Node system = node.getNode("jcr:system");
        assertThat(system, is(notNullValue()));
        assertThat(system.getPath(), is("/jcr:system"));
    }

    @Test
    public void shouldAllowCreatingManyUnstructuredNodesWithSameNameSiblings() throws Exception {
        Stopwatch sw = new Stopwatch();
        System.out.print("Iterating ");
        for (int i = 0; i != 15; ++i) {
            System.out.print(".");
            // Each iteration adds another node under the root and creates the many nodes under that node ...
            Node node = session.getRootNode().addNode("testNode");
            session.save();

            if (i > 2) {
                sw.start();
            }
            for (int j = 0; j != MANY_NODES_COUNT; ++j) {
                node.addNode("childNode");
            }
            session.save();
            if (i > 2) {
                sw.stop();
            }

            // Now add another node ...
            node.addNode("oneMore");
            session.save();

            node.remove();
            session.save();
            assertThat(session.getRootNode().getNodes().getSize(), is(1L));
        }
        System.out.println();
        System.out.println(sw.getDetailedStatistics());
    }

    @Test
    public void shouldAllowCreatingNodeUnderUnsavedNode() throws Exception {
        Node node = session.getRootNode().addNode("testNode");
        node.addNode("childNode");
        session.save();
    }

    @Test
    public void shouldAllowCreatingManyUnstructuredNodesWithNoSameNameSiblings() throws Exception {
        Stopwatch sw = new Stopwatch();
        System.out.print("Iterating ");
        for (int i = 0; i != 10; ++i) {
            System.out.print(".");
            // Each iteration adds another node under the root and creates the many nodes under that node ...
            Node node = session.getRootNode().addNode("testNode");
            session.save();

            int count = MANY_NODES_COUNT;
            if (i > 2) {
                sw.start();
            }
            for (int j = 0; j != count; ++j) {
                node.addNode("childNode" + j);
            }
            session.save();
            if (i > 2) {
                sw.stop();
            }

            // Now add another node ...
            node.addNode("oneMore");
            session.save();

            node.remove();
            session.save();
            assertThat(session.getRootNode().getNodes().getSize(), is(1L));
        }
        System.out.println();
        System.out.println(sw.getDetailedStatistics());
    }

    @Test
    public void shouldAllowSmallerSubgraph() throws Exception {
        repeatedlyCreateSubgraph(5, 2, 4, 0, false, true);
    }

    @Test
    public void shouldAllowSmallSubgraph() throws Exception {
        repeatedlyCreateSubgraph(5, 2, 10, 7, false, true);
    }

    protected void repeatedlyCreateSubgraph( int samples,
                                             int depth,
                                             int numberOfChildrenPerNode,
                                             int numberOfPropertiesPerNode,
                                             boolean useSns,
                                             boolean print ) throws Exception {
        Node node = session.getRootNode().addNode("testArea");
        session.save();

        Stopwatch sw = new Stopwatch();
        if (print) {
            System.out.print("Iterating ");
        }
        int numNodesEach = 0;
        for (int i = 0; i != samples; ++i) {
            System.out.print(".");
            sw.start();
            numNodesEach = createSubgraph(session, node, depth, numberOfChildrenPerNode, numberOfPropertiesPerNode, useSns, 1);
            sw.stop();
            session.save();
        }

        // session.getRootNode().getNode("testArea").remove();
        // session.save();
        // assertThat(session.getRootNode().getNodes().getSize(), is(1L)); // only '/jcr:system'
        if (print) {
            System.out.println();
            System.out.println("Create subgraphs with " + numNodesEach + " nodes each: " + sw.getSimpleStatistics());
        }

        // Now try getting a node at one level down ...
        String name = "childNode";
        int index = numberOfChildrenPerNode / 2;
        String path = useSns ? (name + "[" + index + "]") : (name + index);
        sw.reset();
        sw.start();
        Node randomNode = node.getNode(path);
        sw.stop();
        assertThat(randomNode, is(notNullValue()));
        if (print) {
            System.out.println("Find " + randomNode.getPath() + ": " + sw.getTotalDuration());
        }
    }

    @Test
    @Performance
    public void shouldGetNodePathsInFlatLargeHierarchyWithSns() throws Exception {
        boolean print = true;

        // insert 100k nodes with 10 props each under the same parent in batches of 500
        int initialNodeCount = 100000;
        int insertBatchSize = 500;
        int insertBatches = initialNodeCount / insertBatchSize;
        int propertiesPerChild = 10;

        // create a parent with a number of nodes initially
        Node parent = session.getRootNode().addNode("testRoot");
        session.save();

        Stopwatch globalSw = new Stopwatch();
        globalSw.start();
        if (print) {
            System.out.println("Starting to insert batches...");
        }
        for (int i = 0; i < insertBatches; i++) {
            // reload the parent in the session after it was saved
            parent = session.getNode("/testRoot");
            createSubgraph(session, parent, 1, insertBatchSize, propertiesPerChild, true, 1);
        }
        globalSw.stop();
        if (print) {
            System.out.println("Inserted " + initialNodeCount + " nodes in: " + globalSw.getSimpleStatistics());
        }
        globalSw.reset();
        globalSw.start();
        Stopwatch readSW = new Stopwatch();
        // add additional batches of nodes while reading the paths after each batch of children was added
        int batchCount = 36;
        int batchSize = 1000;
        for (int i = 0; i < batchCount; i++) {
            // creates batchSize
            long childCountAtBatchStart = session.getNode("/testRoot").getNodes().getSize();
            int newChildrenCount = createSubgraph(session, parent, 1, batchSize, propertiesPerChild, true, 1);
            readSW.start();

            // load each of the newly added children into the session and get their paths
            final long newChildCount = childCountAtBatchStart + newChildrenCount;

            for (long j = childCountAtBatchStart; j < newChildCount; j++) {
                final String childAbsPath = "/testRoot/childNode[" + j + "]";
                final Node child = session.getNode(childAbsPath);
                child.getPath();
                child.getName();
            }
            readSW.lap();

            // change the parent & save so that it's flushed from the cache
            session.getNode("/testRoot").setProperty("test", "test");
            session.save();

            // now get the paths of each child via parent relative path navigation
            for (long j = childCountAtBatchStart; j <= newChildCount; j++) {
                final String childName = "childNode[" + j + "]";
                final Node child = session.getNode("/testRoot").getNode(childName);
                child.getPath();
                child.getName();
            }
            readSW.lap();

            // change the parent & save so that it's flushed from the cache
            session.getNode("/testRoot").setProperty("test", "test1");
            session.save();

            // iterate through all the children of the parent and read the path
            NodeIterator nodeIterator = session.getNode("/testRoot").getNodes();
            while (nodeIterator.hasNext()) {
                final Node child = nodeIterator.nextNode();
                child.getPath();
                child.getName();
            }
            readSW.stop();
            if (print) {
                System.out.println("Time to read batch " + i + " : " + readSW.getSimpleStatistics());
            }
            readSW.reset();

            // change the parent & save so that it's flushed from the cache
            session.getNode("/testRoot").setProperty("test", "test2");
            session.save();

            globalSw.lap();
        }
        if (print) {
            System.out.println("Overall time to read:" + globalSw.getSimpleStatistics());
        }
    }

    @Test
    @Performance
    @FixFor( "MODE-2266" )
    public void insertNodesInFlatHierarchyWithParentThatAllowsSNS() throws Exception {
        int totalNodeCount = 100000;
        int childrenPerNode = 1000;
        int propertiesPerNode = 0;
        String nodeType = "nt:unstructured";

        session.getRootNode().addNode("testRoot", nodeType);
        session.save();
        Stopwatch sw = new Stopwatch();
        sw.start();
        int totalNumberOfNodes = createSubgraphBreadthFirst(1, nodeType, "/testRoot", totalNodeCount, childrenPerNode,
                                                            propertiesPerNode, true);
        sw.stop();
        System.out.println("Total time to insert " + totalNumberOfNodes + " nodes in batches of " + childrenPerNode + ": "
                           + sw.getSimpleStatistics());
    }

    @Test
    @Performance
    @FixFor( "MODE-2266" )
    public void insertNodesInFlatHierarchyWithParentThatDisallowsSNS() throws Exception {
        int totalNodeCount = 1000000;
        int childrenPerNode = 1000;
        int propertiesPerNode = 0;
        String nodeType = "nt:folder";

        session.getRootNode().addNode("testRoot", nodeType);
        session.save();
        Stopwatch sw = new Stopwatch();
        sw.start();
        int totalNumberOfNodes = createSubgraphBreadthFirst(1, nodeType, "/testRoot", totalNodeCount, childrenPerNode,
                                                            propertiesPerNode, false);
        sw.stop();
        System.out.println("Total time to insert " + totalNumberOfNodes + " nodes in batches of " + childrenPerNode + ": "
                           + sw.getSimpleStatistics());
    }

    @Test
    @Performance
    @FixFor( "MODE-2266" )
    public void insertNodesInDeepHierarchyWithParentThatAllowsSNS() throws Exception {
        int totalNodeCount = 1000000;
        int childrenPerNode = 1000;
        int propertiesPerNode = 0;
        String nodeType = "nt:unstructured";

        session.getRootNode().addNode("testRoot", nodeType);
        session.save();
        Stopwatch sw = new Stopwatch();
        sw.start();
        int totalNumberOfNodes = createSubgraphDepthFirst(nodeType, "/testRoot", totalNodeCount, childrenPerNode,
                                                          propertiesPerNode, true);
        sw.stop();
        System.out.println("Total time to insert " + totalNumberOfNodes + " nodes in batches of " + childrenPerNode + ": "
                           + sw.getSimpleStatistics());
    }

    @Test
    @Performance
    @FixFor( "MODE-2266" )
    public void insertNodesInDeepHierarchyWithParentThatDisallowsSNS() throws Exception {
        int totalNodeCount = 100000;
        int childrenPerNode = 1000;
        int propertiesPerNode = 0;
        String nodeType = "nt:folder";

        session.getRootNode().addNode("testRoot", nodeType);
        session.save();
        Stopwatch sw = new Stopwatch();
        sw.start();
        int totalNumberOfNodes = createSubgraphDepthFirst(nodeType, "/testRoot", totalNodeCount, childrenPerNode,
                                                          propertiesPerNode, false);
        sw.stop();
        System.out.println("Total time to insert " + totalNumberOfNodes + " nodes in batches of " + childrenPerNode + ": "
                           + sw.getSimpleStatistics());
    }

    /**
     * Creates a balanced subgraph of {@code totalNumberOfNodes} nodes, where each parent will have as close as possible to
     * {@code numberOfChildrenPerNode} children. The code will save the session after each set of children has been inserted under
     * a parent.
     * 
     * @param level
     * @param nodeType
     * @param parentAbsPath
     * @param totalNumberOfNodes
     * @param numberOfChildrenPerNode
     * @param numberOfPropertiesPerNode
     * @param useSns
     * @return the total number of nodes created
     * @throws RepositoryException
     * @throws RepositoryException
     */
    protected int createSubgraphBreadthFirst( int level,
                                              String nodeType,
                                              String parentAbsPath,
                                              int totalNumberOfNodes,
                                              int numberOfChildrenPerNode,
                                              int numberOfPropertiesPerNode,
                                              boolean useSns ) throws RepositoryException {
        int numberCreated = 0;
        if (totalNumberOfNodes < numberOfChildrenPerNode) {
            numberOfChildrenPerNode = totalNumberOfNodes;
        }

        List<String> childPaths = new ArrayList<String>();
        Stopwatch sw = new Stopwatch();
        sw.start();

        Node parentNode = session.getNode(parentAbsPath);
        for (int i = 0; i != numberOfChildrenPerNode; ++i) {
            Node child = parentNode.addNode(useSns ? "childNode" : ("childNode" + i), nodeType);
            for (int j = 0; j != numberOfPropertiesPerNode; ++j) {
                String value = (i % 5 == 0) ? LARGE_STRING_VALUE : SMALL_STRING_VALUE;
                child.setProperty("property" + j, value);
            }
            numberCreated++;
            childPaths.add(child.getPath());
        }
        session.save();
        sw.stop();
        System.out.println("Time to insert " + numberCreated + " nodes on level " + level + ": " + sw.getSimpleStatistics());

        if (numberCreated == totalNumberOfNodes) {
            return numberCreated;
        }
        int remainingNodes = totalNumberOfNodes - numberCreated;
        if (remainingNodes <= 0) {
            return numberCreated;
        }
        int totalNodesForChild = remainingNodes / numberOfChildrenPerNode;
        int overflow = remainingNodes % numberOfChildrenPerNode;

        if (totalNodesForChild > 0) {
            for (String childPath : childPaths) {
                numberCreated += createSubgraphBreadthFirst(level + 1, nodeType, childPath, totalNodesForChild,
                                                            numberOfChildrenPerNode, numberOfPropertiesPerNode, useSns);
            }
        }

        if (overflow > 0) {
            // add some extra children to the last child from the list
            numberCreated += createSubgraphBreadthFirst(level + 1, nodeType, childPaths.get(childPaths.size() - 1), overflow,
                                                        overflow, numberOfPropertiesPerNode, useSns);
        }
        return numberCreated;
    }

    /**
     * Creates an "extremely" left-unbalanced subgraph of {@code totalNumberOfNodes} nodes, where each level will have
     * {@code numberOfChildrenPerNode} nodes under the left-most node. The code will save the session after each set of children
     * has been inserted under a parent.
     *
     * @param nodeType
     * @param parentAbsPath
     * @param totalNumberOfNodes
     * @param numberOfChildrenPerNode
     * @param numberOfPropertiesPerNode
     * @param useSns
     * @return the total number of nodes created
     * @throws RepositoryException
     */
    protected int createSubgraphDepthFirst( String nodeType,
                                            String parentAbsPath,
                                            int totalNumberOfNodes,
                                            int numberOfChildrenPerNode,
                                            int numberOfPropertiesPerNode,
                                            boolean useSns ) throws RepositoryException {
        if (totalNumberOfNodes < numberOfChildrenPerNode) {
            numberOfChildrenPerNode = totalNumberOfNodes;
        }

        String firstChildPath;
        Stopwatch sw = new Stopwatch();
        sw.start();
        int level = 1;
        do {
            sw.reset();
            sw.start();
            firstChildPath = null;
            Node parentNode = session.getNode(parentAbsPath);

            for (int i = 0; i != numberOfChildrenPerNode; ++i) {
                Node child = parentNode.addNode(useSns ? "childNode" : ("childNode" + i), nodeType);
                for (int j = 0; j != numberOfPropertiesPerNode; ++j) {
                    String value = (i % 5 == 0) ? LARGE_STRING_VALUE : SMALL_STRING_VALUE;
                    child.setProperty("property" + j, value);
                }
                if (firstChildPath == null) {
                    firstChildPath = child.getPath();
                }
            }
            session.save();
            sw.stop();
            System.out.println("Time to insert " + numberOfChildrenPerNode + " nodes on level " + level++ + ": "
                               + sw.getSimpleStatistics());
            totalNumberOfNodes -= numberOfChildrenPerNode;
            parentAbsPath = firstChildPath;

        } while (totalNumberOfNodes > 0);
        return totalNumberOfNodes;
    }

    /**
     * Create a structured subgraph by generating nodes with the supplied number of properties and children, to the supplied
     * maximum subgraph depth.
     *
     * @param session the session that should be used; may not be null
     * @param parentNode the parent node under which the subgraph is to be created
     * @param depthRemaining the depth of the subgraph; must be a positive number
     * @param numberOfChildrenPerNode the number of child nodes to create under each node
     * @param numberOfPropertiesPerNode the number of properties to create on each node; must be 0 or more
     * @param useSns true if the child nodes under a parent should be same-name-siblings, or false if they should each have their
     *        own unique name
     * @param depthToSave
     * @return the number of nodes created in the subgraph
     * @throws RepositoryException if there is a problem
     */
    protected int createSubgraph( Session session,
                                  Node parentNode,
                                  int depthRemaining,
                                  int numberOfChildrenPerNode,
                                  int numberOfPropertiesPerNode,
                                  boolean useSns,
                                  int depthToSave ) throws RepositoryException {
        int numberCreated = 0;
        for (int i = 0; i != numberOfChildrenPerNode; ++i) {
            Node child = parentNode.addNode(useSns ? "childNode" : ("childNode" + i));
            for (int j = 0; j != numberOfPropertiesPerNode; ++j) {
                String value = (i % 10 == 0) ? LARGE_STRING_VALUE : SMALL_STRING_VALUE;
                child.setProperty("property" + j, value);
            }
            numberCreated += 1;
            if (depthRemaining > 1) {
                numberCreated += createSubgraph(session, child, depthRemaining - 1, numberOfChildrenPerNode,
                                                numberOfPropertiesPerNode, useSns, depthToSave);
            }
        }
        if (depthRemaining == depthToSave) {
            session.save();
        }
        return numberCreated;
    }

    protected int calculateTotalNumberOfNodesInTree( int numberOfChildrenPerNode,
                                                     int depth,
                                                     boolean countRoot ) {
        assert depth > 0;
        assert numberOfChildrenPerNode > 0;
        int totalNumber = 0;
        for (int i = 0; i <= depth; ++i) {
            totalNumber += (int)Math.pow(numberOfChildrenPerNode, i);
        }
        return countRoot ? totalNumber : totalNumber - 1;
    }

    protected String getTotalAndAverageDuration( Stopwatch stopwatch,
                                                 long numNodes ) {
        long totalDurationInMilliseconds = TimeUnit.NANOSECONDS.toMillis(stopwatch.getTotalDuration().longValue());
        long avgDuration = totalDurationInMilliseconds / numNodes;
        String units = " millisecond(s)";
        if (avgDuration < 1L) {
            long totalDurationInMicroseconds = TimeUnit.NANOSECONDS.toMicros(stopwatch.getTotalDuration().longValue());
            avgDuration = totalDurationInMicroseconds / numNodes;
            units = " microsecond(s)";
        }
        return "total = " + stopwatch.getTotalDuration() + "; avg = " + avgDuration + units;
    }
}
