/*
 * Copyright (C) 2020 Grakn Labs
 *
 * This program is free software: you can redistribute it and/or modify
 * it under the terms of the GNU Affero General Public License as
 * published by the Free Software Foundation, either version 3 of the
 * License, or (at your option) any later version.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU Affero General Public License for more details.
 *
 * You should have received a copy of the GNU Affero General Public License
 * along with this program.  If not, see <https://www.gnu.org/licenses/>.
 */

package graql.lang.parser;

import graql.grammar.GraqlBaseVisitor;
import graql.grammar.GraqlLexer;
import graql.grammar.GraqlParser;
import graql.lang.Graql;
import graql.lang.exception.GraqlException;
import graql.lang.pattern.Pattern;
import graql.lang.property.HasAttributeProperty;
import graql.lang.property.IsaProperty;
import graql.lang.property.RelationProperty;
import graql.lang.property.ValueProperty;
import graql.lang.query.GraqlCompute;
import graql.lang.query.GraqlDefine;
import graql.lang.query.GraqlDelete;
import graql.lang.query.GraqlGet;
import graql.lang.query.GraqlInsert;
import graql.lang.query.GraqlQuery;
import graql.lang.query.GraqlUndefine;
import graql.lang.query.MatchClause;
import graql.lang.query.builder.Computable;
import graql.lang.query.builder.Filterable;
import graql.lang.statement.Statement;
import graql.lang.statement.Variable;
import grakn.common.util.Triple;
import org.antlr.v4.runtime.BailErrorStrategy;
import org.antlr.v4.runtime.CharStream;
import org.antlr.v4.runtime.CharStreams;
import org.antlr.v4.runtime.CommonTokenStream;
import org.antlr.v4.runtime.DefaultErrorStrategy;
import org.antlr.v4.runtime.ParserRuleContext;
import org.antlr.v4.runtime.atn.PredictionMode;
import org.antlr.v4.runtime.misc.ParseCancellationException;
import org.antlr.v4.runtime.tree.TerminalNode;

import java.time.LocalDate;
import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter;
import java.util.ArrayList;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Set;
import java.util.function.Function;
import java.util.stream.Collectors;
import java.util.stream.Stream;

import static graql.lang.Graql.and;
import static graql.lang.Graql.not;
import static graql.lang.Graql.type;
import static grakn.common.util.Collections.triple;
import static graql.lang.util.StringUtil.unescapeRegex;
import static java.util.stream.Collectors.toList;

/**
 * Graql query string parser to produce Graql Java objects
 */
public class Parser extends GraqlBaseVisitor {

    private <CONTEXT extends ParserRuleContext, RETURN> RETURN parseQuery(
            String queryString, Function<GraqlParser, CONTEXT> parserMethod, Function<CONTEXT, RETURN> visitor
    ) {
        if (queryString == null || queryString.isEmpty()) {
            throw GraqlException.create("Query String is NULL or Empty");
        }

        ErrorListener errorListener = ErrorListener.of(queryString);
        CharStream charStream = CharStreams.fromString(queryString);
        GraqlLexer lexer = new GraqlLexer(charStream);

        lexer.removeErrorListeners();
        lexer.addErrorListener(errorListener);

        CommonTokenStream tokens = new CommonTokenStream(lexer);
        GraqlParser parser = new GraqlParser(tokens);

        parser.removeErrorListeners();
        parser.addErrorListener(errorListener);

        // BailErrorStrategy + SLL is a very fast parsing strategy for queries
        // that are expected to be correct. However, it may not be able to
        // provide detailed/useful error message, if at all.
        parser.setErrorHandler(new BailErrorStrategy());
        parser.getInterpreter().setPredictionMode(PredictionMode.SLL);

        CONTEXT queryContext;
        try {
            queryContext = parserMethod.apply(parser);
        } catch (ParseCancellationException e) {
            // We parse the query one more time, with "strict strategy" :
            // DefaultErrorStrategy + LL_EXACT_AMBIG_DETECTION
            // This was not set to default parsing strategy, but it is useful
            // to produce detailed/useful error message
            parser.setErrorHandler(new DefaultErrorStrategy());
            parser.getInterpreter().setPredictionMode(PredictionMode.LL_EXACT_AMBIG_DETECTION);
            queryContext = parserMethod.apply(parser);

            throw GraqlException.create(errorListener.toString());
        }

        return visitor.apply(queryContext);
    }

    @SuppressWarnings("unchecked")
    public <T extends GraqlQuery> T parseQueryEOF(String queryString) {
        return (T) parseQuery(queryString, GraqlParser::eof_query, this::visitEof_query);
    }

    @SuppressWarnings("unchecked")
    public <T extends GraqlQuery> Stream<T> parseQueryListEOF(String queryString) {
        return (Stream<T>) parseQuery(queryString, GraqlParser::eof_query_list, this::visitEof_query_list);
    }

