/*
 * 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.wayang.core.plan.wayangplan;

import org.apache.commons.lang3.Validate;
import org.apache.wayang.core.api.Configuration;
import org.apache.wayang.core.optimizer.OptimizationContext;
import org.apache.wayang.core.optimizer.cardinality.CardinalityEstimate;
import org.apache.wayang.core.optimizer.cardinality.CardinalityPusher;
import org.apache.wayang.core.optimizer.cardinality.DefaultCardinalityPusher;
import org.apache.wayang.core.platform.Platform;

import java.io.Serializable;

import java.lang.reflect.Field;
import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
import java.util.HashSet;
import java.util.LinkedList;
import java.util.Objects;
import java.util.Queue;
import java.util.Set;
import java.util.stream.Collectors;

/**
 * An operator is any node that within a {@link WayangPlan}.
 * <p>An operator is basically determined by
 * <ul>
 * <li>its type,</li>
 * <li>its configuration,</li>
 * <li>its {@link InputSlot}s, and</li>
 * <li>its {@link OutputSlot}s.</li>
 * </ul>
 * The former two aspects are handled by subclassed, the latter two are basic features of every operator.
 * <p>{@link Slot}s are typed input and output declarations of each operator and can be connected to each other
 * to form a full {@link WayangPlan}. Moreover, we distinguish between two kinds of {@link InputSlot}s:
 * <ol>
 * <li><b>Regular.</b>Each operator will have set up these {@link InputSlot}s already during its creation.
 * They are indexed from 0 to the number of {@link InputSlot}s - 1.</li>
 * <li><b>Broadcast.</b>Some operators permit for broadcast {@link InputSlot}s. These are dynamically added and
 * will be indexed after the regular ones. Also, their execution semantics differ: Broadcast input data will be
 * provided <i>before</i> the regular data.</li>
 * </ol>
 */
public interface Operator extends Serializable {

    /**
     * @return the number of {@link InputSlot}s of this instance; inclusive of broadcast {@link InputSlot}s
     */
    default int getNumInputs() {
        return this.getAllInputs().length;
    }

    /**
     * @return the number of non-broadcast {@link InputSlot}s of this instance
     */
    default int getNumRegularInputs() {
        return this.getNumInputs() - this.getNumBroadcastInputs();
    }

    /**
     * @return the number of broadcast {@link InputSlot}s of this instance
     */
    default int getNumBroadcastInputs() {
        return (int) Arrays.stream(this.getAllInputs())
                .filter(Objects::nonNull)
                .filter(InputSlot::isBroadcast)
                .count();
    }

    /**
     * @return the number of {@link OutputSlot}s of this instance
     */
    default int getNumOutputs() {
        return this.getAllOutputs().length;
    }

    /**
     * @return the {@link InputSlot}s of this instance; inclusive of broadcast {@link InputSlot}s
     */
    InputSlot<?>[] getAllInputs();

    /**
     * @return the {@link OutputSlot}s of this instance
     */
    OutputSlot<?>[] getAllOutputs();

    /**
     * Sets the {@link InputSlot} of this instance. This method must only be invoked, when the input index is not
     * yet filled.
     *
     * @param index at which the {@link InputSlot} should be placed
     * @param input the new {@link InputSlot}
     */
    default void setInput(int index, InputSlot<?> input) {
        assert index < this.getNumRegularInputs() && this.getInput(index) == null;
        assert input.getOwner() == this;
        ((InputSlot[]) this.getAllInputs())[index] = input;
    }

    /**
     * Sets the {@link OutputSlot} of this instance. This method must only be invoked, when the output index is not
     * yet filled.
     *
     * @param index  at which the {@link OutputSlot} should be placed
     * @param output the new {@link OutputSlot}
     */
    default void setOutput(int index, OutputSlot<?> output) {
        assert index < this.getNumOutputs() && this.getOutput(index) == null;
        assert output.getOwner() == this;
        ((OutputSlot[]) this.getAllOutputs())[index] = output;
    }

    /**
     * Retrieve an {@link InputSlot} of this instance using its index.
     *
     * @param index of the {@link InputSlot}
     * @return the requested {@link InputSlot}
     */
    default InputSlot<?> getInput(int index) {
        final InputSlot[] allInputs = this.getAllInputs();
        Validate.inclusiveBetween(0, allInputs.length - 1, index, "Illegal input index %d for %s.", index, this);
        return allInputs[index];
    }

