/*
 * Copyright (c) 1998, 2020 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
package org.eclipse.persistence.eis.interactions;

import java.io.*;
import java.util.*;
import javax.resource.*;
import javax.resource.cci.*;
import org.eclipse.persistence.internal.helper.*;
import org.eclipse.persistence.internal.sessions.AbstractRecord;
import org.eclipse.persistence.internal.sessions.AbstractSession;
import org.eclipse.persistence.internal.databaseaccess.Accessor;
import org.eclipse.persistence.internal.databaseaccess.DatasourceCall;
import org.eclipse.persistence.eis.*;

/**
 * Defines the specification for a call to a JCA interaction.
 * Builds the input and output records from the arguments.
 *
 * @author James
 * @since OracleAS TopLink 10<i>g</i> (10.0.3)
 */
public abstract class EISInteraction extends DatasourceCall {

    /** Adapter specific interaction spec. */
    protected InteractionSpec interactionSpec;

    /** Name of the function the interaction describes. */
    protected String functionName;

    /** Name to pass to the input record creation. */
    protected String inputRecordName;

    /** Adapter specific properties may be added. */
    protected Map properties;

    /** Holds database row of input values. */
    protected AbstractRecord inputRow;

    /** Defines the arguments to the interaction, these can be the values or argument names/fields the values come from. */
    protected Vector arguments;

    /**
     * Defines the output argument names as defined in the output record for the interaction.
     * This is shared as indexed interaction may still have mapped results.
     */
    protected Vector outputArgumentNames;

    /** Defines the field values the output arguments of the interaction map to.  These are order dependent with the names. */
    protected Vector outputArguments;

    /** Path to the desired output record if nested. */
    protected String outputResultPath;

    /**
     * PUBLIC:
     * Default constructor.
     */
    public EISInteraction() {
        super();
        this.functionName = "";
        this.inputRecordName = "";
        this.outputResultPath = "";
    }

    /**
     * PUBLIC:
     * Define the output argument to the interaction and the field/argument name to be substitute for it.
     * This is only required if an output row is not used.
     * The parameterAndArgumentFieldName is the name of the output record argument expected,
     * and is the field or argument name to be used to be used for it.
     * These names are assumed to be the same, if not this method can be called with two arguments.
     */
    public void addOutputArgument(String parameterAndArgumentFieldName) {
        addOutputArgument(parameterAndArgumentFieldName, parameterAndArgumentFieldName);
    }

    /**
     * PUBLIC:
     * Define the output argument to the interaction and the field/argument name to be substitute for it.
     * This is only required if an output row is not used.
     * The parameterName is the name of the output record argument expected.
     * The argumentFieldName is the field or argument name to be used to be used for it.
     * If these names are the same (as they normally are) this method can be called with a single argument.
     */
    public void addOutputArgument(String parameterName, String argumentFieldName) {
        getOutputArgumentNames().addElement(parameterName);
        getOutputArguments().addElement(new DatabaseField(argumentFieldName));
    }

    /**
     * The argument fields or values to the interaction that map into the input record.
     */
    public Vector getArguments() {
        // This is lazy initialized to conserv space on calls that have no parameters.
        if (arguments == null) {
            arguments = new Vector();
        }
        return arguments;
    }

    /**
     * INTERNAL:
     * The argument fields or values to the interaction that map into the input record.
     */
    public void setArguments(Vector arguments) {
        this.arguments = arguments;
    }

    /**
     * Return if argumented.
     */
    public boolean hasArguments() {
        return (arguments != null) && (!arguments.isEmpty());
    }

    public boolean isEISInteraction() {
        return true;
    }

    /**
     * PUBLIC:
     * The output result path defines the root key for the MappedRecord that
     * the desired interaction result is nested into.
     * This is required for read interactions that need a nested record to build from the mapped object.
     */
    public String getOutputResultPath() {
        return outputResultPath;
    }

    /**
     * PUBLIC:
     * The output result path defines the root key for the MappedRecord that
     * the desired interaction result is nested into.
     * This is required for read interactions that need a nested record to build from the mapped object.
     */
    public void setOutputResultPath(String outputResultPath) {
        this.outputResultPath = outputResultPath;
    }

    /**
     * The argument names for the output record.
     */
    public Vector getOutputArgumentNames() {
        // This is lazy initialized to conserv space on calls that have no parameters.
        if (outputArgumentNames == null) {
            outputArgumentNames = new Vector();
        }
        return outputArgumentNames;
    }

    /**
     * The argument fields to the interaction that map into the output record.
     */
    public Vector getOutputArguments() {
        // This is lazy initialized to conserv space on calls that have no parameters.
        if (outputArguments == null) {
            outputArguments = new Vector();
        }
        return outputArguments;
    }

    /**
     * The output arguments.
     */
    public void setOutputArguments(Vector outputArguments) {
        this.outputArguments = outputArguments;
    }

