/*******************************************************************************
 * 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.ofbiz.entity.condition;

import java.util.Collection;
import java.util.List;
import java.util.Locale;
import java.util.Map;

import org.apache.ofbiz.base.util.Debug;
import org.apache.ofbiz.base.util.ObjectType;
import org.apache.ofbiz.base.util.UtilGenerics;
import org.apache.ofbiz.entity.Delegator;
import org.apache.ofbiz.entity.DelegatorFactory;
import org.apache.ofbiz.entity.GenericEntity;
import org.apache.ofbiz.entity.GenericEntityException;
import org.apache.ofbiz.entity.GenericModelException;
import org.apache.ofbiz.entity.config.model.Datasource;
import org.apache.ofbiz.entity.model.ModelEntity;
import org.apache.ofbiz.entity.model.ModelField;
import org.apache.ofbiz.entity.model.ModelFieldType;

/**
 * Encapsulates simple expressions used for specifying queries
 *
 */
@SuppressWarnings("serial")
public final class EntityExpr extends EntityCondition {
    public static final String module = EntityExpr.class.getName();

    private final Object lhs;
    private final EntityOperator<Object, Object, ?> operator;
    private final Object rhs;

    public <L,R,LL,RR> EntityExpr(L lhs, EntityComparisonOperator<LL,RR> operator, R rhs) {
        if (lhs == null) {
            throw new IllegalArgumentException("The field name/value cannot be null");
        }
        if (operator == null) {
            throw new IllegalArgumentException("The operator argument cannot be null");
        }

        if (rhs == null || rhs == GenericEntity.NULL_FIELD) {
            if (!EntityOperator.NOT_EQUAL.equals(operator) && !EntityOperator.EQUALS.equals(operator)) {
                throw new IllegalArgumentException("Operator must be EQUALS or NOT_EQUAL when right/rhs argument is NULL ");
            }
        }

        if (EntityOperator.BETWEEN.equals(operator)) {
            if (!(rhs instanceof Collection<?>) || (((Collection<?>) rhs).size() != 2)) {
                throw new IllegalArgumentException("BETWEEN Operator requires a Collection with 2 elements for the right/rhs argument");
            }
        }

        if (lhs instanceof String) {
            this.lhs = EntityFieldValue.makeFieldValue((String) lhs);
        } else {
            this.lhs = lhs;
        }
        this.operator = UtilGenerics.cast(operator);
        this.rhs = rhs;
    }

    public EntityExpr(EntityCondition lhs, EntityJoinOperator operator, EntityCondition rhs) {
        if (lhs == null) {
            throw new IllegalArgumentException("The left EntityCondition argument cannot be null");
        }
        if (rhs == null) {
            throw new IllegalArgumentException("The right EntityCondition argument cannot be null");
        }
        if (operator == null) {
            throw new IllegalArgumentException("The operator argument cannot be null");
        }

        this.lhs = lhs;
        this.operator = UtilGenerics.cast(operator);
        this.rhs = rhs;
    }

    public Object getLhs() {
        return lhs;
    }

    public <L,R,T> EntityOperator<L,R,T> getOperator() {
        return UtilGenerics.cast(operator);
    }

    public Object getRhs() {
        return rhs;
    }

    @Override
    public boolean isEmpty() {
        return operator.isEmpty(lhs, rhs);
    }

    @Override
    public String makeWhereString(ModelEntity modelEntity, List<EntityConditionParam> entityConditionParams, Datasource datasourceInfo) {

        this.checkRhsType(modelEntity, null);

        StringBuilder sql = new StringBuilder();
        operator.addSqlValue(sql, modelEntity, entityConditionParams, true, lhs, rhs, datasourceInfo);
        return sql.toString();
    }

    @Override
    public boolean mapMatches(Delegator delegator, Map<String, ? extends Object> map) {
        return operator.mapMatches(delegator, map, lhs, rhs);
    }

    @Override
    public void checkCondition(ModelEntity modelEntity) throws GenericModelException {
        if (lhs instanceof EntityCondition) {
            ((EntityCondition) lhs).checkCondition(modelEntity);
            ((EntityCondition) rhs).checkCondition(modelEntity);
        }
    }