    /**
     * Retrieve an {@link OutputSlot} of this instance using its index.
     *
     * @param index of the {@link OutputSlot}
     * @return the requested {@link OutputSlot}
     */
    default OutputSlot<?> getOutput(int index) {
        final OutputSlot[] allOutputs = this.getAllOutputs();
        if (index < 0 || index >= allOutputs.length) {
            throw new IllegalArgumentException(String.format("Illegal output index: %d.", index));
        }
        return allOutputs[index];
    }

    /**
     * Retrieve an {@link InputSlot} of this instance by its name.
     *
     * @param name of the {@link InputSlot}
     * @return the requested {@link InputSlot}
     */
    default InputSlot<?> getInput(String name) {
        for (InputSlot inputSlot : this.getAllInputs()) {
            if (inputSlot.getName().equals(name)) return inputSlot;
        }
        throw new IllegalArgumentException(String.format("No slot with such name: %s", name));
    }

    /**
     * Retrieve an {@link OutputSlot} of this instance by its name.
     *
     * @param name of the {@link OutputSlot}
     * @return the requested {@link OutputSlot}
     */
    default OutputSlot<?> getOutput(String name) {
        for (OutputSlot outputSlot : this.getAllOutputs()) {
            if (outputSlot.getName().equals(name)) return outputSlot;
        }
        throw new IllegalArgumentException(String.format("No slot with such name: %s", name));
    }

    /**
     * @return whether this instance permits broadcast {@link InputSlot}s besides their regular {@link InputSlot}s
     */
    boolean isSupportingBroadcastInputs();

    /**
     * Register an {@link InputSlot} as broadcast input of this instance.
     *
     * @param broadcastInput the {@link InputSlot} to be registered
     * @return the assigned index of the {@link InputSlot}
     */
    int addBroadcastInput(InputSlot<?> broadcastInput);

    /**
     * Connect an output of this operator to the input of a second operator.
     *
     * @param thisOutputIndex index of the output slot to connect to
     * @param that            operator to connect to
     * @param thatInputIndex  index of the input slot to connect from
     */
    @SuppressWarnings("unchecked")
    default <T> void connectTo(int thisOutputIndex, Operator that, int thatInputIndex) {
        final InputSlot<T> inputSlot = (InputSlot<T>) that.getInput(thatInputIndex);
        final OutputSlot<T> outputSlot = (OutputSlot<T>) this.getOutput(thisOutputIndex);
        if (!inputSlot.getType().isSupertypeOf(outputSlot.getType())) {
            throw new IllegalArgumentException(String.format(
                    "Cannot connect %s of %s to %s of type %s.",
                    outputSlot, outputSlot.getType(), inputSlot, inputSlot.getType()));
        }
        outputSlot.connectTo(inputSlot);
    }

    /**
     * Connect an output of this operator to the input of a second operator.
     *
     * @param thisOutputName name of the output slot to connect to
     * @param that           operator to connect to
     * @param thatInputName  name of the input slot to connect from
     */
    @SuppressWarnings("unchecked")
    default <T> void connectTo(String thisOutputName, Operator that, String thatInputName) {
        final InputSlot<T> inputSlot = (InputSlot<T>) that.getInput(thatInputName);
        final OutputSlot<T> outputSlot = (OutputSlot<T>) this.getOutput(thisOutputName);
        if (!inputSlot.getType().isSupertypeOf(outputSlot.getType())) {
            throw new IllegalArgumentException("Cannot connect slots: mismatching types");
        }
        outputSlot.connectTo(inputSlot);
    }

    /**
     * Connect an output of this operator as a broadcast input of a second operator.
     *
     * @param thisOutputIndex index of the output slot to connect to
     * @param that            operator to connect to
     * @param broadcastName   name of the broadcast that will be used by the operator to identify the broadcast; must
     *                        be unique among all {@link InputSlot}s
     */
    default void broadcastTo(int thisOutputIndex, Operator that, String broadcastName) {
        final OutputSlot<?> output = this.getOutput(thisOutputIndex);
        final InputSlot<?> broadcastInput = new InputSlot<>(broadcastName, that, true, output.getType());
        final int broadcastIndex = that.addBroadcastInput(broadcastInput);
        this.connectTo(thisOutputIndex, that, broadcastIndex);
    }

    /**
     * Connect an output of this operator as a broadcast input of a second operator.
     *
     * @param thisOutputName name of the output slot to connect to
     * @param that           operator to connect to
     * @param broadcastName  name of the broadcast that will be used by the operator to identify the broadcast; must
     *                       be unique among all {@link InputSlot}s
     */
    default void broadcastTo(String thisOutputName, Operator that, String broadcastName) {
        final OutputSlot<?> output = this.getOutput(thisOutputName);
        final InputSlot<?> broadcastInput = new InputSlot<>(broadcastName, that, true, output.getType());
        final int broadcastIndex = that.addBroadcastInput(broadcastInput);
        this.connectTo(output.getIndex(), that, broadcastIndex);
    }

