/*
 * 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.tinkerpop.gremlin.process.traversal;

import org.apache.tinkerpop.gremlin.util.function.ConstantSupplier;

import java.io.Serializable;
import java.util.Optional;
import java.util.Set;
import java.util.function.BiConsumer;
import java.util.function.BinaryOperator;
import java.util.function.Supplier;
import java.util.function.UnaryOperator;

/**
 * A {@link Traversal} can maintain global sideEffects.
 * Unlike {@link Traverser} "sacks" which are local sideEffects, TraversalSideEffects are accessible by all {@link Traverser} instances within the {@link Traversal}.
 *
 * @author Marko A. Rodriguez (http://markorodriguez.com)
 */
public interface TraversalSideEffects extends Cloneable, Serializable, AutoCloseable {

    /**
     * Return true if the key is a registered side-effect.
     *
     * @param key the key to check for existence
     * @return whether the key exists or not
     */
    public default boolean exists(final String key) {
        return this.keys().contains(key);
    }

    /**
     * Get the sideEffect associated with the provided key.
     * If the sideEffect contains an object for the key, return it.
     * Else if the sideEffect has a registered {@code Supplier} for that key, generate the object, store the object in the sideEffects, and return it.
     *
     * @param key the key to get the value for
     * @param <V> the type of the value to retrieve
     * @return the value associated with key
     * @throws IllegalArgumentException if the key does not reference an object or a registered supplier.
     */
    public <V> V get(final String key) throws IllegalArgumentException;

    /**
     * Set the specified key to the specified value.
     * This method should not be used in a distributed environment. Instead, use {@link TraversalSideEffects#add(String, Object)}.
     * This method is only safe when there is only one representation of the side-effect and thus, not distributed across threads or machines.
     *
     * @param key   the key they key of the side-effect
     * @param value the value the new value for the side-effect
     * @throws IllegalArgumentException if the key does not reference a registered side-effect.
     */
    public void set(final String key, final Object value) throws IllegalArgumentException;

    /**
     * Remove both the value and registered {@link java.util.function.Supplier} associated with provided key.
     *
     * @param key the key of the value and registered supplier to remove
     */
    public void remove(final String key);

    /**
     * The keys of the sideEffect which includes registered {@code Supplier} keys. In essence, that which is possible
     * to {@link #get(String)}.
     *
     * @return the keys of the sideEffect
     */
    public Set<String> keys();

    /**
     * Invalidate the side effect cache for traversal.
     */
    public default void close() throws Exception {
        // do nothing
    }

    /**
     * Determines if there are any side-effects to be retrieved.
     */
    public default boolean isEmpty() {
        return keys().size() == 0;
    }

    /**
     * Register a side-effect with the {@link TraversalSideEffects} providing a {@link Supplier} and a {@link BinaryOperator}.
     * If a null value is provided for the supplier or reducer, then it no supplier or reducer is registered.
     *
     * @param key          the key of the side-effect value
     * @param initialValue the initial value supplier
     * @param reducer      the reducer to use for merging a distributed side-effect value into a single value
     * @param <V>          the type of the side-effect value
     */
    public <V> void register(final String key, final Supplier<V> initialValue, final BinaryOperator<V> reducer);

    /**
     * Register a side-effect with the {@link TraversalSideEffects} providing a {@link Supplier} and a {@link BinaryOperator}.
     * The registration will only overwrite a supplier or reducer if no supplier or reducer existed prior.
     * If a null value is provided for the supplier or reducer, then it no supplier or reducer is registered.
     *
     * @param key          the key of the side-effect value
     * @param initialValue the initial value supplier
     * @param reducer      the reducer to use for merging a distributed side-effect value into a single value
     * @param <V>          the type of the side-effect value
     */
    public <V> void registerIfAbsent(final String key, final Supplier<V> initialValue, final BinaryOperator<V> reducer);

    /**
     * Get the reducer associated with the side-effect key. If no reducer was registered, then {@link Operator#assign} is provided.
     *
     * @param key the key of the side-effect
     * @param <V> the type of the side-effect value
     * @return the registered reducer
     * @throws IllegalArgumentException if no side-effect exists for the provided key
     */
    public <V> BinaryOperator<V> getReducer(final String key) throws IllegalArgumentException;

