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

import java.util.Collection;
import java.util.Collections;
import java.util.List;
import java.util.Set;

import javax.jcr.PropertyType;

import org.apache.jackrabbit.oak.api.PropertyValue;
import org.apache.jackrabbit.oak.commons.PathUtils;
import org.apache.jackrabbit.oak.spi.query.fulltext.FullTextExpression;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;

/**
 * The filter for an index lookup that contains a number of restrictions that
 * are combined with AND. Possible restrictions are a property restriction, a
 * path restriction, a node type restriction, and a fulltext restriction.
 * <p>
 * A property restriction could be that the property must exist, or that the
 * property value has to be within a certain range.
 * <p>
 * A path restriction could be a restriction to a certain subtree, a parent of a
 * certain path, or equality to a certain path.
 */
public interface Filter {

    /**
     * Get the list of property restrictions, if any. Each property may contain
     * multiple restrictions, for example x=1 and x=2. For this case, only
     * multi-valued properties match that contain both 1 and 2.
     *
     * @return the conditions (an empty collection if not used)
     */
    Collection<PropertyRestriction> getPropertyRestrictions();

    /**
     * Get the fulltext search conditions, if any.
     *
     * @return the conditions (an empty collection if not used)
     */
    @Deprecated
    Collection<String> getFulltextConditions();

    /**
     * Get the fulltext search condition expression, if any.
     *
     * @return the condition (null if none)
     */
    FullTextExpression getFullTextConstraint();

    QueryLimits getQueryLimits();

    /**
     * check whether a certain (valid) path is accessible (can be read) from the user associated with the query Session
     * @param path a valid JCR path
     * @return <code>true</code> if path can be read by the calling user, <code>false</code> otherwise.
     */
    boolean isAccessible(String path);

    /**
     * Whether the filter contains a native condition.
     *
     * @return true if it does
     */
    boolean containsNativeConstraint();

    /**
     * Get the most restrictive property restriction for the given property, if
     * any.
     *
     * @param propertyName the property name
     * @return the first restriction, or null if there is no restriction for
     *         this property
     */
    PropertyRestriction getPropertyRestriction(String propertyName);

    /**
     * Get the all property restriction for the given property.
     *
     * @param propertyName the property name
     * @return the list of restrictions (possibly empty, never null)
     */
    List<PropertyRestriction> getPropertyRestrictions(String propertyName);

    /**
     * Get the path restriction type.
     *
     * @return the path restriction type
     */
    PathRestriction getPathRestriction();

    /**
     * Get the path, or "/" if there is no path restriction set.
     *
     * @return the path
     */
    String getPath();

    /**
     * Get the plan for the path.
     *
     * @return the plan
     */
    String getPathPlan();

    /**
     * Returns the name of the filter node type.
     *
     * @return nodetype name
     */
    @Nullable
    String getNodeType();

    /**
     * Checks whether nodes of all types can match this filter.
     *
     * @return {@code true} iff there are no type restrictions
     */
    boolean matchesAllTypes();

    /**
     * Returns the names of the filter node type and all its supertypes.
     *
     * @return supertype name
     */
    @NotNull
    Set<String> getSupertypes();

    /**
     * Returns the names of all matching primary node types.
     *
     * @return primary node type names
     */
    @NotNull
    Set<String> getPrimaryTypes();

    /**
     * Returns the names of all matching mixin node types.
     *
     * @return mixin node type names
     */
    @NotNull
    Set<String> getMixinTypes();

    /**
     * Get the complete query statement. The statement should only be used for
     * logging purposes.
     *
     * @return the query statement (possibly null)
     */
    @Nullable
    String getQueryStatement();

    /**
     * If the filter condition can not possibly match any row, due to a
     * contradiction in the query (for example "x=1 and x=2").
     *
     * @return true if the filter condition can not match any row
     */
    boolean isAlwaysFalse();

    /**
     * A restriction for a property.
     */
    class PropertyRestriction {

        /**
         * The name of the property.
         */
        public String propertyName;

        /**
         * The first value to read, or null to read from the beginning.
         */
        public PropertyValue first;

        /**
         * Whether values that match the first should be returned.
         */
        public boolean firstIncluding;

        /**
         * The last value to read, or null to read until the end.
         */
        public PropertyValue last;

        /**
         * Whether values that match the last should be returned.
         */
        public boolean lastIncluding;

        /**
         * Whether this is a like constraint. in this case only the 'first'
         * value should be taken into consideration
         */
        public boolean isLike;

        /**
         * A list of possible values, for conditions of the type
         * "x=1 or x=2 or x=3".
         */
        public List<PropertyValue> list;

        /**
         * The property type, if restricted.
         * If not restricted, this field is set to PropertyType.UNDEFINED.
         */
        public int propertyType = PropertyType.UNDEFINED;

        public boolean isNullRestriction() {
            return first == null && last == null && list == null && lastIncluding && firstIncluding;
        }

        public boolean isNotNullRestriction() {
            return first == null && last == null && list == null && !lastIncluding && !firstIncluding;
        }

        @Override
        public String toString() {
            return (toStringFromTo() + " " + toStringList()).trim();
        }

        private String toStringList() {
            if (list == null) {
                return "";
            }
            StringBuilder buff = new StringBuilder("in(");
            int i = 0;
            for (PropertyValue p : list) {
                if (i++ > 0) {
                    buff.append(", ");
                }
                buff.append(p.toString());
            }
            buff.append(')');
            return buff.toString();
        }

