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

import org.apache.openjpa.jdbc.meta.ClassMapping;
import org.apache.openjpa.jdbc.meta.Discriminator;
import org.apache.openjpa.jdbc.meta.strats.NoneDiscriminatorStrategy;
import org.apache.openjpa.jdbc.meta.strats.VerticalClassStrategy;
import org.apache.openjpa.jdbc.sql.DBDictionary;
import org.apache.openjpa.jdbc.sql.Raw;
import org.apache.openjpa.kernel.exps.AggregateListener;
import org.apache.openjpa.kernel.exps.Arguments;
import org.apache.openjpa.kernel.exps.Expression;
import org.apache.openjpa.kernel.exps.ExpressionFactory;
import org.apache.openjpa.kernel.exps.FilterListener;
import org.apache.openjpa.kernel.exps.Literal;
import org.apache.openjpa.kernel.exps.Parameter;
import org.apache.openjpa.kernel.exps.Path;
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.meta.ClassMetaData;
import org.apache.openjpa.util.UserException;

/**
 * Expression factory implementation that can be used to execute queries
 * via SQL.
 *
 * @author Abe White
 */
public class JDBCExpressionFactory
    implements ExpressionFactory, Serializable {

    private static final Val NULL = new Null();

    private static final Localizer _loc = Localizer.forPackage(JDBCExpressionFactory.class);

    private final ClassMapping _type;
    private final SelectConstructor _cons = new SelectConstructor();
    private int _getMapValueAlias = 0;
    
    private boolean _isBooleanLiteralAsNumeric = true;

    /**
     * Constructor. Supply the type we're querying against.
     */
    public JDBCExpressionFactory(ClassMapping type) {
        _type = type;
    }

    public void setBooleanLiteralAsNumeric(boolean isBooleanLiteralAsNumeric) {
        _isBooleanLiteralAsNumeric = isBooleanLiteralAsNumeric;
    }
    
    /**
     * Use to create SQL select.
     */
    public SelectConstructor getSelectConstructor() {
        return _cons;
    }

    public Expression emptyExpression() {
        return new EmptyExpression();
    }

    public Expression asExpression(Value v) {
        return equal(v, newLiteral(Boolean.TRUE, Literal.TYPE_BOOLEAN));
    }

    public Expression equal(Value v1, Value v2) {
        // if we're comparing an unaccessed bound variable, like in:
        // coll.contains (var) && var == x, then translate into:
        // coll.contains (x)
        if (v1 instanceof PCPath && ((PCPath) v1).isUnaccessedVariable())
            return contains(v1, v2);
        if (v2 instanceof PCPath && ((PCPath) v2).isUnaccessedVariable())
            return contains(v2, v1);
        if (v1 instanceof Type || v2 instanceof Type) {
            Value val = v1 instanceof Type ? v1 : v2;
            verifyTypeOperation(val, null, false);
            return new EqualTypeExpression((Val) v1, (Val) v2);
        }
        return new EqualExpression((Val) v1, (Val) v2);
    }

    private void verifyTypeOperation(Value val, Value param, boolean isNotEqual) {
        if (val.getPath() == null)
            return;
        PCPath path = (PCPath) val.getPath();
        Discriminator disc = ((Type) val).getDiscriminator();
        if (disc == null || !(val.getMetaData().getPCSuperclass() != null ||
            val.getMetaData().getPCSubclasses().length > 0))
            throw new UserException(_loc.
                get("invalid-type-argument", path.last() != null ? path.getPCPathString() : path.getSchemaAlias()));
        
        if (disc.getColumns().length == 0) {
            if (disc.getStrategy() instanceof NoneDiscriminatorStrategy) {
                // limited support for table per class inheritance hierarchy
                if (path.last() != null)
                    throw new UserException(_loc.
                        get("type-argument-unsupported", path.last().getName())); 
                if (isNotEqual) {
                    if (param != null && param instanceof Null)
                        throw new UserException(_loc.
                            get("type-in-expression-unsupported", path.getSchemaAlias()));
                    else
                        throw new UserException(_loc.
                            get("type-not-equal-unsupported", path.getSchemaAlias()));
                }
            }
            if (param != null && param instanceof CollectionParam)
                throw new UserException(_loc.
                    get("collection-param-unsupported")); 
        }
    }

    public Expression notEqual(Value v1, Value v2) {
        if (v1 instanceof Type || v2 instanceof Type) {
            Value val = v1 instanceof Type ? v1 : v2;
            Value param = val == v1 ? (v2 instanceof Null ? v2 : null) : (v1 instanceof Null ? v1 : null);
            verifyTypeOperation(val, param, true);
            return new NotEqualTypeExpression((Val) v1, (Val) v2);
        }
        return new NotEqualExpression((Val) v1, (Val) v2);
    }

    public Expression lessThan(Value v1, Value v2) {
        return new CompareExpression((Val) v1, (Val) v2,
            CompareExpression.LESS);
    }

    public Expression greaterThan(Value v1, Value v2) {
        return new CompareExpression((Val) v1, (Val) v2,
            CompareExpression.GREATER);
    }

    public Expression lessThanEqual(Value v1, Value v2) {
        return new CompareExpression((Val) v1, (Val) v2,
            CompareExpression.LESS_EQUAL);
    }

    public Expression greaterThanEqual(Value v1, Value v2) {
        return new CompareExpression((Val) v1, (Val) v2,
            CompareExpression.GREATER_EQUAL);
    }

    public Expression isEmpty(Value val) {
        return new IsEmptyExpression((Val) val);
    }

    public Expression isNotEmpty(Value val) {
        return new IsNotEmptyExpression((Val) val);
    }

    public Expression contains(Value map, Value arg) {
        if (map instanceof Const) {
            if (arg instanceof Type) {
                // limited support for table per class inheritance
                verifyTypeOperation(arg, map, false);
                if (((ClassMapping) arg.getMetaData()).getDiscriminator().getColumns().length == 0)
                    return new EqualTypeExpression((Val) arg, (Val) map);
            }

            return new InExpression((Val) arg, (Const) map);
        }
        if (map instanceof SubQ)
            return new InSubQExpression((Val) arg, (SubQ) map);
        return new ContainsExpression((Val) map, (Val) arg);
    }

    public Expression containsKey(Value map, Value arg) {
        if (map instanceof Const)
            return new InKeyExpression((Val) arg, (Const) map);
        return new ContainsKeyExpression((Val) map, (Val) arg);
    }

    public Expression containsValue(Value map, Value arg) {
        if (map instanceof Const)
            return new InValueExpression((Val) arg, (Const) map);
        return new ContainsExpression((Val) map, (Val) arg);
    }

    public Expression isInstance(Value val, Class c) {
        if (val instanceof Const)
            return new ConstInstanceofExpression((Const) val, c);
        return new InstanceofExpression((PCPath) val, c);
    }

    public Expression and(Expression exp1, Expression exp2) {
        if (exp1 instanceof BindVariableExpression)
            return new BindVariableAndExpression((BindVariableExpression) exp1,
                (Exp) exp2);
        if (exp2 instanceof BindVariableExpression)
            return new BindVariableAndExpression((BindVariableExpression) exp2,
                (Exp) exp1);
        return new AndExpression((Exp) exp1, (Exp) exp2);
    }

    public Expression or(Expression exp1, Expression exp2) {
        return new OrExpression((Exp) exp1, (Exp) exp2);
    }

    public Expression not(Expression exp) {
        if (!(exp instanceof IsNotEmptyExpression) && 
            !(exp instanceof InSubQExpression) &&
            HasContainsExpressionVisitor.hasContains(exp))
            return new NotContainsExpression((Exp) exp);
        return new NotExpression((Exp) exp);
    }

    public Expression bindVariable(Value var, Value val) {
        // handle the strange case of using a constant path to bind a
        // variable; in these cases the variable acts like an unbound
        // variable that we limit by using an IN clause on the constant
        // value collection
        if (val instanceof Const) {
            PCPath path = new PCPath(_type, (Variable) var);
            path.setMetaData(var.getMetaData());
            return new InExpression(path, (Const) val);
        }
        return new BindVariableExpression((Variable) var, (PCPath) val, false);
    }

    public Expression bindKeyVariable(Value var, Value val) {
        // handle the strange case of using a constant path to bind a
        // variable; in these cases the variable acts like an unbound
        // variable that we limit by using an IN clause on the constant
        // value collection
        if (val instanceof Const) {
            PCPath path = new PCPath(_type, (Variable) var);
            path.setMetaData(var.getMetaData());
            return new InKeyExpression(path, (Const) val);
        }
        return new BindVariableExpression((Variable) var, (PCPath) val, true);
    }

    public Expression bindValueVariable(Value var, Value val) {
        return bindVariable(var, val);
    }

    public Expression startsWith(Value v1, Value v2) {
        return new StartsWithExpression((Val) v1, (Val) v2);
    }

    public Expression endsWith(Value v1, Value v2) {
        return new EndsWithExpression((Val) v1, (Val) v2);
    }

    public Expression notMatches(Value v1, Value v2,
        String single, String multi, String esc) {
        return not(matches(v1, v2, single, multi, esc));
    }

    public Expression matches(Value v1, Value v2,
        String single, String multi, String esc) {
        if (!(v2 instanceof Const))
            throw new UserException(_loc.get("const-only", "matches"));
        if (esc == null && _type.getMappingRepository().
                getDBDictionary().requiresSearchStringEscapeForLike == true) { 
            esc = _type.getMappingRepository().
                getDBDictionary().searchStringEscape;
        }        
        return new MatchesExpression((Val) v1, (Const) v2, single, multi, esc);
    }

    public Subquery newSubquery(ClassMetaData candidate, boolean subs,
        String alias) {
        DBDictionary dict = _type.getMappingRepository().getDBDictionary();
        dict.assertSupport(dict.supportsSubselect, "SupportsSubselect");
        return new SubQ((ClassMapping) candidate, subs, alias);
    }

    public Path newPath() {
        return new PCPath(_type);
    }

    public Path newPath(Value val) {
        if (val instanceof Const)
            return new ConstPath((Const) val);
        if (val instanceof SubQ)
            return new PCPath((SubQ) val);
        return new PCPath(_type, (Variable) val);
    }

    public Literal newLiteral(Object val, int ptype) {
        return new Lit(val, ptype);
    }

    public Literal newTypeLiteral(Object val, int ptype) {
        return new TypeLit(val, ptype);
    }

    public Value getThis() {
        return new PCPath(_type);
    }

    public Value getNull() {
        return NULL;
    }

    public <T extends Date> Value getCurrentDate(Class<T> dateType) {
        return new CurrentDate(dateType);
    }

    public <T extends Date> Value getCurrentTime(Class<T> dateType) {
        return  new CurrentDate(dateType);
    }

    public <T extends Date> Value getCurrentTimestamp(Class<T> dateType) {
        return  new CurrentDate(dateType);
    }

    public Parameter newParameter(Object name, Class type) {
        return new Param(name, type);
    }

    public Parameter newCollectionValuedParameter(Object key, Class type) {
        return new CollectionParam(key, type);
    }

    public Value newExtension(FilterListener listener, Value target,
        Value arg) {
        return new Extension((JDBCFilterListener) listener,
            (Val) target, (Val) arg, _type);
    }

    public Value newAggregate(AggregateListener listener, Value arg) {
        return new Aggregate((JDBCAggregateListener) listener,
            (Val) arg, _type);
    }

    public Arguments newArgumentList(Value v1, Value v2) {
        return new Args((Val) v1, (Val) v2);
    }
    
    public Arguments newArgumentList(Value... vs) {
        if (vs == null)
           return new Args(null);
        Val[] vals = new Val[vs.length];
        int i = 0;
        for (Value v : vs) {
            vals[i++] = (Val)v;
        }
        return new Args(vals);
    }

    public Value newUnboundVariable(String name, Class type) {
        return new Variable(name, type);
    }

    public Value newBoundVariable(String name, Class type) {
        return newUnboundVariable(name, type);
    }

    public Value cast(Value val, Class cls) {
        val.setImplicitType(cls);
        return val;
    }

    public Value add(Value v1, Value v2) {
        return new Math((Val) v1, (Val) v2, Math.ADD);
    }

    public Value subtract(Value v1, Value v2) {
        return new Math((Val) v1, (Val) v2, Math.SUBTRACT);
    }

    public Value multiply(Value v1, Value v2) {
        return new Math((Val) v1, (Val) v2, Math.MULTIPLY);
    }

    public Value divide(Value v1, Value v2) {
        return new Math((Val) v1, (Val) v2, Math.DIVIDE);
    }

    public Value mod(Value v1, Value v2) {
        return new Math((Val) v1, (Val) v2, Math.MOD);
    }

    public Value abs(Value val) {
        return new Abs((Val) val);
    }

    public Value indexOf(Value v1, Value v2) {
        return new IndexOf((Val) v1, (Val) v2);
    }

    public Value concat(Value v1, Value v2) {
        return new Concat((Val) v1, (Val) v2);
    }

    public Value stringLength(Value str) {
        return new StringLength((Val) str);
    }

    public Value trim(Value str, Value trimChar, Boolean where) {
        return new Trim((Val) str, (Val) trimChar, where);
    }

    public Value sqrt(Value val) {
        return new Sqrt((Val) val);
    }

    public Value substring(Value v1, Value v2) {
        return new Substring((Val) v1, (Val) v2);
    }

    public Value toUpperCase(Value val) {
        return new ToUpperCase((Val) val);
    }

    public Value toLowerCase(Value val) {
        return new ToLowerCase((Val) val);
    }

    public Value avg(Value val) {
        return new Avg((Val) val);
    }

    public Value count(Value val) {
        return new Count((Val) val);
    }

    public Value distinct(Value val) {
        return new Distinct((Val) val);
    }

    public Value max(Value val) {
        return new Max((Val) val);
    }

    public Value min(Value val) {
        return new Min((Val) val);
    }

    public Value sum(Value val) {
        return new Sum((Val) val);
    }

    public Value any(Value val) {
        return new Any((Val) val);
    }

    public Value all(Value val) {
        return new All((Val) val);
    }

    public Value size(Value val) {
        return new Size((Val) val);
    }

    public Value index(Value val) {
        ((PCPath) val).verifyIndexedField();
        return new Index((Val) val);
    }

    public Value type(Value val) {
        return new Type((Val) val);
    }

    public Value mapEntry(Value key, Value val) {
        return new MapEntry((Val) key, (Val) val);
    }

    public Value mapKey(Value key, Value val) {
        return new MapKey((Val) key);
    }

    public Value getKey(Value val) {
        ((PCPath) val).getKey();
        return val;
    }

    public Value getObjectId(Value val) {
        if (val instanceof Const)
            return new ConstGetObjectId((Const) val);
        return new GetObjectId((PCPath) val);
    }

    public Value getMapValue(Value map, Value arg) {
        return new GetMapValue((Val) map, (Val) arg, 
            "gmv" + _getMapValueAlias++);
    }

    private Value getLiteralRawString(Value val) {
        if (val instanceof Lit) {
            Lit lit = (Lit) val;
            StringBuilder value = new StringBuilder();
            int pType = lit.getParseType(); 
            if (pType == Literal.TYPE_SQ_STRING ||
                pType == Literal.TYPE_STRING)
                value.append("'").append(lit.getValue().toString()).append("'");
            else if (pType == Literal.TYPE_BOOLEAN) {
                Boolean boolVal = (Boolean)lit.getValue();
                if (_isBooleanLiteralAsNumeric)
                    value.append(boolVal ? "1" : "0");
                else
                    value.append(boolVal ? "true" : "false");
            } else if (pType == Literal.TYPE_ENUM) {
                lit.setRaw(true);
                return val;
            } else
                value.append(lit.getValue().toString());
            lit.setValue(new Raw(value.toString()));
            return lit;
        }
        return val;
    }

    public Value simpleCaseExpression(Value caseOperand, Expression[] exp,
            Value val1) {
        Exp[] exps = new Exp[exp.length];
        for (int i = 0; i < exp.length; i++)
            exps[i] = (Exp) exp[i];
        val1 = getLiteralRawString(val1);
        return new SimpleCaseExpression((Val) caseOperand, exps,
            (Val) val1);
    }

    public Value generalCaseExpression(Expression[] exp,
            Value val) {
        Exp[] exps = new Exp[exp.length];
        for (int i = 0; i < exp.length; i++)
            exps[i] = (Exp) exp[i];
        val = getLiteralRawString(val);
        return new GeneralCaseExpression(exps, (Val) val);
    }

    public Expression whenCondition(Expression exp, Value val) {
        val = getLiteralRawString(val);
        return new WhenCondition((Exp) exp, (Val) val);
    }

    public Expression whenScalar(Value val1, Value val2) {
        val1 = getLiteralRawString(val1);
        val2 = getLiteralRawString(val2);
        return new WhenScalar((Val) val1, (Val) val2);
    }

    public Value coalesceExpression(Value[] vals) {;
        Object[] values = new Val[vals.length];
        for (int i = 0; i < vals.length; i++) {
            values[i] = getLiteralRawString(vals[i]);
        }
        return new CoalesceExpression((Val[]) values);
    }

    public Value nullIfExpression(Value val1, Value val2) {
        val1 = getLiteralRawString(val1);
        val2 = getLiteralRawString(val2);
        return new NullIfExpression((Val) val1, (Val) val2);
    }
    
    public Value newFunction(String functionName, Class<?> resultType, Value... args) {
        return new DatastoreFunction(functionName, resultType, newArgumentList(args));
    }
    
    public boolean isVerticalType(Value val) {
        if (!(val instanceof Type))
            return false;
        ClassMapping cm = (ClassMapping)((Type)val).getMetaData();
        String strat = cm.getMappingInfo().getHierarchyStrategy(); 
        if (strat != null && strat.equals(VerticalClassStrategy.ALIAS)) 
            return true;
        return false;
    }
}
