/**
 * Copyright 2012-2015 Niall Gallagher
 *
 * Licensed 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.googlecode.cqengine.query.parser.sql.support;

import com.googlecode.cqengine.attribute.Attribute;
import com.googlecode.cqengine.query.Query;
import com.googlecode.cqengine.query.QueryFactory;
import com.googlecode.cqengine.query.logical.And;
import com.googlecode.cqengine.query.logical.Or;
import com.googlecode.cqengine.query.option.AttributeOrder;
import com.googlecode.cqengine.query.option.OrderByOption;
import com.googlecode.cqengine.query.option.QueryOptions;
import com.googlecode.cqengine.query.parser.common.QueryParser;
import com.googlecode.cqengine.query.parser.sql.grammar.SQLGrammarBaseListener;
import com.googlecode.cqengine.query.parser.sql.grammar.SQLGrammarParser;
import org.antlr.v4.runtime.ParserRuleContext;
import org.antlr.v4.runtime.tree.ParseTree;

import java.util.*;

import static com.googlecode.cqengine.query.parser.common.ParserUtils.*;

/**
 * @author Niall Gallagher
 */
public class SQLAntlrListener<O> extends SQLGrammarBaseListener {

    /*
        NOTE: this class depends on classes auto-generated by the antlr4-maven-plugin.
        Run "mvn clean compile" to generate those classes.
    */

    protected final QueryParser<O> queryParser;
    // A map of parent context, to parsed child queries belonging to that context...
    protected final Map<ParserRuleContext, Collection<Query<O>>> childQueries = new HashMap<ParserRuleContext, Collection<Query<O>>>();
    // The parsed orderBy clause, if found...
    protected OrderByOption<O> orderByOption = null;
    protected int numQueriesEncountered = 0;
    protected int numQueriesParsed = 0;

    public SQLAntlrListener(QueryParser<O> queryParser) {
        this.queryParser = queryParser;
    }

    // ======== Handler methods for each type of query defined in the antlr grammar... ========

    @Override
    public void exitAndQuery(SQLGrammarParser.AndQueryContext ctx) {
        addParsedQuery(ctx, new And<O>(childQueries.get(ctx)));
    }

    @Override
    public void exitOrQuery(SQLGrammarParser.OrQueryContext ctx) {
        addParsedQuery(ctx, new Or<O>(childQueries.get(ctx)));
    }

    @Override
    public void exitNotQuery(SQLGrammarParser.NotQueryContext ctx) {
        addParsedQuery(ctx, QueryFactory.not(childQueries.get(ctx).iterator().next()));
    }

    @Override
    public void exitEqualQuery(SQLGrammarParser.EqualQueryContext ctx) {
        Attribute<O, Object> attribute = queryParser.getAttribute(ctx.attributeName(), Object.class);
        Object value = queryParser.parseValue(attribute, ctx.queryParameter());
        addParsedQuery(ctx, QueryFactory.equal(attribute, value));
    }

    @Override
    public void exitNotEqualQuery(SQLGrammarParser.NotEqualQueryContext ctx) {
        Attribute<O, Object> attribute = queryParser.getAttribute(ctx.attributeName(), Object.class);
        Object value = queryParser.parseValue(attribute, ctx.queryParameter());
        addParsedQuery(ctx, QueryFactory.not(QueryFactory.equal(attribute, value)));
    }

    @Override
    @SuppressWarnings("unchecked")
    public void exitLessThanOrEqualToQuery(SQLGrammarParser.LessThanOrEqualToQueryContext ctx) {
        Attribute<O, Comparable> attribute = queryParser.getAttribute(ctx.attributeName(), Comparable.class);
        Comparable value = queryParser.parseValue(attribute, ctx.queryParameter());
        addParsedQuery(ctx, QueryFactory.lessThanOrEqualTo(attribute, value));
    }

    @Override
    @SuppressWarnings("unchecked")
    public void exitLessThanQuery(SQLGrammarParser.LessThanQueryContext ctx) {
        Attribute<O, Comparable> attribute = queryParser.getAttribute(ctx.attributeName(), Comparable.class);
        Comparable value = queryParser.parseValue(attribute, ctx.queryParameter());
        addParsedQuery(ctx, QueryFactory.lessThan(attribute, value));
    }