    public Pattern parsePatternEOF(String patternString) {
        return parseQuery(patternString, GraqlParser::eof_pattern, this::visitEof_pattern);
    }

    public Stream<? extends Pattern> parsePatternListEOF(String patternsString) {
        return parseQuery(patternsString, GraqlParser::eof_pattern_list, this::visitEof_pattern_list);
    }

    // GLOBAL HELPER METHODS ===================================================

    private Variable getVar(TerminalNode variable) {
        // Remove '$' prefix
        String name = variable.getSymbol().getText().substring(1);

        if (name.equals(Graql.Token.Char.UNDERSCORE.toString())) {
            return new Variable();
        } else {
            return new Variable(name);
        }
    }

    // PARSER VISITORS =========================================================

    @Override
    public GraqlQuery visitEof_query(GraqlParser.Eof_queryContext ctx) {
        return visitQuery(ctx.query());
    }

    @Override
    public Stream<? extends GraqlQuery> visitEof_query_list(GraqlParser.Eof_query_listContext ctx) {
        return ctx.query().stream().map(this::visitQuery);
    }

    @Override
    public Pattern visitEof_pattern(GraqlParser.Eof_patternContext ctx) {
        return visitPattern(ctx.pattern());
    }

    @Override
    public Stream<? extends Pattern> visitEof_pattern_list(GraqlParser.Eof_pattern_listContext ctx) {
        return ctx.pattern().stream().map(this::visitPattern);
    }

    // GRAQL QUERIES ===========================================================

    @Override
    public GraqlQuery visitQuery(GraqlParser.QueryContext ctx) {
        if (ctx.query_define() != null) {
            return visitQuery_define(ctx.query_define());

        } else if (ctx.query_undefine() != null) {
            return visitQuery_undefine(ctx.query_undefine());

        } else if (ctx.query_insert() != null) {
            return visitQuery_insert(ctx.query_insert());

        } else if (ctx.query_delete() != null) {
            return visitQuery_delete(ctx.query_delete());

        } else if (ctx.query_get() != null) {
            return visitQuery_get(ctx.query_get());

        } else if (ctx.query_get_aggregate() != null) {
            return visitQuery_get_aggregate(ctx.query_get_aggregate());

        } else if (ctx.query_get_group() != null) {
            return visitQuery_get_group(ctx.query_get_group());

        } else if (ctx.query_get_group_agg() != null) {
            return visitQuery_get_group_agg(ctx.query_get_group_agg());

        } else if (ctx.query_compute() != null) {
            return visitQuery_compute(ctx.query_compute());

        } else {
            throw new IllegalArgumentException("Unrecognised Graql Query: " + ctx.getText());
        }
    }

    @Override
    public GraqlDefine visitQuery_define(GraqlParser.Query_defineContext ctx) {
        List<Statement> vars = ctx.statement_type().stream()
                .map(this::visitStatement_type)
                .collect(toList());
        return Graql.define(vars);
    }

    @Override
    public GraqlUndefine visitQuery_undefine(GraqlParser.Query_undefineContext ctx) {
        List<Statement> vars = ctx.statement_type().stream()
                .map(this::visitStatement_type)
                .collect(toList());
        return Graql.undefine(vars);
    }

    @Override
    public GraqlInsert visitQuery_insert(GraqlParser.Query_insertContext ctx) {
        List<Statement> statements = ctx.statement_instance().stream()
                .map(this::visitStatement_instance)
                .collect(toList());

        if (ctx.pattern() != null && !ctx.pattern().isEmpty()) {
            LinkedHashSet<Pattern> patterns = ctx.pattern().stream().map(this::visitPattern)
                    .collect(Collectors.toCollection(LinkedHashSet::new));

            return Graql.match(patterns).insert(statements);
        } else {
            return Graql.insert(statements);
        }
    }

    @Override
    public GraqlDelete visitQuery_delete(GraqlParser.Query_deleteContext ctx) {
        List<Statement> statements = ctx.statement_instance().stream()
                .map(this::visitStatement_instance)
                .collect(toList());

        MatchClause match = Graql.match(ctx.pattern()
                .stream().map(this::visitPattern)
                .collect(Collectors.toCollection(LinkedHashSet::new)));

        return new GraqlDelete(match, statements);
    }

    @Override
    public GraqlGet visitQuery_get(GraqlParser.Query_getContext ctx) {
        LinkedHashSet<Variable> vars = visitVariables(ctx.variables());
        MatchClause match = Graql.match(ctx.pattern()
                .stream().map(this::visitPattern)
                .collect(Collectors.toCollection(LinkedHashSet::new)));

        if (ctx.filters().getChildCount() == 0) {
            return new GraqlGet(match, vars);
        } else {
            Triple<Filterable.Sorting, Long, Long> filters = visitFilters(ctx.filters());
            return new GraqlGet(match, vars, filters.first(), filters.second(), filters.third());
        }
    }

