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

import com.google.common.annotations.VisibleForTesting;
import org.apache.atlas.AtlasErrorCode;
import org.apache.atlas.exception.AtlasBaseException;
import org.apache.atlas.glossary.GlossaryUtils;
import org.apache.atlas.model.TypeCategory;
import org.apache.atlas.model.discovery.SearchParameters;
import org.apache.atlas.model.typedef.AtlasStructDef;
import org.apache.atlas.repository.Constants;
import org.apache.atlas.type.AtlasBuiltInTypes;
import org.apache.atlas.type.AtlasEntityType;
import org.apache.atlas.type.AtlasStructType;
import org.apache.atlas.type.AtlasType;
import org.apache.atlas.type.AtlasTypeRegistry;
import org.apache.commons.collections.CollectionUtils;
import org.apache.commons.lang3.StringUtils;

import java.text.DateFormat;
import java.text.ParseException;
import java.text.SimpleDateFormat;
import java.util.ArrayList;
import java.util.Date;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.Set;
import java.util.TimeZone;
import java.util.regex.Pattern;
import java.util.stream.Collectors;
import java.util.stream.Stream;

import static org.apache.atlas.model.discovery.SearchParameters.ALL_CLASSIFICATIONS;
import static org.apache.atlas.model.discovery.SearchParameters.NO_CLASSIFICATIONS;
import static org.apache.atlas.type.AtlasStructType.AtlasAttribute.AtlasRelationshipEdgeDirection.IN;
import static org.apache.atlas.type.AtlasStructType.AtlasAttribute.AtlasRelationshipEdgeDirection.OUT;

public class GremlinQueryComposer {
    private static final String ISO8601_FORMAT              = "yyyy-MM-dd'T'HH:mm:ss.SSS'Z'";
    private static final String ISO8601_DATE_FORMAT         = "yyyy-MM-dd";
    private static final String REGEX_ALPHA_NUMERIC_PATTERN = "[a-zA-Z0-9]+";
    private static final String EMPTY_STRING                = "";
    private static final int    DEFAULT_QUERY_RESULT_LIMIT  = 25;
    private static final int    DEFAULT_QUERY_RESULT_OFFSET = 0;

    private static final ThreadLocal<DateFormat[]> DSL_DATE_FORMAT = ThreadLocal.withInitial(() -> {
        final String[]     formats = {ISO8601_FORMAT, ISO8601_DATE_FORMAT};
        final DateFormat[] dfs     = new DateFormat[formats.length];

        for (int i = 0; i < formats.length; i++) {
            dfs[i] = new SimpleDateFormat(formats[i]);

            dfs[i].setTimeZone(TimeZone.getTimeZone("UTC"));
        }

        return dfs;
    });

    private final GremlinClauseList      queryClauses        = new GremlinClauseList();
    private final Set<String>            attributesProcessed = new HashSet<>();
    private final Lookup                 lookup;
    private final AtlasDSL.QueryMetadata queryMetadata;
    private final int                    providedLimit;
    private final int                    providedOffset;
    private final Context                context;
    private final GremlinQueryComposer   parent;
    private       boolean                hasTrait;

    public GremlinQueryComposer(Lookup registryLookup, Context context, AtlasDSL.QueryMetadata qmd, int limit, int offset, GremlinQueryComposer parent) {
        this.lookup         = registryLookup;
        this.context        = context;
        this.queryMetadata  = qmd;
        this.providedLimit  = limit;
        this.providedOffset = offset;
        this.parent         = parent;

        init();
    }

    public GremlinQueryComposer(Lookup registryLookup, AtlasDSL.QueryMetadata qmd, int limit, int offset) {
        this(registryLookup, new Context(registryLookup), qmd, limit, offset, null);
    }

    public GremlinQueryComposer(AtlasTypeRegistry typeRegistry, AtlasDSL.QueryMetadata qmd, int limit, int offset) {
        this(new RegistryBasedLookup(typeRegistry), qmd, limit, offset);
    }

    @VisibleForTesting
    GremlinQueryComposer(Lookup lookup, Context context, final AtlasDSL.QueryMetadata qmd) {
        this(lookup, context, qmd, DEFAULT_QUERY_RESULT_LIMIT, DEFAULT_QUERY_RESULT_OFFSET, null);
    }

