/*
 * 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:
//     Oracle - initial API and implementation from Oracle TopLink
//     02/08/2012-2.4 Guy Pelletier
//       - 350487: JPA 2.1 Specification defined support for Stored Procedure Calls
package org.eclipse.persistence.queries;

import java.security.AccessController;
import java.security.PrivilegedActionException;
import java.util.HashMap;
import java.util.Iterator;
import java.util.Map;

import org.eclipse.persistence.exceptions.QueryException;
import org.eclipse.persistence.internal.helper.DatabaseField;
import org.eclipse.persistence.internal.localization.ExceptionLocalization;
import org.eclipse.persistence.internal.security.PrivilegedAccessHelper;
import org.eclipse.persistence.internal.security.PrivilegedClassForName;
import org.eclipse.persistence.internal.sessions.AbstractRecord;
import org.eclipse.persistence.mappings.DatabaseMapping;
import org.eclipse.persistence.descriptors.ClassDescriptor;
import org.eclipse.persistence.exceptions.ValidationException;
import org.eclipse.persistence.mappings.OneToOneMapping;
import org.eclipse.persistence.sessions.DatabaseRecord;

/**
 * <p><b>Purpose</b>:
 * Concrete class to represent the EntityResult structure as defined by
 * the EJB 3.0 Persistence specification.  This class is a subcomponent of the
 * SQLResultSetMapping
 *
 * @see SQLResultSetMapping
 * @author Gordon Yorke
 * @since TopLink Java Essentials
 */
public class EntityResult extends SQLResult {
    /** Stores the class name of result  */
    protected String entityClassName;
    protected transient Class entityClass;

    /** Stores the list of FieldResult */
    protected Map fieldResults;

    /** Stores the column that will contain the value to determine the correct subclass
     * to create if applicable.
     */
    protected DatabaseField discriminatorColumn;

    public EntityResult(Class entityClass){
        this.entityClass = entityClass;
        if (this.entityClass == null){
            throw new IllegalArgumentException(ExceptionLocalization.buildMessage("null_value_for_entity_result"));
        }
        this.entityClassName = entityClass.getName();
    }

    public EntityResult(String entityClassName){
        this.entityClassName = entityClassName;
        if (this.entityClassName == null){
            throw new IllegalArgumentException(ExceptionLocalization.buildMessage("null_value_for_entity_result"));
        }
    }

    public void addFieldResult(FieldResult fieldResult){
        if (fieldResult == null || fieldResult.getAttributeName() == null){
            return;
        }
        FieldResult existingFieldResult = (FieldResult)getFieldResults().get(fieldResult.getAttributeName());
        if (existingFieldResult==null){
            getFieldResults().put(fieldResult.getAttributeName(), fieldResult);
        }else{
            existingFieldResult.add(fieldResult);
        }
    }

    /**
     * INTERNAL:
     * Convert all the class-name-based settings in this query to actual class-based
     * settings. This method is used when converting a project that has been built
     * with class names to a project with classes.
     * @param classLoader
     */
    public void convertClassNamesToClasses(ClassLoader classLoader){
        super.convertClassNamesToClasses(classLoader);
        Class entityClass = null;
        try{
            if (PrivilegedAccessHelper.shouldUsePrivilegedAccess()){
                try {
                    entityClass = AccessController.doPrivileged(new PrivilegedClassForName(entityClassName, true, classLoader));
                } catch (PrivilegedActionException exception) {
                    throw ValidationException.classNotFoundWhileConvertingClassNames(entityClassName, exception.getException());
                }
            } else {
                entityClass = PrivilegedAccessHelper.getClassForName(entityClassName, true, classLoader);
            }
        } catch (ClassNotFoundException exc){
            throw ValidationException.classNotFoundWhileConvertingClassNames(entityClassName, exc);
        }
        this.entityClass = entityClass;
    };

    /**
     * Accessor for the internally stored list of FieldResult.  Calling this
     * method will result in a collection being created to store the FieldResult
     */
    public Map getFieldResults(){
        if (this.fieldResults == null){
            this.fieldResults = new HashMap();
        }
        return this.fieldResults;
    }

    /**
     * Returns the column name for the column that will store the value used to
     * determine the subclass type if applicable.
     */
    public DatabaseField getDiscriminatorColumn(){
        return this.discriminatorColumn;
    }

    /**
     * Sets the column name for the column that will store the value used to
     * determine the subclass type if applicable.
     */
    public void setDiscriminatorColumn(String column){
        if (column == null){
            return;
        }
        this.discriminatorColumn = new DatabaseField(column);
    }

    public void setDiscriminatorColumn(DatabaseField column){
        if (column == null){
            return;
        }
        this.discriminatorColumn = column;
    }