    /**
     * Retrieves the effective occupant of the given {@link InputSlot}, i.e., the {@link OutputSlot} that is
     * either connected to the given or an outer-more, mapped {@link InputSlot}.
     *
     * @param inputIndex of the {@link InputSlot} whose effective occupant is requested
     * @return the effective occupant or {@code null} if none
     */
    @SuppressWarnings("unchecked")
    default <T> OutputSlot<T> getEffectiveOccupant(int inputIndex) {
        return this.getOutermostInputSlot((InputSlot<T>) this.getInput(inputIndex)).getOccupant();
    }

    /**
     * Retrieves the effective occupant of the given {@link InputSlot}, i.e., the {@link OutputSlot} that is
     * either connected to the given or an outer-more, mapped {@link InputSlot}.
     *
     * @param input whose effective occupant is requested
     * @return the effective occupant or {@code null} if none
     */
    default <T> OutputSlot<T> getEffectiveOccupant(InputSlot<T> input) {
        return this.getOutermostInputSlot(input).getOccupant();
    }

    /**
     * Retrieve the outermost {@link InputSlot} if this operator is nested in other operators.
     *
     * @param input the slot to track
     * @return the outermost {@link InputSlot}
     * @see #getParent()
     */
    default <T> InputSlot<T> getOutermostInputSlot(InputSlot<T> input) {
        if (!this.isOwnerOf(input)) {
            throw new IllegalArgumentException("Slot does not belong to this operator.");
        }

        if (input.getOccupant() != null) {
            return input;
        }

        // Try to exit through the parent.
        final OperatorContainer container = this.getContainer();
        if (container != null) {
            final InputSlot<T> tracedInput = container.traceInput(input);
            if (tracedInput != null) {
                return container.toOperator().getOutermostInputSlot(tracedInput);
            }
        }

        return input;
    }

    /**
     * Retrieve the {@link InputSlot} of the enclosing {@link OperatorContainer} that represents the given one.
     *
     * @param input the slot to track
     * @return the outer {@link InputSlot} or {@code null} if none
     * @see #getParent()
     */
    default <T> InputSlot<T> getOuterInputSlot(InputSlot<T> input) {
        assert this.isOwnerOf(input);

        // Try to exit through the parent.
        final OperatorContainer container = this.getContainer();
        return container != null ? container.traceInput(input) : null;
    }

    /**
     * Retrieve the outermost {@link OutputSlot}s if this operator is nested in other operators.
     *
     * @param output the slot to track
     * @return the outermost {@link InputSlot}
     * @see #getParent()
     */
    default <T> Collection<OutputSlot<T>> getOutermostOutputSlots(OutputSlot<T> output) {
        Validate.isTrue(this.isOwnerOf(output));

        if (!output.getOccupiedSlots().isEmpty()) {
            return Collections.singleton(output);
        }

        // Try to exit through the parent.
        final OperatorContainer container = this.getContainer();
        if (container != null) {
            final Collection<OutputSlot<T>> followedOutputs = container.followOutput(output);
            if (!followedOutputs.isEmpty()) {
                return followedOutputs.stream()
                        .flatMap(followedOutput -> container.toOperator().getOutermostOutputSlots(followedOutput).stream())
                        .collect(Collectors.toList());
            }
        }

        return Collections.singleton(output);
    }

    default boolean isOwnerOf(Slot<?> slot) {
        return slot.getOwner() == this;
    }

    /**
     * Declare forward rules. Execution engines may take the chance to optimize the executed plans by having
     * forwarded data by-pass this instance. However, note the specific semantics of a forward rule: If an
     * {@link Operator} serves an {@link OutputSlot} that is involved in a foward rule, it will do so by forwarding.
     * If the {@link OutputSlot} is not served, then the forwarding does not apply.
     *
     * @return {@link OutputSlot}s to which this instance forwards the given {@code input}.
     * @see #isReading(InputSlot)
     */
    default Collection<OutputSlot<?>> getForwards(InputSlot<?> input) {
        assert this.isOwnerOf(input);
        return Collections.emptyList();
    }

    /**
     * Checks whether this instance is not connected to any other instance via its {@link Slot}s. This is a typical
     * property of instances used for {@link org.apache.wayang.core.optimizer.channels.ChannelConversion}s.
     *
     * @return whether this instance is unconnected
     */
    default boolean isUnconnected() {
        for (InputSlot<?> inputSlot : this.getAllInputs()) {
            if (inputSlot.getOccupant() != null) return false;
        }
        for (OutputSlot<?> outputSlot : this.getAllOutputs()) {
            if (!outputSlot.getOccupiedSlots().isEmpty()) return false;
        }
        return true;
    }

