/**
 * 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
 * <p>
 * http://www.apache.org/licenses/LICENSE-2.0
 * <p>
 * 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.atlas.model.discovery;

import com.fasterxml.jackson.annotation.JsonAutoDetect;
import com.fasterxml.jackson.annotation.JsonCreator;
import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
import com.fasterxml.jackson.annotation.JsonInclude;
import com.fasterxml.jackson.annotation.JsonValue;
import org.apache.atlas.SortOrder;

import java.io.Serializable;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.Set;

import static com.fasterxml.jackson.annotation.JsonAutoDetect.Visibility.NONE;
import static com.fasterxml.jackson.annotation.JsonAutoDetect.Visibility.PUBLIC_ONLY;

@JsonAutoDetect(getterVisibility = PUBLIC_ONLY, setterVisibility = PUBLIC_ONLY, fieldVisibility = NONE)
@JsonInclude(JsonInclude.Include.NON_NULL)
@JsonIgnoreProperties(ignoreUnknown = true)
public class SearchParameters implements Serializable {
    private static final long serialVersionUID = 1L;

    public static final String WILDCARD_CLASSIFICATIONS = "*";
    public static final String ALL_CLASSIFICATIONS      = "_CLASSIFIED";
    public static final String NO_CLASSIFICATIONS       = "_NOT_CLASSIFIED";
    public static final String ALL_ENTITY_TYPES         = "_ALL_ENTITY_TYPES";
    public static final String ALL_CLASSIFICATION_TYPES = "_ALL_CLASSIFICATION_TYPES";

    private String         query;
    private String         typeName;
    private String         classification;
    private String         relationshipName;
    private String         termName;
    private String         sortBy;
    private boolean        excludeDeletedEntities;
    private boolean        includeClassificationAttributes;
    private boolean        includeSubTypes           = true;
    private boolean        includeSubClassifications = true;
    private boolean        excludeHeaderAttributes;
    private int            limit;
    private int            offset;
    private String         marker;
    private FilterCriteria entityFilters;
    private FilterCriteria tagFilters;
    private FilterCriteria relationshipFilters;
    private Set<String>    attributes;
    private SortOrder      sortOrder;

    /**
     * @return The type of query
     */
    public String getQuery() {
        return query;
    }

    /**
     * Set query type
     *
     * @param query type
     */
    public void setQuery(String query) {
        this.query = query;
    }

    /**
     * @return Type name to search on
     */
    public String getTypeName() {
        return typeName;
    }

    /**
     * Set the type name to search on
     *
     * @param typeName type name
     */
    public void setTypeName(String typeName) {
        this.typeName = typeName;
    }

    /**
     * @return termName to search on
     */
    public String getTermName() {
        return termName;
    }

    /**
     * Set the classification/tag to search on
     *
     * @param termName classification/tag name
     */
    public void setTermName(String termName) {
        this.termName = termName;
    }

    /**
     * @return Classification/tag to search on
     */
    public String getClassification() {
        return classification;
    }

    /**
     * Set the classification/tag to search on
     *
     * @param classification classification/tag name
     */
    public void setClassification(String classification) {
        this.classification = classification;
    }

    public String getRelationshipName() {
        return relationshipName;
    }

    public void setRelationshipName(String relationshipName) {
        this.relationshipName = relationshipName;
    }

    /**
     * @return True iff deleted entities are excluded
     */
    public boolean getExcludeDeletedEntities() {
        return excludeDeletedEntities;
    }

    /**
     * Exclude deleted entities from search
     *
     * @param excludeDeletedEntities boolean flag
     */
    public void setExcludeDeletedEntities(boolean excludeDeletedEntities) {
        this.excludeDeletedEntities = excludeDeletedEntities;
    }

    /**
     * @return True if classification attributes are included in search result.
     */
    public boolean getIncludeClassificationAttributes() {
        return includeClassificationAttributes;
    }

    /**
     * Include classificatio attributes in search result.
     *
     * @param includeClassificationAttributes boolean flag
     */
    public void setIncludeClassificationAttributes(boolean includeClassificationAttributes) {
        this.includeClassificationAttributes = includeClassificationAttributes;
    }

    /**
     * @return True iff sub-type entities are to be included
     */
    public boolean getIncludeSubTypes() {
        return includeSubTypes;
    }

    /**
     * Include sub-type entities in search
     *
     * @param includeSubTypes boolean flag
     */
    public void setIncludeSubTypes(boolean includeSubTypes) {
        this.includeSubTypes = includeSubTypes;
    }

    /**
     * @return True iff sub-classifications are to be included
     */
    public boolean getIncludeSubClassifications() {
        return includeSubClassifications;
    }

    /**
     * Include sub-classifications in search
     *
     * @param includeSubClassifications boolean flag
     */
    public void setIncludeSubClassifications(boolean includeSubClassifications) {
        this.includeSubClassifications = includeSubClassifications;
    }

    /**
     * @return Max number of results to be returned
     */
    public int getLimit() {
        return limit;
    }

    /**
     * Restrict the results to the specified limit
     *
     * @param limit max number of results
     */
    public void setLimit(int limit) {
        this.limit = limit;
    }

    /**
     * @return Offset(pagination) of the results
     */
    public int getOffset() {
        return offset;
    }

    /**
     * @param offset
     */
    public void setOffset(int offset) {
        this.offset = offset;
    }

    /**
     * @return marker (offset of the next page)
     */
    public String getMarker() {
        return marker;
    }

    /**
     * @param marker
     */
    public void setMarker(String marker) {
        this.marker = marker;
    }

    /**
     * Entity attribute filters for the type (if type name is specified)
     *
     * @return
     */
    public FilterCriteria getEntityFilters() {
        return entityFilters;
    }

    /**
     * Filter the entities on this criteria
     *
     * @param entityFilters
     */
    public void setEntityFilters(FilterCriteria entityFilters) {
        this.entityFilters = entityFilters;
    }

    /**
     * Tag attribute filters for the classification/tag (if tag name is specified)
     *
     * @return
     */
    public FilterCriteria getTagFilters() {
        return tagFilters;
    }

    /**
     * Filter the tag/classification on this criteria
     *
     * @param tagFilters
     */
    public void setTagFilters(FilterCriteria tagFilters) {
        this.tagFilters = tagFilters;
    }

    /**
     * Relationship attribute filters for the relationship
     *
     * @return
     */
    public FilterCriteria getRelationshipFilters() {
        return relationshipFilters;
    }

    /**
     * Filter the relationship on this criteria
     *
     * @param relationshipFilters
     */
    public void setRelationshipFilters(FilterCriteria relationshipFilters) {
        this.relationshipFilters = relationshipFilters;
    }

    public boolean getExcludeHeaderAttributes() {
        return excludeHeaderAttributes;
    }

    public void setExcludeHeaderAttributes(boolean excludeHeaderAttributes) {
        this.excludeHeaderAttributes = excludeHeaderAttributes;
    }

    /**
     * Attribute values included in the results
     *
     * @return
     */
    public Set<String> getAttributes() {
        return attributes;
    }

    /**
     * Return these attributes in the result response
     *
     * @param attributes
     */
    public void setAttributes(Set<String> attributes) {
        this.attributes = attributes;
    }

    /**
     * @return Attribute on which to sort the results
     */
    public String getSortBy() {
        return sortBy;
    }

    /**
     * Sort the results based on sortBy attribute
     *
     * @param sortBy Attribute on which to sort the results
     */
    public void setSortBy(String sortBy) {
        this.sortBy = sortBy;
    }

    /**
     * @return Sorting order of the results
     */
    public SortOrder getSortOrder() {
        return sortOrder;
    }

    /**
     * Sorting order to sort the results
     *
     * @param sortOrder ASCENDING vs DESCENDING
     */
    public void setSortOrder(SortOrder sortOrder) {
        this.sortOrder = sortOrder;
    }

    @Override
    public int hashCode() {
        return Objects.hash(query, typeName, classification, termName, includeSubTypes, includeSubClassifications,
                excludeDeletedEntities, includeClassificationAttributes, excludeHeaderAttributes, limit, offset, entityFilters,
                tagFilters, attributes, sortBy, sortOrder);
    }

    @Override
    public boolean equals(Object o) {
        if (this == o) {
            return true;
        } else if (o == null || getClass() != o.getClass()) {
            return false;
        }

        SearchParameters that = (SearchParameters) o;

        return excludeDeletedEntities == that.excludeDeletedEntities &&
                includeClassificationAttributes == that.includeClassificationAttributes &&
                includeSubTypes == that.includeSubTypes &&
                includeSubClassifications == that.includeSubClassifications &&
                excludeHeaderAttributes == that.excludeHeaderAttributes &&
                limit == that.limit &&
                offset == that.offset &&
                Objects.equals(query, that.query) &&
                Objects.equals(typeName, that.typeName) &&
                Objects.equals(classification, that.classification) &&
                Objects.equals(termName, that.termName) &&
                Objects.equals(entityFilters, that.entityFilters) &&
                Objects.equals(tagFilters, that.tagFilters) &&
                Objects.equals(attributes, that.attributes) &&
                Objects.equals(sortBy, that.sortBy) &&
                Objects.equals(sortOrder, that.sortOrder);
    }

    @Override
    public String toString() {
        return toString(new StringBuilder()).toString();
    }

    public StringBuilder toString(StringBuilder sb) {
        if (sb == null) {
            sb = new StringBuilder();
        }

        sb.append('{');
        sb.append("query='").append(query).append('\'');
        sb.append(", typeName='").append(typeName).append('\'');
        sb.append(", classification='").append(classification).append('\'');
        sb.append(", termName='").append(termName).append('\'');
        sb.append(", includeSubTypes='").append(includeSubTypes).append('\'');
        sb.append(", includeSubClassifications='").append(includeSubClassifications).append('\'');
        sb.append(", excludeDeletedEntities=").append(excludeDeletedEntities);
        sb.append(", includeClassificationAttributes=").append(includeClassificationAttributes);
        sb.append(", excludeHeaderAttributes=").append(excludeHeaderAttributes);
        sb.append(", limit=").append(limit);
        sb.append(", offset=").append(offset);
        sb.append(", entityFilters=").append(entityFilters);
        sb.append(", tagFilters=").append(tagFilters);
        sb.append(", attributes=").append(attributes);
        sb.append(", sortBy=").append(sortBy).append('\'');
        sb.append(", sortOrder=").append(sortOrder).append('\'');
        sb.append('}');

        return sb;
    }

    /**
     * NOTE : The names added in the String array should always contain first value in lower case
     */
    public enum Operator {
        LT(new String[] {"<", "lt"}),
        GT(new String[] {">", "gt"}),
        LTE(new String[] {"<=", "lte"}),
        GTE(new String[] {">=", "gte"}),
        EQ(new String[] {"=", "eq"}),
        NEQ(new String[] {"!=", "neq"}),
        IN(new String[] {"in", "IN"}),
        LIKE(new String[] {"like", "LIKE"}),
        STARTS_WITH(new String[] {"startsWith", "STARTSWITH", "begins_with", "BEGINS_WITH"}),
        ENDS_WITH(new String[] {"endsWith", "ENDSWITH", "ends_with", "ENDS_WITH"}),
        CONTAINS(new String[] {"contains", "CONTAINS"}),
        NOT_CONTAINS(new String[] {"not_contains", "NOT_CONTAINS"}),
        CONTAINS_ANY(new String[] {"containsAny", "CONTAINSANY", "contains_any", "CONTAINS_ANY"}),
        CONTAINS_ALL(new String[] {"containsAll", "CONTAINSALL", "contains_all", "CONTAINS_ALL"}),
        IS_NULL(new String[] {"isNull", "ISNULL", "is_null", "IS_NULL"}),
        NOT_NULL(new String[] {"notNull", "NOTNULL", "not_null", "NOT_NULL"}),
        TIME_RANGE(new String[] {"timerange", "TIMERANGE", "time_range", "TIME_RANGE"}),
        NOT_EMPTY(new String[] {"notEmpty", "NOTEMPTY", "not_empty", "NOT_EMPTY"});
        static final Map<String, Operator> operatorsMap = new HashMap<>();

        private final String[] symbols;

        Operator(String[] symbols) {
            this.symbols = symbols;
        }

        @JsonCreator
        public static Operator fromString(String symbol) {
            return operatorsMap.get(symbol);
        }

        @JsonValue
        public String getSymbol() {
            return symbols[0];
        }

        public String[] getSymbols() {
            return symbols;
        }

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

        static {
            for (Operator operator : Operator.values()) {
                for (String s : operator.symbols) {
                    operatorsMap.put(s, operator);
                }
            }
        }
    }

    /**
     * Supported search operations
     * Logical comparision operators can only be used with numbers or dates
     * IN, LIKE, startsWith, endsWith, CONTAINS can only be used with strings or text
     */

    @JsonAutoDetect(getterVisibility = PUBLIC_ONLY, setterVisibility = PUBLIC_ONLY, fieldVisibility = NONE)
    @JsonInclude(JsonInclude.Include.NON_NULL)
    @JsonIgnoreProperties(ignoreUnknown = true)
    public static class FilterCriteria {
        // Single condition
        private String   attributeName;
        private Operator operator;
        private String   attributeValue;

        // Complex conditions
        private Condition            condition;
        private List<FilterCriteria> criterion;

        public String getAttributeName() {
            return attributeName;
        }

        public void setAttributeName(String attributeName) {
            this.attributeName = attributeName;
        }

        public Operator getOperator() {
            return operator;
        }

        public void setOperator(Operator operator) {
            this.operator = operator;
        }

        public String getAttributeValue() {
            return attributeValue;
        }

        public void setAttributeValue(String attributeValue) {
            this.attributeValue = attributeValue;
        }

        public Condition getCondition() {
            return condition;
        }

        public void setCondition(Condition condition) {
            this.condition = condition;
        }

        public List<FilterCriteria> getCriterion() {
            return criterion;
        }

        public void setCriterion(List<FilterCriteria> criterion) {
            this.criterion = criterion;
        }

        @Override
        public int hashCode() {
            return Objects.hash(attributeName, operator, attributeValue, condition, criterion);
        }

        @Override
        public boolean equals(Object o) {
            if (this == o) {
                return true;
            } else if (o == null || getClass() != o.getClass()) {
                return false;
            }

            FilterCriteria that = (FilterCriteria) o;

            return Objects.equals(attributeName, that.attributeName) &&
                    Objects.equals(operator, that.operator) &&
                    Objects.equals(attributeValue, that.attributeValue) &&
                    condition == that.condition &&
                    Objects.equals(criterion, that.criterion);
        }

        @Override
        public String toString() {
            return toString(new StringBuilder()).toString();
        }

        public StringBuilder toString(StringBuilder sb) {
            if (sb == null) {
                sb = new StringBuilder();
            }

            sb.append('{');
            sb.append("attributeName='").append(attributeName).append('\'');
            sb.append(", operator=").append(operator);
            sb.append(", attributeValue='").append(attributeValue).append('\'');
            sb.append(", condition=").append(condition);
            sb.append(", criterion=").append(criterion);
            sb.append('}');

            return sb;
        }

        // Can be presented as a group of conditions or a single condition
        public enum Condition { AND, OR }
    }
}