    /**
     * Get the supplier associated with the side-effect key. If no supplier was registered, then {@link ConstantSupplier} is provided.
     *
     * @param key the key of the side-effect
     * @param <V> the type of the side-effect value
     * @return the registered supplier
     * @throws IllegalArgumentException if no side-effect exists for the provided key
     */
    public <V> Supplier<V> getSupplier(final String key) throws IllegalArgumentException;

    /**
     * Add a value to the global side-effect value.
     * This should be used by steps to ensure that side-effects are merged properly in a distributed environment.
     * {@link TraversalSideEffects#set(String, Object)}  should only be used in single-threaded systems or by a master traversal in a distributed environment.
     *
     * @param key   the key of the side-effect.
     * @param value the partital value (to be merged) of the side-effect.
     * @throws IllegalArgumentException if no side-effect exists for the provided key
     */
    public void add(final String key, final Object value) throws IllegalArgumentException;

    /**
     * Set the initial value of each {@link Traverser} "sack" along with the operators for splitting and merging sacks.
     * If no split operator is provided, then a direct memory copy is assumed (this is typically good for primitive types and strings).
     * If no merge operator is provided, then traversers with sacks will not be merged.
     *
     * @param initialValue  the initial value supplier of the traverser sack
     * @param splitOperator the split operator for splitting traverser sacks
     * @param mergeOperator the merge operator for merging traverser sacks
     * @param <S>           the sack type
     */
    public <S> void setSack(final Supplier<S> initialValue, final UnaryOperator<S> splitOperator, final BinaryOperator<S> mergeOperator);

    /**
     * If sacks are enabled, get the initial value of the {@link Traverser} sack.
     * If its not enabled, then <code>null</code> is returned.
     *
     * @param <S> the sack type
     * @return the supplier of the initial value of the traverser sack
     */
    public <S> Supplier<S> getSackInitialValue();

    /**
     * If sacks are enabled and a split operator has been specified, then get it (else get <code>null</code>).
     * The split operator is used to split a sack when a bifurcation in a {@link Traverser} happens.
     *
     * @param <S> the sack type
     * @return the operator for splitting a traverser sack
     */
    public <S> UnaryOperator<S> getSackSplitter();

    /**
     * If sacks are enabled and a merge function has been specified, then get it (else get <code>null</code>).
     * The merge function is used to merge two sacks when two {@link Traverser}s converge.
     *
     * @param <S> the sack type
     * @return the operator for merging two traverser sacks
     */
    public <S> BinaryOperator<S> getSackMerger();

    ////////////

    public default <V> void forEach(final BiConsumer<String, V> biConsumer) {
        this.keys().forEach(key -> biConsumer.accept(key, this.<V>get(key)));
    }

    /**
     * Cloning is used to duplicate the sideEffects typically in distributed execution environments.
     *
     * @return The cloned sideEffects
     */
    @SuppressWarnings("CloneDoesntDeclareCloneNotSupportedException")
    public TraversalSideEffects clone();

    /**
     * Add the current {@link TraversalSideEffects} values, suppliers, and reducers to the provided {@link TraversalSideEffects}.
     * The implementation should (under the hood), use {@link TraversalSideEffects#registerIfAbsent(String, Supplier, BinaryOperator)} so that
     * if the argument {@link TraversalSideEffects} already has a registered supplier or binary operator, then don't overwrite it.
     *
     * @param sideEffects the sideEffects to add this traversal's sideEffect data to.
     */
    public void mergeInto(final TraversalSideEffects sideEffects);

    public static class Exceptions {

        public static IllegalArgumentException sideEffectKeyCanNotBeEmpty() {
            return new IllegalArgumentException("Side-effect key can not be the empty string");
        }

        public static IllegalArgumentException sideEffectKeyCanNotBeNull() {
            return new IllegalArgumentException("Side-effect key can not be null");
        }

        public static IllegalArgumentException sideEffectValueCanNotBeNull() {
            return new IllegalArgumentException("Side-effect value can not be null");
        }

        public static IllegalArgumentException sideEffectKeyDoesNotExist(final String key) {
            return new IllegalArgumentException("The side-effect key does not exist in the side-effects: " + key);
        }
    }
}