    /**
     * Set the output argument names.
     */
    public void setOutputArgumentNames(Vector outputArgumentNames) {
        this.outputArgumentNames = outputArgumentNames;
    }

    /**
     * Return if argumented.
     */
    public boolean hasOutputArguments() {
        return (outputArguments != null) && (!outputArguments.isEmpty());
    }

    /**
     * Set the default record name from the descriptor.
     */
    public void prepare(AbstractSession session) {
        if (getInputRecordName().length() == 0) {
            if ((getQuery() != null) && (getQuery().getDescriptor() instanceof EISDescriptor)) {
                EISDescriptor descriptor = (EISDescriptor)getQuery().getDescriptor();
                setInputRecordName(descriptor.getDataTypeName());
            } else {
                setInputRecordName("input");
            }
        }
        super.prepare(session);
    }

    /**
     * Create the appropriate record element for the data value.
     * If the value is a collection, create a collection of elements,
     * if the value is a map, create a nested map,
     * otherwise just return the value (primitive data).
     */
    public Object createRecordElement(String elementName, Object value, EISAccessor accessor) {
        try {
            Object element = value;

            // Handle nested collections.
            if (element instanceof List) {
                // Convert each element in the list.
                List values = (List)element;
                List elements = new Vector(values.size());
                for (int index = 0; index < values.size(); index++) {
                    elements.add(createRecordElement(elementName, values.get(index), accessor));
                }
                element = elements;
                // Handle nested rows.
            } else if (value instanceof AbstractRecord) {
                AbstractRecord valuesRow = (AbstractRecord)value;

                // The record name for the row must be determined,
                // currently the SDK uses the table name of the row's field for this,
                // ideally this would be a property on the row.
                String recordName = elementName;
                if (valuesRow.size() > 0) {
                    recordName = valuesRow.getFields().get(0).getTableName();
                }
                MappedRecord record = accessor.getRecordFactory().createMappedRecord(recordName);
                for (Iterator keysIterator = valuesRow.getFields().iterator();
                         keysIterator.hasNext();) {
                    DatabaseField field = (DatabaseField)keysIterator.next();
                    Object elementValue = createRecordElement(field.getName(), valuesRow.get(field), accessor);
                    accessor.getEISPlatform().setValueInRecord(field.getName(), elementValue, record, accessor);
                }
                element = record;
            }
            return element;
        } catch (ResourceException exception) {
            throw EISException.resourceException(exception, accessor, null);
        }
    }

    /**
     * PUBLIC:
     * Return the JCA InteractionSpec that defines this EIS interaction.
     * The InteractionSpec is JCA adapter specific and typically
     * defines the function name.
     */
    public InteractionSpec getInteractionSpec() {
        return interactionSpec;
    }

    /**
     * PUBLIC:
     * Set the JCA InteractionSpec that defines this EIS interaction.
     * The InteractionSpec is JCA adapter specific and typically
     * defines the function name.
     */
    public void setInteractionSpec(InteractionSpec interactionSpec) {
        this.interactionSpec = interactionSpec;
    }

    /**
     * Return the string for logging purposes.
     */
    public String getLogString(Accessor accessor) {
        StringWriter writer = new StringWriter();
        writer.write("Executing ");
        writer.write(toString());
        writer.write(Helper.cr());
        writer.write("\tspec => ");
        writer.write(String.valueOf(getInteractionSpec()));
        writer.write(Helper.cr());
        writer.write("\tproperties => ");
        writer.write(String.valueOf(getProperties()));
        writer.write(Helper.cr());
        writer.write("\tinput => [");
        if (!getParameters().isEmpty()) {
            for (Iterator iterator = getParameters().iterator(); iterator.hasNext();) {
                Object parameter = iterator.next();
                writer.write(String.valueOf(parameter));
                if (iterator.hasNext()) {
                    writer.write(", ");
                } else {
                    writer.write("]");
                }
            }
        } else {
            writer.write(String.valueOf(getInputRow()));
            writer.write("]");
        }
        return writer.toString();
    }

    /**
     * Arguments to the interaction can be passed two ways.
     * The entire argument row can be converted to the input record,
     * or the arguments from the row can be translated into the interaction parameters.
     */
    public void translate(AbstractRecord translationRow, AbstractRecord modifyRow, AbstractSession session) {
        if (modifyRow != null) {
            // Avoid clearing a prepared row.
            setInputRow(modifyRow);
        }
        if (hasArguments()) {
            List parametersValues = new ArrayList(getArguments().size());
            for (int index = 0; index < getArguments().size(); index++) {
                Object argument = getArguments().elementAt(index);

                // The argument is either a value or a databasefield that needs to be translated.
                if ((argument != null) && (argument instanceof DatabaseField)) {
                    DatabaseField field = (DatabaseField)argument;
                    Object value = translationRow.get(field);

                    //BUG#12345:modifyRow is null for reads
                    if ((value == null) && (modifyRow != null)) {
                        value = modifyRow.get(field);
                    }
                    parametersValues.add(value);
                } else {
                    parametersValues.add(argument);
                }
            }
            setParameters(parametersValues);
        }
    }

