/*
 * 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.benchmark.util;

import com.google.common.base.Strings;
import com.google.common.collect.Lists;
import org.apache.jackrabbit.JcrConstants;
import org.apache.jackrabbit.commons.JcrUtils;
import org.apache.jackrabbit.oak.plugins.index.IndexConstants;
import org.apache.jackrabbit.oak.plugins.index.lucene.LuceneIndexConstants;
import org.apache.jackrabbit.oak.plugins.index.property.OrderedIndex;
import org.apache.jackrabbit.oak.plugins.index.property.PropertyIndexEditorProvider;
import org.apache.jackrabbit.oak.plugins.index.search.FulltextIndexConstants;
import org.apache.jackrabbit.oak.spi.nodetype.NodeTypeConstants;
import org.jetbrains.annotations.Nullable;

import javax.jcr.Node;
import javax.jcr.Property;
import javax.jcr.PropertyType;
import javax.jcr.RepositoryException;
import javax.jcr.Session;
import javax.jcr.Value;

import java.util.Arrays;
import java.util.List;
import java.util.Map;

/**
 * A simple utility class for Oak indexes.
 */
public class OakIndexUtils {
    
    /**
     * A property index
     */
    public static class PropertyIndex {
        
        private String indexName;
        
        private String propertyName;
        
        private String[] nodeTypeNames;

        /**
         * Set the index name. If not set, the index name is the property name.
         * 
         * @param indexName the index name
         * @return this
         */
        public PropertyIndex name(String indexName) {
            this.indexName = indexName;
            return this;
        }

        /**
         * Set the property name. This field is mandatory.
         * 
         * @param propertyName the property name
         * @return this
         */
        public PropertyIndex property(String propertyName) {
            this.propertyName = propertyName;
            return this;
        }

        /**
         * Restrict the node types.
         * 
         * @param nodeTypeNames the list of declaring node types
         * @return this
         */
        public PropertyIndex nodeTypes(String... nodeTypeNames) {
            this.nodeTypeNames = nodeTypeNames;
            return this;
        }

        /**
         * Create the index.
         * <p>
         * If this is not a Oak repository, this method does nothing.
         * <p>
         * If a matching index already exists, this method verifies that the
         * definition matches. If no such index exists, a new one is created.
         * 
         * @param session the session to use for creating the index
         * @return the index node
         * @throws RepositoryException if writing to the repository failed, the
         *             index definition is incorrect, or if such an index exists
         *             but is not compatible with this definition (for example,
         *             a different property is indexed)
         */
        public @Nullable Node create(Session session) throws RepositoryException {
           return create(session,PropertyIndexEditorProvider.TYPE);
        }
        