    public void addFrom(String typeName) {
        IdentifierHelper.Info typeInfo = createInfo(typeName);

        if (context.shouldRegister(typeInfo.get())) {
            context.registerActive(typeInfo.get());

            IdentifierHelper.Info ia = createInfo(typeInfo.get());

            if (ia.isTrait()) {
                String traitName = ia.get();

                if (traitName.equals(ALL_CLASSIFICATIONS)) {
                    addTrait(GremlinClause.ANY_TRAIT, ia);
                } else if (traitName.equals(NO_CLASSIFICATIONS)) {
                    addTrait(GremlinClause.NO_TRAIT, ia);
                } else {
                    addTrait(GremlinClause.TRAIT, ia);
                }
            } else {
                if (ia.hasSubtypes()) {
                    add(GremlinClause.HAS_TYPE_WITHIN, ia.getSubTypes());
                } else {
                    add(GremlinClause.HAS_TYPE, ia);
                }
            }
        } else {
            IdentifierHelper.Info ia = createInfo(typeInfo.get());

            introduceType(ia);
        }
    }

    public void addFromProperty(String typeName, String attribute) {
        if (!isNestedQuery()) {
            addFrom(typeName);
        }

        add(GremlinClause.HAS_PROPERTY, createInfo(attribute));
    }

    public void addIsA(String typeName, String traitName) {
        if (!isNestedQuery()) {
            addFrom(typeName);
        }

        IdentifierHelper.Info traitInfo = createInfo(traitName);

        if (StringUtils.equals(traitName, ALL_CLASSIFICATIONS)) {
            addTrait(GremlinClause.ANY_TRAIT, traitInfo);
        } else if (StringUtils.equals(traitName, NO_CLASSIFICATIONS)) {
            addTrait(GremlinClause.NO_TRAIT, traitInfo);
        } else {
            addTrait(GremlinClause.TRAIT, traitInfo);
        }
    }

    public void addHasTerm(String typeName, String termName) {
        String   qualifiedAttributeSeperator = String.valueOf(GlossaryUtils.INVALID_NAME_CHARS[0]);
        String[] terms                       = termName.split(qualifiedAttributeSeperator);
        String   attributeToSearch;

        if (terms.length > 1) {
            attributeToSearch = GlossaryUtils.QUALIFIED_NAME_ATTR;
        } else {
            termName          = terms[0];
            attributeToSearch = GlossaryUtils.NAME;
        }

        add(GremlinClause.TERM, attributeToSearch, IdentifierHelper.removeQuotes(termName));
    }

