/*
 * Copyright (c) 1998, 2018 Oracle and/or its affiliates. All rights reserved.
 *
 * This program and the accompanying materials are made available under the
 * terms of the Eclipse Public License v. 2.0 which is available at
 * http://www.eclipse.org/legal/epl-2.0,
 * or the Eclipse Distribution License v. 1.0 which is available at
 * http://www.eclipse.org/org/documents/edl-v10.php.
 *
 * SPDX-License-Identifier: EPL-2.0 OR BSD-3-Clause
 */

// Contributors:
//     tware - initial implementation
package org.eclipse.persistence.internal.expressions;

import java.io.BufferedWriter;
import java.io.IOException;
import java.util.ArrayList;
import java.util.List;
import java.util.Vector;

import org.eclipse.persistence.descriptors.ClassDescriptor;
import org.eclipse.persistence.exceptions.QueryException;
import org.eclipse.persistence.expressions.Expression;
import org.eclipse.persistence.internal.helper.DatabaseField;
import org.eclipse.persistence.internal.helper.DatabaseTable;
import org.eclipse.persistence.internal.queries.ContainerPolicy;
import org.eclipse.persistence.internal.queries.InterfaceContainerPolicy;
import org.eclipse.persistence.mappings.CollectionMapping;
import org.eclipse.persistence.mappings.DatabaseMapping;
import org.eclipse.persistence.mappings.querykeys.ForeignReferenceQueryKey;
import org.eclipse.persistence.mappings.querykeys.QueryKey;
import org.eclipse.persistence.queries.ReadQuery;

public class MapEntryExpression extends QueryKeyExpression {

    protected boolean returnMapEntry = false;

    public MapEntryExpression(Expression base) {
        this();
        this.baseExpression = base;
    }

    public MapEntryExpression() {
        this.shouldQueryToManyRelationship = true;
        this.hasQueryKey = true;
        this.hasMapping = false;
    }

    /**
     * INTERNAL:
     * Find the alias for a given table.  A TableEntry is a place holder and its base expression holds
     * all the relevant information.  Get the alias from the baseExpression
     */
    @Override
    public DatabaseTable aliasForTable(DatabaseTable table) {
        return ((DataExpression)getBaseExpression()).aliasForTable(table);
    }

    /**
     * Set this expression to represent a Map.Entry rather than the Map's key
     */
    public void returnMapEntry(){
        returnMapEntry = true;
    }

    /**
     * INTERNAL:
     * This expression is built on a different base than the one we want. Rebuild it and
     * return the root of the new tree
     */
    @Override
    public Expression rebuildOn(Expression newBase) {
        Expression newLocalBase = getBaseExpression().rebuildOn(newBase);
        Expression result = null;

        if (returnMapEntry){
            result = newLocalBase.mapEntry();
        } else {
            result = newLocalBase.mapKey();
        }

        result.setSelectIfOrderedBy(selectIfOrderedBy());
        return result;
    }

    /**
     * INTERNAL:
     * A special version of rebuildOn where the newBase need not be a new
     * ExpressionBuilder but any expression.
     * <p>
     * For nested joined attributes, the joined attribute query must have
     * its joined attributes rebuilt relative to it.
     */
    @Override
    public Expression rebuildOn(Expression oldBase, Expression newBase) {
        if (this == oldBase) {
            return newBase;
        }
        Expression newLocalBase = ((QueryKeyExpression)getBaseExpression()).rebuildOn(oldBase, newBase);
        Expression result = null;

        if (returnMapEntry){
            result = newLocalBase.mapEntry();
        } else {
            result = newLocalBase.mapKey();
        }
        result.setSelectIfOrderedBy(selectIfOrderedBy());
        return result;
    }

    /**
     * INTERNAL:
     * Used for debug printing.
     */
    @Override
    public String descriptionOfNodeType() {
        if (returnMapEntry){
            return "MapEntry";
        } else {
            return "MapKey";
        }
    }

    /**
     * INTERNAL:
     */
    @Override
    public Expression existingDerivedTable(DatabaseTable table) {
        if (baseExpression.isDataExpression()){
            return ((DataExpression)baseExpression).existingDerivedTable(table);
        }
        return super.existingDerivedTable(table);
    }

    /**
     * Exclude any tables defined by base.
     */
    @Override
    public List<DatabaseTable> getOwnedTables() {
        return null;
    }

    @Override
    public ClassDescriptor getDescriptor() {
        if (isAttribute()) {
            return null;
        }
        if (descriptor == null) {
            // Look first for query keys, then mappings. Ultimately we should have query keys
            // for everything and can dispense with the mapping part.
            ForeignReferenceQueryKey queryKey = (ForeignReferenceQueryKey)getQueryKeyOrNull();
            if (queryKey != null) {
                descriptor = getSession().getDescriptor(queryKey.getReferenceClass());
                return descriptor;
            }
            if (getMapping() == null) {
                throw QueryException.invalidQueryKeyInExpression(this);
            }

            // We assume this is either a foreign reference or an aggregate mapping
            descriptor = getMapping().getContainerPolicy().getDescriptorForMapKey();
            if (getMapping().isVariableOneToOneMapping()) {
                throw QueryException.cannotQueryAcrossAVariableOneToOneMapping(getMapping(), descriptor);
            }
        }
        return descriptor;
    }