    @Override
    @SuppressWarnings("unchecked")
    public void exitGreaterThanOrEqualToQuery(SQLGrammarParser.GreaterThanOrEqualToQueryContext ctx) {
        Attribute<O, Comparable> attribute = queryParser.getAttribute(ctx.attributeName(), Comparable.class);
        Comparable value = queryParser.parseValue(attribute, ctx.queryParameter());
        addParsedQuery(ctx, QueryFactory.greaterThanOrEqualTo(attribute, value));
    }

    @Override
    @SuppressWarnings("unchecked")
    public void exitGreaterThanQuery(SQLGrammarParser.GreaterThanQueryContext ctx) {
        Attribute<O, Comparable> attribute = queryParser.getAttribute(ctx.attributeName(), Comparable.class);
        Comparable value = queryParser.parseValue(attribute, ctx.queryParameter());
        addParsedQuery(ctx, QueryFactory.greaterThan(attribute, value));
    }

    @Override
    @SuppressWarnings("unchecked")
    public void exitBetweenQuery(SQLGrammarParser.BetweenQueryContext ctx) {
        Attribute<O, Comparable> attribute = queryParser.getAttribute(ctx.attributeName(), Comparable.class);
        List<? extends ParseTree> queryParameters = ctx.queryParameter();
        Comparable lowerValue = queryParser.parseValue(attribute, queryParameters.get(0));
        Comparable upperValue = queryParser.parseValue(attribute, queryParameters.get(1));
        addParsedQuery(ctx, QueryFactory.between(attribute, lowerValue, upperValue));
    }

    @Override
    @SuppressWarnings("unchecked")
    public void exitNotBetweenQuery(SQLGrammarParser.NotBetweenQueryContext ctx) {
        Attribute<O, Comparable> attribute = queryParser.getAttribute(ctx.attributeName(), Comparable.class);
        List<? extends ParseTree> queryParameters = ctx.queryParameter();
        Comparable lowerValue = queryParser.parseValue(attribute, queryParameters.get(0));
        Comparable upperValue = queryParser.parseValue(attribute, queryParameters.get(1));
        addParsedQuery(ctx, QueryFactory.not(QueryFactory.between(attribute, lowerValue, upperValue)));
    }

    @Override
    public void exitInQuery(SQLGrammarParser.InQueryContext ctx) {
        Attribute<O, Object> attribute = queryParser.getAttribute(ctx.attributeName(), Object.class);
        List<? extends ParseTree> queryParameters = ctx.queryParameter();
        Collection<Object> values = new ArrayList<Object>(queryParameters.size());
        for (ParseTree queryParameter : queryParameters) {
            Object value = queryParser.parseValue(attribute, queryParameter);
            values.add(value);
        }
        addParsedQuery(ctx, QueryFactory.in(attribute, values));
    }

    @Override
    public void exitNotInQuery(SQLGrammarParser.NotInQueryContext ctx) {
        Attribute<O, Object> attribute = queryParser.getAttribute(ctx.attributeName(), Object.class);
        List<? extends ParseTree> queryParameters = ctx.queryParameter();
        Collection<Object> values = new ArrayList<Object>(queryParameters.size());
        for (ParseTree queryParameter : queryParameters) {
            Object value = queryParser.parseValue(attribute, queryParameter);
            values.add(value);
        }
        addParsedQuery(ctx, QueryFactory.not(QueryFactory.in(attribute, values)));
    }

    @Override
    public void exitStartsWithQuery(SQLGrammarParser.StartsWithQueryContext ctx) {
        Attribute<O, String> attribute = queryParser.getAttribute(ctx.attributeName(), String.class);
        String value = queryParser.parseValue(attribute, ctx.queryParameterTrailingPercent());
        value = value.substring(0, value.length() - 1);
        addParsedQuery(ctx, QueryFactory.startsWith(attribute, value));
    }
    
    @Override
    public void exitIsPrefixOfQuery(SQLGrammarParser.IsPrefixOfQueryContext ctx) {
        Attribute<O, String> attribute = queryParser.getAttribute(ctx.attributeName(), String.class);
        String value = queryParser.parseValue(attribute, ctx.queryParameter());
        addParsedQuery(ctx, QueryFactory.isPrefixOf(attribute, value));
    }

