/*
 * 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.nodetype.write;

import javax.annotation.Nonnull;

import static org.apache.jackrabbit.oak.api.Type.NAME;
import static org.apache.jackrabbit.oak.plugins.index.IndexConstants.INDEX_DEFINITIONS_NODE_TYPE;
import static org.apache.jackrabbit.oak.plugins.index.IndexConstants.TYPE_PROPERTY_NAME;
import static org.apache.jackrabbit.oak.plugins.memory.EmptyNodeState.EMPTY_NODE;
import static org.apache.jackrabbit.oak.plugins.memory.ModifiedNodeState.squeeze;
import static org.apache.jackrabbit.oak.plugins.version.VersionConstants.REP_VERSIONSTORAGE;

import com.google.common.collect.ImmutableList;

import org.apache.jackrabbit.oak.api.PropertyState;
import org.apache.jackrabbit.oak.api.Type;
import org.apache.jackrabbit.oak.plugins.document.bundlor.BundlingConfigInitializer;
import org.apache.jackrabbit.oak.plugins.index.IndexConstants;
import org.apache.jackrabbit.oak.plugins.index.IndexUtils;
import org.apache.jackrabbit.oak.plugins.index.counter.NodeCounterEditorProvider;
import org.apache.jackrabbit.oak.plugins.memory.MemoryNodeStore;
import org.apache.jackrabbit.oak.plugins.name.NamespaceEditorProvider;
import org.apache.jackrabbit.oak.plugins.name.Namespaces;
import org.apache.jackrabbit.oak.plugins.nodetype.NodeTypeConstants;
import org.apache.jackrabbit.oak.plugins.nodetype.TypeEditorProvider;
import org.apache.jackrabbit.oak.plugins.tree.RootFactory;
import org.apache.jackrabbit.oak.plugins.version.VersionConstants;
import org.apache.jackrabbit.oak.spi.commit.CompositeEditorProvider;
import org.apache.jackrabbit.oak.spi.commit.EditorHook;
import org.apache.jackrabbit.oak.spi.lifecycle.RepositoryInitializer;
import org.apache.jackrabbit.oak.spi.state.ApplyDiff;
import org.apache.jackrabbit.oak.spi.state.NodeBuilder;
import org.apache.jackrabbit.oak.spi.state.NodeState;
import org.apache.jackrabbit.oak.spi.state.NodeStore;

/**
 * {@code InitialContent} implements a {@link RepositoryInitializer} and
 * registers built-in node types when the micro kernel becomes available.
 */
public class InitialContent implements RepositoryInitializer, NodeTypeConstants {

    public static final NodeState INITIAL_CONTENT = createInitialContent();

    private static NodeState createInitialContent() {
        NodeBuilder builder = EMPTY_NODE.builder();
        new InitialContent().initialize(builder);
        return squeeze(builder.getNodeState());
    }

    /**
     * Whether to pre-populate the version store with intermediate nodes.
     */
    private boolean prePopulateVS = false;

    @Override
    public void initialize(@Nonnull NodeBuilder builder) {
        builder.setProperty(JCR_PRIMARYTYPE, NT_REP_ROOT, Type.NAME);

        if (!builder.hasChildNode(JCR_SYSTEM)) {
            NodeBuilder system = builder.child(JCR_SYSTEM);
            system.setProperty(JCR_PRIMARYTYPE, NT_REP_SYSTEM, Type.NAME);

            system.child(JCR_VERSIONSTORAGE)
                    .setProperty(JCR_PRIMARYTYPE, REP_VERSIONSTORAGE, Type.NAME);
            system.child(JCR_NODE_TYPES)
                    .setProperty(JCR_PRIMARYTYPE, NT_REP_NODE_TYPES, Type.NAME);
            system.child(VersionConstants.JCR_ACTIVITIES)
                    .setProperty(JCR_PRIMARYTYPE, VersionConstants.REP_ACTIVITIES, Type.NAME);

            Namespaces.setupNamespaces(system);
        }

        BundlingConfigInitializer.INSTANCE.initialize(builder);

        NodeBuilder versionStorage = builder.child(JCR_SYSTEM)
                .child(JCR_VERSIONSTORAGE);
        if (prePopulateVS && !isInitialized(versionStorage)) {
            createIntermediateNodes(versionStorage);
        }

        if (!builder.hasChildNode(IndexConstants.INDEX_DEFINITIONS_NAME)) {
            NodeBuilder index = IndexUtils.getOrCreateOakIndex(builder);

            NodeBuilder uuid = IndexUtils.createIndexDefinition(index, "uuid", true, true,
                    ImmutableList.<String>of(JCR_UUID), null);
            uuid.setProperty("info",
                    "Oak index for UUID lookup (direct lookup of nodes with the mixin 'mix:referenceable').");
            NodeBuilder nodetype = IndexUtils.createIndexDefinition(index, "nodetype", true, false,
                    ImmutableList.of(JCR_PRIMARYTYPE, JCR_MIXINTYPES), null);
            nodetype.setProperty("info", 
                    "Oak index for queries with node type, and possibly path restrictions, " + 
                    "for example \"/jcr:root/content//element(*, mix:language)\".");
            IndexUtils.createReferenceIndex(index);
            
            index.child("counter")
                    .setProperty(JCR_PRIMARYTYPE, INDEX_DEFINITIONS_NODE_TYPE, NAME)
                    .setProperty(TYPE_PROPERTY_NAME, NodeCounterEditorProvider.TYPE)
                    .setProperty(IndexConstants.ASYNC_PROPERTY_NAME, 
                            IndexConstants.ASYNC_PROPERTY_NAME)
                    .setProperty("info", "Oak index that allows to estimate " + 
                            "how many nodes are stored below a given path, " + 
                            "to decide whether traversing or using an index is faster.");
        }

        // squeeze node state before it is passed to store (OAK-2411)
        NodeState base = squeeze(builder.getNodeState());
        NodeStore store = new MemoryNodeStore(base);
        NodeTypeRegistry.registerBuiltIn(RootFactory.createSystemRoot(
                store, new EditorHook(new CompositeEditorProvider(
                new NamespaceEditorProvider(),
                new TypeEditorProvider())), null, null, null, null));
        NodeState target = store.getRoot();
        target.compareAgainstBaseState(base, new ApplyDiff(builder));
    }

    /**
     * Instructs the initializer to pre-populate the version store with
     * intermediate nodes.
     *
     * @return this instance.
     */
    public InitialContent withPrePopulatedVersionStore() {
        this.prePopulateVS = true;
        return this;
    }
    
    //--------------------------< internal >------------------------------------
    
    private boolean isInitialized(NodeBuilder versionStorage) {
        PropertyState init = versionStorage.getProperty(":initialized");
        return init != null && init.getValue(Type.LONG) > 0;
    }

    private void createIntermediateNodes(NodeBuilder versionStorage) {
        String fmt = "%02x";
        versionStorage.setProperty(":initialized", 1);
        for (int i = 0; i < 0xff; i++) {
            NodeBuilder c = storageChild(versionStorage, String.format(fmt, i));
            for (int j = 0; j < 0xff; j++) {
                storageChild(c, String.format(fmt, j));
            }
        }
    }
    
    private NodeBuilder storageChild(NodeBuilder node, String name) {
        NodeBuilder c = node.child(name);
        if (!c.hasProperty(JCR_PRIMARYTYPE)) {
            c.setProperty(JCR_PRIMARYTYPE, REP_VERSIONSTORAGE, Type.NAME);
        }
        return c;
    }
}
