/*
 * Copyright 2017 HugeGraph Authors
 *
 * 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 com.baidu.hugegraph.traversal.optimize;

import java.util.ArrayList;
import java.util.Collection;
import java.util.Iterator;
import java.util.List;
import java.util.function.BiPredicate;
import java.util.function.Function;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

import org.apache.tinkerpop.gremlin.process.traversal.Compare;
import org.apache.tinkerpop.gremlin.process.traversal.Contains;
import org.apache.tinkerpop.gremlin.process.traversal.Order;
import org.apache.tinkerpop.gremlin.process.traversal.P;
import org.apache.tinkerpop.gremlin.process.traversal.Step;
import org.apache.tinkerpop.gremlin.process.traversal.Traversal;
import org.apache.tinkerpop.gremlin.process.traversal.dsl.graph.GraphTraversal;
import org.apache.tinkerpop.gremlin.process.traversal.step.HasContainerHolder;
import org.apache.tinkerpop.gremlin.process.traversal.step.filter.FilterStep;
import org.apache.tinkerpop.gremlin.process.traversal.step.filter.HasStep;
import org.apache.tinkerpop.gremlin.process.traversal.step.filter.RangeGlobalStep;
import org.apache.tinkerpop.gremlin.process.traversal.step.map.CountGlobalStep;
import org.apache.tinkerpop.gremlin.process.traversal.step.map.GraphStep;
import org.apache.tinkerpop.gremlin.process.traversal.step.map.MaxGlobalStep;
import org.apache.tinkerpop.gremlin.process.traversal.step.map.MeanGlobalStep;
import org.apache.tinkerpop.gremlin.process.traversal.step.map.MinGlobalStep;
import org.apache.tinkerpop.gremlin.process.traversal.step.map.NoOpBarrierStep;
import org.apache.tinkerpop.gremlin.process.traversal.step.map.OrderGlobalStep;
import org.apache.tinkerpop.gremlin.process.traversal.step.map.PropertiesStep;
import org.apache.tinkerpop.gremlin.process.traversal.step.map.SumGlobalStep;
import org.apache.tinkerpop.gremlin.process.traversal.step.sideEffect.IdentityStep;
import org.apache.tinkerpop.gremlin.process.traversal.step.util.ElementValueComparator;
import org.apache.tinkerpop.gremlin.process.traversal.step.util.HasContainer;
import org.apache.tinkerpop.gremlin.process.traversal.step.util.ReducingBarrierStep;
import org.apache.tinkerpop.gremlin.process.traversal.util.AndP;
import org.apache.tinkerpop.gremlin.process.traversal.util.ConnectiveP;
import org.apache.tinkerpop.gremlin.process.traversal.util.OrP;
import org.apache.tinkerpop.gremlin.process.traversal.util.TraversalHelper;
import org.apache.tinkerpop.gremlin.structure.Edge;
import org.apache.tinkerpop.gremlin.structure.Element;
import org.apache.tinkerpop.gremlin.structure.Property;
import org.apache.tinkerpop.gremlin.structure.PropertyType;
import org.apache.tinkerpop.gremlin.structure.T;
import org.apache.tinkerpop.gremlin.structure.Vertex;

import com.baidu.hugegraph.HugeException;
import com.baidu.hugegraph.HugeGraph;
import com.baidu.hugegraph.backend.BackendException;
import com.baidu.hugegraph.backend.id.Id;
import com.baidu.hugegraph.backend.page.PageInfo;
import com.baidu.hugegraph.backend.page.PageState;
import com.baidu.hugegraph.backend.query.Aggregate.AggregateFunc;
import com.baidu.hugegraph.backend.query.Condition;
import com.baidu.hugegraph.backend.query.Condition.Relation;
import com.baidu.hugegraph.backend.query.Condition.RelationType;
import com.baidu.hugegraph.backend.query.ConditionQuery;
import com.baidu.hugegraph.backend.query.Query;
import com.baidu.hugegraph.exception.NotSupportException;
import com.baidu.hugegraph.iterator.FilterIterator;
import com.baidu.hugegraph.schema.PropertyKey;
import com.baidu.hugegraph.schema.SchemaLabel;
import com.baidu.hugegraph.structure.HugeElement;
import com.baidu.hugegraph.structure.HugeProperty;
import com.baidu.hugegraph.type.HugeType;
import com.baidu.hugegraph.type.define.Directions;
import com.baidu.hugegraph.type.define.HugeKeys;
import com.baidu.hugegraph.util.CollectionUtil;
import com.baidu.hugegraph.util.DateUtil;
import com.baidu.hugegraph.util.E;
import com.baidu.hugegraph.util.JsonUtil;
import com.google.common.collect.ImmutableList;

public final class TraversalUtil {

    public static final String P_CALL = "P.";

    public static HugeGraph getGraph(Step<?, ?> step) {
        return (HugeGraph) step.getTraversal().getGraph().get();
    }

    public static void extractHasContainer(HugeGraphStep<?, ?> newStep,
                                           Traversal.Admin<?, ?> traversal) {
        Step<?, ?> step = newStep;
        do {
            step = step.getNextStep();
            if (step instanceof HasStep) {
                HasContainerHolder holder = (HasContainerHolder) step;
                for (HasContainer has : holder.getHasContainers()) {
                    if (!GraphStep.processHasContainerIds(newStep, has)) {
                        newStep.addHasContainer(has);
                    }
                }
                TraversalHelper.copyLabels(step, step.getPreviousStep(), false);
                traversal.removeStep(step);
            }
        } while (step instanceof HasStep || step instanceof NoOpBarrierStep);
    }

    public static void extractHasContainer(HugeVertexStep<?> newStep,
                                           Traversal.Admin<?, ?> traversal) {
        Step<?, ?> step = newStep;
        do {
            if (step instanceof HasStep) {
                HasContainerHolder holder = (HasContainerHolder) step;
                for (HasContainer has : holder.getHasContainers()) {
                    newStep.addHasContainer(has);
                }
                TraversalHelper.copyLabels(step, step.getPreviousStep(), false);
                traversal.removeStep(step);
            }
            step = step.getNextStep();
        } while (step instanceof HasStep || step instanceof NoOpBarrierStep);
    }

    public static void extractOrder(Step<?, ?> newStep,
                                    Traversal.Admin<?, ?> traversal) {
        Step<?, ?> step = newStep;
        do {
            step = step.getNextStep();
            if (step instanceof OrderGlobalStep) {
                QueryHolder holder = (QueryHolder) newStep;
                @SuppressWarnings("resource")
                OrderGlobalStep<?, ?> orderStep = (OrderGlobalStep<?, ?>) step;
                orderStep.getComparators().forEach(comp -> {
                    ElementValueComparator<?> comparator =
                            (ElementValueComparator<?>) comp.getValue1();
                    holder.orderBy(comparator.getPropertyKey(),
                                   (Order) comparator.getValueComparator());
                });
                TraversalHelper.copyLabels(step, newStep, false);
                traversal.removeStep(step);
            }
            step = step.getNextStep();
        } while (step instanceof OrderGlobalStep ||
                 step instanceof IdentityStep);
    }

    public static void extractRange(Step<?, ?> newStep,
                                    Traversal.Admin<?, ?> traversal,
                                    boolean extractOnlyLimit) {
        QueryHolder holder = (QueryHolder) newStep;
        Step<?, ?> step = newStep;
        do {
            step = step.getNextStep();
            if (step instanceof RangeGlobalStep) {
                @SuppressWarnings("unchecked")
                RangeGlobalStep<Object> range = (RangeGlobalStep<Object>) step;
                /*
                 * NOTE: keep the step to limit results after query from DB
                 * due to `limit`(in DB) may not be implemented accurately.
                 * but the backend driver should ensure `offset` accurately.
                 */
                // TraversalHelper.copyLabels(step, newStep, false);
                // traversal.removeStep(step);
                if (extractOnlyLimit) {
                    // May need to retain offset for multiple sub-queries
                    holder.setRange(0, range.getHighRange());
                } else {
                    long limit = holder.setRange(range.getLowRange(),
                                                 range.getHighRange());
                    RangeGlobalStep<Object> newRange = new RangeGlobalStep<>(
                                                       traversal, 0, limit);
                    TraversalHelper.replaceStep(range, newRange, traversal);
                }
            }
        } while (step instanceof RangeGlobalStep ||
                 step instanceof IdentityStep ||
                 step instanceof NoOpBarrierStep);
    }

    public static void extractCount(Step<?, ?> newStep,
                                    Traversal.Admin<?, ?> traversal) {
        Step<?, ?> step = newStep;
        do {
            step = step.getNextStep();
            if (step instanceof CountGlobalStep) {
                QueryHolder holder = (QueryHolder) newStep;
                holder.setCount();
            }
        } while (step instanceof CountGlobalStep ||
                 step instanceof FilterStep ||
                 step instanceof IdentityStep ||
                 step instanceof NoOpBarrierStep);
    }

    public static void extractAggregateFunc(Step<?, ?> newStep,
                                            Traversal.Admin<?, ?> traversal) {
        PropertiesStep<?> propertiesStep = null;
        Step<?, ?> step = newStep;
        do {
            step = step.getNextStep();
            if (step instanceof PropertiesStep) {
                @SuppressWarnings("resource")
                PropertiesStep<?> propStep = (PropertiesStep<?>) step;
                if (propStep.getReturnType() == PropertyType.VALUE &&
                    propStep.getPropertyKeys().length == 1) {
                    propertiesStep = propStep;
                }
            } else if (propertiesStep != null &&
                       step instanceof ReducingBarrierStep) {
                AggregateFunc aggregateFunc;
                if (step instanceof CountGlobalStep) {
                    aggregateFunc = AggregateFunc.COUNT;
                } else if (step instanceof MaxGlobalStep) {
                    aggregateFunc = AggregateFunc.MAX;
                } else if (step instanceof MinGlobalStep) {
                    aggregateFunc = AggregateFunc.MIN;
                } else if (step instanceof MeanGlobalStep) {
                    aggregateFunc = AggregateFunc.AVG;
                } else if (step instanceof SumGlobalStep) {
                    aggregateFunc = AggregateFunc.SUM;
                } else {
                    aggregateFunc = null;
                }

                if (aggregateFunc != null) {
                    QueryHolder holder = (QueryHolder) newStep;
                    holder.setAggregate(aggregateFunc,
                                        propertiesStep.getPropertyKeys()[0]);
                    traversal.removeStep(step);
                    traversal.removeStep(propertiesStep);
                }
            }
        } while (step instanceof FilterStep ||
                 step instanceof PropertiesStep ||
                 step instanceof IdentityStep ||
                 step instanceof NoOpBarrierStep);
    }

    public static ConditionQuery fillConditionQuery(
                                 List<HasContainer> hasContainers,
                                 ConditionQuery query,
                                 HugeGraph graph) {
        HugeType resultType = query.resultType();

        for (HasContainer has : hasContainers) {
            Condition condition = convHas2Condition(has, resultType, graph);
            query.query(condition);
        }
        return query;
    }

    public static Condition convHas2Condition(HasContainer has,
                                              HugeType type,
                                              HugeGraph graph) {
        P<?> p = has.getPredicate();
        E.checkArgument(p != null, "The predicate of has(%s) is null", has);
        BiPredicate<?, ?> bp = p.getBiPredicate();
        Condition condition;
        if (keyForContainsKeyOrValue(has.getKey())) {
            condition = convContains2Relation(graph, has);
        } else if (bp instanceof Compare) {
            condition = convCompare2Relation(graph, type, has);
        } else if (bp instanceof RelationType) {
            condition = convRelationType2Relation(graph, type, has);
        } else if (bp instanceof Contains) {
            condition = convIn2Relation(graph, type, has);
        } else if (p instanceof AndP) {
            condition = convAnd(graph, type, has);
        } else if (p instanceof OrP) {
            condition = convOr(graph, type, has);
        } else {
            // TODO: deal with other Predicate
            throw newUnsupportedPredicate(p);
        }
        return condition;
    }

    public static Condition convAnd(HugeGraph graph,
                                    HugeType type,
                                    HasContainer has) {
        P<?> p = has.getPredicate();
        assert p instanceof AndP;
        @SuppressWarnings("unchecked")
        List<P<Object>> predicates = ((AndP<Object>) p).getPredicates();
        if (predicates.size() < 2) {
            throw newUnsupportedPredicate(p);
        }

        Condition cond = null;
        for (P<Object> predicate : predicates) {
            HasContainer newHas = new HasContainer(has.getKey(), predicate);
            Condition newCond = convHas2Condition(newHas, type, graph);
            if (cond == null) {
                cond = newCond;
            } else {
                cond = Condition.and(newCond, cond);
            }
        }
        return cond;
    }

    public static Condition convOr(HugeGraph graph,
                                   HugeType type,
                                   HasContainer has) {
        P<?> p = has.getPredicate();
        assert p instanceof OrP;
        @SuppressWarnings("unchecked")
        List<P<Object>> predicates = ((OrP<Object>) p).getPredicates();
        if (predicates.size() < 2) {
            throw newUnsupportedPredicate(p);
        }

        Condition cond = null;
        for (P<Object> predicate : predicates) {
            HasContainer newHas = new HasContainer(has.getKey(), predicate);
            Condition newCond = convHas2Condition(newHas, type, graph);
            if (cond == null) {
                cond = newCond;
            } else {
                cond = Condition.or(newCond, cond);
            }
        }
        return cond;
    }

    private static Relation convCompare2Relation(HugeGraph graph,
                                                 HugeType type,
                                                 HasContainer has) {
        assert type.isGraph();
        BiPredicate<?, ?> bp = has.getPredicate().getBiPredicate();
        assert bp instanceof Compare;

        return isSysProp(has.getKey()) ?
               convCompare2SyspropRelation(graph, type, has) :
               convCompare2UserpropRelation(graph, type, has);
    }


    private static Relation convCompare2SyspropRelation(HugeGraph graph,
                                                        HugeType type,
                                                        HasContainer has) {
        BiPredicate<?, ?> bp = has.getPredicate().getBiPredicate();
        assert bp instanceof Compare;

        HugeKeys key = token2HugeKey(has.getKey());
        E.checkNotNull(key, "token key");
        Object value = convSysValueIfNeeded(graph, type, key, has.getValue());

        switch ((Compare) bp) {
            case eq:
                return Condition.eq(key, value);
            case gt:
                return Condition.gt(key, value);
            case gte:
                return Condition.gte(key, value);
            case lt:
                return Condition.lt(key, value);
            case lte:
                return Condition.lte(key, value);
            case neq:
                return Condition.neq(key, value);
        }

        throw newUnsupportedPredicate(has.getPredicate());
    }

    private static Relation convCompare2UserpropRelation(HugeGraph graph,
                                                         HugeType type,
                                                         HasContainer has) {
        BiPredicate<?, ?> bp = has.getPredicate().getBiPredicate();
        assert bp instanceof Compare;

        String key = has.getKey();
        PropertyKey pkey = graph.propertyKey(key);
        Id pkeyId = pkey.id();
        Object value = validPropertyValue(has.getValue(), pkey);

        switch ((Compare) bp) {
            case eq:
                return Condition.eq(pkeyId, value);
            case gt:
                return Condition.gt(pkeyId, value);
            case gte:
                return Condition.gte(pkeyId, value);
            case lt:
                return Condition.lt(pkeyId, value);
            case lte:
                return Condition.lte(pkeyId, value);
            case neq:
                return Condition.neq(pkeyId, value);
        }

        throw newUnsupportedPredicate(has.getPredicate());
    }

    private static Condition convRelationType2Relation(HugeGraph graph,
                                                       HugeType type,
                                                       HasContainer has) {
        assert type.isGraph();
        BiPredicate<?, ?> bp = has.getPredicate().getBiPredicate();
        assert bp instanceof RelationType;

        String key = has.getKey();
        PropertyKey pkey = graph.propertyKey(key);
        Id pkeyId = pkey.id();
        Object value = validPropertyValue(has.getValue(), pkey);
        return new Condition.UserpropRelation(pkeyId, (RelationType) bp, value);
    }

    public static Condition convIn2Relation(HugeGraph graph,
                                            HugeType type,
                                            HasContainer has) {
        BiPredicate<?, ?> bp = has.getPredicate().getBiPredicate();
        assert bp instanceof Contains;
        Collection<?> values = (Collection<?>) has.getValue();

        String originKey = has.getKey();
        if (values.size() > 1) {
            E.checkArgument(!originKey.equals(T.key) &&
                            !originKey.equals(T.value),
                            "Not support hasKey() or hasValue() with " +
                            "multiple values");
        }

        HugeKeys hugeKey = token2HugeKey(originKey);
        List<?> valueList;
        if (hugeKey != null) {
            valueList = convSysListValueIfNeeded(graph, type, hugeKey, values);
            switch ((Contains) bp) {
                case within:
                    return Condition.in(hugeKey, valueList);
                case without:
                    return Condition.nin(hugeKey, valueList);
            }
        } else {
            valueList = new ArrayList<>(values);
            String key = has.getKey();
            PropertyKey pkey = graph.propertyKey(key);

            switch ((Contains) bp) {
                case within:
                    return Condition.in(pkey.id(), valueList);
                case without:
                    return Condition.nin(pkey.id(), valueList);
            }
        }

        throw newUnsupportedPredicate(has.getPredicate());
    }

    public static Condition convContains2Relation(HugeGraph graph,
                                                  HasContainer has) {
        // Convert contains-key or contains-value
        BiPredicate<?, ?> bp = has.getPredicate().getBiPredicate();
        E.checkArgument(bp == Compare.eq, "CONTAINS query with relation " +
                        "'%s' is not supported", bp);

        HugeKeys key = token2HugeKey(has.getKey());
        E.checkNotNull(key, "token key");
        Object value = has.getValue();

        if (keyForContainsKey(has.getKey())) {
            if (value instanceof String) {
                value = graph.propertyKey((String) value).id();
            }
            return Condition.containsKey(key, value);
        } else {
            assert keyForContainsValue(has.getKey());
            return Condition.containsValue(key, value);
        }
    }

    public static BackendException newUnsupportedPredicate(P<?> predicate) {
        return new BackendException("Unsupported predicate: '%s'", predicate);
    }

    public static HugeKeys string2HugeKey(String key) {
        HugeKeys hugeKey = token2HugeKey(key);
        return hugeKey != null ? hugeKey : HugeKeys.valueOf(key);
    }

    public static HugeKeys token2HugeKey(String key) {
        if (key.equals(T.label.getAccessor())) {
            return HugeKeys.LABEL;
        } else if (key.equals(T.id.getAccessor())) {
            return HugeKeys.ID;
        } else if (keyForContainsKeyOrValue(key)) {
            return HugeKeys.PROPERTIES;
        }
        return null;
    }

    public static boolean keyForContainsKeyOrValue(String key) {
        return key.equals(T.key.getAccessor()) ||
               key.equals(T.value.getAccessor());
    }

    public static boolean keyForContainsKey(String key) {
        return key.equals(T.key.getAccessor());
    }

    public static boolean keyForContainsValue(String key) {
        return key.equals(T.value.getAccessor());
    }

    @SuppressWarnings("unchecked")
    public static <V> Iterator<V> filterResult(
                                  List<HasContainer> hasContainers,
                                  Iterator<? extends Element> iterator) {
        Iterator<?> result = new FilterIterator<>(iterator, elem -> {
            return HasContainer.testAll(elem, hasContainers);
        });
        return (Iterator<V>) result;
    }

    public static void convAllHasSteps(Traversal.Admin<?, ?> traversal) {
        // Extract all has steps in traversal
        @SuppressWarnings("rawtypes")
        List<HasStep> steps =
                      TraversalHelper.getStepsOfAssignableClassRecursively(
                      HasStep.class, traversal);
        HugeGraph graph = (HugeGraph) traversal.getGraph().get();
        for (HasStep<?> step : steps) {
            TraversalUtil.convHasStep(graph, step);
        }
    }

    public static void convHasStep(HugeGraph graph, HasStep<?> step) {
        HasContainerHolder holder = step;
        for (HasContainer has : holder.getHasContainers()) {
            convPredicateValue(graph, has);
        }
    }

    private static void convPredicateValue(HugeGraph graph,
                                           HasContainer has) {
        // No need to convert if key is sysprop
        if (isSysProp(has.getKey())) {
            return;
        }
        PropertyKey pkey = graph.propertyKey(has.getKey());
        updatePredicateValue(has.getPredicate(), pkey);
    }

    private static void updatePredicateValue(P<?> predicate, PropertyKey pkey) {
        List<P<Object>> leafPredicates = new ArrayList<>();
        collectPredicates(leafPredicates, ImmutableList.of(predicate));
        for (P<Object> pred : leafPredicates) {
            Object value = validPropertyValue(pred.getValue(), pkey);
            pred.setValue(value);
        }
    }

    private static boolean isSysProp(String key) {
        if (QueryHolder.SYSPROP_PAGE.equals(key)) {
            return true;
        }
        // Return true if key is ~id, ~label, ~key and ~value
        return token2HugeKey(key) != null;
    }

    @SuppressWarnings({ "unchecked", "rawtypes" })
    private static void collectPredicates(List<P<Object>> results,
                                          List<P<?>> predicates) {
        for (P<?> p : predicates) {
            if (p instanceof ConnectiveP) {
                collectPredicates(results, ((ConnectiveP) p).getPredicates());
            } else {
                results.add((P<Object>) p);
            }
        }
    }

    private static Object convSysValueIfNeeded(HugeGraph graph,
                                               HugeType type,
                                               HugeKeys key,
                                               Object value) {
        if (key == HugeKeys.LABEL && !(value instanceof Id)) {
            value = SchemaLabel.getLabelId(graph, type, value);
        } else if (key == HugeKeys.ID && !(value instanceof Id)) {
            value = HugeElement.getIdValue(type, value);
        }
        return value;
    }

    private static List<?> convSysListValueIfNeeded(HugeGraph graph,
                                                    HugeType type,
                                                    HugeKeys key,
                                                    Collection<?> values) {
        List<Object> newValues = new ArrayList<>(values.size());
        for (Object value : values) {
            newValues.add(convSysValueIfNeeded(graph, type, key, value));
        }
        return newValues;
    }

    public static Iterator<Edge> filterResult(Vertex vertex,
                                              Directions dir,
                                              Iterator<Edge> edges) {
        return new FilterIterator<>(edges, edge -> {
            if (dir == Directions.OUT && vertex.equals(edge.outVertex()) ||
                dir == Directions.IN && vertex.equals(edge.inVertex())) {
                return true;
            }
            return false;
        });
    }

    public static Query.Order convOrder(Order order) {
        return order == Order.desc ? Query.Order.DESC : Query.Order.ASC;
    }

    private static <V> V validPropertyValue(V value, PropertyKey pkey) {
        if (pkey.cardinality().single() && value instanceof Collection &&
            !pkey.dataType().isBlob()) {
            // Expect single but got collection, like P.within([])
            Collection<?> collection = (Collection<?>) value;
            Collection<Object> validValues = new ArrayList<>();
            for (Object element : collection) {
                Object validValue = pkey.validValue(element);
                if (validValue == null) {
                    validValues = null;
                    break;
                }
                validValues.add(validValue);
            }
            if (validValues == null) {
                List<Class<?>> classes = new ArrayList<>();
                for (Object v : (Collection<?>) value) {
                    classes.add(v == null ? null : v.getClass());
                }
                E.checkArgument(false,
                                "Invalid data type of query value in %s, " +
                                "expect %s for '%s', actual got %s",
                                value, pkey.dataType(), pkey.name(),
                                value == null ? null : classes);
            }

            @SuppressWarnings("unchecked")
            V validValue = (V) validValues;
            return validValue;
        }

        V validValue;
        if (pkey.cardinality().multiple() && !(value instanceof Collection)) {
            // Expect non-single but got single, like P.contains(value)
            List<V> values = CollectionUtil.toList(value);
            values = pkey.validValue(values);
            validValue = values != null ? values.get(0) : null;
        } else {
            validValue = pkey.validValue(value);
        }

        if (validValue == null) {
            E.checkArgument(false,
                            "Invalid data type of query value '%s', " +
                            "expect %s for '%s', actual got %s",
                            value, pkey.dataType(), pkey.name(),
                            value == null ? null : value.getClass());
        }
        return validValue;
    }

    public static void retriveSysprop(List<HasContainer> hasContainers,
                                      Function<HasContainer, Boolean> func) {
        for (Iterator<HasContainer> iter = hasContainers.iterator();
             iter.hasNext();) {
            HasContainer container = iter.next();
            if (container.getKey().startsWith("~") && func.apply(container)) {
                iter.remove();
            }
        }
    }

    public static String page(GraphTraversal<?, ?> traversal) {
        QueryHolder holder = firstPageStep(traversal);
        E.checkState(holder != null,
                     "Invalid paging traversal: %s", traversal.getClass());
        Object page = holder.metadata(PageInfo.PAGE);
        if (page == null) {
            return null;
        }
        /*
         * Page is instance of PageInfo if traversal with condition like:
         * g.V().has("x", 1).has("~page", "").
         * Page is instance of PageState if traversal without condition like:
         * g.V().has("~page", "")
         */
        assert page instanceof PageInfo || page instanceof PageState;
        return page.toString();
    }

    public static QueryHolder rootStep(GraphTraversal<?, ?> traversal) {
        for (final Step<?, ?> step : traversal.asAdmin().getSteps()) {
            if (step instanceof QueryHolder) {
                return (QueryHolder) step;
            }
        }
        return null;
    }

    public static QueryHolder firstPageStep(GraphTraversal<?, ?> traversal) {
        for (final Step<?, ?> step : traversal.asAdmin().getSteps()) {
            if (step instanceof QueryHolder &&
                ((QueryHolder) step).queryInfo().paging()) {
                return (QueryHolder) step;
            }
        }
        return null;
    }

    public static boolean testProperty(Property<?> prop, Object expected) {
        Object actual = prop.value();
        P<Object> predicate;
        if (expected instanceof String &&
            ((String) expected).startsWith(TraversalUtil.P_CALL)) {
            predicate = TraversalUtil.parsePredicate(((String) expected));
        } else {
            predicate = ConditionP.eq(expected);
        }
        updatePredicateValue(predicate, ((HugeProperty<?>) prop).propertyKey());
        return predicate.test(actual);
    }

    public static P<Object> parsePredicate(String predicate) {
        /*
         * Extract P from json string like {"properties": {"age": "P.gt(18)"}}
         * the `predicate` may actually be like "P.gt(18)"
         */
        Pattern pattern = Pattern.compile("^P\\.([a-z]+)\\(([\\S ]*)\\)$");
        Matcher matcher = pattern.matcher(predicate);
        if (!matcher.find()) {
            throw new HugeException("Invalid predicate: %s", predicate);
        }

        String method = matcher.group(1);
        String value = matcher.group(2);
        switch (method) {
            case "eq":
                return P.eq(predicateNumber(value));
            case "neq":
                return P.neq(predicateNumber(value));
            case "lt":
                return P.lt(predicateNumber(value));
            case "lte":
                return P.lte(predicateNumber(value));
            case "gt":
                return P.gt(predicateNumber(value));
            case "gte":
                return P.gte(predicateNumber(value));
            case "between":
                Number[] params = predicateNumbers(value, 2);
                return P.between(params[0], params[1]);
            case "inside":
                params = predicateNumbers(value, 2);
                return P.inside(params[0], params[1]);
            case "outside":
                params = predicateNumbers(value, 2);
                return P.outside(params[0], params[1]);
            case "within":
                return P.within(predicateArgs(value));
            case "contains":
                // Just for inner use case like auth filter
                return ConditionP.contains(predicateArg(value));
            default:
                throw new NotSupportException("predicate '%s'", method);
        }
    }

    public static Condition parsePredicate(PropertyKey pk, String predicate) {
        Pattern pattern = Pattern.compile("^P\\.([a-z]+)\\(([\\S ]*)\\)$");
        Matcher matcher = pattern.matcher(predicate);
        if (!matcher.find()) {
            throw new HugeException("Invalid predicate: %s", predicate);
        }

        String method = matcher.group(1);
        String value = matcher.group(2);
        Object validValue;
        switch (method) {
            case "eq":
                validValue = validPropertyValue(predicateNumber(value), pk);
                return Condition.eq(pk.id(), validValue);
            case "neq":
                validValue = validPropertyValue(predicateNumber(value), pk);
                return Condition.neq(pk.id(), validValue);
            case "lt":
                validValue = validPropertyValue(predicateNumber(value), pk);
                return Condition.lt(pk.id(), validValue);
            case "lte":
                validValue = validPropertyValue(predicateNumber(value), pk);
                return Condition.lte(pk.id(), validValue);
            case "gt":
                validValue = validPropertyValue(predicateNumber(value), pk);
                return Condition.gt(pk.id(), validValue);
            case "gte":
                validValue = validPropertyValue(predicateNumber(value), pk);
                return Condition.gte(pk.id(), validValue);
            case "between":
                Number[] params = predicateNumbers(value, 2);
                Object v1 = validPropertyValue(params[0], pk);
                Object v2 = validPropertyValue(params[1], pk);
                return Condition.and(Condition.gte(pk.id(), v1),
                                     Condition.lt(pk.id(), v2));
            case "inside":
                params = predicateNumbers(value, 2);
                v1 = validPropertyValue(params[0], pk);
                v2 = validPropertyValue(params[1], pk);
                return Condition.and(Condition.gt(pk.id(), v1),
                                     Condition.lt(pk.id(), v2));
            case "outside":
                params = predicateNumbers(value, 2);
                v1 = validPropertyValue(params[0], pk);
                v2 = validPropertyValue(params[1], pk);
                return Condition.and(Condition.lt(pk.id(), v1),
                                     Condition.gt(pk.id(), v2));
            case "within":

                List<T> values = predicateArgs(value);
                List<T> validValues = new ArrayList<>(values.size());
                for (T v : validValues) {
                    validValues.add(validPropertyValue(v, pk));
                }
                return Condition.in(pk.id(), validValues);
            default:
                throw new NotSupportException("predicate '%s'", method);
        }
    }

    private static Number predicateNumber(String value) {
        try {
            return JsonUtil.fromJson(value, Number.class);
        } catch (Exception e) {
            // Try to parse date
            if (e.getMessage().contains("not a valid number") ||
                e.getMessage().contains("Unexpected character ('-'")) {
                try {
                    if (value.startsWith("\"")) {
                        value = JsonUtil.fromJson(value, String.class);
                    }
                    return DateUtil.parse(value).getTime();
                } catch (Exception ignored) {}
            }

            throw new HugeException(
                      "Invalid value '%s', expect a number", e, value);
        }
    }

    private static Number[] predicateNumbers(String value, int count) {
        List<Number> values = predicateArgs(value);
        if (values.size() != count) {
            throw new HugeException("Invalid numbers size %s, expect %s",
                                    values.size(), count);
        }
        for (int i = 0; i < count; i++) {
            Object v = values.get(i);
            if (v instanceof Number) {
                continue;
            }
            try {
                v = predicateNumber(v.toString());
            } catch (Exception ignored) {
                // pass
            }
            if (v instanceof Number) {
                values.set(i, (Number) v);
                continue;
            }
            throw new HugeException(
                      "Invalid value '%s', expect a list of number", value);
        }
        return values.toArray(new Number[values.size()]);
    }

    @SuppressWarnings("unchecked")
    private static <V> V predicateArg(String value) {
        try {
            return (V) JsonUtil.fromJson(value, Object.class);
        } catch (Exception e) {
            throw new HugeException(
                      "Invalid value '%s', expect a single value", e, value);
        }
    }

    @SuppressWarnings("unchecked")
    private static <V> List<V> predicateArgs(String value) {
        try {
            return JsonUtil.fromJson("[" + value + "]", List.class);
        } catch (Exception e) {
            throw new HugeException(
                      "Invalid value '%s', expect a list", e, value);
        }
    }
}