    /**
     * INTERNAL:
     */
    @Override
    public DatabaseField getField() {
        if (!isAttribute()) {
            return null;
        }
        DatabaseField field = getInterfaceContainerPolicy().getDirectKeyField(getMapping());
        return field;
    }

    /**
     * INTERNAL:
     * Return all the fields
     */
    @Override
    public Vector getFields() {
        Vector result = new Vector();
        InterfaceContainerPolicy icp = getInterfaceContainerPolicy();
        // if this is a map entry get all the fields for both the key and the value
        if (returnMapEntry || !icp.isMappedKeyMapPolicy()){
            result.addAll(getBaseExpression().getFields());
        } else if (isAttribute()) {
            DatabaseField field = getField();
            if (field != null) {
                result.add(field);
            }
        } else {
            result.addAll(getInterfaceContainerPolicy().getAdditionalFieldsForJoin(getMapping()));
        }
        return result;
    }

    /**
     * INTERNAL:
     */
    @Override
    public List<DatabaseField> getSelectionFields(ReadQuery query) {
        ArrayList<DatabaseField> result = new ArrayList<DatabaseField>();
        InterfaceContainerPolicy icp = getInterfaceContainerPolicy();
        // if this is a map entry get all the fields for both the key and the value
        if (returnMapEntry || !icp.isMappedKeyMapPolicy()){
            result.addAll(getBaseExpression().getSelectionFields(query));
        } else if (isAttribute()) {
            DatabaseField field = getField();
            if (field != null) {
                result.add(field);
            }
        } else {
            result.addAll(getInterfaceContainerPolicy().getAdditionalFieldsForJoin(getMapping()));
        }
        return result;
    }

    @Override
    public CollectionMapping getMapping() {
        return (CollectionMapping)((QueryKeyExpression)getBaseExpression()).getMapping();
    }

    @Override
    public QueryKey getQueryKeyOrNull() {
        if (!hasQueryKey) {
            return null;
        }

        InterfaceContainerPolicy cp = getInterfaceContainerPolicy();
        if (queryKey == null) {
            if (returnMapEntry){
                return null;
            } else {
                queryKey = cp.createQueryKeyForMapKey();
            }
        }
        return queryKey;

    }

    /**
     * INTERNAL:
     * Return if the expression is for a direct mapped attribute.
     */
    @Override
    public boolean isAttribute() {
        if (isAttributeExpression == null) {
            if (returnMapEntry){
                return false;
            }
            InterfaceContainerPolicy containerPolicy = getInterfaceContainerPolicy();
            return containerPolicy.isMapKeyAttribute();

        }
        return isAttributeExpression.booleanValue();
    }

    /**
     * INTERNAL:
     */
    @Override
    public boolean isMapEntryExpression(){
        return true;
    }

    private InterfaceContainerPolicy getInterfaceContainerPolicy(){
        DatabaseMapping mapping = getMapping();
        if (mapping == null) {
            throw QueryException.noMappingForMapEntryExpression(getBaseExpression());
        }

        if (!mapping.isCollectionMapping()){
            throw QueryException.mapEntryExpressionForNonCollection(getBaseExpression(), getMapping());
        }
        InterfaceContainerPolicy cp = null;
        try{
            cp = (InterfaceContainerPolicy)getMapping().getContainerPolicy();
        } catch (ClassCastException e){
            throw QueryException.mapEntryExpressionForNonMap(getBaseExpression(), getMapping());
        }
        return cp;
    }

    /**
     * INTERNAL:
     * Mapping criteria will be provided by the base expression
     */
    @Override
    public Expression mappingCriteria(Expression base) {
        return null;
    }

    public boolean shouldReturnMapEntry(){
        return returnMapEntry;
    }


    /**
     * Do any required validation for this node. Throw an exception if it's incorrect.
     */
    @Override
    public void validateNode() {
        if ((getQueryKeyOrNull() == null) && (getMapping() == null)) {
            throw QueryException.invalidQueryKeyInExpression(getName());
        }
        if (!getMapping().isCollectionMapping()) {
            throw QueryException.mapEntryExpressionForNonCollection(getBaseExpression(), getMapping());
        }
        ContainerPolicy cp = getMapping().getContainerPolicy();
        if ((cp == null) || !cp.isMapPolicy()) {
            throw QueryException.mapEntryExpressionForNonMap(getBaseExpression(), getMapping());
        }
    }

    /**
     * INTERNAL:
     * Used to print a debug form of the expression tree.
     */
    @Override
    public void writeDescriptionOn(BufferedWriter writer) throws IOException {
        writer.write(descriptionOfNodeType());
    }

}

