/*
 * 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 org.apache.openjpa.jdbc.kernel.exps;

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

import org.apache.openjpa.jdbc.kernel.JDBCStoreQuery;
import org.apache.openjpa.jdbc.meta.ClassMapping;
import org.apache.openjpa.jdbc.sql.DBDictionary;
import org.apache.openjpa.jdbc.sql.Joins;
import org.apache.openjpa.jdbc.sql.SQLBuffer;
import org.apache.openjpa.jdbc.sql.Select;
import org.apache.openjpa.kernel.exps.AbstractExpressionVisitor;
import org.apache.openjpa.kernel.exps.Constant;
import org.apache.openjpa.kernel.exps.Context;
import org.apache.openjpa.kernel.exps.Expression;
import org.apache.openjpa.kernel.exps.QueryExpressions;
import org.apache.openjpa.kernel.exps.Subquery;
import org.apache.openjpa.kernel.exps.Value;
import org.apache.openjpa.lib.util.Localizer;
import org.apache.openjpa.util.UnsupportedException;

/**
 * Turns parsed queries into selects.
 *
 * @author Abe White
 */
public class SelectConstructor
    implements Serializable {

    private boolean _extent = false;
    private Select _subselect = null;
    private static final Localizer _loc = Localizer.forPackage(SelectConstructor.class);

    /**
     * Return true if we know the select to have on criteria; to be an extent.
     * Note that even if this method returns false, {@link #evaluate} may still
     * return null if we haven't cached whether the query is an extent yet.
     */
    public boolean isExtent() {
        return _extent;
    }

    public void setSubselect(Select subselect) {
        _subselect = subselect;
    }

    /**
     * Evaluate the expression, returning a new select and filling in any
     * associated expression state. Use {@link #select} to then select the data.
     * 
     * @param ctx fill with execution context
     * @param state will be filled with expression state
     */
    public Select evaluate(ExpContext ctx, Select parent, String alias, 
        QueryExpressions exps, QueryExpressionsState state) {
        // already know that this query is equivalent to an extent?
        Select sel;
        if (_extent) {
            sel = ctx.store.getSQLFactory().newSelect();
            sel.setAutoDistinct((exps.distinct & exps.DISTINCT_AUTO) != 0);
            return sel;
        }

        // create a new select and initialize it with the joins needed for
        // the criteria of this query
        sel = newSelect(ctx, parent, alias, exps, state);
        sel.setTablePerClassMeta(ctx.tpcMeta);

        // create where clause; if there are no where conditions and
        // no ordering or projections, we return null to signify that this
        // query should be treated like an extent
        Select inner = sel.getFromSelect();
        SQLBuffer where = buildWhere((inner != null) ? inner : sel, ctx, 
            state.filter, exps.filter);
        if (where == null && exps.projections.length == 0
            && exps.ordering.length == 0
            && (sel.getJoins() == null || sel.getJoins().isEmpty())) {
            _extent = true;
            sel.setAutoDistinct((exps.distinct & exps.DISTINCT_AUTO) != 0);
            return sel;
        }

        // now set sql criteria; it goes on the inner select if present
        if (inner != null)
            inner.where(where);
        else
            sel.where(where);

        // apply grouping and having.  this does not select the grouping
        // columns, just builds the GROUP BY clauses.  we don't build the
        // ORDER BY clauses yet because if we decide to add this select
        // to a union, the ORDER BY values get aliased differently
        if (exps.having != null) {
            Exp havingExp = (Exp) exps.having;
            SQLBuffer buf = new SQLBuffer(ctx.store.getDBDictionary());
            havingExp.appendTo(sel, ctx, state.having, buf);
            sel.having(buf);
        }
        for (int i = 0; i < exps.grouping.length; i++)
            ((Val) exps.grouping[i]).groupBy(sel, ctx, state.grouping[i]);
        
        if (exps.projections.length == 1) {
            Val val = (Val) exps.projections[0];
            if (val instanceof Count && ((Count)val).isCountDistinctMultiCols()) {
                Select newSel = ctx.store.getSQLFactory().newSelect();
                newSel.select("COUNT(*)", val);
                newSel.setExpectedResultCount(1, true);
                newSel.setFromSelect(sel);
                sel.setExpectedResultCount(0, true);
                sel = newSel;
            }
        }        
        return sel;
    }

    /**
     * Return a new select with expressions initialized.
     */
    private Select newSelect(ExpContext ctx, Select parent,
        String alias, QueryExpressions exps, QueryExpressionsState state) {
        Select subselect = JDBCStoreQuery.getThreadLocalSelect(_subselect);
        Select sel = parent != null ? subselect
            : ctx.store.getSQLFactory().newSelect();
        sel.setAutoDistinct((exps.distinct & exps.DISTINCT_AUTO) != 0);
        sel.setJoinSyntax(ctx.fetch.getJoinSyntax());
        sel.setParent(parent, alias);

        Context[] qryCtx = JDBCStoreQuery.getThreadLocalContext();
        Context lctx = null;
        for (int i = 0; i < qryCtx.length; i++) {
            if (qryCtx[i].cloneFrom == exps.ctx()) {
                lctx = qryCtx[i];
                break;
            }
        }

        if (sel.ctx() == null)
            sel.setContext(lctx);

        if (parent == null && lctx.getSubselContexts() != null) {
            // this is the case subselect was created before parent got created
            List<Context> subselCtxs = lctx.getSubselContexts();
            for (Context subselCtx : subselCtxs) {
                Select subsel = (Select) subselCtx.getSelect();
                Subquery subquery = subselCtx.getSubquery();
                subsel.setParent(sel, subquery.getCandidateAlias());
            }
        }
        if (HasContainsExpressionVisitor.hasContains(exps.filter)) {
            sel.setHasSubselect(true);
        }
        initialize(sel, ctx, exps, state);

        if (!sel.getAutoDistinct()) {
            if ((exps.distinct & exps.DISTINCT_TRUE) != 0)
                sel.setDistinct(true);
            else if ((exps.distinct & exps.DISTINCT_FALSE) != 0)
                sel.setDistinct(false);
        } else if (exps.projections.length > 0) {
            if (!sel.isDistinct() && (exps.distinct & exps.DISTINCT_TRUE) != 0){
                // if the select is not distinct but the query is, force
                // the select to be distinct
                sel.setDistinct(true);
            } else if (sel.isDistinct()) {
                // when aggregating data or making a non-distinct projection
                // from a distinct select, we have to select from a tmp
                // table formed by a distinct subselect in the from clause;
                // this subselect selects the pks of the candidate (to
                // get unique candidate values) and needed field values and
                // applies the where conditions; the outer select applies
                // ordering, grouping, etc
                boolean agg = exps.isAggregate();
                boolean candidate = ProjectionExpressionVisitor.
                    hasCandidateProjections(exps.projections);
                if (agg || (candidate 
                    && (exps.distinct & exps.DISTINCT_TRUE) == 0)) {
                    DBDictionary dict = ctx.store.getDBDictionary();
                    dict.assertSupport(dict.supportsSubselect,
                        "SupportsSubselect");

                    Select inner = sel;
                    sel = ctx.store.getSQLFactory().newSelect();
                    sel.setParent(parent, alias);
                    sel.setDistinct(agg
                        && (exps.distinct & exps.DISTINCT_TRUE) != 0);
                    sel.setFromSelect(inner);

                // auto-distincting happens to get unique candidate instances
                // back; don't auto-distinct if the user isn't selecting 
                // candidate data
                } else if (!candidate 
                    && (exps.distinct & exps.DISTINCT_TRUE) == 0) 
                    sel.setDistinct(false);
            }
        }
        return sel;
    }

    /**
     * Initialize all expressions.
     */
    private void initialize(Select sel, ExpContext ctx, QueryExpressions exps, 
        QueryExpressionsState state) {
        Map contains = null;
        if (HasContainsExpressionVisitor.hasContains(exps.filter)
            || HasContainsExpressionVisitor.hasContains(exps.having))
            contains = new HashMap(7);

        // initialize filter and having expressions
        Exp filterExp = (Exp) exps.filter;
        state.filter = filterExp.initialize(sel, ctx, contains);
        Exp havingExp = (Exp) exps.having;
        if (havingExp != null)
            state.having = havingExp.initialize(sel, ctx, contains);

        // get the top-level joins and null the expression's joins
        // at the same time so they aren't included in the where/having SQL
        Joins filterJoins = state.filter.joins;
        Joins havingJoins = (state.having == null) ? null : state.having.joins;
        Joins joins = sel.and(filterJoins, havingJoins);

        // initialize result values
        if (exps.projections.length > 0) {
            state.projections = new ExpState[exps.projections.length];
            Val resultVal;
            for (int i = 0; i < exps.projections.length; i++) {
                resultVal = (Val) exps.projections[i];
                if (!ctx.store.getDBDictionary().supportsParameterInSelect && resultVal instanceof Lit) {
                    ((Lit)resultVal).setRaw(true);
                }
                // have to join through to related type for pc object 
                // projections; this ensures that we have all our joins cached
                state.projections[i] = resultVal.initialize(sel, ctx, 
                    Val.JOIN_REL | Val.FORCE_OUTER);
                if (exps.projections.length > 1 && resultVal instanceof Count) {
                    if (((Count)resultVal).isCountDistinctMultiCols())
                        throw new UnsupportedException(_loc.get("count-distinct-multi-col-only"));
                }
                joins = sel.and(joins, state.projections[i].joins);
            }
        }

        // initialize grouping
        if (exps.grouping.length > 0) {
            state.grouping = new ExpState[exps.grouping.length];
            Val groupVal;
            for (int i = 0; i < exps.grouping.length; i++) {
                groupVal = (Val) exps.grouping[i];
                // have to join through to related type for pc object groupings;
                // this ensures that we have all our joins cached
                state.grouping[i] = groupVal.initialize(sel, ctx, Val.JOIN_REL);
                joins = sel.and(joins, state.grouping[i].joins);
            }
        }

        // initialize ordering
        if (exps.ordering.length > 0) {
            state.ordering = new ExpState[exps.ordering.length];
            Val orderVal;
            for (int i = 0; i < exps.ordering.length; i++) {
                orderVal = (Val) exps.ordering[i];
                if (contains(orderVal, exps.grouping)) 
                    state.ordering[i] = orderVal.initialize(sel, ctx, Val.JOIN_REL);
                else
                    state.ordering[i] = orderVal.initialize(sel, ctx, 0);
                    
                joins = sel.and(joins, state.ordering[i].joins);
            }
        }
        sel.where(joins);
    }
    
    private boolean contains(Val orderVal, Value[] grouping) {
        for (int i = 0; i < grouping.length; i++) {
            Val groupVal = (Val) grouping[i];
            if (orderVal.equals(groupVal))
                return true;
        }
        return false;
    }

    /**
     * Create the where sql.
     */
    private SQLBuffer buildWhere(Select sel, ExpContext ctx, ExpState state, 
        Expression filter) {
        // create where buffer
        SQLBuffer where = new SQLBuffer(ctx.store.getDBDictionary());
        where.append("(");
        Exp filterExp = (Exp) filter;
        filterExp.appendTo(sel, ctx, state, where);

        if (where.sqlEquals("(") || where.sqlEquals("(1 = 1"))
            return null;
        return where.append(")");
    }

    /**
     * Select the data for this query.
     */
    public void select(Select sel, ExpContext ctx, ClassMapping mapping,
        boolean subclasses, QueryExpressions exps, QueryExpressionsState state,
        int eager) {
        Select inner = sel.getFromSelect();
        Val val;
        Joins joins = null;
        boolean isCountDistinctMultiCols = false;

        if (sel.getSubselectPath() != null)
            joins = sel.newJoins().setSubselect(sel.getSubselectPath());

        // build ordering clauses before select so that any eager join
        // ordering gets applied after query ordering
        for (int i = 0; i < exps.ordering.length; i++)
            ((Val) exps.ordering[i]).orderBy(sel, ctx, state.ordering[i],
                exps.ascending[i]);

        // if no result string set, select matching objects like normal
        if (exps.projections.length == 0 && sel.getParent() == null) {
            int subs = (subclasses) ? Select.SUBS_JOINABLE : Select.SUBS_NONE;
            sel.selectIdentifier(mapping, subs, ctx.store, ctx.fetch, eager);
        } else if (exps.projections.length == 0) {
            // subselect for objects; we really just need the primary key values
            sel.select(mapping.getPrimaryKeyColumns(), joins);
        } else {
            if (exps.projections.length == 1) {
                val = (Val) exps.projections[0];
                if (val instanceof Count && ((Count)val).isCountDistinctMultiCols()) {
                    isCountDistinctMultiCols = true;
                    if (sel.getParent() != null)
                        throw new UnsupportedException(_loc.get("count-distinct-multi-col-subselect-unsupported"));
                }
            }            

            // if we have an inner select, we need to select the candidate
            // class' pk columns to guarantee unique instances
            if (inner != null && !isCountDistinctMultiCols)
                inner.select(mapping.getPrimaryKeyColumns(), joins);

            // select each result value; no need to pass on the eager mode since
            // under projections we always use EAGER_NONE
            boolean pks = sel.getParent() != null;
            for (int i = 0; i < exps.projections.length; i++) {
                val = (Val) exps.projections[i];
                if (inner != null) {
                    if (!isCountDistinctMultiCols)
                        val.selectColumns(inner, ctx, state.projections[i], pks);
                    else
                        val.select(inner, ctx, state.projections[i], pks);
                } else
                    val.select(sel, ctx, state.projections[i], pks);
            }

            // make sure having columns are selected since it is required by 
            // some DBs.  put them last so they don't affect result processing
            if (exps.having != null && inner != null)
                ((Exp) exps.having).selectColumns(inner, ctx, state.having, 
                    true);
        }

        // select ordering columns, since it is required by some DBs.  put them
        // last so they don't affect result processing
        for (int i = 0; i < exps.ordering.length; i++) {
            val = (Val) exps.ordering[i];
            if (inner != null)
                val.selectColumns(inner, ctx, state.ordering[i], true);
            val.select(sel, ctx, state.ordering[i], true);
        }

        // add conditions limiting the projections to the proper classes; if
        // this isn't a projection or a subq then they will already be added
        if (exps.projections.length > 0 || sel.getParent() != null) {
            ctx.store.loadSubclasses(mapping);
            mapping.getDiscriminator().addClassConditions((inner != null) 
                ? inner : sel, subclasses, joins);
        }
    }

    /**
     * Used to check whether a query's result projections are on the candidate.
     */
    private static class ProjectionExpressionVisitor
        extends AbstractExpressionVisitor {

        private boolean _candidate = false;
        private int _level = 0;

        public static boolean hasCandidateProjections(Value[] projs) {
            ProjectionExpressionVisitor v = new ProjectionExpressionVisitor();
            for (int i = 0; i < projs.length; i++) {
                projs[i].acceptVisit(v);
                if (v._candidate)
                    return true;
            }
            return false;
        }

        public void enter(Value val) {
            if (!_candidate) {
                _candidate = (_level == 0 && val instanceof Constant)
                    || (val instanceof PCPath 
                    && !((PCPath) val).isVariablePath());
            }
            _level++;
        }

        public void exit(Value val) {
            _level--;
        }
    }
}
