/*
 * Copyright (c) 1998, 2020 Oracle and/or its affiliates. All rights reserved.
 * Copyright (c) 2019, 2020 IBM Corporation. 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
//     11/10/2011-2.4 Guy Pelletier
//       - 357474: Address primaryKey option from tenant discriminator column
//     10/01/2018: Will Dazey
//       - #253: Add support for embedded constructor results with CriteriaBuilder
package org.eclipse.persistence.queries;

import java.io.*;
import java.util.*;
import java.lang.reflect.Constructor;
import java.security.AccessController;
import java.security.PrivilegedActionException;

import org.eclipse.persistence.descriptors.ClassDescriptor;
import org.eclipse.persistence.exceptions.*;
import org.eclipse.persistence.expressions.ExpressionOperator;
import org.eclipse.persistence.internal.expressions.FunctionExpression;
import org.eclipse.persistence.internal.expressions.MapEntryExpression;
import org.eclipse.persistence.internal.helper.*;
import org.eclipse.persistence.internal.identitymaps.CacheId;
import org.eclipse.persistence.internal.queries.*;
import org.eclipse.persistence.internal.security.PrivilegedAccessHelper;
import org.eclipse.persistence.internal.security.PrivilegedInvokeConstructor;
import org.eclipse.persistence.internal.sessions.AbstractRecord;
import org.eclipse.persistence.mappings.*;
import org.eclipse.persistence.mappings.foundation.AbstractColumnMapping;
import org.eclipse.persistence.sessions.DatabaseRecord;
import org.eclipse.persistence.sessions.Session;

/**
 * <b>Purpose</b>: A single row (type) result for a ReportQuery<p>
 *
 * <b>Description</b>: Represents a single row of attribute values (converted using mapping) for
 * a ReportQuery. The attributes can be from various objects.
 *
 * <b>Responsibilities</b>:<ul>
 * <li> Converted field values into object attribute values.
 * <li> Provide access to values by index or item name
 * </ul>
 *
 * @author Doug Clarke
 * @since TOPLink/Java 2.0
 */
public class ReportQueryResult implements Serializable, Map {

    /** Item names to lookup result values */
    protected List<String> names;

    /** Actual converted attribute values */
    protected List<Object> results;

    /** Id value if the retrievPKs flag was set on the ReportQuery. These can be used to get the actual object */
    protected Object primaryKey;

    /** If an objectLevel distinct is used then generate unique key for this result */
    // GF_ISSUE_395
    protected StringBuffer key;

    /**
     * INTERNAL:
     * Used to create test results
     */
    public ReportQueryResult(List<Object> results, Object primaryKeyValues) {
        this.results = results;
        this.primaryKey = primaryKeyValues;
    }

    public ReportQueryResult(ReportQuery query, AbstractRecord row, Vector toManyResults) {
        super();
        this.names = query.getNames();
        buildResult(query, row, toManyResults);
    }

    /**
     * INTERNAL:
     * Create an array of attribute values (converted from raw field values using the mapping).
     */
    protected void buildResult(ReportQuery query, AbstractRecord row, Vector toManyData) {
        // GF_ISSUE_395
        if (query.shouldDistinctBeUsed() && (query.shouldFilterDuplicates())) {
            this.key = new StringBuffer();
        }

        if (query.shouldRetrievePrimaryKeys()) {
            setId(query.getDescriptor().getObjectBuilder().extractPrimaryKeyFromRow(row, query.getSession()));
            // For bug 3115576 this is only used for EXISTS sub-selects so no result is needed.
        }

        List<Object> results = new ArrayList<Object>();
        for(ReportItem item: query.getItems()) {
            if (item.isConstructorItem()) {
                Object result = processConstructorItem(query, row, toManyData, (ConstructorReportItem) item);
                results.add(result);
            } else if (item.getAttributeExpression() != null && item.getAttributeExpression().isClassTypeExpression()) {
                Object value = processItem(query, row, toManyData, item);
                ClassDescriptor descriptor = ((org.eclipse.persistence.internal.expressions.ClassTypeExpression)item.getAttributeExpression()).getContainingDescriptor(query);
                if (descriptor != null && descriptor.hasInheritance()) {
                    value = descriptor.getInheritancePolicy().classFromValue(value, query.getSession());
                } else {
                    value = query.getSession().getDatasourcePlatform().convertObject(value, Class.class);
                }
                results.add(value);
            } else {
                // Normal items
                Object value = processItem(query, row, toManyData, item);
                results.add(value);
            }
        }

        setResults(results);
    }