    @Override
    public Triple<Filterable.Sorting, Long, Long> visitFilters(GraqlParser.FiltersContext ctx) {
        Filterable.Sorting order = null;
        long offset = -1;
        long limit = -1;

        if (ctx.sort() != null) {
            Variable var = getVar(ctx.sort().VAR_());
            order = ctx.sort().ORDER_() == null ? new Filterable.Sorting(var) :
                    new Filterable.Sorting(var, Graql.Token.Order.of(ctx.sort().ORDER_().getText()));
        }
        if (ctx.offset() != null) {
            offset = getInteger(ctx.offset().INTEGER_());
        }
        if (ctx.limit() != null) {
            limit = getInteger(ctx.limit().INTEGER_());
        }

        return triple(order, offset, limit);
    }

    /**
     * Visits the aggregate query node in the parsed syntax tree and builds the
     * appropriate aggregate query object
     *
     * @param ctx reference to the parsed aggregate query string
     * @return An AggregateQuery object
     */
    @Override
    public GraqlGet.Aggregate visitQuery_get_aggregate(GraqlParser.Query_get_aggregateContext ctx) {
        GraqlParser.Function_aggregateContext function = ctx.function_aggregate();

        return new GraqlGet.Aggregate(visitQuery_get(ctx.query_get()),
                Graql.Token.Aggregate.Method.of(function.function_method().getText()),
                function.VAR_() != null ? getVar(function.VAR_()) : null);
    }

    @Override
    public GraqlGet.Group visitQuery_get_group(GraqlParser.Query_get_groupContext ctx) {
        Variable var = getVar(ctx.function_group().VAR_());
        return visitQuery_get(ctx.query_get()).group(var);
    }

    @Override
    public GraqlGet.Group.Aggregate visitQuery_get_group_agg(GraqlParser.Query_get_group_aggContext ctx) {
        Variable var = getVar(ctx.function_group().VAR_());
        GraqlParser.Function_aggregateContext function = ctx.function_aggregate();

        return new GraqlGet.Group.Aggregate(visitQuery_get(ctx.query_get()).group(var),
                Graql.Token.Aggregate.Method.of(function.function_method().getText()),
                function.VAR_() != null ? getVar(function.VAR_()) : null);
    }

    // DELETE AND GET QUERY MODIFIERS ==========================================

    @Override
    public LinkedHashSet<Variable> visitVariables(GraqlParser.VariablesContext ctx) {
        return ctx.VAR_().stream()
                .map(this::getVar)
                .collect(Collectors.toCollection(LinkedHashSet::new));
    }

    // COMPUTE QUERY ===========================================================

    @Override
    public GraqlCompute visitQuery_compute(GraqlParser.Query_computeContext ctx) {

        if (ctx.compute_conditions().conditions_count() != null) {
            return visitConditions_count(ctx.compute_conditions().conditions_count());
        } else if (ctx.compute_conditions().conditions_value() != null) {
            return visitConditions_value(ctx.compute_conditions().conditions_value());
        } else if (ctx.compute_conditions().conditions_path() != null) {
            return visitConditions_path(ctx.compute_conditions().conditions_path());
        } else if (ctx.compute_conditions().conditions_central() != null) {
            return visitConditions_central(ctx.compute_conditions().conditions_central());
        } else if (ctx.compute_conditions().conditions_cluster() != null) {
            return visitConditions_cluster(ctx.compute_conditions().conditions_cluster());
        } else {
            throw new IllegalArgumentException("Unrecognised Graql Compute Query: " + ctx.getText());
        }
    }

    @Override
    public GraqlCompute.Statistics.Count visitConditions_count(GraqlParser.Conditions_countContext ctx) {
        GraqlCompute.Statistics.Count compute = Graql.compute().count();

        if (ctx.input_count() != null) {
            compute = compute.in(visitType_labels(ctx.input_count().compute_scope().type_labels()));
        }

        return compute;
    }