    @Override
    public void exitEndsWithQuery(SQLGrammarParser.EndsWithQueryContext ctx) {
        Attribute<O, String> attribute = queryParser.getAttribute(ctx.attributeName(), String.class);
        String value = queryParser.parseValue(attribute, ctx.queryParameterLeadingPercent());
        value = value.substring(1, value.length());
        addParsedQuery(ctx, QueryFactory.endsWith(attribute, value));
    }

    @Override
    public void exitContainsQuery(SQLGrammarParser.ContainsQueryContext ctx) {
        Attribute<O, String> attribute = queryParser.getAttribute(ctx.attributeName(), String.class);
        String value = queryParser.parseValue(attribute, ctx.queryParameterLeadingAndTrailingPercent());
        value = value.substring(1, value.length() - 1);
        addParsedQuery(ctx, QueryFactory.contains(attribute, value));
    }

    @Override
    public void exitHasQuery(SQLGrammarParser.HasQueryContext ctx) {
        Attribute<O, Object> attribute = queryParser.getAttribute(ctx.attributeName(), Object.class);
        addParsedQuery(ctx, QueryFactory.has(attribute));
    }

    @Override
    public void exitNotHasQuery(SQLGrammarParser.NotHasQueryContext ctx) {
        Attribute<O, Object> attribute = queryParser.getAttribute(ctx.attributeName(), Object.class);
        addParsedQuery(ctx, QueryFactory.not(QueryFactory.has(attribute)));
    }

    /** This handler is called for all queries, allows us to validate that no handlers are missing. */
    @Override
    public void exitQuery(SQLGrammarParser.QueryContext ctx) {
        numQueriesEncountered++;
        validateAllQueriesParsed(numQueriesEncountered, numQueriesParsed);
    }

    @Override
    public void exitOrderByClause(SQLGrammarParser.OrderByClauseContext ctx) {
        List<AttributeOrder<O>> attributeOrders = new ArrayList<AttributeOrder<O>>();
        for (SQLGrammarParser.AttributeOrderContext orderContext : ctx.attributeOrder()) {
            Attribute<O, Comparable> attribute = queryParser.getAttribute(orderContext.attributeName(), Comparable.class);
            boolean descending = orderContext.direction() != null && orderContext.direction().K_DESC() != null;
            attributeOrders.add(new AttributeOrder<O>(attribute, descending));
        }
        this.orderByOption = QueryFactory.orderBy(attributeOrders);
    }

    // ======== Utility methods... ========

    /**
     * Adds the given query to a list of child queries which have not yet been wrapped in a parent query.
     */
    void addParsedQuery(ParserRuleContext currentContext, Query<O> parsedQuery) {
        // Retrieve the possibly null parent query...
        ParserRuleContext parentContext = getParentContextOfType(currentContext, getAndOrNotContextClasses());
        Collection<Query<O>> childrenOfParent = this.childQueries.get(parentContext);
        if (childrenOfParent == null) {
            childrenOfParent = new ArrayList<Query<O>>();
            this.childQueries.put(parentContext, childrenOfParent); // parentContext will be null if this is root query
        }
        childrenOfParent.add(parsedQuery);
        numQueriesParsed++;
    }

    /**
     * Can be called when parsing has finished, to retrieve the parsed query.
     */
    public Query<O> getParsedQuery() {
        Collection<Query<O>> rootQuery = childQueries.get(null);
        if (rootQuery == null) {
            // There was no WHERE clause...
            return QueryFactory.all(this.queryParser.getObjectType());
        }
        validateExpectedNumberOfChildQueries(1, rootQuery.size());
        return rootQuery.iterator().next();
    }

    /**
     * Can be called when parsing has finished, to retrieve the {@link QueryOptions}, which may include an
     * {@link OrderByOption} if found in the string query.
     *
     * @return The parsed {@link QueryOptions}
     */
    public QueryOptions getQueryOptions() {
        OrderByOption<O> orderByOption = this.orderByOption;
        return orderByOption != null ? QueryFactory.queryOptions(orderByOption) : QueryFactory.noQueryOptions();
    }

    protected Class[] getAndOrNotContextClasses() {
        return new Class[] {SQLGrammarParser.AndQueryContext.class, SQLGrammarParser.OrQueryContext.class, SQLGrammarParser.NotQueryContext.class};
    }

}