    @Override
    protected void addValue(StringBuilder buffer, ModelField field, Object value, List<EntityConditionParam> params) {
        if (rhs instanceof EntityFunction.UPPER) {
            if (value instanceof String) {
                value = ((String) value).toUpperCase(Locale.getDefault());
            }
        }
        super.addValue(buffer, field, value, params);
    }

    @Override
    public EntityCondition freeze() {
        return operator.freeze(lhs, rhs);
    }

    @Override
    public void visit(EntityConditionVisitor visitor) {
        visitor.acceptEntityOperator(operator, lhs, rhs);
    }

    @Override
    public void accept(EntityConditionVisitor visitor) {
        visitor.acceptEntityExpr(this);
    }

    public void checkRhsType(ModelEntity modelEntity, Delegator delegator) {
        if (this.rhs == null || this.rhs == GenericEntity.NULL_FIELD || modelEntity == null) {
            return;
        }

        Object value = this.rhs;
        if (this.rhs instanceof EntityFunction<?>) {
            value = UtilGenerics.<EntityFunction<?>>cast(this.rhs).getOriginalValue();
        }

        if (value instanceof Collection<?>) {
            Collection<?> valueCol = UtilGenerics.cast(value);
            if (valueCol.size() > 0) {
                value = valueCol.iterator().next();
            } else {
                value = null;
            }
        }

        if (delegator == null) {
            // this will be the common case for now as the delegator isn't available where we want to do this
            // we'll cheat a little here and assume the default delegator
            delegator = DelegatorFactory.getDelegator("default");
        }

        String fieldName = null;
        ModelField curField;
        if (this.lhs instanceof EntityFieldValue) {
            EntityFieldValue efv = (EntityFieldValue) this.lhs;
            fieldName = efv.getFieldName();
            curField = efv.getModelField(modelEntity);
        } else {
            // nothing to check
            return;
        }

        if (curField == null) {
            throw new IllegalArgumentException("FieldName " + fieldName + " not found for entity: " + modelEntity.getEntityName());
        }
        ModelFieldType type = null;
        try {
            type = delegator.getEntityFieldType(modelEntity, curField.getType());
        } catch (GenericEntityException e) {
            Debug.logWarning(e, module);
        }
        if (type == null) {
            throw new IllegalArgumentException("Type " + curField.getType() + " not found for entity [" + modelEntity.getEntityName() + "]; probably because there is no datasource (helper) setup for the entity group that this entity is in: [" + delegator.getEntityGroupName(modelEntity.getEntityName()) + "]");
        }
        if (value instanceof EntityConditionSubSelect){
            ModelFieldType valueType = null;
            try {
                ModelEntity valueModelEntity= ((EntityConditionSubSelect) value).getModelEntity();
                valueType = delegator.getEntityFieldType(valueModelEntity,  valueModelEntity.getField(((EntityConditionSubSelect) value).getKeyFieldName()).getType());
            } catch (GenericEntityException e) {
                Debug.logWarning(e, module);
            }
            if (valueType == null) {
                throw new IllegalArgumentException("Type " + curField.getType() + " not found for entity [" + modelEntity.getEntityName() + "]; probably because there is no datasource (helper) setup for the entity group that this entity is in: [" + delegator.getEntityGroupName(modelEntity.getEntityName()) + "]");
            }
          // make sure the type of keyFieldName of EntityConditionSubSelect  matches the field Java type
            try {
                if (!ObjectType.instanceOf(ObjectType.loadClass(valueType.getJavaType()), type.getJavaType())) {
                    String errMsg = "Warning using ["+ value.getClass().getName() + "] and entity field [" + modelEntity.getEntityName() + "." + curField.getName() + "]. The Java type of keyFieldName : [" + valueType.getJavaType()+ "] is not compatible with the Java type of the field [" + type.getJavaType() + "]";
                    // eventually we should do this, but for now we'll do a "soft" failure: throw new IllegalArgumentException(errMsg);
                    Debug.logWarning(new Exception("Location of database type warning"), "=-=-=-=-=-=-=-=-= Database type warning in EntityExpr =-=-=-=-=-=-=-=-= " + errMsg, module);
                }
            } catch (ClassNotFoundException e) {
                String errMsg = "Warning using ["+ value.getClass().getName() + "] and entity field [" + modelEntity.getEntityName() + "." + curField.getName() + "]. The Java type of keyFieldName : [" + valueType.getJavaType()+ "] could not be found]";
                // eventually we should do this, but for now we'll do a "soft" failure: throw new IllegalArgumentException(errMsg);
                Debug.logWarning(e, "=-=-=-=-=-=-=-=-= Database type warning in EntityExpr =-=-=-=-=-=-=-=-= " + errMsg, module);
             }
        } else if (value instanceof EntityFieldValue) {
            EntityFieldValue efv = (EntityFieldValue) this.lhs;
            String rhsFieldName = efv.getFieldName();
            ModelField rhsField = efv.getModelField(modelEntity);
            if (rhsField == null) {
                throw new IllegalArgumentException("FieldName " + rhsFieldName + " not found for entity: " + modelEntity.getEntityName());
            }
            ModelFieldType rhsType = null;
            try {
                rhsType = delegator.getEntityFieldType(modelEntity, rhsField.getType());
            } catch (GenericEntityException e) {
                Debug.logWarning(e, module);
            }
            try {
                if (!ObjectType.instanceOf(ObjectType.loadClass(rhsType.getJavaType()), type.getJavaType())) {
                    String errMsg = "Warning using ["+ value.getClass().getName() + "] and entity field [" + modelEntity.getEntityName() + "." + curField.getName() + "]. The Java type [" + rhsType.getJavaType() + "] of rhsFieldName : [" + rhsFieldName + "] is not compatible with the Java type of the field [" + type.getJavaType() + "]";
                    // eventually we should do this, but for now we'll do a "soft" failure: throw new IllegalArgumentException(errMsg);
                    Debug.logWarning(new Exception("Location of database type warning"), "=-=-=-=-=-=-=-=-= Database type warning in EntityExpr =-=-=-=-=-=-=-=- " + errMsg, module);
                }
            } catch (ClassNotFoundException e) {
                String errMsg = "Warning using ["+ value.getClass().getName() + "] and entity field [" + modelEntity.getEntityName() + "." + curField.getName() + "]. The Java type [" + rhsType.getJavaType() + "] of rhsFieldName : [" + rhsFieldName + "] could not be found]";
                // eventually we should do this, but for now we'll do a "soft" failure: throw new IllegalArgumentException(errMsg);
                Debug.logWarning(e, "=-=-=-=-=-=-=-=-= Database type warning in EntityExpr =-=-=-=-=-=-=-=-= " + errMsg, module);
            }
        } else {
        // make sure the type matches the field Java type
            if (!ObjectType.instanceOf(value, type.getJavaType())) {
                String errMsg = "In entity field [" + modelEntity.getEntityName() + "." + curField.getName() + "] set the value passed in [" + value.getClass().getName() + "] is not compatible with the Java type of the field [" + type.getJavaType() + "]";
                // eventually we should do this, but for now we'll do a "soft" failure: throw new IllegalArgumentException(errMsg);
                Debug.logWarning(new Exception("Location of database type warning"), "=-=-=-=-=-=-=-=-= Database type warning in EntityExpr =-=-=-=-=-=-=-=-= " + errMsg, module);
            }
        }
    }

    @Override
    public boolean equals(Object obj) {
        if (!(obj instanceof EntityExpr)) {
            return false;
        }
        EntityExpr other = (EntityExpr) obj;
        boolean isEqual = equals(lhs, other.lhs) &&
               equals(operator, other.operator) &&
               equals(rhs, other.rhs);
        return isEqual;
    }

    @Override
    public int hashCode() {
        return hashCode(lhs) +
               hashCode(operator) +
               hashCode(rhs);
    }
}