    @Override
    public GraqlCompute.Statistics.Value visitConditions_value(GraqlParser.Conditions_valueContext ctx) {
        GraqlCompute.Statistics.Value compute;
        Graql.Token.Compute.Method method = Graql.Token.Compute.Method.of(ctx.compute_method().getText());

        if (method == null) {
            throw new IllegalArgumentException("Unrecognised Graql Compute Statistics method: " + ctx.getText());

        } else if (method.equals(Graql.Token.Compute.Method.MAX)) {
            compute = Graql.compute().max();

        } else if (method.equals(Graql.Token.Compute.Method.MIN)) {
            compute = Graql.compute().min();

        } else if (method.equals(Graql.Token.Compute.Method.MEAN)) {
            compute = Graql.compute().mean();

        } else if (method.equals(Graql.Token.Compute.Method.MEDIAN)) {
            compute = Graql.compute().median();

        } else if (method.equals(Graql.Token.Compute.Method.SUM)) {
            compute = Graql.compute().sum();

        } else if (method.equals(Graql.Token.Compute.Method.STD)) {
            compute = Graql.compute().std();

        } else {
            throw new IllegalArgumentException("Unrecognised Graql Compute Statistics method: " + ctx.getText());
        }

        for (GraqlParser.Input_valueContext valueCtx : ctx.input_value()) {

            if (valueCtx.compute_target() != null) {
                compute = compute.of(visitType_labels(valueCtx.compute_target().type_labels()));

            } else if (valueCtx.compute_scope() != null) {
                compute = compute.in(visitType_labels(valueCtx.compute_scope().type_labels()));

            } else {
                throw new IllegalArgumentException("Unrecognised Graql Compute Statistics condition: " + ctx.getText());
            }
        }

        return compute;
    }

    @Override
    public GraqlCompute.Path visitConditions_path(GraqlParser.Conditions_pathContext ctx) {
        GraqlCompute.Path compute = Graql.compute().path();

        for (GraqlParser.Input_pathContext pathCtx : ctx.input_path()) {

            if (pathCtx.compute_direction() != null) {
                String id = pathCtx.compute_direction().ID_().getText();

                if (pathCtx.compute_direction().FROM() != null) {
                    compute = compute.from(id);

                } else if (pathCtx.compute_direction().TO() != null) {
                    compute = compute.to(id);
                }
            } else if (pathCtx.compute_scope() != null) {
                compute = compute.in(visitType_labels(pathCtx.compute_scope().type_labels()));

            } else {
                throw new IllegalArgumentException("Unrecognised Graql Compute Path condition: " + ctx.getText());
            }
        }

        return compute;
    }

    @Override
    public GraqlCompute.Centrality visitConditions_central(GraqlParser.Conditions_centralContext ctx) {
        GraqlCompute.Centrality compute = Graql.compute().centrality();

        for (GraqlParser.Input_centralContext centralityCtx : ctx.input_central()) {

            if (centralityCtx.compute_target() != null) {
                compute = compute.of(visitType_labels(centralityCtx.compute_target().type_labels()));

            } else if (centralityCtx.compute_scope() != null) {
                compute = compute.in(visitType_labels(centralityCtx.compute_scope().type_labels()));

            } else if (centralityCtx.compute_config() != null) {
                compute = (GraqlCompute.Centrality) setComputeConfig(compute, centralityCtx.compute_config());

            } else {
                throw new IllegalArgumentException("Unrecognised Graql Compute Centrality condition: " + ctx.getText());
            }
        }

        return compute;
    }

    @Override
    public GraqlCompute.Cluster visitConditions_cluster(GraqlParser.Conditions_clusterContext ctx) {
        GraqlCompute.Cluster compute = Graql.compute().cluster();

        for (GraqlParser.Input_clusterContext clusterCtx : ctx.input_cluster()) {

            if (clusterCtx.compute_scope() != null) {
                compute = compute.in(visitType_labels(clusterCtx.compute_scope().type_labels()));

            } else if (clusterCtx.compute_config() != null) {
                compute = (GraqlCompute.Cluster) setComputeConfig(compute, clusterCtx.compute_config());
            } else {
                throw new IllegalArgumentException("Unrecognised Graql Compute Cluster condition: " + ctx.getText());
            }
        }

        return compute;
    }

    private Computable.Configurable setComputeConfig(Computable.Configurable compute, GraqlParser.Compute_configContext ctx) {
        if (ctx.USING() != null) {
            compute = compute.using(Graql.Token.Compute.Algorithm.of(ctx.compute_algorithm().getText()));

        } else if (ctx.WHERE() != null) {
            compute = compute.where(visitCompute_args(ctx.compute_args()));
        }

        return compute;
    }

    @Override
    public List<GraqlCompute.Argument> visitCompute_args(GraqlParser.Compute_argsContext ctx) {

        List<GraqlParser.Compute_argContext> argContextList = new ArrayList<>();
        List<GraqlCompute.Argument> argList = new ArrayList<>();

        if (ctx.compute_arg() != null) {
            argContextList.add(ctx.compute_arg());
        } else if (ctx.compute_args_array() != null) {
            argContextList.addAll(ctx.compute_args_array().compute_arg());
        }

        for (GraqlParser.Compute_argContext argContext : argContextList) {
            if (argContext.MIN_K() != null) {
                argList.add(GraqlCompute.Argument.minK(getInteger(argContext.INTEGER_())));

            } else if (argContext.K() != null) {
                argList.add(GraqlCompute.Argument.k(getInteger(argContext.INTEGER_())));

            } else if (argContext.SIZE() != null) {
                argList.add(GraqlCompute.Argument.size(getInteger(argContext.INTEGER_())));

            } else if (argContext.CONTAINS() != null) {
                argList.add(GraqlCompute.Argument.contains(argContext.ID_().getText()));
            }
        }

        return argList;
    }