    /**
     * Tells whether the given {@code input} is read by this operator. If not, the optimizer can make use of this
     * insight.
     *
     * @see #getForwards(InputSlot)
     */
    default boolean isReading(InputSlot<?> input) {
        return true;
    }

    /**
     * @return whether this {@code input} is used to close a feedback loop (i.e., a flow graph cycle)
     */
    default boolean isFeedbackInput(InputSlot<?> input) {
        assert this.isOwnerOf(input);
        return this.isLoopHead() && ((LoopHeadOperator) this).getLoopBodyInputs().contains(input);
    }

    /**
     * @return whether this {@code output} is used to start a feedback loop (i.e., a flow graph cycle)
     */
    default boolean isFeedforwardOutput(OutputSlot<?> ouput) {
        assert this.isOwnerOf(ouput);
        return this.isLoopHead() && ((LoopHeadOperator) this).getLoopBodyOutputs().contains(ouput);
    }


    /**
     * Tells whether this operator is a sink, i.e., it has no outputs.
     *
     * @return whether this operator is a sink
     */
    default boolean isSink() {
        return this.getNumOutputs() == 0;
    }

    /**
     * Tells whether this operator is a source, i.e., it has no inputs.
     *
     * @return whether this operator is a source
     */
    default boolean isSource() {
        return this.getNumInputs() == 0;
    }

    default boolean isSubplan() {
        return this instanceof Subplan;
    }

    default boolean isLoopSubplan() {
        return this instanceof LoopSubplan;
    }

    default boolean isAlternative() {
        return this instanceof OperatorAlternative;
    }

    default boolean isExecutionOperator() {
        return this instanceof ExecutionOperator;
    }

    /**
     * Identify this instance as the head of a loop. It is the only kind of {@link Operator} that can cause
     * data flow cycles.
     *
     * @return whether this instance is the head of a loop
     */
    default boolean isLoopHead() {
        return this instanceof LoopHeadOperator;
    }

    /**
     * @return whether this is an elementary operator
     */
    default boolean isElementary() {
        return true;
    }

    /**
     * @return whether this is a conversion operator
     */
    default boolean isConversion() {
        return false;
    }

    /**
     * This method is part of the visitor pattern and calls the appropriate visit method on {@code visitor}.
     */
    <Payload, Return> Return accept(TopDownPlanVisitor<Payload, Return> visitor, OutputSlot<?> outputSlot, Payload payload);

//    <Payload, Return> Map<InputSlot, Return> accept(BottomUpPlanVisitor<Payload, Return> visitor, InputSlot<?> inputSlot, Payload payload);

    /**
     * Operators can be nested in other operators, e.g., in a {@link Subplan} or a {@link OperatorAlternative}.
     *
     * @return the parent of this operator or {@code null} if this is a top-level operator
     */
    default CompositeOperator getParent() {
        final OperatorContainer container = this.getContainer();
        return container == null ? null : container.toOperator();
    }

    OperatorContainer getContainer();

    /**
     * Operators can be nested in other operators, e.g., in a {@link Subplan} or a {@link OperatorAlternative.Alternative}.
     *
     * @param newContainer the new container of this operator or {@code null} to declare it top-level
     */
    void setContainer(OperatorContainer newContainer);

    /**
     * @return the innermost {@link LoopSubplan} containing this instance
     */
    default LoopSubplan getInnermostLoop() {
        final CompositeOperator parent = this.getParent();
        if (parent == null) {
            return null;
        } else if (parent.isLoopSubplan()) {
            return (LoopSubplan) parent;
        } else {
            return parent.getInnermostLoop();
        }
    }

    /**
     * @return the stack of nested {@link LoopSubplan}s of this instance, from inside to outside
     */
    default LinkedList<LoopSubplan> getLoopStack() {
        LinkedList<LoopSubplan> loopStack = new LinkedList<>();
        LoopSubplan nextLoop = this.getInnermostLoop();
        while (nextLoop != null) {
            loopStack.addLast(nextLoop);
            nextLoop = nextLoop.getInnermostLoop();
        }
        return loopStack;
    }

    /**
     * Each operator is associated with an epoch, which is a logical timestamp for the operator's creation.
     * This value is the lowest timestamp and default epoch.
     */
    int FIRST_EPOCH = 0;

