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

import javax.annotation.CheckForNull;
import javax.jcr.PropertyType;

import org.apache.jackrabbit.oak.api.PropertyState;
import org.apache.jackrabbit.oak.api.Type;
import org.apache.jackrabbit.oak.commons.PathUtils;
import org.apache.jackrabbit.oak.plugins.index.lucene.IndexDefinition.IndexingRule;
import org.apache.jackrabbit.oak.plugins.index.lucene.util.LuceneIndexHelper;
import org.apache.jackrabbit.oak.spi.state.NodeState;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import static com.google.common.collect.ImmutableList.copyOf;
import static com.google.common.collect.Iterables.toArray;
import static org.apache.jackrabbit.oak.commons.PathUtils.elements;
import static org.apache.jackrabbit.oak.commons.PathUtils.isAbsolute;
import static org.apache.jackrabbit.oak.plugins.index.lucene.LuceneIndexConstants.FIELD_BOOST;
import static org.apache.jackrabbit.oak.plugins.index.lucene.LuceneIndexConstants.PROP_IS_REGEX;
import static org.apache.jackrabbit.oak.plugins.index.lucene.util.ConfigUtil.getOptionalValue;

class PropertyDefinition {
    private static final Logger log = LoggerFactory.getLogger(PropertyDefinition.class);
    /**
     * The default boost: 1.0f.
     */
    static final float DEFAULT_BOOST = 1.0f;

    /**
     * Property name. By default derived from the NodeState name which has the
     * property definition. However in case property name is a pattern, relative
     * property etc then it should be defined via 'name' property in NodeState.
     * In such case NodeState name can be set to anything
     */
    final String name;

    private final int propertyType;
    /**
     * The boost value for a property.
     */
    final float boost;

    final boolean isRegexp;

    final boolean index;

    final boolean stored;

    final boolean nodeScopeIndex;

    final boolean propertyIndex;

    final boolean analyzed;

    final boolean ordered;

    final boolean nullCheckEnabled;

    final boolean notNullCheckEnabled;

    final int includedPropertyTypes;

    final boolean relative;

    final boolean useInSuggest;

    final boolean useInSpellcheck;

    final boolean facet;

    final String[] ancestors;

    final boolean excludeFromAggregate;

    /**
     * Property name excluding the relativePath. For regular expression based definition
     * its set to null
     */
    @CheckForNull
    final String nonRelativeName;

    public PropertyDefinition(IndexingRule idxDefn, String nodeName, NodeState defn) {
        this.isRegexp = getOptionalValue(defn, PROP_IS_REGEX, false);
        this.name = getName(defn, nodeName);
        this.relative = isRelativeProperty(name);
        this.boost = getOptionalValue(defn, FIELD_BOOST, DEFAULT_BOOST);

        //By default if a property is defined it is indexed
        this.index = getOptionalValue(defn, LuceneIndexConstants.PROP_INDEX, true);
        this.stored = getOptionalValueIfIndexed(defn, LuceneIndexConstants.PROP_USE_IN_EXCERPT, false);
        this.nodeScopeIndex = getOptionalValueIfIndexed(defn, LuceneIndexConstants.PROP_NODE_SCOPE_INDEX, false);

        //If boost is specified then that field MUST be analyzed
        if (defn.hasProperty(FIELD_BOOST)){
            this.analyzed = true;
        } else {
            this.analyzed = getOptionalValueIfIndexed(defn, LuceneIndexConstants.PROP_ANALYZED, false);
        }

        //If node is not set for full text then a property definition indicates that definition is for property index
        this.propertyIndex = getOptionalValueIfIndexed(defn, LuceneIndexConstants.PROP_PROPERTY_INDEX, false);
        this.ordered = getOptionalValueIfIndexed(defn, LuceneIndexConstants.PROP_ORDERED, false);
        this.includedPropertyTypes = IndexDefinition.getSupportedTypes(defn, LuceneIndexConstants.PROP_INCLUDED_TYPE,
                IndexDefinition.TYPES_ALLOW_ALL);

        //TODO Add test case for above cases

        this.propertyType = getPropertyType(idxDefn, nodeName, defn);
        this.useInSuggest = getOptionalValueIfIndexed(defn, LuceneIndexConstants.PROP_USE_IN_SUGGEST, false);
        this.useInSpellcheck = getOptionalValueIfIndexed(defn, LuceneIndexConstants.PROP_USE_IN_SPELLCHECK, false);
        this.nullCheckEnabled = getOptionalValueIfIndexed(defn, LuceneIndexConstants.PROP_NULL_CHECK_ENABLED, false);
        this.notNullCheckEnabled = getOptionalValueIfIndexed(defn, LuceneIndexConstants.PROP_NOT_NULL_CHECK_ENABLED, false);
        this.excludeFromAggregate = getOptionalValueIfIndexed(defn, LuceneIndexConstants.PROP_EXCLUDE_FROM_AGGREGATE, false);
        this.nonRelativeName = determineNonRelativeName();
        this.ancestors = computeAncestors(name);
        this.facet = getOptionalValueIfIndexed(defn, LuceneIndexConstants.PROP_FACETS, false);
        validate();
    }


