/*
 * 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.oak.plugins.observation;

import static org.apache.jackrabbit.oak.api.Type.NAMES;
import static org.apache.jackrabbit.oak.api.Type.STRING;
import static org.apache.jackrabbit.oak.plugins.tree.TreeConstants.OAK_CHILD_ORDER;
import static org.apache.jackrabbit.oak.plugins.memory.EmptyNodeState.MISSING_NODE;
import static org.apache.jackrabbit.oak.spi.state.MoveDetector.SOURCE_PATH;

import java.util.HashSet;
import java.util.Iterator;
import java.util.LinkedList;
import java.util.List;

import org.apache.jackrabbit.oak.api.PropertyState;
import org.apache.jackrabbit.oak.commons.collections.ListUtils;
import org.apache.jackrabbit.oak.spi.state.NodeState;
import org.apache.jackrabbit.oak.spi.state.NodeStateDiff;
import org.apache.jackrabbit.oak.commons.PerfLogger;
import org.jetbrains.annotations.NotNull;
import org.slf4j.LoggerFactory;

/**
 * Continuation-based content diff implementation that generates
 * {@link EventHandler} callbacks by recursing down a content diff
 * in a way that guarantees that only a finite number of callbacks
 * will be made during a {@link #generate()} method call, regardless
 * of how large or complex the content diff is.
 * <p>
 * A simple usage pattern would look like this:
 * <pre>
 * EventGenerator generator = new EventGenerator(before, after, handler);
 * while (!generator.isDone()) {
 *     generator.generate();
 * }
 * </pre>
 */
public class EventGenerator {

    private static final PerfLogger perfLogger = new PerfLogger(
            LoggerFactory.getLogger(EventGenerator.class.getName()
                    + ".perf"));

    /**
     * Maximum number of content changes to process during the
     * execution of a single diff continuation.
     */
    private static final int MAX_CHANGES_PER_CONTINUATION = 10000;

    /**
     * Maximum number of continuations queued for future processing.
     * Once this limit has been reached, we'll start pushing for the
     * processing of property-only diffs, which will automatically
     * help reduce the backlog.
     */
    private static final int MAX_QUEUED_CONTINUATIONS = 1000;

    private final LinkedList<Continuation> continuations = new LinkedList<>();

    /**
     * Creates a new generator instance. Changes to process need to be added
     * through {@link #addHandler(NodeState, NodeState, EventHandler)}
     */
    public EventGenerator() {}

    /**
     * Creates a new generator instance for processing the given changes.
     */
    public EventGenerator(
            @NotNull NodeState before, @NotNull NodeState after,
            @NotNull EventHandler handler) {
        continuations.addFirst(new Continuation(handler, before, after, 0));
    }

    public void addHandler(NodeState before, NodeState after, EventHandler handler) {
        continuations.addFirst(new Continuation(handler, before, after, 0));
    }

    /**
     * Checks whether there are no more content changes to be processed.
     */
    public boolean isDone() {
        return continuations.isEmpty();
    }

    /**
     * Generates a finite number of {@link EventHandler} callbacks based
     * on the content changes that have yet to be processed. Further processing
     * (even if no callbacks were made) may be postponed to a future
     * {@link #generate()} call, until the {@link #isDone()} method finally
     * return {@code true}.
     */
    public void generate() {
        if (!continuations.isEmpty()) {
            final Continuation c = continuations.removeFirst();
            final long start = perfLogger
                    .start("generate: Starting event generation");
            c.run();
            perfLogger.end(start, 1, "generate: Generated {} events",
                    c.counter);
        }
    }

    private class Continuation implements NodeStateDiff, Runnable {

        /**
         * Filtered handler of detected content changes.
         */
        private final EventHandler handler;

        /**
         * Before state, possibly non-existent.
         */
        private final NodeState before;

        /**
         * After state, possibly non-existent.
         */
        private final NodeState after;

        /**
         * Number of initial changes to skip.
         */
        private final int skip;

        /**
         * Number of changes seen so far.
         */
        private int counter = 0;

        private Continuation(
                EventHandler handler, NodeState before, NodeState after,
                int skip) {
            this.handler = handler;
            this.before = before;
            this.after = after;
            this.skip = skip;
        }

        //------------------------------------------------------< Runnable >--

        /**
         * Continues the content diff from the point where this
         * continuation was created.
         */
        @Override
        public void run() {
            if (skip == 0) {
                // Only call enter if this is not a continuation that hit
                // the MAX_CHANGES_PER_CONTINUATION limit before
                handler.enter(before, after);
            }
            if (after.compareAgainstBaseState(before, this)) {
                // Only call leave if this continuation exists normally and not
                // as a result of hitting the MAX_CHANGES_PER_CONTINUATION limit
                handler.leave(before, after);
            }
        }

        //-------------------------------------------------< NodeStateDiff >--

        @Override
        public boolean propertyAdded(PropertyState after) {
            if (beforeEvent()) {
                handler.propertyAdded(after);
                return afterEvent();
            } else {
                return true;
            }
        }