        private String toStringFromTo() {
            if (isNullRestriction()) {
                return "is null";
            } else if (isNotNullRestriction()) {
                return "is not null";
            }
            String f = first == null ? "" : first.toString();
            String l = last == null ? "" : last.toString();
            if (f.equals(l)) {
                return f;
            }
            String fi = first == null ? "" : (firstIncluding ? "[" : "(");
            String li = last == null ? "" : (lastIncluding ? "]" : ")");
            return fi + f + ".." + l + li;
        }

        /**
         * How restrictive a condition is.
         *
         * @return 0 for "is not null", 10 for equality, and 5 for everything
         *         else
         */
        public int sortOrder() {
            if (first == null && last == null) {
                if (list == null) {
                    return 0;
                }
                return 5;
            }
            if (first == last) {
                return 10;
            }
            return 5;
        }

        @Override
        public int hashCode() {
            // generated code (Eclipse)
            final int prime = 31;
            int result = 1;
            result = prime * result + ((first == null) ? 0 : first.hashCode());
            result = prime * result + (firstIncluding ? 1231 : 1237);
            result = prime * result + (isLike ? 1231 : 1237);
            result = prime * result + ((last == null) ? 0 : last.hashCode());
            result = prime * result + (lastIncluding ? 1231 : 1237);
            result = prime * result + ((list == null) ? 0 : list.hashCode());
            result = prime * result +
                    ((propertyName == null) ? 0 : propertyName.hashCode());
            result = prime * result + propertyType;
            return result;
        }

        @Override
        public boolean equals(Object obj) {
            // generated code (Eclipse)
            if (this == obj) {
                return true;
            }
            if (obj == null) {
                return false;
            }
            if (getClass() != obj.getClass()) {
                return false;
            }
            PropertyRestriction other = (PropertyRestriction) obj;
            if (first == null) {
                if (other.first != null) {
                    return false;
                }
            } else if (!first.equals(other.first)) {
                return false;
            }
            if (firstIncluding != other.firstIncluding) {
                return false;
            }
            if (isLike != other.isLike) {
                return false;
            }
            if (last == null) {
                if (other.last != null) {
                    return false;
                }
            } else if (!last.equals(other.last)) {
                return false;
            }
            if (lastIncluding != other.lastIncluding) {
                return false;
            }
            if (list == null) {
                if (other.list != null) {
                    return false;
                }
            } else if (!list.equals(other.list)) {
                return false;
            }
            if (propertyName == null) {
                if (other.propertyName != null) {
                    return false;
                }
            } else if (!propertyName.equals(other.propertyName)) {
                return false;
            }
            if (propertyType != other.propertyType) {
                return false;
            }
            return true;
        }

    }

    /**
     * The path restriction type.
     */
    enum PathRestriction {

        /**
         * All nodes.
         */
        NO_RESTRICTION("*"),

        /**
         * A parent of this node.
         */
        PARENT("/.."),

        /**
         * This exact node only.
         */
        EXACT(""),

        /**
         * All direct child nodes.
         */
        DIRECT_CHILDREN("/*"),

        /**
         * All direct and indirect child nodes (excluding the node with the
         * given path).
         */
        ALL_CHILDREN("//*");

        private final String name;

        PathRestriction(String name) {
            this.name = name;
        }

        @Override
        public String toString() {
            return name;
        }

    }

    Filter EMPTY_FILTER = new Filter() {

        private final QueryLimits EMPTY_LIMITS = new QueryLimits() {

            @Override
            public long getLimitInMemory() {
                return Long.MAX_VALUE;
            }

            @Override
            public long getLimitReads() {
                return Long.MAX_VALUE;
            }

            @Override
            public boolean getFullTextComparisonWithoutIndex() {
                return false;
            }

            @Override
            public boolean getFailTraversal() {
                return false;
            }

        };

        @Override
        public Collection<PropertyRestriction> getPropertyRestrictions() {
            return Collections.emptyList();
        }

        @Override
        public Collection<String> getFulltextConditions() {
            return Collections.emptyList();
        }

        @Override
        public FullTextExpression getFullTextConstraint() {
            return null;
        }

        @Override
        public QueryLimits getQueryLimits() {
            return EMPTY_LIMITS;
        }

        @Override
        public boolean isAccessible(String path) {
            return false;
        }

        @Override
        public boolean containsNativeConstraint() {
            return false;
        }

        @Override
        public PropertyRestriction getPropertyRestriction(String propertyName) {
            return null;
        }

        @Override
        public List<PropertyRestriction> getPropertyRestrictions(String propertyName) {
            return Collections.emptyList();
        }

        @Override
        public PathRestriction getPathRestriction() {
            return PathRestriction.NO_RESTRICTION;
        }

        @Override
        public String getPath() {
            return PathUtils.ROOT_PATH;
        }

        @Override
        public String getPathPlan() {
            return PathRestriction.NO_RESTRICTION.toString();
        }

        @Nullable
        @Override
        public String getNodeType() {
            return null;
        }

        @Override
        public boolean matchesAllTypes() {
            return false;
        }

        @NotNull
        @Override
        public Set<String> getSupertypes() {
            return Collections.emptySet();
        }

        @NotNull
        @Override
        public Set<String> getPrimaryTypes() {
            return Collections.emptySet();
        }

        @NotNull
        @Override
        public Set<String> getMixinTypes() {
            return Collections.emptySet();
        }

        @Nullable
        @Override
        public String getQueryStatement() {
            return null;
        }

        @Override
        public boolean isAlwaysFalse() {
            return false;
        }
    };

}