        public @Nullable Node create(Session session,String indexType) throws RepositoryException {
           Node index;
           if (!session.getWorkspace().getNodeTypeManager().hasNodeType(
                        "oak:QueryIndexDefinition")) {
                // not an Oak repository
                return null;
            }
            if (session.hasPendingChanges()) {
                throw new RepositoryException("The session has pending changes");
            }
            if (indexName == null) {
                indexName = propertyName;
            }
            if (propertyName == null) {
                throw new RepositoryException("Index property name not set");
            }
            if (nodeTypeNames != null) {
                if (nodeTypeNames.length == 0) {
                    // setting the node types to an empty array means "all node types"
                    // (same as not setting it)
                    nodeTypeNames = null;
                } else {
                    Arrays.sort(nodeTypeNames);
                }
            }
            Node root = session.getRootNode();
            Node indexDef;
            if (!root.hasNode(IndexConstants.INDEX_DEFINITIONS_NAME)) {
                indexDef = root.addNode(IndexConstants.INDEX_DEFINITIONS_NAME, 
                        JcrConstants.NT_UNSTRUCTURED);
                session.save();
            } else {
                indexDef = root.getNode(IndexConstants.INDEX_DEFINITIONS_NAME);
            }

            if (indexDef.hasNode(indexName)) {
                // verify the index matches
                index = indexDef.getNode(indexName);
                if (index.hasProperty(IndexConstants.UNIQUE_PROPERTY_NAME)) {
                    Property p = index.getProperty(IndexConstants.UNIQUE_PROPERTY_NAME);
                    if (p.getBoolean()) {
                        throw new RepositoryException(
                                "Index already exists, but is unique");
                    }
                }
                String type = index.getProperty(
                        IndexConstants.TYPE_PROPERTY_NAME).getString();
                if (!type.equals(indexType)) {
                    throw new RepositoryException(
                            "Index already exists, but is of type " + type);
                }
                Value[] v = index.getProperty(IndexConstants.PROPERTY_NAMES).getValues();
                if (v.length != 1) {
                    String[] list = new String[v.length];
                    for (int i = 0; i < v.length; i++) {
                        list[i] = v[i].getString();
                    }
                    throw new RepositoryException(
                            "Index already exists, but is not just one property, but " + Arrays.toString(list));
                }
                if (!propertyName.equals(v[0].getString())) {
                    throw new RepositoryException(
                            "Index already exists, but is for property " + v[0].getString());
                }
                if (index.hasProperty(IndexConstants.DECLARING_NODE_TYPES)) {
                    v = index.getProperty(IndexConstants.DECLARING_NODE_TYPES).getValues();
                    String[] list = new String[v.length];
                    for (int i = 0; i < v.length; i++) {
                        list[i] = v[i].getString();
                    }
                    Arrays.sort(list);
                    if (Arrays.equals(list,  nodeTypeNames)) {
                        throw new RepositoryException(
                                "Index already exists, but with different node types: " + Arrays.toString(list));
                    }
                } else if (nodeTypeNames != null) {
                    throw new RepositoryException(
                            "Index already exists, but without node type restriction");
                }
                // matches
                return index;
            }
            index = indexDef.addNode(indexName, IndexConstants.INDEX_DEFINITIONS_NODE_TYPE);
            index.setProperty(IndexConstants.TYPE_PROPERTY_NAME, indexType);
            index.setProperty(IndexConstants.REINDEX_PROPERTY_NAME, 
                    true);
            index.setProperty(IndexConstants.PROPERTY_NAMES, 
                    new String[] { propertyName }, PropertyType.NAME);
            if (nodeTypeNames != null) {
                index.setProperty(IndexConstants.DECLARING_NODE_TYPES,
                        nodeTypeNames);
            }
            session.save();
            return index;
        }
    }

    /**
     * Helper method to create or update a property index definition.
     *
     * @param session the session
     * @param indexDefinitionName the name of the node for the index definition
     * @param propertyNames the list of properties to index
     * @param unique if unique or not
     * @param enclosingNodeTypes the enclosing node types
     * @return the node just created
     * @throws RepositoryException the repository exception
     */
    public static Node propertyIndexDefinition(Session session, String indexDefinitionName,
            String[] propertyNames, boolean unique,
            String[] enclosingNodeTypes) throws RepositoryException {
        
        Node root = session.getRootNode();
        Node indexDefRoot = JcrUtils.getOrAddNode(root, IndexConstants.INDEX_DEFINITIONS_NAME,
                NodeTypeConstants.NT_UNSTRUCTURED);
        Node indexDef = JcrUtils.getOrAddNode(indexDefRoot, indexDefinitionName,
                IndexConstants.INDEX_DEFINITIONS_NODE_TYPE);
        indexDef.setProperty(IndexConstants.TYPE_PROPERTY_NAME, PropertyIndexEditorProvider.TYPE);
        indexDef.setProperty(IndexConstants.REINDEX_PROPERTY_NAME, true);
        indexDef.setProperty(IndexConstants.PROPERTY_NAMES, propertyNames,
                PropertyType.NAME);
        indexDef.setProperty(IndexConstants.UNIQUE_PROPERTY_NAME, unique);

        if (enclosingNodeTypes != null && enclosingNodeTypes.length != 0) {
            indexDef.setProperty(IndexConstants.DECLARING_NODE_TYPES, enclosingNodeTypes,
                PropertyType.NAME);
        }
        session.save();
        
        return indexDef;
    }