    // QUERY PATTERNS ==========================================================

    @Override
    public Set<Pattern> visitPatterns(GraqlParser.PatternsContext ctx) {
        return ctx.pattern().stream()
                .map(this::visitPattern)
                .collect(Collectors.toCollection(LinkedHashSet::new));
    }

    @Override
    public Pattern visitPattern(GraqlParser.PatternContext ctx) {

        if (ctx.pattern_statement() != null) {
            return visitPattern_statement(ctx.pattern_statement());

        } else if (ctx.pattern_disjunction() != null) {
            return visitPattern_disjunction(ctx.pattern_disjunction());

        } else if (ctx.pattern_conjunction() != null) {
            return visitPattern_conjunction(ctx.pattern_conjunction());

        } else if (ctx.pattern_negation() != null) {
            return visitPattern_negation(ctx.pattern_negation());

        } else {
            throw new IllegalArgumentException("Unrecognised Pattern: " + ctx.getText());
        }
    }

    @Override
    public Pattern visitPattern_disjunction(GraqlParser.Pattern_disjunctionContext ctx) {
        Set<Pattern> patterns = ctx.patterns().stream()
                .map(patternsCtx -> {
                    Set<Pattern> patternSet = visitPatterns(patternsCtx);
                    if (patternSet.size() > 1) return and(patternSet);
                    else return patternSet.iterator().next();
                })
                .collect(Collectors.toCollection(LinkedHashSet::new));
        return Graql.or(patterns);
    }

    @Override
    public Pattern visitPattern_conjunction(GraqlParser.Pattern_conjunctionContext ctx) {
        return and(visitPatterns(ctx.patterns()));
    }

    @Override
    public Pattern visitPattern_negation(GraqlParser.Pattern_negationContext ctx) {
        Set<Pattern> patterns = visitPatterns(ctx.patterns());

        if (patterns.size() == 1) {
            return not(patterns.iterator().next());

        } else {
            return not(and(patterns));
        }
    }

    // PATTERN STATEMENTS ======================================================

    @Override
    public Statement visitPattern_statement(GraqlParser.Pattern_statementContext ctx) {
        // TODO: restrict for Match VS Define VS Insert

        if (ctx.statement_instance() != null) {
            return visitStatement_instance(ctx.statement_instance());

        } else if (ctx.statement_type() != null) {
            return visitStatement_type(ctx.statement_type());

        } else {
            throw new IllegalArgumentException("Unrecognised Statement class: " + ctx.getText());
        }
    }

    // TYPE STATEMENTS =========================================================

    @Override
    public Statement visitStatement_type(GraqlParser.Statement_typeContext ctx) {
        // TODO: restrict for Define VS Match for all usage of visitType(...)

        Statement type = visitType(ctx.type());

        for (GraqlParser.Type_propertyContext property : ctx.type_property()) {

            if (property.ABSTRACT() != null) {
                type = type.isAbstract();

            } else if (property.SUB_() != null) {
                Graql.Token.Property sub = Graql.Token.Property.of(property.SUB_().getText());

                if (sub != null && sub.equals(Graql.Token.Property.SUB)) {
                    type = type.sub(visitType(property.type(0)));

                } else if (sub != null && sub.equals(Graql.Token.Property.SUBX)) {
                    type = type.subX(visitType(property.type(0)));

                } else {
                    throw new IllegalArgumentException("Unrecognised SUB Property: " + property.type(0).getText());
                }

            } else if (property.KEY() != null) {
                type = type.key(visitType(property.type(0)));

            } else if (property.HAS() != null) {
                type = type.has(visitType(property.type(0)));

            } else if (property.PLAYS() != null) {
                type = type.plays(visitType(property.type(0)));

            } else if (property.RELATES() != null) {
                if (property.AS() != null) {
                    type = type.relates(visitType(property.type(0)),
                            visitType(property.type(1)));
                } else {
                    type = type.relates(visitType(property.type(0)));
                }
            } else if (property.VALUE() != null) {
                type = type.value(Graql.Token.ValueType.of(property.value_type().getText()));

            } else if (property.REGEX() != null) {
                type = type.regex(visitRegex(property.regex()));

            } else if (property.WHEN() != null) {
                type = type.when(and(
                        property.pattern().stream()
                                .map(this::visitPattern)
                                .collect(Collectors.toList())
                ));
            } else if (property.THEN() != null) {
                type = type.then(and(
                        property.statement_instance().stream()
                                .map(this::visitStatement_instance)
                                .collect(Collectors.toList())
                ));
            } else if (property.TYPE() != null) {
                type = type.type(visitType_label(property.type_label()));

            } else {
                throw new IllegalArgumentException("Unrecognised Type Statement: " + property.getText());
            }
        }

        return type;
    }