    /**
     * Create the appropriate input record for this interaction.
     * Populate the data into the record from this interaction's arguments.
     */
    public abstract javax.resource.cci.Record createInputRecord(EISAccessor accessor);

    /**
     * INTERNAL:
     * Build a database row from the record returned from the interaction.
     */
    public abstract AbstractRecord buildRow(javax.resource.cci.Record record, EISAccessor accessor);

    /**
     * Build a collection of database rows from the Record returned from the interaction.
     * This handles IndexedRecords used as sets of result records,
     * and a single MappedRecord with a list of result records.
     */
    public Vector buildRows(javax.resource.cci.Record record, EISAccessor accessor) {
        Vector rows = null;
        if (record instanceof IndexedRecord) {
            IndexedRecord indexedRecord = (IndexedRecord)record;
            rows = new Vector(indexedRecord.size());
            for (int index = 0; index < indexedRecord.size(); index++) {
                Object element = indexedRecord.get(index);
                if (element instanceof javax.resource.cci.Record) {
                    rows.addElement(buildRow((javax.resource.cci.Record)element, accessor));
                } else {
                    // It is a single row record.
                    rows.add(buildRow(record, accessor));
                    return rows;
                }
            }
        } else if (record instanceof MappedRecord) {
            MappedRecord mappedRecord = (MappedRecord)record;

            // Handle the case of a single output argument of the entire row contained within the return record.
            if (getOutputResultPath().length() > 0) {
                Object element = mappedRecord.get(getOutputResultPath());

                // Handles nested rows inside an IndexedRecord, MappedRecord or List.
                if (element instanceof IndexedRecord) {
                    return buildRows((IndexedRecord)element, accessor);
                } else if (element instanceof MappedRecord) {
                    mappedRecord = (MappedRecord)element;
                } else if (element instanceof List) {
                    List elements = (List)element;
                    rows = new Vector(elements.size());
                    for (int index = 0; index < elements.size(); index++) {
                        Object elementValue = elements.get(index);
                        if (elementValue instanceof javax.resource.cci.Record) {
                            rows.addElement(buildRow((javax.resource.cci.Record)elementValue, accessor));
                        } else {
                            rows.add(elementValue);
                        }
                    }
                    return rows;
                }
            }
            rows = new Vector(1);
            AbstractRecord row = new EISMappedRecord(mappedRecord, accessor);
            rows.add(row);
        } else {
            rows = new Vector(1);
        }
        return rows;
    }

    /**
     * PUBLIC:
     * Return the function name of the interaction.
     * A function name is a common properties for a CCI InteractionSpec.
     * This cannot be generically set on the spec as it is not in the interface
     * however the EIS platform may make use of this in build the interaction spec.
     */
    public String getFunctionName() {
        return functionName;
    }

    /**
     * PUBLIC:
     * Set the function name of the interaction.
     * A function name is a common properties for a CCI InteractionSpec.
     * This cannot be generically set on the spec as it is not in the interface
     * however the EIS platform may make use of this in build the interaction spec.
     */
    public void setFunctionName(String functionName) {
        this.functionName = functionName;
    }

    /**
     * PUBLIC:
     * Return the input record name.
     * This is used as the name passed to the adapter when creating the input record.
     */
    public String getInputRecordName() {
        return inputRecordName;
    }

    /**
     * PUBLIC:
     * Set the input record name.
     * This is used as the name passed to the adapter when creating the input record.
     */
    public void setInputRecordName(String recordName) {
        this.inputRecordName = recordName;
    }

    /**
     * Returns the adapter specific properties.
     */
    public Map getProperties() {
        if (properties == null) {
            properties = new HashMap(5);
        }
        return properties;
    }

    /**
     * PUBLIC:
     * Returns the adapter property associated the given name.
     * If the interaction is associated with a query that has a descriptor,
     * The descriptor's properties are automatically inherited.
     */
    public Object getProperty(String name) {
        Object value = getProperties().get(name);

        // Check descriptor property if available.
        if ((value == null) && (getQuery() != null) && (getQuery().getDescriptor() != null)) {
            value = getQuery().getDescriptor().getProperty(name);
        }
        return value;
    }

    /**
     * Set the adapter specific properties.
     */
    public void setProperties(Map properties) {
        this.properties = properties;
    }

    /**
     * PUBLIC:
     * Set the adapter specific property.
     */
    public void setProperty(String name, Object value) {
        getProperties().put(name, value);
    }

    /**
     * INTERNAL:
     * Return the input database row.
     */
    public AbstractRecord getInputRow() {
        return inputRow;
    }

    /**
     * PUBLIC:
     * Set the input database row.
     */
    public void setInputRow(AbstractRecord inputRow) {
        this.inputRow = inputRow;
    }

    public String toString() {
        return Helper.getShortClassName(getClass()) + "(" + getFunctionName() + ")";
    }
}