    public void addWhere(String lhs, String operator, String rhs) {
        String currentType = context.getActiveTypeName();

        //in case if trait type is registered and lhs has trait attributes
        if (currentType != null && lookup.isTraitType(currentType)) {
            context.setActiveTypeToUnknown();
        }

        IdentifierHelper.Info org                     = null;
        IdentifierHelper.Info lhsI                    = createInfo(lhs);
        boolean               rhsIsNotDateOrNumOrBool = false;

        if (!lhsI.isPrimitive()) {
            introduceType(lhsI);

            org  = lhsI;
            lhsI = createInfo(lhs);

            lhsI.setTypeName(org.getTypeName());

            if (org.isTrait()) {
                setHasTrait();
            }
        }

        if (!context.validator.isValidQualifiedName(lhsI.getQualifiedName(), lhsI.getRaw())) {
            return;
        }

        if (lhsI.isDate()) {
            rhs = parseDate(rhs);
        } else if (lhsI.isNumeric()) {
            if (!StringUtils.equals(lhsI.getAttributeName(), Constants.IS_INCOMPLETE_PROPERTY_KEY)) {
                rhs = parseNumber(rhs, this.context);
            }
        } else if (!IdentifierHelper.isTrueOrFalse(rhs)) {
            rhsIsNotDateOrNumOrBool = true;
        }

        rhs = addQuotesIfNecessary(lhsI, rhs);

        SearchParameters.Operator op = SearchParameters.Operator.fromString(operator);

        if (StringUtils.equals(lhsI.getAttributeName(), Constants.IS_INCOMPLETE_PROPERTY_KEY)) {
            addForIsIncompleteClause(lhsI, op, rhs);
        } else {
            if (op == SearchParameters.Operator.LIKE) {
                final AtlasStructType.AtlasAttribute             attribute = context.getActiveEntityType().getAttribute(lhsI.getAttributeName());
                final AtlasStructDef.AtlasAttributeDef.IndexType indexType = attribute.getAttributeDef().getIndexType();

                if (indexType == AtlasStructDef.AtlasAttributeDef.IndexType.STRING || !containsNumberAndLettersOnly(rhs)) {
                    String escapeRhs = IdentifierHelper.escapeCharacters(IdentifierHelper.getFixedRegEx(rhs));

                    add(GremlinClause.STRING_CONTAINS, getPropertyForClause(lhsI), escapeRhs);
                } else {
                    add(GremlinClause.TEXT_CONTAINS, getPropertyForClause(lhsI), IdentifierHelper.getFixedRegEx(rhs));
                }
            } else if (op == SearchParameters.Operator.IN) {
                add(GremlinClause.HAS_OPERATOR, getPropertyForClause(lhsI), "within", rhs);
            } else if (op == SearchParameters.Operator.NEQ && rhsIsNotDateOrNumOrBool) {
                String propertyName  = getPropertyForClause(lhsI);
                Object normalizedRhs = getNormalizedAttrVal(lhsI, IdentifierHelper.removeQuotes(rhs));

                add(GremlinClause.HAS_NOT_OPERATOR, propertyName, normalizedRhs.toString(), propertyName);
            } else {
                Object normalizedRhs = getNormalizedAttrVal(lhsI, IdentifierHelper.removeQuotes(rhs));

                addWithNormalizedValue(GremlinClause.HAS_OPERATOR, getPropertyForClause(lhsI), op.getSymbols()[1], normalizedRhs, rhs);
            }
        }

        // record that the attribute has been processed so that the select clause doesn't add a attr presence check
        attributesProcessed.add(lhsI.getQualifiedName());

        if (org != null && org.isReferredType()) {
            add(GremlinClause.DEDUP);

            if (org.getEdgeDirection() != null) {
                GremlinClause gremlinClauseForEdgeLabel = org.getEdgeDirection().equals(IN) ? GremlinClause.OUT : GremlinClause.IN;

                add(gremlinClauseForEdgeLabel, org.getEdgeLabel());
            } else {
                add(GremlinClause.OUT, org.getEdgeLabel());
            }

            context.registerActive(currentType);
        }
    }

    public void addAndClauses(List<GremlinQueryComposer> queryComposers) {
        List<String> clauses = addToSubClause(queryComposers);

        add(GremlinClause.AND, String.join(",", clauses));
    }

    public void addOrClauses(List<GremlinQueryComposer> queryComposers) {
        List<String> clauses = addToSubClause(queryComposers);

        add(GremlinClause.OR, String.join(",", clauses));
    }

    public Set<String> getAttributesProcessed() {
        return attributesProcessed;
    }

    public void addProcessedAttributes(Set<String> attributesProcessed) {
        this.attributesProcessed.addAll(attributesProcessed);
    }

    public void addSelect(SelectClauseComposer selectClauseComposer) {
        process(selectClauseComposer);

        // If the query contains orderBy and groupBy then the transformation determination is deferred to the method processing orderBy
        if (!(queryMetadata.hasOrderBy() && queryMetadata.hasGroupBy())) {
            addSelectTransformation(selectClauseComposer, null, false);
        }

        this.context.setSelectClauseComposer(selectClauseComposer);
    }

    public GremlinQueryComposer createNestedProcessor() {
        return new GremlinQueryComposer(lookup, this.context, queryMetadata, this.providedLimit, this.providedOffset, this);
    }

    public GremlinQueryComposer newInstance() {
        return new GremlinQueryComposer(lookup, new Context(lookup), queryMetadata, this.providedLimit, this.providedOffset, null);
    }

    public void addFromAlias(String typeName, String alias) {
        addFrom(typeName);
        addAsClause(alias);
        context.registerAlias(alias);
    }

    public void addAsClause(String alias) {
        add(GremlinClause.AS, alias);
    }

    public void addGroupBy(String item) {
        addGroupByClause(item);
    }

    public void addLimit(String limit, String offset) {
        SelectClauseComposer scc = context.getSelectClauseComposer();

        if (scc == null) {
            addLimitHelper(limit, offset);
        } else {
            if (!scc.hasAggregators()) {
                addLimitHelper(limit, offset);
            }
        }
    }

    public void addDefaultLimit() {
        addLimit(Integer.toString(providedLimit), Integer.toString(providedOffset));
    }

    public String get() {
        close();

        boolean  mustTransform = !isNestedQuery() && queryMetadata.needTransformation();
        String[] items         = getFormattedClauses(mustTransform);

        return mustTransform ? getTransformedClauses(items) : String.join(".", items);
    }