    // INSTANCE STATEMENTS =====================================================

    @Override
    public Statement visitStatement_instance(GraqlParser.Statement_instanceContext ctx) {
        // TODO: restrict for Insert VS Match

        if (ctx.statement_thing() != null) {
            return visitStatement_thing(ctx.statement_thing());

        } else if (ctx.statement_relation() != null) {
            return visitStatement_relation(ctx.statement_relation());

        } else if (ctx.statement_attribute() != null) {
            return visitStatement_attribute(ctx.statement_attribute());

        } else {
            throw new IllegalArgumentException("Unrecognised Instance Statement: " + ctx.getText());
        }
    }

    @Override
    @SuppressWarnings("Duplicates")
    public Statement visitStatement_thing(GraqlParser.Statement_thingContext ctx) {
        // TODO: restrict for Insert VS Match

        Statement instance = Graql.var(getVar(ctx.VAR_(0)));

        if (ctx.ISA_() != null) {
            instance = instance.isa(getIsaProperty(ctx.ISA_(), ctx.type()));

        } else if (ctx.ID() != null) {
            instance = instance.id(ctx.ID_().getText());

        } else if (ctx.NEQ() != null) {
            instance = instance.not(getVar(ctx.VAR_(1)));
        }

        if (ctx.attributes() != null) {
            for (HasAttributeProperty attribute : visitAttributes(ctx.attributes())) {
                instance = instance.has(attribute);
            }
        }

        return instance;
    }

    @Override
    @SuppressWarnings("Duplicates")
    public Statement visitStatement_relation(GraqlParser.Statement_relationContext ctx) {
        // TODO: restrict for Insert VS Match

        Statement instance;
        if (ctx.VAR_() != null) {
            instance = Graql.var(getVar(ctx.VAR_()));
        } else {
            instance = Graql.var(new Variable(false));
        }

        instance = instance.rel(visitRelation(ctx.relation()));
        if (ctx.ISA_() != null) {
            instance = instance.isa(getIsaProperty(ctx.ISA_(), ctx.type()));
        }

        if (ctx.attributes() != null) {
            for (HasAttributeProperty attribute : visitAttributes(ctx.attributes())) {
                instance = instance.has(attribute);
            }
        }

        return instance;
    }

    @Override
    @SuppressWarnings("Duplicates")
    public Statement visitStatement_attribute(GraqlParser.Statement_attributeContext ctx) {
        // TODO: restrict for Insert VS Match

        Statement instance;
        if (ctx.VAR_() != null) {
            instance = Graql.var(getVar(ctx.VAR_()));
        } else {
            instance = Graql.var();
        }

        instance = instance.attribute(new ValueProperty<>(visitOperation(ctx.operation())));
        if (ctx.ISA_() != null) {
            instance = instance.isa(getIsaProperty(ctx.ISA_(), ctx.type()));
        }

        if (ctx.attributes() != null) {
            for (HasAttributeProperty attribute : visitAttributes(ctx.attributes())) {
                instance = instance.has(attribute);
            }
        }

        return instance;
    }

    private IsaProperty getIsaProperty(TerminalNode isaToken, GraqlParser.TypeContext ctx) {
        Graql.Token.Property isa = Graql.Token.Property.of(isaToken.getText());

        if (isa != null && isa.equals(Graql.Token.Property.ISA)) {
            return new IsaProperty(visitType(ctx));

        } else if (isa != null && isa.equals(Graql.Token.Property.ISAX)) {
            return new IsaProperty(visitType(ctx), true);

        } else {
            throw new IllegalArgumentException("Unrecognised ISA property: " + ctx.getText());
        }
    }

    // ATTRIBUTE STATEMENT CONSTRUCT ===============================================

    @Override
    public List<HasAttributeProperty> visitAttributes(GraqlParser.AttributesContext ctx) {
        return ctx.attribute().stream().map(this::visitAttribute).collect(toList());
    }

    @Override
    public HasAttributeProperty visitAttribute(GraqlParser.AttributeContext ctx) {
        String type = ctx.type_label().getText();

        if (ctx.VAR_() != null) {
            Statement variable = Graql.var(getVar(ctx.VAR_()));
            return new HasAttributeProperty(type, variable);
        } else if (ctx.operation() != null) {
            Statement value = Graql.var().attribute(new ValueProperty<>(visitOperation(ctx.operation())));
            return new HasAttributeProperty(type, value);
        } else {
            throw new IllegalArgumentException("Unrecognised MATCH HAS statement: " + ctx.getText());
        }
    }