    private Object processConstructorItem(ReportQuery query, AbstractRecord row, Vector toManyData, ConstructorReportItem constructorItem) {
        // For constructor items need to process each constructor argument.
        Class[] constructorArgTypes = constructorItem.getConstructorArgTypes();
        int numberOfArguments = constructorItem.getReportItems().size();
        Object[] constructorArgs = new Object[numberOfArguments];
        for (int argumentIndex = 0; argumentIndex < numberOfArguments; argumentIndex++) {
            ReportItem argumentItem = (ReportItem)constructorItem.getReportItems().get(argumentIndex);
            Object result = null;
            if(argumentItem.isConstructorItem()) {
                result = processConstructorItem(query, row, toManyData, (ConstructorReportItem) argumentItem);
            } else {
                result = processItem(query, row, toManyData, argumentItem);
            }
            constructorArgs[argumentIndex] = ConversionManager.getDefaultManager().convertObject(result, constructorArgTypes[argumentIndex]);
        }
        try {
            Constructor constructor = constructorItem.getConstructor();
            Object returnValue = null;
            if (PrivilegedAccessHelper.shouldUsePrivilegedAccess()){
                try {
                    returnValue = AccessController.doPrivileged(new PrivilegedInvokeConstructor(constructor, constructorArgs));
                } catch (PrivilegedActionException exception) {
                    throw QueryException.exceptionWhileUsingConstructorExpression(exception.getException(), query);
                }
            } else {
                returnValue = PrivilegedAccessHelper.invokeConstructor(constructor, constructorArgs);
            }
            return returnValue;
        } catch (ReflectiveOperationException exc) {
            throw QueryException.exceptionWhileUsingConstructorExpression(exc, query);
        }
    }

    private Object processItemFromMapping(ReportQuery query, AbstractRecord row, DatabaseMapping mapping, ReportItem item, int itemIndex) {
        Object value = null;

        // If mapping is not null then it must be a direct mapping - see Reportitem.init.
        // Check for non database (EIS) records to use normal get.
        if (row instanceof DatabaseRecord) {
            value = row.getValues().get(itemIndex);
        } else {
            value = row.get(mapping.getField());
        }

        //Bug 421056: JPA 2.1; section 4.8.5
        if(item.getAttributeExpression().isFunctionExpression()) {
            FunctionExpression exp = (FunctionExpression) item.getAttributeExpression();
            int selector = exp.getOperator().getSelector();
            //a value of null for max/min implies no rows could be applied
            //we want to return null, per the spec, here before the mapping gets to alter the value
            if(value == null && (((selector == ExpressionOperator.Maximum) 
                    || (selector == ExpressionOperator.Minimum))
                    && query.getSession().getProject().allowNullResultMaxMin())) {
                return value;
            }
        }

        //If the mapping was set on the ReportItem, then use the mapping to convert the value
        if (mapping.isAbstractColumnMapping()) {
            value = ((AbstractColumnMapping)mapping).getObjectValue(value, query.getSession());
        } else if (mapping.isDirectCollectionMapping()) {
            value = ((DirectCollectionMapping)mapping).getObjectValue(value, query.getSession());
        }
        return value;
    }