    public List<String> getErrorList() {
        return context.getErrorList();
    }

    public void addOrderBy(String name, boolean isDesc) {
        IdentifierHelper.Info ia = createInfo(name);

        if (queryMetadata.hasSelect() && queryMetadata.hasGroupBy()) {
            addSelectTransformation(this.context.selectClauseComposer, getPropertyForClause(ia), isDesc);
        } else if (queryMetadata.hasGroupBy()) {
            addOrderByClause(ia, isDesc);
            moveToLast(GremlinClause.GROUP_BY);
        } else {
            addOrderByClause(ia, isDesc);
        }
    }

    public boolean hasFromClause() {
        return queryClauses.contains(GremlinClause.HAS_TYPE) != -1 || queryClauses.contains(GremlinClause.HAS_TYPE_WITHIN) != -1;
    }

    public void remove(GremlinClause clause) {
        int index = queryClauses.contains(clause);

        if (-1 == index) {
            return;
        }

        queryClauses.remove(index);
    }

    public GremlinClauseList getQueryClauses() {
        return queryClauses;
    }

    public void add(GremlinClauseValue gv) {
        queryClauses.add(gv);
    }

    public void addAll(GremlinClauseList gcList) {
        if (gcList != null) {
            List<GremlinQueryComposer.GremlinClauseValue> list = gcList.getList();

            if (CollectionUtils.isNotEmpty(list)) {
                queryClauses.clear();

                for (GremlinClauseValue value : list) {
                    queryClauses.add(value);
                }
            }
        }
    }

    public GremlinClauseList clauses() {
        return queryClauses;
    }

    public SelectClauseComposer getSelectComposer() {
        return this.context.selectClauseComposer;
    }

    public boolean hasAnyTraitAttributeClause() {
        return this.hasTrait;
    }

    private void setHasTrait() {
        this.hasTrait = true;
    }

    private void addForIsIncompleteClause(IdentifierHelper.Info lhsI, SearchParameters.Operator op, String rhs) {
        GremlinClause clause = GremlinClause.HAS_OPERATOR;

        rhs = rhs.replace("'", "").replace("\"", "");

        switch (op) {
            case EQ:
                if (IdentifierHelper.isCompleteValue(rhs)) {
                    clause = GremlinClause.HAS_NOT_PROPERTY;
                } else if (IdentifierHelper.isInCompleteValue(rhs)) {
                    rhs = Constants.INCOMPLETE_ENTITY_VALUE.toString();
                }
                break;

            case NEQ:
                if (IdentifierHelper.isCompleteValue(rhs)) {
                    op  = SearchParameters.Operator.EQ;
                    rhs = Constants.INCOMPLETE_ENTITY_VALUE.toString();
                } else if (IdentifierHelper.isInCompleteValue(rhs)) {
                    clause = GremlinClause.HAS_NOT_PROPERTY;
                }
                break;
        }

        Object normalizedRhs = getNormalizedAttrVal(lhsI, IdentifierHelper.removeQuotes(rhs));

        addWithNormalizedValue(clause, getPropertyForClause(lhsI), op.getSymbols()[1], normalizedRhs, rhs);
    }

    private Object getNormalizedAttrVal(IdentifierHelper.Info attrInfo, String attrVal) {
        Object          ret        = attrVal;
        AtlasEntityType entityType = context.getActiveEntityType();

        if (entityType != null && StringUtils.isNotEmpty(attrVal)) {
            String    attrName      = attrInfo.getAttributeName();
            AtlasType attributeType = entityType.getAttributeType(attrName);

            if (attributeType != null) {
                Object normalizedValue = attributeType.getNormalizedValue(attrVal);

                if (normalizedValue != null && attributeType instanceof AtlasBuiltInTypes.AtlasDateType) {
                    ret = ((Date) normalizedValue).getTime();
                } else {
                    ret = normalizedValue;
                }
            }
        }

        return ret;
    }

    private boolean containsNumberAndLettersOnly(String rhs) {
        return Pattern.matches(REGEX_ALPHA_NUMERIC_PATTERN, IdentifierHelper.removeWildcards(rhs));
    }

    private String parseNumber(String rhs, Context context) {
        return rhs.replace("'", "").replace("\"", "") + context.getNumericTypeFormatter();
    }