    // RELATION STATEMENT CONSTRUCT ============================================

    public RelationProperty visitRelation(GraqlParser.RelationContext ctx) {
        List<RelationProperty.RolePlayer> rolePlayers = new ArrayList<>();

        for (GraqlParser.Role_playerContext rolePlayerCtx : ctx.role_player()) {
            Statement player = new Statement(getVar(rolePlayerCtx.player().VAR_()));
            if (rolePlayerCtx.type() != null) {
                Statement role = visitType(rolePlayerCtx.type());
                rolePlayers.add(new RelationProperty.RolePlayer(role, player));
            } else {
                rolePlayers.add(new RelationProperty.RolePlayer(null, player));
            }
        }
        return new RelationProperty(rolePlayers);
    }

    // TYPE, LABEL, AND IDENTIFIER CONSTRUCTS ==================================

    @Override
    public Statement visitType(GraqlParser.TypeContext ctx) {
        if (ctx.type_label() != null) {
            return type(visitType_label(ctx.type_label()));
        } else {
            return new Statement(getVar(ctx.VAR_()));
        }
    }

    @Override
    public LinkedHashSet<String> visitType_labels(GraqlParser.Type_labelsContext ctx) {
        List<GraqlParser.Type_labelContext> labelsList = new ArrayList<>();

        if (ctx.type_label() != null) {
            labelsList.add(ctx.type_label());
        } else if (ctx.type_label_array() != null) {
            labelsList.addAll(ctx.type_label_array().type_label());
        }

        return labelsList.stream()
                .map(this::visitType_label)
                .collect(Collectors.toCollection(LinkedHashSet::new));
    }

    @Override
    public String visitType_label(GraqlParser.Type_labelContext ctx) {
        if (ctx.type_native() != null) {
            return ctx.type_native().getText();
        } else if (ctx.type_name() != null) {
            return ctx.type_name().getText();
        } else {
            return ctx.unreserved().getText();
        }
    }

    // ATTRIBUTE OPERATION CONSTRUCTS ==========================================

    @Override // TODO: this visitor method should not return a Predicate if we have the right data structure
    public ValueProperty.Operation<?> visitOperation(GraqlParser.OperationContext ctx) {
        if (ctx.assignment() != null) {
            return visitAssignment(ctx.assignment());
        } else if (ctx.comparison() != null) {
            return visitComparison(ctx.comparison());
        } else {
            throw new IllegalArgumentException("Unreconigsed Attribute Operation: " + ctx.getText());
        }
    }

    @Override
    public ValueProperty.Operation<?> visitAssignment(GraqlParser.AssignmentContext ctx) {
        Object value = visitValue(ctx.value());

        if (value instanceof Integer) {
            return new ValueProperty.Operation.Assignment.Number<>(((Integer) value));
        } else if (value instanceof Long) {
            return new ValueProperty.Operation.Assignment.Number<>((Long) value);
        } else if (value instanceof Float) {
            return new ValueProperty.Operation.Assignment.Number<>((Float) value);
        } else if (value instanceof Double) {
            return new ValueProperty.Operation.Assignment.Number<>((Double) value);
        } else if (value instanceof Boolean) {
            return new ValueProperty.Operation.Assignment.Boolean((Boolean) value);
        } else if (value instanceof String) {
            return new ValueProperty.Operation.Assignment.String((String) value);
        } else if (value instanceof LocalDateTime) {
            return new ValueProperty.Operation.Assignment.DateTime((LocalDateTime) value);
        } else {
            throw new IllegalArgumentException("Unrecognised Value Assignment: " + ctx.getText());
        }
    }