    /**
     * INTERNAL:
     * Return a value from an item and database row (converted from raw field values using the mapping).
     */
    protected Object processItem(ReportQuery query, AbstractRecord row, Vector toManyData, ReportItem item) {
        JoinedAttributeManager joinManager = null;
        if (item.hasJoining()) {
            joinManager = item.getJoinedAttributeManager();
            if (joinManager.isToManyJoin()) {
                // PERF: Only reset data-result if unset, must only occur once per item, not per row (n vs n^2).
                if (joinManager.getDataResults_() == null) {
                    joinManager.setDataResults(new ArrayList(toManyData), query.getSession());
                }
            }
        }

        Object value = null;
        int rowSize = row.size();
        int itemIndex = item.getResultIndex();

        ClassDescriptor descriptor = item.getDescriptor();
        DatabaseMapping mapping = item.getMapping();

        if (item.getAttributeExpression() != null) {
            if (descriptor == null && mapping != null) {
                descriptor = mapping.getReferenceDescriptor();
            }
            if (mapping != null && (mapping.isAbstractColumnMapping() || mapping.isDirectCollectionMapping())) {

                if (itemIndex >= rowSize) {
                    throw QueryException.reportQueryResultSizeMismatch(itemIndex + 1, rowSize);
                }

                value = processItemFromMapping(query, row, mapping, item, itemIndex);

                // GF_ISSUE_395+
                if (this.key != null) {
                    this.key.append(value);
                    this.key.append("_");
                }
            } else if (descriptor != null) {
                // Item is for an object result.
                int size = descriptor.getAllSelectionFields(query).size();
                if (itemIndex + size > rowSize) {
                    throw QueryException.reportQueryResultSizeMismatch(itemIndex + size, rowSize);
                }
                AbstractRecord subRow = row;
                // Check if at the start of the row, then avoid building a subRow.
                if (itemIndex > 0) {
                    Vector trimedFields = new NonSynchronizedSubVector(row.getFields(), itemIndex, rowSize);
                    Vector trimedValues = new NonSynchronizedSubVector(row.getValues(), itemIndex, rowSize);
                    subRow = new DatabaseRecord(trimedFields, trimedValues);
                }
                if (mapping != null && mapping.isAggregateObjectMapping()){
                    value = ((AggregateObjectMapping)mapping).buildAggregateFromRow(subRow, null, null, joinManager, query, false, query.getSession(), true);
                } else {
                    //TODO : Support prefrechedCacheKeys in report query
                    value = descriptor.getObjectBuilder().buildObject(query, subRow, joinManager);
                }

                // this covers two possibilities
                // 1. We want the actual Map.Entry from the table rather than the just the key
                // 2. The map key is extracted from the owning object rather than built with
                // a specific mapping.  This could happen in a MapContainerPolicy
                if (item.getAttributeExpression().isMapEntryExpression() && mapping.isCollectionMapping()){
                    Object rowKey = null;
                    if (mapping.getContainerPolicy().isMapPolicy() && !mapping.getContainerPolicy().isMappedKeyMapPolicy()){
                        rowKey = ((MapContainerPolicy)mapping.getContainerPolicy()).keyFrom(value, query.getSession());
                    } else {
                        rowKey = mapping.getContainerPolicy().buildKey(subRow, query, null, query.getSession(), true);
                    }
                    if (((MapEntryExpression)item.getAttributeExpression()).shouldReturnMapEntry()){
                        value = new Association(rowKey, value);
                    } else {
                        value = rowKey;
                    }
                }
                // GF_ISSUE_395
                if (this.key != null) {
                    Object primaryKey = descriptor.getObjectBuilder().extractPrimaryKeyFromRow(subRow, query.getSession());
                    if (primaryKey != null){//GF3233 NPE is caused by processing the null PK being extracted from referenced target with null values in database.
                        this.key.append(primaryKey);
                    }
                    this.key.append("_");
                }
            } else {
                value = row.getValues().get(itemIndex);
                // GF_ISSUE_395
                if (this.key != null) {
                    this.key.append(value);
                }
            }
        }
        return value;
    }

    /**
     * PUBLIC:
     * Clear the contents of the result.
     */
    public void clear() {
        this.names = new ArrayList<String>();
        this.results = new ArrayList<Object>();
    }

    /**
     * PUBLIC:
     * Check if the value is contained in the result.
     */
    public boolean contains(Object value) {
        return containsValue(value);
    }

    /**
     * PUBLIC:
     * Check if the key is contained in the result.
     */
    public boolean containsKey(Object key) {
        return getNames().contains(key);
    }

    /**
     * PUBLIC:
     * Check if the value is contained in the result.
     */
    public boolean containsValue(Object value) {
        return getResults().contains(value);
    }

    /**
     * OBSOLETE:
     * Return an enumeration of the result values.
     * @see #values()
     */
    public Enumeration elements() {
        return new Vector(getResults()).elements();
    }

    /**
     * PUBLIC:
     * Returns a set of the keys.
     */
    public Set entrySet() {
        return new EntrySet();
    }