    private void addWithNormalizedValue(GremlinClause clause, String propertyForClause, String symbol, Object normalizedRhs, String strValue) {
        queryClauses.add(new GremlinClauseValue(clause, propertyForClause, symbol, normalizedRhs, strValue));
    }

    private long getDateFormat(String s) {
        for (DateFormat dateFormat : DSL_DATE_FORMAT.get()) {
            try {
                return dateFormat.parse(s).getTime();
            } catch (ParseException ignored) {
                // ignore
            }
        }

        context.validator.check(false, AtlasErrorCode.INVALID_DSL_INVALID_DATE, s);

        return -1;
    }

    private List<String> addToSubClause(List<GremlinQueryComposer> clauses) {
        for (GremlinQueryComposer entry : clauses) {
            this.addSubClauses(this.queryClauses.size(), entry.getQueryClauses());
        }

        return clauses.stream().map(GremlinQueryComposer::get).collect(Collectors.toList());
    }

    private String getPropertyForClause(IdentifierHelper.Info ia) {
        String vertexPropertyName = lookup.getVertexPropertyName(ia.getTypeName(), ia.getAttributeName());

        if (StringUtils.isNotEmpty(vertexPropertyName)) {
            return vertexPropertyName;
        }

        if (StringUtils.isNotEmpty(ia.getQualifiedName())) {
            return ia.getQualifiedName();
        }

        return ia.getRaw();
    }

    private void process(SelectClauseComposer scc) {
        if (scc.getItems() == null) {
            return;
        }

        for (int i = 0; i < scc.getItems().length; i++) {
            IdentifierHelper.Info ia = createInfo(scc.getItem(i));

            if (StringUtils.isEmpty(ia.getQualifiedName())) {
                context.getErrorList().add("Unable to find qualified name for " + ia.getAttributeName());

                continue;
            }

            if (scc.isAggregatorWithArgument(i) && !ia.isPrimitive()) {
                context.check(false, AtlasErrorCode.INVALID_DSL_SELECT_INVALID_AGG, ia.getQualifiedName());

                return;
            }

            if (!scc.getItem(i).equals(scc.getLabel(i))) {
                context.addAlias(scc.getLabel(i), ia.getQualifiedName());
            }

            if (scc.updateAsApplicable(i, getPropertyForClause(ia), ia.getQualifiedName())) {
                continue;
            }

            scc.setIsSelectNoop(hasNoopCondition(ia));

            if (scc.getIsSelectNoop()) {
                return;
            }

            if (introduceType(ia)) {
                scc.incrementTypesIntroduced();
                scc.setIsSelectNoop(!ia.hasParts());

                if (ia.hasParts()) {
                    scc.assign(i, getPropertyForClause(createInfo(ia.get())), GremlinClause.INLINE_GET_PROPERTY);
                }
            } else {
                scc.assign(i, getPropertyForClause(ia), GremlinClause.INLINE_GET_PROPERTY);
                scc.setIsPrimitiveAttr(i);
            }
        }

        context.validator.check(!scc.hasMultipleReferredTypes(), AtlasErrorCode.INVALID_DSL_SELECT_REFERRED_ATTR, Integer.toString(scc.getIntroducedTypesCount()));
        context.validator.check(!scc.hasMixedAttributes(), AtlasErrorCode.INVALID_DSL_SELECT_ATTR_MIXING);
    }

    private boolean hasNoopCondition(IdentifierHelper.Info ia) {
        return !ia.isPrimitive() && !ia.isAttribute() && context.hasAlias(ia.getRaw());
    }

    private void addLimitHelper(final String limit, final String offset) {
        if (offset.equalsIgnoreCase("0")) {
            add(GremlinClause.LIMIT, limit, limit);
        } else {
            addRangeClause(offset, limit);
        }
    }

    private String getTransformedClauses(String[] items) {
        String ret;
        String body     = Stream.of(items).filter(Objects::nonNull).collect(Collectors.joining("."));
        String inlineFn = queryClauses.getValue(queryClauses.size() - 1);
        String funCall  = String.format(inlineFn, body);

        if (isNestedQuery()) {
            ret = String.join(".", queryClauses.getValue(0), funCall);
        } else {
            ret = queryClauses.getValue(0) + funCall;
        }

        return ret;
    }

