/*
 * 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 io.trino.sql.planner.plan;

import com.fasterxml.jackson.annotation.JsonCreator;
import com.fasterxml.jackson.annotation.JsonProperty;
import com.google.common.base.Function;
import com.google.common.collect.FluentIterable;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableListMultimap;
import com.google.common.collect.ImmutableMap;
import com.google.common.collect.Iterables;
import com.google.common.collect.ListMultimap;
import com.google.common.collect.Multimap;
import com.google.common.collect.Multimaps;
import com.google.errorprone.annotations.Immutable;
import io.trino.sql.planner.Symbol;
import io.trino.sql.tree.SymbolReference;

import java.util.Collection;
import java.util.List;
import java.util.Map;

import static com.google.common.base.Preconditions.checkArgument;
import static com.google.common.collect.ImmutableList.toImmutableList;
import static java.util.Objects.requireNonNull;

@Immutable
public abstract class SetOperationNode
        extends PlanNode
{
    private final List<PlanNode> sources;
    private final ListMultimap<Symbol, Symbol> outputToInputs;
    private final List<Symbol> outputs;

    @JsonCreator
    protected SetOperationNode(
            @JsonProperty("id") PlanNodeId id,
            @JsonProperty("sources") List<PlanNode> sources,
            @JsonProperty("outputToInputs") ListMultimap<Symbol, Symbol> outputToInputs,
            @JsonProperty("outputs") List<Symbol> outputs)
    {
        super(id);

        requireNonNull(sources, "sources is null");
        checkArgument(!sources.isEmpty(), "Must have at least one source");
        requireNonNull(outputToInputs, "outputToInputs is null");
        requireNonNull(outputs, "outputs is null");

        this.sources = ImmutableList.copyOf(sources);
        this.outputToInputs = ImmutableListMultimap.copyOf(outputToInputs);
        this.outputs = ImmutableList.copyOf(outputs);

        for (Collection<Symbol> inputs : this.outputToInputs.asMap().values()) {
            checkArgument(inputs.size() == this.sources.size(), "Every source needs to map its symbols to an output %s operation symbol", this.getClass().getSimpleName());
        }

        // Make sure each source positionally corresponds to their Symbol values in the Multimap
        for (int i = 0; i < sources.size(); i++) {
            for (Collection<Symbol> expectedInputs : this.outputToInputs.asMap().values()) {
                checkArgument(sources.get(i).getOutputSymbols().contains(Iterables.get(expectedInputs, i)), "Source does not provide required symbols");
            }
        }
    }

    @Override
    @JsonProperty("sources")
    public List<PlanNode> getSources()
    {
        return sources;
    }

    @Override
    @JsonProperty("outputs")
    public List<Symbol> getOutputSymbols()
    {
        return outputs;
    }

    @JsonProperty("outputToInputs")
    public ListMultimap<Symbol, Symbol> getSymbolMapping()
    {
        return outputToInputs;
    }

    public List<Symbol> sourceOutputLayout(int sourceIndex)
    {
        // Make sure the sourceOutputLayout symbols are listed in the same order as the corresponding output symbols
        return getOutputSymbols().stream()
                .map(symbol -> outputToInputs.get(symbol).get(sourceIndex))
                .collect(toImmutableList());
    }

    /**
     * Returns the output to input symbol mapping for the given source channel
     */
    public Map<Symbol, SymbolReference> sourceSymbolMap(int sourceIndex)
    {
        ImmutableMap.Builder<Symbol, SymbolReference> builder = ImmutableMap.builder();
        for (Map.Entry<Symbol, Collection<Symbol>> entry : outputToInputs.asMap().entrySet()) {
            builder.put(entry.getKey(), Iterables.get(entry.getValue(), sourceIndex).toSymbolReference());
        }

        return builder.buildOrThrow();
    }

    /**
     * Returns the input to output symbol mapping for the given source channel.
     * A single input symbol can map to multiple output symbols, thus requiring a Multimap.
     */
    public Multimap<Symbol, SymbolReference> outputSymbolMap(int sourceIndex)
    {
        return Multimaps.transformValues(FluentIterable.from(getOutputSymbols())
                .toMap(outputToSourceSymbolFunction(sourceIndex))
                .asMultimap()
                .inverse(), Symbol::toSymbolReference);
    }

    private Function<Symbol, Symbol> outputToSourceSymbolFunction(int sourceIndex)
    {
        return outputSymbol -> outputToInputs.get(outputSymbol).get(sourceIndex);
    }
}