    /**
     * If 'analyzed' is enabled then property value would be used to evaluate the
     * contains clause related to those properties. In such mode also some properties
     * would be skipped from analysis
     *
     * @param propertyName name of the property to check. As property definition might
     *                     be regEx based this is required to be passed explicitly
     * @return true if the property value should be tokenized/analyzed
     */
    public boolean skipTokenization(String propertyName) {
        //For regEx case check against a whitelist
        if (isRegexp && LuceneIndexHelper.skipTokenization(propertyName)){
            return true;
        }
        return !analyzed;
    }

    public boolean fulltextEnabled(){
        return index && (analyzed || nodeScopeIndex);
    }

    public boolean propertyIndexEnabled(){
        return index && propertyIndex;
    }

    public boolean isTypeDefined(){
        return propertyType != PropertyType.UNDEFINED;
    }

    /**
     * Returns the property type. If no explicit type is defined the default is assumed
     * to be {@link javax.jcr.PropertyType#STRING}
     *
     * @return propertyType as per javax.jcr.PropertyType
     */
    public int getType(){
        //If no explicit type is defined we assume it to be string
        return isTypeDefined() ? propertyType : PropertyType.STRING;
    }

    public boolean includePropertyType(int type){
        return IndexDefinition.includePropertyType(includedPropertyTypes, type);
    }

    @Override
    public String toString() {
        return "PropertyDefinition{" +
                "name='" + name + '\'' +
                ", propertyType=" + propertyType +
                ", boost=" + boost +
                ", isRegexp=" + isRegexp +
                ", index=" + index +
                ", stored=" + stored +
                ", nodeScopeIndex=" + nodeScopeIndex +
                ", propertyIndex=" + propertyIndex +
                ", analyzed=" + analyzed +
                ", ordered=" + ordered +
                ", useInSuggest=" + useInSuggest+
                ", nullCheckEnabled=" + nullCheckEnabled +
                ", notNullCheckEnabled=" + notNullCheckEnabled +
                '}';
    }

    static boolean isRelativeProperty(String propertyName){
        return !isAbsolute(propertyName) && PathUtils.getNextSlash(propertyName, 0) > 0;
    }

    //~---------------------------------------------< internal >

    private boolean getOptionalValueIfIndexed(NodeState definition, String propName, boolean defaultVal){
        //If property is not to be indexed then all other config would be
        //set to false ignoring whatever is defined in config for them
        if (!index){
            return false;
        }
        return getOptionalValue(definition, propName, defaultVal);
    }

    private void validate() {
        if (nullCheckEnabled && isRegexp){
            throw new IllegalStateException(String.format("%s can be set to true for property definition using " +
                    "regular expression", LuceneIndexConstants.PROP_NULL_CHECK_ENABLED));
        }
    }

    private String determineNonRelativeName() {
        if (isRegexp){
            return null;
        }

        if (!relative){
            return name;
        }

        return PathUtils.getName(name);
    }

    private static String[] computeAncestors(String parentPath) {
        return toArray(copyOf(elements(PathUtils.getParentPath(parentPath))), String.class);
    }


    private static String getName(NodeState definition, String defaultName){
        PropertyState ps = definition.getProperty(LuceneIndexConstants.PROP_NAME);
        return ps == null ? defaultName : ps.getValue(Type.STRING);
    }

    private static int getPropertyType(IndexingRule idxDefn, String name, NodeState defn) {
        int type = PropertyType.UNDEFINED;
        if (defn.hasProperty(LuceneIndexConstants.PROP_TYPE)) {
            String typeName = defn.getString(LuceneIndexConstants.PROP_TYPE);
            try {
                type = PropertyType.valueFromName(typeName);
            } catch (IllegalArgumentException e) {
                log.warn("Invalid property type {} for property {} in Index {}", typeName, name, idxDefn);
            }
        }
        return type;
    }
}