    private String[] getFormattedClauses(boolean needTransformation) {
        String[] items    = new String[queryClauses.size()];
        int      startIdx = needTransformation ? 1 : 0;
        int      endIdx   = needTransformation ? queryClauses.size() - 1 : queryClauses.size();

        for (int i = startIdx; i < endIdx; i++) {
            items[i] = queryClauses.getValue(i);
        }

        return items;
    }

    private void addSelectTransformation(final SelectClauseComposer selectClauseComposer, final String orderByQualifiedAttrName, final boolean isDesc) {
        GremlinClause gremlinClause;

        if (selectClauseComposer.getIsSelectNoop()) {
            gremlinClause = GremlinClause.SELECT_NOOP_FN;
        } else if (queryMetadata.hasGroupBy()) {
            gremlinClause = selectClauseComposer.onlyAggregators() ? GremlinClause.SELECT_ONLY_AGG_GRP_FN : GremlinClause.SELECT_MULTI_ATTR_GRP_FN;
        } else {
            gremlinClause = selectClauseComposer.onlyAggregators() ? GremlinClause.SELECT_ONLY_AGG_FN : GremlinClause.SELECT_FN;
        }

        if (StringUtils.isEmpty(orderByQualifiedAttrName)) {
            add(0, gremlinClause,
                    selectClauseComposer.getLabelHeader(),
                    selectClauseComposer.getAssignmentExprString(),
                    selectClauseComposer.getItemsString(),
                    EMPTY_STRING);
        } else {
            int           itemIdx    = selectClauseComposer.getAttrIndex(orderByQualifiedAttrName);
            GremlinClause sortClause = GremlinClause.INLINE_DEFAULT_TUPLE_SORT;

            if (itemIdx != -1) {
                sortClause = isDesc ? GremlinClause.INLINE_TUPLE_SORT_DESC : GremlinClause.INLINE_TUPLE_SORT_ASC;
            }

            String idxStr = String.valueOf(itemIdx);

            add(0, gremlinClause,
                    selectClauseComposer.getLabelHeader(),
                    selectClauseComposer.getAssignmentExprString(),
                    selectClauseComposer.getItemsString(),
                    sortClause.get(idxStr, idxStr));
        }

        add(GremlinClause.INLINE_TRANSFORM_CALL);
    }

    private String addQuotesIfNecessary(IdentifierHelper.Info rhsI, String rhs) {
        if (rhsI.isNumeric()) {
            return rhs;
        }

        if (IdentifierHelper.isTrueOrFalse(rhs)) {
            return rhs;
        }

        if (IdentifierHelper.isQuoted(rhs)) {
            return rhs;
        }

        return IdentifierHelper.getQuoted(rhs);
    }

    private String parseDate(String rhs) {
        String s = IdentifierHelper.isQuoted(rhs) ? IdentifierHelper.removeQuotes(rhs) : rhs;

        return String.format("'%d'", getDateFormat(s));
    }

    private void close() {
        if (isNestedQuery()) {
            return;
        }

        // Need de-duping at the end so that correct results are fetched
        if (queryClauses.size() > 2) {
            // QueryClauses should've something more that just g.V() (hence 2)
            add(GremlinClause.DEDUP);
            // Range and limit must be present after the de-duping construct
            moveToLast(GremlinClause.RANGE);
            moveToLast(GremlinClause.LIMIT);
        }

        if (!queryMetadata.hasLimitOffset()) {
            addDefaultLimit();
        }

        if (queryClauses.isEmpty()) {
            queryClauses.clear();

            return;
        }

        moveToLast(GremlinClause.LIMIT);
        add(GremlinClause.TO_LIST);
        moveToLast(GremlinClause.INLINE_TRANSFORM_CALL);
    }

    private boolean isNestedQuery() {
        return this.parent != null;
    }

    private void addSubClauses(int index, GremlinClauseList queryClauses) {
        this.queryClauses.addSubClauses(index, queryClauses);
    }

    private void moveToLast(GremlinClause clause) {
        int index = queryClauses.contains(clause);

        if (-1 == index) {
            return;
        }

        GremlinClauseValue gcv = queryClauses.remove(index);

        queryClauses.add(gcv);
    }

    private void init() {
        if (!isNestedQuery()) {
            add(GremlinClause.G);
            add(GremlinClause.V);
        } else {
            add(GremlinClause.NESTED_START);
        }
    }