    @Override
    public ValueProperty.Operation<?> visitComparison(GraqlParser.ComparisonContext ctx) {
        String comparatorStr;
        Object value;

        if (ctx.comparator() != null) {
            comparatorStr = ctx.comparator().getText();
        } else if (ctx.CONTAINS() != null) {
            comparatorStr = ctx.CONTAINS().getText();
        } else if (ctx.LIKE() != null) {
            comparatorStr = ctx.LIKE().getText();
        } else {
            throw new IllegalArgumentException("Unrecognised Value Comparison: " + ctx.getText());
        }

        Graql.Token.Comparator comparator = Graql.Token.Comparator.of(comparatorStr);
        if (comparator == null) {
            throw new IllegalArgumentException("Unrecognised Value Comparator: " + comparatorStr);
        }

        if (ctx.comparable() != null) {
            if (ctx.comparable().value() != null) {
                value = visitValue(ctx.comparable().value());
            } else if (ctx.comparable().VAR_() != null) {
                value = new Statement(getVar(ctx.comparable().VAR_()));
            } else {
                throw new IllegalArgumentException("Unrecognised Comparable value: " + ctx.comparable().getText());
            }
        } else if (ctx.containable() != null) {
            if (ctx.containable().STRING_() != null) {
                value = getString(ctx.containable().STRING_());
            } else if (ctx.containable().VAR_() != null) {
                value = new Statement(getVar(ctx.containable().VAR_()));
            } else {
                throw new IllegalArgumentException("Unrecognised Containable value: " + ctx.containable().getText());
            }
        } else if (ctx.regex() != null) {
            value = visitRegex(ctx.regex());
        } else {
            throw new IllegalArgumentException("Unrecognised Value Comparison: " + ctx.getText());
        }

        if (value instanceof Integer) {
            return new ValueProperty.Operation.Comparison.Number<>(comparator, ((Integer) value));
        } else if (value instanceof Long) {
            return new ValueProperty.Operation.Comparison.Number<>(comparator, (Long) value);
        } else if (value instanceof Float) {
            return new ValueProperty.Operation.Comparison.Number<>(comparator, (Float) value);
        } else if (value instanceof Double) {
            return new ValueProperty.Operation.Comparison.Number<>(comparator, (Double) value);
        } else if (value instanceof Boolean) {
            return new ValueProperty.Operation.Comparison.Boolean(comparator, (Boolean) value);
        } else if (value instanceof String) {
            return new ValueProperty.Operation.Comparison.String(comparator, (String) value);
        } else if (value instanceof LocalDateTime) {
            return new ValueProperty.Operation.Comparison.DateTime(comparator, (LocalDateTime) value);
        } else if (value instanceof Statement) {
            return new ValueProperty.Operation.Comparison.Variable(comparator, (Statement) value);
        } else {
            throw new IllegalArgumentException("Unrecognised Value Comparison: " + ctx.getText());
        }
    }

    // LITERAL INPUT VALUES ====================================================

    @Override
    public String visitRegex(GraqlParser.RegexContext ctx) {
        return unescapeRegex(unquoteString(ctx.STRING_()));
    }

    @Override
    public Graql.Token.ValueType visitValue_type(GraqlParser.Value_typeContext valueClass) {
        if (valueClass.BOOLEAN() != null) {
            return Graql.Token.ValueType.BOOLEAN;
        } else if (valueClass.DATETIME() != null) {
            return Graql.Token.ValueType.DATETIME;
        } else if (valueClass.DOUBLE() != null) {
            return Graql.Token.ValueType.DOUBLE;
        } else if (valueClass.LONG() != null) {
            return Graql.Token.ValueType.LONG;
        } else if (valueClass.STRING() != null) {
            return Graql.Token.ValueType.STRING;
        } else {
            throw new IllegalArgumentException("Unrecognised Value Class: " + valueClass);
        }
    }

    @Override
    public Object visitValue(GraqlParser.ValueContext ctx) {
        if (ctx.STRING_() != null) {
            return getString(ctx.STRING_());

        } else if (ctx.INTEGER_() != null) {
            return getInteger(ctx.INTEGER_());

        } else if (ctx.REAL_() != null) {
            return getReal(ctx.REAL_());

        } else if (ctx.BOOLEAN_() != null) {
            return getBoolean(ctx.BOOLEAN_());

        } else if (ctx.DATE_() != null) {
            return getDate(ctx.DATE_());

        } else if (ctx.DATETIME_() != null) {
            return getDateTime(ctx.DATETIME_());

        } else {
            throw new IllegalArgumentException("Unrecognised Literal token: " + ctx.getText());
        }
    }

    private String getString(TerminalNode string) {
        // Remove surrounding quotes
        return unquoteString(string);
    }

    private String unquoteString(TerminalNode string) {
        return string.getText().substring(1, string.getText().length() - 1);
    }

    private long getInteger(TerminalNode number) {
        return Long.parseLong(number.getText());
    }

    private double getReal(TerminalNode real) {
        return Double.valueOf(real.getText());
    }

    private boolean getBoolean(TerminalNode bool) {
        Graql.Token.Literal literal = Graql.Token.Literal.of(bool.getText());

        if (literal != null && literal.equals(Graql.Token.Literal.TRUE)) {
            return true;

        } else if (literal != null && literal.equals(Graql.Token.Literal.FALSE)) {
            return false;

        } else {
            throw new IllegalArgumentException("Unrecognised Boolean token: " + bool.getText());
        }
    }

    private LocalDateTime getDate(TerminalNode date) {
        return LocalDate.parse(date.getText(),
                DateTimeFormatter.ISO_LOCAL_DATE).atStartOfDay();
    }

    private LocalDateTime getDateTime(TerminalNode dateTime) {
        return LocalDateTime.parse(dateTime.getText(), DateTimeFormatter.ISO_LOCAL_DATE_TIME);
    }
}