    /**
     * <i>Optional operation for non-composite operators.</i>
     *
     * @param epoch the operator's new epoch value
     * @see #FIRST_EPOCH
     */
    void setEpoch(int epoch);


    /**
     * <i>Optional operation for non-composite operators.</i>
     *
     * @return the operator's epoch value
     * @see #FIRST_EPOCH
     */
    int getEpoch();

    /**
     * Provide a {@link CardinalityPusher} for the {@link Operator}.
     *
     * @param configuration if the {@link CardinalityPusher} depends on further ones, use this to obtain the latter
     * @return the {@link CardinalityPusher}
     */
    default CardinalityPusher getCardinalityPusher(final Configuration configuration) {
        return new DefaultCardinalityPusher(this, configuration.getCardinalityEstimatorProvider());
    }

    /**
     * Tells if this instance should be executed/implemented only on a certain set of {@link Platform}s.
     *
     * @return the targeted {@link Platform}s or an empty {@link Set} if there is no such restriction
     */
    Set<Platform> getTargetPlatforms();

    /**
     * <i>Optional operation.</i> Restrict this instance to be executed/implemented on a certain {@link Platform}s or
     * allow a further one if there is already a restriction in place.
     */
    void addTargetPlatform(Platform platform);

    /**
     * Convenience version of {@link Operator#propagateOutputCardinality(int, OptimizationContext.OperatorContext)},
     * where the adjacent {@link InputSlot}s reside in the same {@link OptimizationContext} as the {@code operatorContext}.
     */
    default void propagateOutputCardinality(int outputIndex, OptimizationContext.OperatorContext operatorContext) {
        this.propagateOutputCardinality(outputIndex, operatorContext, operatorContext.getOptimizationContext());
    }

    /**
     * Propagates the {@link CardinalityEstimate} of an {@link OutputSlot} within the {@code operatorContext} to
     * <ul>
     * <li>fed {@link InputSlot}s (which in turn are asked to propagate) and</li>
     * <li><b>inner</b>, mapped {@link OutputSlot}s.</li>
     * </ul>
     *
     * @param outputIndex     of the {@link OutputSlot}
     * @param operatorContext holds the {@link CardinalityEstimate} to be propagated
     * @param targetContext   to which the {@link CardinalityEstimate}s should be propagated
     */
    void propagateOutputCardinality(int outputIndex,
                                    OptimizationContext.OperatorContext operatorContext,
                                    OptimizationContext targetContext);

    /**
     * Propagates the {@link CardinalityEstimate} of an {@link InputSlot} within the {@code operatorContext}
     * to <b>inner</b>, mapped {@link InputSlot}s.
     *
     * @param inputIndex      of the {@link InputSlot}
     * @param operatorContext holds the {@link CardinalityEstimate} to be propagated
     */
    void propagateInputCardinality(int inputIndex,
                                   OptimizationContext.OperatorContext operatorContext);

    /**
     * Collect all inner {@link OutputSlot}s that are mapped to the given {@link OutputSlot}.
     */
    <T> Set<OutputSlot<T>> collectMappedOutputSlots(OutputSlot<T> output);

    /**
     * Collect all inner {@link InputSlot}s that are mapped to the given {@link InputSlot}.
     */
    <T> Set<InputSlot<T>> collectMappedInputSlots(InputSlot<T> input);

    /**
     * Provides an instance's name.
     *
     * @return the name of this instance or {@code null} if none
     */
    String getName();

    /**
     * Provide a name for this instance.
     *
     * @param name the name
     */
    void setName(String name);

    /**
     * Collects all fields of this instance that have a {@link EstimationContextProperty} annotation.
     *
     * @return the fields
     */
    default Collection<String> getEstimationContextProperties() {
        Set<String> properties = new HashSet<>(2);
        Queue<Class<?>> classQueue = new LinkedList<>();
        classQueue.add(this.getClass());
        while (!classQueue.isEmpty()) {
            final Class<?> cls = classQueue.poll();
            if (cls.getSuperclass() != null) classQueue.add(cls.getSuperclass());
            for (Field declaredField : cls.getDeclaredFields()) {
                final EstimationContextProperty annotation = declaredField.getDeclaredAnnotation(EstimationContextProperty.class);
                if (annotation != null) {
                    properties.add(declaredField.getName());
                }
            }
        }
        return properties;
    }

    /*
     * Collects all in and outputs of this operator instance and connects
     * the operator given as parameter to them, effectively rendering this
     * instance useless
     *
     * @param operator the operator to replace this one with
     */
    default void replaceWith(Operator operator) {
        InputSlot.stealConnections(this, operator);
        OutputSlot.stealConnections(this, operator);
    }
}