    private boolean introduceType(IdentifierHelper.Info ia) {
        if (ia.isReferredType()) {
            if (ia.getEdgeDirection() != null) {
                GremlinClause gremlinClauseForEdgeLabel = ia.getEdgeDirection().equals(OUT) ? GremlinClause.OUT : GremlinClause.IN;

                add(gremlinClauseForEdgeLabel, ia.getEdgeLabel());
            } else {
                add(GremlinClause.OUT, ia.getEdgeLabel());
            }

            context.registerActive(ia);
        }

        return ia.isReferredType();
    }

    private IdentifierHelper.Info createInfo(String actualTypeName) {
        return IdentifierHelper.create(context, lookup, actualTypeName);
    }

    private void addRangeClause(String startIndex, String endIndex) {
        if (queryMetadata.hasSelect()) {
            add(queryClauses.size() - 1, GremlinClause.RANGE, startIndex, startIndex, endIndex, startIndex, startIndex, endIndex);
        } else {
            add(GremlinClause.RANGE, startIndex, startIndex, endIndex, startIndex, startIndex, endIndex);
        }
    }

    private void addOrderByClause(IdentifierHelper.Info ia, boolean descr) {
        add((!descr) ? GremlinClause.ORDER_BY : GremlinClause.ORDER_BY_DESC, ia);
    }

    private void addGroupByClause(String name) {
        IdentifierHelper.Info ia = createInfo(name);

        add(GremlinClause.GROUP_BY, ia);
    }

    private void add(GremlinClause clause, IdentifierHelper.Info idInfo) {
        if (context != null && !context.validator.isValid(context, clause, idInfo)) {
            return;
        }

        add(clause, getPropertyForClause(idInfo));
    }

    private void add(GremlinClause clause, String... args) {
        queryClauses.add(new GremlinClauseValue(clause, args));
    }

    private void add(int idx, GremlinClause clause, String... args) {
        queryClauses.add(idx, new GremlinClauseValue(clause, args));
    }

    private void addTrait(GremlinClause clause, IdentifierHelper.Info idInfo) {
        if (context != null && !context.validator.isValid(context, clause, idInfo)) {
            return;
        }

        add(clause, idInfo.get(), idInfo.get());
    }

    public static class GremlinClauseValue {
        private final GremlinClause clause;
        private final String        value;
        private final String[]      values;
        private final Object        rawValue;

        public GremlinClauseValue(GremlinClause clause, String property, String operator, Object rawValue, String str) {
            this.clause   = clause;
            this.value    = clause.get(property, operator, str);
            this.values   = new String[] {property, operator, str};
            this.rawValue = rawValue;
        }

        public GremlinClauseValue(GremlinClause clause, String... values) {
            this.clause   = clause;
            this.value    = clause.get(values);
            this.values   = values;
            this.rawValue = null;
        }

        public GremlinClause getClause() {
            return clause;
        }

        public String getClauseWithValue() {
            return value;
        }

        public String[] getValues() {
            return values;
        }

        public Object getRawValue() {
            return this.rawValue;
        }

        @Override
        public String toString() {
            return String.format("%s", clause);
        }
    }

    @VisibleForTesting
    static class Context {
        private static final AtlasStructType UNKNOWN_TYPE = new AtlasStructType(new AtlasStructDef());

        private final Lookup               lookup;
        private final ClauseValidator      validator;
        private final Map<String, String>  aliasMap = new HashMap<>();
        private       AtlasType            activeType;
        private       SelectClauseComposer selectClauseComposer;
        private       String               numericTypeFormatter = "";

        public Context(Lookup lookup) {
            this.lookup    = lookup;
            this.validator = new ClauseValidator();
        }

        public void registerActive(String typeName) {
            if (shouldRegister(typeName)) {
                try {
                    activeType = lookup.getType(typeName);

                    aliasMap.put(typeName, typeName);
                } catch (AtlasBaseException e) {
                    validator.check(e, AtlasErrorCode.INVALID_DSL_UNKNOWN_TYPE, typeName);

                    activeType = UNKNOWN_TYPE;
                }
            }
        }

        public void setActiveTypeToUnknown() {
            activeType = UNKNOWN_TYPE;
        }

        public void registerActive(IdentifierHelper.Info info) {
            if (validator.check(StringUtils.isNotEmpty(info.getTypeName()), AtlasErrorCode.INVALID_DSL_UNKNOWN_TYPE, info.getRaw())) {
                registerActive(info.getTypeName());
            } else {
                activeType = UNKNOWN_TYPE;
            }
        }

        public AtlasEntityType getActiveEntityType() {
            return (activeType instanceof AtlasEntityType) ? (AtlasEntityType) activeType : null;
        }