    /**
     * Defines the virtual entrySet.
     */
    protected class EntrySet extends AbstractSet {
        public Iterator iterator() {
            return new EntryIterator();
        }
        public int size() {
            return ReportQueryResult.this.size();
        }
        public boolean contains(Object object) {
            if (!(object instanceof Entry)) {
                return false;
            }
            return ReportQueryResult.this.containsKey(((Entry)object).getKey());
        }
        public boolean remove(Object object) {
            if (!(object instanceof Entry)) {
                return false;
            }
            ReportQueryResult.this.remove(((Entry)object).getKey());
            return true;
        }
        public void clear() {
            ReportQueryResult.this.clear();
        }
    }

    /**
     * Entry class for implementing Map interface.
     */
    protected static class RecordEntry implements Entry {
        Object key;
        Object value;

        public RecordEntry(Object key, Object value) {
            this.key = key;
            this.value = value;
        }

        public Object getKey() {
            return key;
        }

        public Object getValue() {
            return value;
        }

        public Object setValue(Object value) {
            Object oldValue = this.value;
            this.value = value;
            return oldValue;
        }

        public boolean equals(Object object) {
            if (!(object instanceof Map.Entry)) {
                return false;
            }
            Map.Entry entry = (Map.Entry)object;
            return compare(key, entry.getKey()) && compare(value, entry.getValue());
        }

        public int hashCode() {
            return ((key == null) ? 0 : key.hashCode()) ^ ((value == null) ? 0 : value.hashCode());
        }

        public String toString() {
            return key + "=" + value;
        }

        private boolean compare(Object object1, Object object2) {
            return (object1 == null ? object2 == null : object1.equals(object2));
        }
    }

    /**
     * Defines the virtual entrySet iterator.
     */
    protected class EntryIterator implements Iterator {
        int index;

        EntryIterator() {
            this.index = 0;
        }

        public boolean hasNext() {
            return this.index < ReportQueryResult.this.size();
        }

        public Object next() {
            if (!hasNext()) {
                throw new NoSuchElementException();
            }
            this.index++;
            return new RecordEntry(getNames().get(this.index - 1), getResults().get(this.index - 1));
        }

        public void remove() {
            if (this.index >= ReportQueryResult.this.size()) {
                throw new IllegalStateException();
            }
            ReportQueryResult.this.remove(getNames().get(this.index));
        }
    }

    /**
     * Defines the virtual keySet iterator.
     */
    protected class KeyIterator extends EntryIterator {
        public Object next() {
            if (!hasNext()) {
                throw new NoSuchElementException();
            }
            this.index++;
            return getNames().get(this.index - 1);
        }
    }

    /**
     * PUBLIC:
     * Compare if the two results are equal.
     */
    public boolean equals(Object anObject) {
        if (anObject instanceof ReportQueryResult) {
            return equals((ReportQueryResult)anObject);
        }

        return false;
    }

    /**
     * INTERNAL:
     * Used in testing to compare if results are correct.
     */
    public boolean equals(ReportQueryResult result) {
        if (this == result) {
            return true;
        }
        if (!getResults().equals(result.getResults())) {
            return false;
        }

        // Compare PKs
        if (getId() != null) {
            if (result.getId() == null) {
                return false;
            }
            return getId().equals(getId());
        }

        return true;
    }

    @Override
    public int hashCode() {
        List<Object> results = getResults();
        Object id = getId();
        int result = results != null ? results.hashCode() : 0;
        result = 31 * result + (id != null ? id.hashCode() : 0);
        return result;
    }

    /**
     * PUBLIC:
     * Return the value for given item name.
     */
    public Object get(Object name) {
        if (name instanceof String) {
            return get((String)name);
        }

        return null;
    }

    /**
     * PUBLIC:
     * Return the value for given item name.
     */
    public Object get(String name) {
        int index = getNames().indexOf(name);
        if (index == -1) {
            return null;
        }

        return getResults().get(index);
    }

    /**
     * PUBLIC:
     * Return the indexed value from result.
     */
    public Object getByIndex(int index) {
        return getResults().get(index);
    }

    /**
     * INTERNAL:
     * Return the unique key for this result
     */
    public String getResultKey(){
        if (this.key != null){
            return this.key.toString();
        }
        return null;
    }

    /**
     * PUBLIC:
     * Return the names of report items, provided to ReportQuery.
     */
    public List<String> getNames() {
        return names;
    }

    /**
     * PUBLIC:
     * Return the Id for the result or null if not requested.
     */
    public Object getId() {
        return primaryKey;
    }

