/*
 * 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.ignite.internal.configuration;

import java.util.Map;
import java.util.NoSuchElementException;
import java.util.SortedMap;
import java.util.TreeMap;
import java.util.function.Function;
import org.apache.ignite.configuration.RootKey;
import org.apache.ignite.internal.configuration.tree.ConfigurationSource;
import org.apache.ignite.internal.configuration.tree.ConfigurationVisitor;
import org.apache.ignite.internal.configuration.tree.InnerNode;
import org.jetbrains.annotations.Nullable;

/**
 * Holder of root configurations.
 */
public final class SuperRoot extends InnerNode {
    /** Root configurations. Mapping: {@link RootKey#key} -> configuration. */
    private final SortedMap<String, RootInnerNode> roots = new TreeMap<>();

    /** Function that creates root node by root name or returns {@code null} if root name is not found. */
    private final Function<String, RootInnerNode> nodeCreator;

    /**
     * Copy constructor.
     */
    private SuperRoot(SuperRoot superRoot) {
        roots.putAll(superRoot.roots);

        nodeCreator = superRoot.nodeCreator;
    }

    /**
     * Constructor.
     *
     * @param nodeCreator Function that creates root node by root name or returns {@code null} if root name is not found.
     */
    public SuperRoot(Function<String, RootInnerNode> nodeCreator) {
        this(nodeCreator, Map.of());
    }

    /**
     * Constructor.
     *
     * @param nodeCreator Function that creates root node by root name or returns {@code null} if root name is not found.
     * @param roots       Map of roots belonging to this super root.
     */
    public SuperRoot(Function<String, RootInnerNode> nodeCreator, Map<RootKey<?, ?>, InnerNode> roots) {
        this.nodeCreator = nodeCreator;

        for (Map.Entry<RootKey<?, ?>, InnerNode> entry : roots.entrySet()) {
            this.roots.put(entry.getKey().key(), new RootInnerNode(entry.getKey(), entry.getValue().copy()));
        }
    }

    /**
     * Adds a root to the super root.
     *
     * @param rootKey Root key.
     * @param root    Root node.
     */
    public void addRoot(RootKey<?, ?> rootKey, InnerNode root) {
        assert !roots.containsKey(rootKey.key()) : rootKey.key() + " : " + roots;

        assertMutability();

        roots.put(rootKey.key(), new RootInnerNode(rootKey, root));
    }

    /**
     * Gets a root.
     *
     * @param rootKey Root key of the desired root.
     * @return Root node.
     */
    @Nullable
    public InnerNode getRoot(RootKey<?, ?> rootKey) {
        RootInnerNode root = roots.get(rootKey.key());

        return root == null ? null : root.node();
    }

    /** {@inheritDoc} */
    @Override
    public <T> void traverseChildren(ConfigurationVisitor<T> visitor, boolean includeInternal) {
        for (Map.Entry<String, RootInnerNode> e : roots.entrySet()) {
            if (includeInternal || !e.getValue().internal()) {
                visitor.visitInnerNode(null, e.getKey(), e.getValue().node());
            }
        }
    }

    /** {@inheritDoc} */
    @Override
    public <T> T traverseChild(
            String key,
            ConfigurationVisitor<T> visitor,
            boolean includeInternal
    ) throws NoSuchElementException {
        RootInnerNode root = roots.get(key);

        if (root == null || (!includeInternal && root.internal())) {
            throw new NoSuchElementException(key);
        } else {
            return visitor.visitInnerNode(null, key, root.node());
        }
    }

    /** {@inheritDoc} */
    @Override
    public void construct(
            String key,
            ConfigurationSource src,
            boolean includeInternal
    ) throws NoSuchElementException {
        assertMutability();

        RootInnerNode root = roots.get(key);

        if (root == null) {
            root = nodeCreator.apply(key);
        }

        if (root == null || !includeInternal && root.internal()) {
            throw new NoSuchElementException(key);
        }

        if (src == null) {
            roots.remove(key);
        } else {
            roots.put(key, root = new RootInnerNode(root));

            src.descend(root.node());
        }
    }

    /** {@inheritDoc} */
    @Override
    public void constructDefault(String key) throws NoSuchElementException {
        throw new NoSuchElementException(key);
    }

    /** {@inheritDoc} */
    @Override
    public Class<?> schemaType() {
        return Object.class;
    }

    /** {@inheritDoc} */
    @Override
    public SuperRoot copy() {
        return new SuperRoot(this);
    }
}