        public String getActiveTypeName() {
            return activeType.getTypeName();
        }

        public AtlasType getActiveType() {
            return activeType;
        }

        public boolean shouldRegister(String typeName) {
            return activeType == null || (activeType != null && !StringUtils.equals(getActiveTypeName(), typeName)) && (activeType != null && !lookup.hasAttribute(this, typeName));
        }

        public void registerAlias(String alias) {
            addAlias(alias, getActiveTypeName());
        }

        public boolean hasAlias(String alias) {
            return aliasMap.containsKey(alias);
        }

        public String getTypeNameFromAlias(String alias) {
            return aliasMap.get(alias);
        }

        public boolean isEmpty() {
            return activeType == null;
        }

        public SelectClauseComposer getSelectClauseComposer() {
            return selectClauseComposer;
        }

        public void setSelectClauseComposer(SelectClauseComposer selectClauseComposer) {
            this.selectClauseComposer = selectClauseComposer;
        }

        public void addAlias(String alias, String typeName) {
            if (aliasMap.containsKey(alias)) {
                check(false, AtlasErrorCode.INVALID_DSL_DUPLICATE_ALIAS, alias, getActiveTypeName());

                return;
            }

            aliasMap.put(alias, typeName);
        }

        public List<String> getErrorList() {
            return validator.getErrorList();
        }

        public boolean error(AtlasBaseException e, AtlasErrorCode ec, String t, String name) {
            return validator.check(e, ec, t, name);
        }

        public boolean check(boolean condition, AtlasErrorCode vm, String... args) {
            return validator.check(condition, vm, args);
        }

        public String getNumericTypeFormatter() {
            return this.numericTypeFormatter;
        }

        public void setNumericTypeFormatter(String formatter) {
            this.numericTypeFormatter = formatter;
        }
    }

    private static class ClauseValidator {
        final List<String> errorList = new ArrayList<>();

        public ClauseValidator() {
        }

        public boolean isValid(Context ctx, GremlinClause clause, IdentifierHelper.Info ia) {
            switch (clause) {
                case TRAIT:
                case ANY_TRAIT:
                case NO_TRAIT:
                    return check(ia.isTrait(), AtlasErrorCode.INVALID_DSL_UNKNOWN_CLASSIFICATION, ia.getRaw());

                case HAS_TYPE:
                    TypeCategory typeCategory = ctx.getActiveType().getTypeCategory();
                    return check(StringUtils.isNotEmpty(ia.getTypeName()) &&
                                    typeCategory == TypeCategory.CLASSIFICATION || typeCategory == TypeCategory.ENTITY,
                            AtlasErrorCode.INVALID_DSL_UNKNOWN_TYPE, ia.getRaw());

                case HAS_PROPERTY:
                    return check(ia.isPrimitive(), AtlasErrorCode.INVALID_DSL_HAS_PROPERTY, ia.getRaw());

                case ORDER_BY:
                    return check(ia.isPrimitive(), AtlasErrorCode.INVALID_DSL_ORDERBY, ia.getRaw());

                case GROUP_BY:
                    return check(ia.isPrimitive(), AtlasErrorCode.INVALID_DSL_SELECT_INVALID_AGG, ia.getRaw());

                default:
                    return getErrorList().isEmpty();
            }
        }

        public boolean check(Exception ex, AtlasErrorCode vm, String... args) {
            String[] extraArgs = getExtraSlotArgs(args, ex.getMessage());

            return check(false, vm, extraArgs);
        }

        public boolean check(boolean condition, AtlasErrorCode vm, String... args) {
            if (!condition) {
                addError(vm, args);
            }

            return condition;
        }

        public void addError(AtlasErrorCode ec, String... messages) {
            errorList.add(ec.getFormattedErrorMessage(messages));
        }

        public List<String> getErrorList() {
            return errorList;
        }

        public boolean isValidQualifiedName(String qualifiedName, String raw) {
            return check(StringUtils.isNotEmpty(qualifiedName), AtlasErrorCode.INVALID_DSL_QUALIFIED_NAME, raw);
        }

        private String[] getExtraSlotArgs(String[] args, String s) {
            String[] argsPlus1 = new String[args.length + 1];

            System.arraycopy(args, 0, argsPlus1, 0, args.length);

            argsPlus1[args.length] = s;

            return argsPlus1;
        }
    }
}