    /**
     * PUBLIC:
     * Return the PKs for the corresponding object or null if not requested.
     * @deprecated since 2.1, replaced by getId()
     * @see #getId()
     */
    @Deprecated
    public Vector<Object> getPrimaryKeyValues() {
        if (this.primaryKey instanceof CacheId) {
            return new Vector(Arrays.asList(((CacheId)this.primaryKey).getPrimaryKey()));
        }
        Vector primaryKey = new Vector(1);
        primaryKey.add(this.primaryKey);
        return primaryKey;
    }

    /**
     * PUBLIC:
     * Return the results.
     */
    public List<Object> getResults() {
        return results;
    }

    /**
     * PUBLIC:
     * Return if the result is empty.
     */
    public boolean isEmpty() {
        return getNames().isEmpty();
    }

    /**
     * OBSOLETE:
     * Return an enumeration of the result names.
     * @see #keySet()
     */
    public Enumeration keys() {
        return new Vector(getNames()).elements();
    }

    /**
     * PUBLIC:
     * Returns a set of the keys.
     */
    public Set keySet() {
        return new KeySet();
    }

    /**
     * Defines the virtual keySet.
     */
    protected class KeySet extends EntrySet {
        public Iterator iterator() {
            return new KeyIterator();
        }
        public boolean contains(Object object) {
        return ReportQueryResult.this.containsKey(object);
        }
        public boolean remove(Object object) {
            return ReportQueryResult.this.remove(object) != null;
        }
    }

    /**
     * ADVANCED:
     * Set the value for given item name.
     */
    public Object put(Object name, Object value) {
        int index = getNames().indexOf(name);
        if (index == -1) {
            getNames().add((String)name);
            getResults().add(value);
            return null;
        }

        Object oldValue = getResults().get(index);
        getResults().set(index, value);
        return oldValue;
    }

    /**
     * PUBLIC:
     * Add all of the elements.
     */
    public void putAll(Map map) {
        Iterator entriesIterator = map.entrySet().iterator();
        while (entriesIterator.hasNext()) {
            Map.Entry entry = (Map.Entry)entriesIterator.next();
            put(entry.getKey(), entry.getValue());
        }
    }

    /**
     * PUBLIC:
     * If the PKs were retrieved with the attributes then this method can be used to read the real object from the database.
     */
    public Object readObject(Class javaClass, Session session) {
        if (getId() == null) {
            throw QueryException.reportQueryResultWithoutPKs(this);
        }

        ReadObjectQuery query = new ReadObjectQuery(javaClass);
        query.setSelectionId(getId());

        return session.executeQuery(query);
    }

    /**
     * INTERNAL:
     * Remove the name key and value from the result.
     */
    public Object remove(Object name) {
        int index = getNames().indexOf(name);
        if (index >= 0) {
            getNames().remove(index);
            Object value = getResults().get(index);
            getResults().remove(index);
            return value;
        }
        return null;
    }

    protected void setNames(List<String> names) {
        this.names = names;
    }

    /**
     * INTERNAL:
     * Set the Id for the result row's object.
     */
    protected void setId(Object primaryKey) {
        this.primaryKey = primaryKey;
    }

    /**
     * INTERNAL:
     * Set the results.
     */
    public void setResults(List<Object> results) {
        this.results = results;
    }

    /**
     * PUBLIC:
     * Return the number of name/value pairs in the result.
     */
    public int size() {
        return getNames().size();
    }

    /**
     * INTERNAL:
     * Converts the ReportQueryResult to a simple array of values.
     */
    public Object[] toArray(){
       List list = getResults();
       return (list == null) ? null : list.toArray();
    }

    /**
     * INTERNAL:
     * Converts the ReportQueryResult to a simple list of values.
     */
    public List toList(){
        return this.getResults();
    }

    public String toString() {
        java.io.StringWriter writer = new java.io.StringWriter();
        writer.write("ReportQueryResult(");
        for (int index = 0; index < getResults().size(); index++) {
            Object resultObj = getResults().get(index);
            writer.write(String.valueOf(resultObj));
            writer.write(" <"
                         + (resultObj == null ? "null" : resultObj.getClass().getName())
                         + ">");
            if (index < (getResults().size() - 1)) {
                writer.write(", ");
            }
        }
        writer.write(")");
        return writer.toString();
    }

    /**
     * PUBLIC:
     * Returns an collection of the values.
     */
    public Collection values() {
        return getResults();
    }
}