    /**
     * INTERNAL:
     * This method is a convenience method for extracting values from Results
     */
    public Object getValueFromRecord(DatabaseRecord record, ResultSetMappingQuery query){
        // From the row data build result entity.
        // To do this let's collect the column based data for this entity from
        // the results and call build object with this new row.
        ClassDescriptor descriptor = query.getSession().getDescriptor(this.entityClass);
        if (descriptor == null) {
            throw new IllegalArgumentException("@EntityResult: entityClass points to unknown entity: "
                    + (this.entityClass != null ? this.entityClass.getName() : "null"));
        }
        DatabaseRecord entityRecord = new DatabaseRecord(descriptor.getFields().size());
        if (descriptor.hasInheritance()) {
            Object value = null;
            if (this.discriminatorColumn == null) {
                value = record.get(descriptor.getInheritancePolicy().getClassIndicatorField());
            } else {
                value = record.getIndicatingNoEntry(this.discriminatorColumn);
                if (value == AbstractRecord.noEntry){
                    throw QueryException.discriminatorColumnNotSelected(this.discriminatorColumn.getName(), getSQLResultMapping().getName());
                }
            }
            entityRecord.put(descriptor.getInheritancePolicy().getClassIndicatorField(), value);

            // if multiple types may have been read get the correct descriptor.
            if ( descriptor.getInheritancePolicy().shouldReadSubclasses()) {
                Class classValue = descriptor.getInheritancePolicy().classFromRow(entityRecord, query.getSession());
                descriptor = query.getSession().getDescriptor(classValue);
            }
        }
        for (Iterator mappings = descriptor.getMappings().iterator(); mappings.hasNext();) {
            DatabaseMapping mapping = (DatabaseMapping)mappings.next();
            FieldResult fieldResult = (FieldResult)this.getFieldResults().get(mapping.getAttributeName());
            if (fieldResult != null){
                if (mapping.getFields().size() == 1 ) {
                    entityRecord.put(mapping.getFields().firstElement(), record.get(fieldResult.getColumn()));
                } else if (mapping.getFields().size() >1){
                    getValueFromRecordForMapping(entityRecord,mapping,fieldResult,record);
                }
            } else {
                for (Iterator fields = mapping.getFields().iterator(); fields.hasNext();) {
                    DatabaseField field = (DatabaseField)fields.next();
                    entityRecord.put(field, record.get(field));
                }
            }
        }
        query.setReferenceClass(this.entityClass);
        query.setDescriptor(descriptor);
        //TODO : support prefetchedCacheKeys in ResultSetMappingQuery
        return descriptor.getObjectBuilder().buildObject(query, entityRecord, null);
    }

    public boolean isEntityResult(){
        return true;
    }

    /**
     * INTERNAL:
     *   This method is for processing all FieldResults for a mapping.  Adds DatabaseFields to the passed in entityRecord
     */
    public void getValueFromRecordForMapping(DatabaseRecord entityRecord,DatabaseMapping mapping, FieldResult fieldResult, DatabaseRecord databaseRecord){
        ClassDescriptor currentDescriptor = mapping.getReferenceDescriptor();
        /** check if this FieldResult contains any other FieldResults, process it if it doesn't */
        if (fieldResult.getFieldResults()==null){
            DatabaseField dbfield = processValueFromRecordForMapping(currentDescriptor,fieldResult.getMultipleFieldIdentifiers(),1);
            /** If it is a 1:1 mapping we need to do the target to source field conversion.  If it is an aggregate, it is fine as it is*/
            if (mapping.isOneToOneMapping()){
                dbfield = (((OneToOneMapping)mapping).getTargetToSourceKeyFields().get(dbfield));
            }
            entityRecord.put(dbfield, databaseRecord.get(fieldResult.getColumn()));
            return;
        }
        /** This processes each FieldResult stored in the collection of FieldResults individually */
        Iterator fieldResults = fieldResult.getFieldResults().iterator();
        while (fieldResults.hasNext()){
            FieldResult tempFieldResult = ((FieldResult)fieldResults.next());
            DatabaseField dbfield = processValueFromRecordForMapping(currentDescriptor,tempFieldResult.getMultipleFieldIdentifiers(),1);
             if (mapping.isOneToOneMapping()){
                dbfield = (((OneToOneMapping)mapping).getTargetToSourceKeyFields().get(dbfield));
            }
            entityRecord.put(dbfield, databaseRecord.get(tempFieldResult.getColumn()));
        }
    }

    /**
     * INTERNAL:
     *   This method is for processing a single FieldResult, returning the DatabaseField it refers to.
     */
    public DatabaseField processValueFromRecordForMapping(ClassDescriptor descriptor, String[] attributeNames, int currentLoc){
        DatabaseMapping mapping = descriptor.getObjectBuilder().getMappingForAttributeName(attributeNames[currentLoc]);
        if (mapping==null){throw QueryException.mappingForFieldResultNotFound(attributeNames,currentLoc);}
        currentLoc++;
        if (attributeNames.length!=currentLoc){
            ClassDescriptor currentDescriptor = mapping.getReferenceDescriptor();
            DatabaseField df= processValueFromRecordForMapping(currentDescriptor, attributeNames, currentLoc);
            if (mapping.isOneToOneMapping()){
                return (((OneToOneMapping)mapping).getTargetToSourceKeyFields().get(df));
            }
            return df;
        }else{
            //this is it.. return this mapping's field
            return mapping.getFields().firstElement();
        }
    }

}
