/*
 * 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.index.property;

import static org.apache.jackrabbit.oak.plugins.index.IndexConstants.INDEX_CONTENT_NODE_NAME;

import java.util.HashSet;
import java.util.Set;
import java.util.function.Predicate;

import org.apache.jackrabbit.oak.plugins.index.property.strategy.ContentMirrorStoreStrategy;
import org.apache.jackrabbit.oak.plugins.index.property.strategy.FilteringIndexStoreStrategy;
import org.apache.jackrabbit.oak.plugins.index.property.strategy.IndexStoreStrategy;
import org.apache.jackrabbit.oak.plugins.index.property.strategy.UniqueEntryStoreStrategy;
import org.apache.jackrabbit.oak.spi.mount.Mount;
import org.apache.jackrabbit.oak.spi.mount.MountInfoProvider;
import org.apache.jackrabbit.oak.spi.state.NodeBuilder;
import org.apache.jackrabbit.oak.spi.state.NodeState;
import org.apache.jackrabbit.oak.spi.state.NodeStateUtils;


import org.apache.jackrabbit.guava.common.collect.ImmutableSet;

public class Multiplexers {

    static boolean RO_PRIVATE_UNIQUE_INDEX;

    private Multiplexers() {
    }

    static {
        // TODO OAK-4645 set default to true once the code is stable
        String ro = System.getProperty(
                "oak.multiplexing.readOnlyPrivateUniqueIndex", "false");
        RO_PRIVATE_UNIQUE_INDEX = Boolean.parseBoolean(ro);
    }

    /** Index storage strategy */
    private static final IndexStoreStrategy UNIQUE = new UniqueEntryStoreStrategy(
            INDEX_CONTENT_NODE_NAME);

    /** Index storage strategy */
    private static final IndexStoreStrategy MIRROR = new ContentMirrorStoreStrategy(
            INDEX_CONTENT_NODE_NAME);

    public static Set<IndexStoreStrategy> getStrategies(boolean unique,
            MountInfoProvider mountInfoProvider, NodeBuilder definition,
            String defaultName) {
        Iterable<String> children = definition.getChildNodeNames();
        return getStrategies(unique, mountInfoProvider, children, defaultName);
    }

    public static Set<IndexStoreStrategy> getStrategies(boolean unique,
            MountInfoProvider mountInfoProvider, NodeState definition,
            String defaultName) {
        Iterable<String> children = definition.getChildNodeNames();
        return getStrategies(unique, mountInfoProvider, children, defaultName);
    }

    private static Set<IndexStoreStrategy> getStrategies(boolean unique,
            MountInfoProvider mountInfoProvider, Iterable<String> children,
            String defaultName) {
        if (mountInfoProvider.hasNonDefaultMounts()) {
            Set<String> names = new HashSet<String>();
            // TODO should this be collected from the index def?
            for (String name : children) {
                if (isIndexStorageNode(name, defaultName)) {
                    names.add(name);
                }
            }
            names.remove(defaultName);
            Set<IndexStoreStrategy> strategies = new HashSet<IndexStoreStrategy>();
            for (Mount m : mountInfoProvider.getNonDefaultMounts()) {
                String n = getNodeForMount(m, defaultName);
                names.remove(n);
                strategies.add(newStrategy(unique, false, n, m));
            }

            Mount defMount = mountInfoProvider.getDefaultMount();
            // TODO what to do with non-default names that are not covered by
            // the mount?
            for (String n : names) {
                strategies.add(newStrategy(unique, true, n, defMount));
            }
            // default mount
            strategies.add(newStrategy(unique, true, defaultName, defMount));
            return strategies;
        } else {
            return unique ? ImmutableSet.of(newUniqueStrategy(defaultName))
                    : ImmutableSet.of(newMirrorStrategy(defaultName));
        }
    }

    private static IndexStoreStrategy newUniqueStrategy(String defaultName) {
        if (INDEX_CONTENT_NODE_NAME.equals(defaultName)) {
            return UNIQUE;
        } else {
            return new UniqueEntryStoreStrategy(defaultName);
        }
    }

    private static IndexStoreStrategy newMirrorStrategy(String defaultName) {
        if (INDEX_CONTENT_NODE_NAME.equals(defaultName)) {
            return MIRROR;
        } else {
            return new ContentMirrorStoreStrategy(defaultName);
        }
    }

    private static IndexStoreStrategy newStrategy(boolean unique,
            boolean defaultMount, String name, Mount m) {
        Predicate<String> filter = newFilter(m);
        boolean readOnly = unique && !m.isDefault() && RO_PRIVATE_UNIQUE_INDEX;
        return unique ? new FilteringIndexStoreStrategy(
                new UniqueEntryStoreStrategy(name), filter, readOnly)
                : new FilteringIndexStoreStrategy(
                        new ContentMirrorStoreStrategy(name), filter);
    }

    private static Predicate<String> newFilter(final Mount m) {
        return new Predicate<String>() {

            @Override
            public boolean test(String p) {
                return m.isMounted(p);
            }
        };
    }

    private static boolean isIndexStorageNode(String name, String defaultName) {
        return NodeStateUtils.isHidden(name)
                && (name.equals(defaultName) || name
                        .endsWith(asSuffix(defaultName)));
    }

    public static String getIndexNodeName(MountInfoProvider mountInfoProvider,
            String path, String defaultName) {
        Mount mount = mountInfoProvider.getMountByPath(path);
        return getNodeForMount(mount, defaultName);
    }

    public static String getNodeForMount(Mount mount, String defaultName) {
        if (mount.isDefault()) {
            return defaultName;
        }
        return ":" + mount.getPathFragmentName() + asSuffix(defaultName);
    }

    private static String asSuffix(String name) {
        return "-" + stripStartingColon(name);
    }

    public static String stripStartingColon(String name) {
        if (name.startsWith(":")) {
            return name.substring(1);
        }
        return name;
    }

}