        @Override
        public boolean propertyChanged(
                PropertyState before, PropertyState after) {
            if (beforeEvent()) {
                // check for reordering of child nodes
                if (OAK_CHILD_ORDER.equals(before.getName())) {
                    // list the child node names before and after the change
                    List<String> beforeNames =
                            ListUtils.toList(before.getValue(NAMES));
                    List<String> afterNames =
                            ListUtils.toList(after.getValue(NAMES));

                    // check only those names that weren't added or removed
                    beforeNames.retainAll(new HashSet<>(afterNames));
                    afterNames.retainAll(new HashSet<>(beforeNames));

                    // Selection sort beforeNames into afterNames,
                    // recording the swaps as we go
                    for (int a = 0; a < afterNames.size() - 1; a++) {
                        String beforeName = beforeNames.get(a);
                        String afterName = afterNames.get(a);
                        if (!afterName.equals(beforeName)) {
                            // Find afterName in the beforeNames list.
                            // This loop is guaranteed to stop because both
                            // lists contain the same names and we've already
                            // processed all previous names.
                            int b = a + 1;
                            while (!afterName.equals(beforeNames.get(b))) {
                                b++;
                            }

                            // Swap the non-matching before name forward.
                            // No need to beforeNames.set(a, afterName),
                            // as we won't look back there anymore.
                            beforeNames.set(b, beforeName);

                            // find the destName of the orderBefore operation
                            String destName = null;
                            Iterator<String> iterator =
                                    after.getValue(NAMES).iterator();
                            while (destName == null && iterator.hasNext()) {
                                if (afterName.equals(iterator.next())) {
                                    if (iterator.hasNext()) {
                                        destName = iterator.next();
                                    }
                                }
                            }

                            // deliver the reordering event
                            handler.nodeReordered(
                                    destName, afterName,
                                    this.after.getChildNode(afterName));
                        }
                    }
                }

                handler.propertyChanged(before, after);
                return afterEvent();
            } else {
                return true;
            }
        }

        @Override
        public boolean propertyDeleted(PropertyState before) {
            if (beforeEvent()) {
                handler.propertyDeleted(before);
                return afterEvent();
            } else {
                return true;
            }
        }

        @Override
        public boolean childNodeAdded(String name, NodeState after) {
            if (fullQueue()) {
                return false;
            } else if (beforeEvent()) {
                PropertyState sourceProperty = after.getProperty(SOURCE_PATH);
                if (sourceProperty != null) {
                    String sourcePath = sourceProperty.getValue(STRING);
                    handler.nodeMoved(sourcePath, name, after);
                }

                handler.nodeAdded(name, after);
                addChildDiff(name, MISSING_NODE, after);
                return afterEvent();
            } else {
                return true;
            }
        }

        @Override
        public boolean childNodeChanged(
                String name, NodeState before, NodeState after) {
            if (fullQueue()) {
                return false;
            } else if (beforeEvent()) {
                addChildDiff(name, before, after);
                return afterEvent();
            } else {
                return true;
            }
        }

        @Override
        public boolean childNodeDeleted(String name, NodeState before) {
            if (fullQueue()) {
                return false;
            } else if (beforeEvent()) {
                handler.nodeDeleted(name, before);
                addChildDiff(name, before, MISSING_NODE);
                return afterEvent();
            } else {
                return true;
            }
        }

        //-------------------------------------------------------< private >--

        /**
         * Schedules a continuation for processing changes within the given
         * child node, if changes within that subtree should be processed.
         */
        private void addChildDiff(
                String name, NodeState before, NodeState after) {
            EventHandler h = handler.getChildHandler(name, before, after);
            if (h != null) {
                continuations.addFirst(new Continuation(h, before, after, 0));
            }
        }

        /**
         * Increases the event counter and checks whether the event should
         * be processed, i.e. whether the initial skip count has been reached.
         */
        private boolean beforeEvent() {
            return ++counter > skip;
        }

        /**
         * Checks whether the diff queue has reached the maximum size limit,
         * and postpones further processing of the current diff to later.
         * Even though this postponement increases the size of the queue
         * beyond the limit, doing so ultimately forces property-only
         * diffs to the beginning of the queue, and thus helps to
         * automatically clean up the backlog.
         */
        private boolean fullQueue() {
            if (counter > skip // must have processed at least one event
                    && continuations.size() >= MAX_QUEUED_CONTINUATIONS) {
                continuations.add(new Continuation(
                        handler, this.before, this.after, counter));
                return true;
            } else {
                return false;
            }
        }

        /**
         * Checks whether enough events have already been processed in this
         * continuation. If that is the case, we postpone further processing
         * to a new continuation that will first skip all the initial events
         * we've already seen. Otherwise we let the current diff continue.
         */
        private boolean afterEvent() {
            if (counter >= skip + MAX_CHANGES_PER_CONTINUATION) {
                continuations.addFirst(
                        new Continuation(handler, before, after, counter));
                return false;
            } else {
                return true;
            }
        }

    }

}