    /**
     * Helper method to create or update an ordered index definition.
     *
     * @param session the session
     * @param indexDefinitionName the name of the node for the index definition
     * @param async whether the indexing is async or not
     * @param propertyNames the list of properties to index
     * @param unique if unique or not
     * @param enclosingNodeTypes the enclosing node types
     * @param direction the direction
     * @return the node just created
     * @throws RepositoryException the repository exception
     */
    public static Node orderedIndexDefinition(Session session, String indexDefinitionName,
        String async, String[] propertyNames, boolean unique,
        String[] enclosingNodeTypes, String direction) throws RepositoryException {

        Node root = session.getRootNode();
        Node indexDefRoot = JcrUtils.getOrAddNode(root, IndexConstants.INDEX_DEFINITIONS_NAME,
            NodeTypeConstants.NT_UNSTRUCTURED);
        Node indexDef = JcrUtils.getOrAddNode(indexDefRoot, indexDefinitionName,
            IndexConstants.INDEX_DEFINITIONS_NODE_TYPE);
        indexDef.setProperty(IndexConstants.TYPE_PROPERTY_NAME, OrderedIndex.TYPE);
        indexDef.setProperty(IndexConstants.PROPERTY_NAMES, propertyNames,
            PropertyType.NAME);

        if (enclosingNodeTypes != null && enclosingNodeTypes.length != 0) {
            indexDef.setProperty(IndexConstants.DECLARING_NODE_TYPES, enclosingNodeTypes,
                PropertyType.NAME);
        }

        if (direction != null) {
            indexDef.setProperty(OrderedIndex.DIRECTION, direction);
        }

        if (async != null) {
            indexDef.setProperty(IndexConstants.ASYNC_PROPERTY_NAME, async);
        }
        indexDef.setProperty(IndexConstants.UNIQUE_PROPERTY_NAME, unique);
        indexDef.setProperty(IndexConstants.REINDEX_PROPERTY_NAME, true);

        session.save();

        return indexDef;
    }

    /**
     * Helper method to create or update a lucene property index definition.
     *
     * @param session the session
     * @param indexDefinitionName the name of the node for the index definition
     * @param propertyNames the list of properties to index
     * @param type the types of the properties in order of the properties
     * @param orderedPropsMap the ordered props and its properties
     * @param persistencePath the path if the persistence=file (default is repository)
     * @return the node just created
     * @throws RepositoryException the repository exception
     */
    public static Node luceneIndexDefinition(Session session, String indexDefinitionName,
        String async, String[] propertyNames, String[] type,
        Map<String, Map<String, String>> orderedPropsMap, String persistencePath)
        throws RepositoryException {

        Node root = session.getRootNode();
        Node indexDefRoot = JcrUtils.getOrAddNode(root, IndexConstants.INDEX_DEFINITIONS_NAME,
            NodeTypeConstants.NT_UNSTRUCTURED);

        Node indexDef = JcrUtils.getOrAddNode(indexDefRoot, indexDefinitionName,
            IndexConstants.INDEX_DEFINITIONS_NODE_TYPE);

        indexDef.setProperty(IndexConstants.TYPE_PROPERTY_NAME, LuceneIndexConstants.TYPE_LUCENE);
        indexDef.setProperty(FulltextIndexConstants.FULL_TEXT_ENABLED, false);
        if (async != null) {
            indexDef.setProperty(IndexConstants.ASYNC_PROPERTY_NAME, async);
        }
        // Set indexed property names
        indexDef.setProperty(FulltextIndexConstants.INCLUDE_PROPERTY_NAMES, propertyNames,
            PropertyType.NAME);

        Node propsNode = JcrUtils.getOrAddNode(indexDef, FulltextIndexConstants.PROP_NODE);
        for (int i = 0; i < propertyNames.length; i++) {
            Node propNode =
                JcrUtils.getOrAddNode(propsNode, propertyNames[i], NodeTypeConstants.NT_OAK_UNSTRUCTURED);
            propNode.setProperty(FulltextIndexConstants.PROP_TYPE, type[i]);
        }

        // Set ordered property names
        if ((orderedPropsMap != null) && !orderedPropsMap.isEmpty()) {
            List<String> orderedProps = Lists.newArrayList();
            for (Map.Entry<String, Map<String, String>> orderedPropEntry : orderedPropsMap
                .entrySet()) {
                Node propNode = JcrUtils.getOrAddNode(propsNode, orderedPropEntry.getKey(),
                    NodeTypeConstants.NT_OAK_UNSTRUCTURED);
                propNode.setProperty(FulltextIndexConstants.PROP_TYPE,
                    orderedPropEntry.getValue().get(FulltextIndexConstants.PROP_TYPE));
                orderedProps.add(orderedPropEntry.getKey());
            }
            if (!orderedProps.isEmpty()) {
                indexDef.setProperty(FulltextIndexConstants.ORDERED_PROP_NAMES,
                    orderedProps.toArray(new String[orderedProps.size()]),
                    PropertyType.NAME);
            }
        }

        // Set file persistence if specified
        if (!Strings.isNullOrEmpty(persistencePath)) {
            indexDef.setProperty(FulltextIndexConstants.PERSISTENCE_NAME,
                FulltextIndexConstants.PERSISTENCE_FILE);
            indexDef.setProperty(FulltextIndexConstants.PERSISTENCE_PATH,
                persistencePath);
        }
        session.save();

        return indexDef;
    }
}
