/*
 * 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
package org.eclipse.persistence.internal.sessions;

import java.io.*;
import java.util.*;

import org.eclipse.persistence.internal.core.sessions.CoreAbstractRecord;
import org.eclipse.persistence.internal.helper.*;
import org.eclipse.persistence.exceptions.*;
import org.eclipse.persistence.sessions.Record;
import org.eclipse.persistence.internal.helper.DatabaseField;

/**
 * <p>
 * <b>Purpose</b>: Define the abstract definition of a record for internal use.
 * Public API should reference the Map or Record interface.
 * Subclasses are DatabaseRecord and XMLRecord.
 * <p>
 * <b>Responsibilities</b>: <ul>
 *      <li> Implement the Record and Map interfaces.
 * </ul>
 * @see DatabaseField
 */
public abstract class AbstractRecord extends CoreAbstractRecord implements Record, Cloneable, Serializable, Map {

    /** Use vector to store the fields/values for optimal performance.*/
    protected Vector<DatabaseField> fields;

    /** Use vector to store the fields/values for optimal performance.*/
    protected Vector values;

    /** Optimize field creation for field name lookup. */
    protected DatabaseField lookupField;

    /** PERF: Cache the row size. */
    protected int size;

    /** INTERNAL: indicator showing that no entry exists for a given key. */
    public static final AbstractRecord.NoEntry noEntry = new AbstractRecord.NoEntry();

    /** INTERNAL: flag for any database field containing a null value */
    protected boolean nullValueInFields;

    /** INTERNAL: SerializedObjectPolicy support */
    protected transient Object sopObject;

    /**
     * INTERNAL:
     * NoEntry: This is used to differentiate between the two kinds
     * of nulls: no entry exists, and the field is actually mapped
     * to null.
     */
    public static class NoEntry {
        private NoEntry() {
        }
    }

    /**
     * INTERNAL:
     *  converts JDBC results to collections of rows.
     */
    public AbstractRecord() {
        this.fields = new NonSynchronizedVector();
        this.values = new NonSynchronizedVector();
        this.size = 0;
        this.nullValueInFields = false;
    }

    /**
     * INTERNAL:
     *  converts JDBC results to collections of rows.
     */
    public AbstractRecord(int initialCapacity) {
        this.fields = new NonSynchronizedVector(initialCapacity);
        this.values = new NonSynchronizedVector(initialCapacity);
        this.size = 0;
        this.nullValueInFields = false;
    }

    /**
     * INTERNAL:
     *  converts JDBC results to collections of rows.
     */
    public AbstractRecord(Vector fields, Vector values) {
        this.fields = fields;
        this.values = values;
        this.nullValueInFields = false;
        resetSize();
    }

    /**
     * INTERNAL:
     *  converts JDBC results to collections of rows.
     */
    public AbstractRecord(Vector fields, Vector values, int size) {
        this.fields = fields;
        this.values = values;
        this.nullValueInFields = false;
        this.size = size;
    }

    /**
     * Reset the row size.
     * This must be reset after any change to the row.
     */
    protected void resetSize() {
        if (this.fields == null) {
            this.size = 0;
        } else {
            this.size = this.fields.size();
        }
    }

    /**
     * INTERNAL:
     * Add the field-value pair to the row.  Will not check,
     * will simply add to the end of the row
     */
    public void add(DatabaseField key, Object value) {
        this.fields.add(key);
        this.values.add(value);
        this.size++;
    }

    /**
     * PUBLIC:
     * Clear the contents of the row.
     */
    public void clear() {
        this.fields = new Vector();
        this.values = new Vector();
        resetSize();
    }

    /**
     * INTERNAL:
     * Clone the row and its values.
     */
    public AbstractRecord clone() {
        try {
            AbstractRecord clone = (AbstractRecord)super.clone();
            clone.fields = (Vector)this.fields.clone();
            clone.values = (Vector)this.values.clone();
            return clone;
        } catch (CloneNotSupportedException exception) {
            throw new InternalError();
        }
    }

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

    /**
     * PUBLIC:
     * Check if the field is contained in the row.
     * Conform to hashtable interface.
     */
    public boolean containsKey(Object key) {
        if (key instanceof String) {
            return containsKey((String)key);
        }
        if (key instanceof DatabaseField) {
            return containsKey((DatabaseField)key);
        }

        return false;
    }

    /**
     * PUBLIC:
     * Check if the field is contained in the row.
     */
    public boolean containsKey(String fieldName) {
        return containsKey(getLookupField(fieldName));
    }

    /**
     * INTERNAL:
     * Check if the field is contained in the row.
     */
    public boolean containsKey(DatabaseField key) {
        // Optimize check.
        int index = key.index;
        if ((index >= 0) && (index < this.size)) {
            DatabaseField field = this.fields.get(index);
            if ((field == key) || field.equals(key)) {
                return true;
            }
        }
        return this.fields.contains(key);
    }

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

    /**
     * PUBLIC:
     * Returns an Enumeration of the values.
     */
    public Enumeration elements() {
        return getValues().elements();
    }

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

    /**
     * PUBLIC:
     * Retrieve the value for the field name.
     * A field is constructed on the name to check the hash table.
     * If missing null is returned.
     */
    public Object get(Object key) {
        if (key instanceof String) {
            return get((String)key);
        } else if (key instanceof DatabaseField) {
            return get((DatabaseField)key);
        }
        return null;
    }

    /**
     * PUBLIC:
     * Retrieve the value for the field name.
     * A field is constructed on the name to check the hash table.
     * If missing null is returned.
     */
    public Object get(String fieldName) {
        return get(getLookupField(fieldName));
    }

    /**
     * Internal: factored out of getIndicatingNoEntry(String) to reduce complexity and have
     * get(string) use get(DatabaseField) instead of getIndicatingNoEntry and then doing an extra check
     * @param fieldName
     * @return
     */
    protected DatabaseField getLookupField(String fieldName){
        if (this.lookupField == null) {
            this.lookupField = new DatabaseField(fieldName);
        } else {
            this.lookupField.resetQualifiedName(fieldName);
        }
        return this.lookupField;
    }

    /**
     * PUBLIC:
     * Retrieve the value for the field name.
     * A field is constructed on the name to check the hash table.
     * If missing DatabaseRow.noEntry is returned.
     */
    public Object getIndicatingNoEntry(String fieldName) {
        // Optimized the field creation.
        return getIndicatingNoEntry(getLookupField(fieldName));
    }

    /**
     * INTERNAL:
     * Retrieve the value for the field. If missing null is returned.
     */
    public Object get(DatabaseField key) {
        // PERF: Direct variable access.
        // ** Code duplicated in getIndicatingNoEntry, replaceAt ensure kept in synch **
        // Optimize check.
        int index = key.index;
        if ((index >= 0) && (index < this.size)) {
            DatabaseField field = this.fields.get(index);
            if ((field == key) || field.equals(key)) {
                return this.values.get(index);
            }
        }
        int fieldsIndex = this.fields.indexOf(key);
        if (fieldsIndex >= 0) {
            // PERF: If the fields index was not set, then set it.
            if (index == -1) {
                key.setIndex(fieldsIndex);
            }
            return this.values.get(fieldsIndex);
        } else {
            return null;
        }
    }

    //----------------------------------------------------------------------------//
    public Object getValues(DatabaseField key) {
        return get(key);
    }

    public Object getValues(String key) {
        return get(key);
    }

    //----------------------------------------------------------------------------//

    /**
     * INTERNAL:
     * Retrieve the value for the field. If missing DatabaseRow.noEntry is returned.
     */
    public Object getIndicatingNoEntry(DatabaseField key) {
        // PERF: Direct variable access.
        // ** Code duplicated in get, ensure kept in synch **
        // Optimize check.
        int index = key.index;
        if ((index >= 0) && (index < this.size)) {
            DatabaseField field = this.fields.get(index);
            if ((field == key) || field.equals(key)) {
                return this.values.get(index);
            }
        }
        int fieldsIndex = this.fields.indexOf(key);
        if (fieldsIndex >= 0) {
            // PERF: If the fields index was not set, then set it.
            if (index == -1) {
                key.setIndex(fieldsIndex);
            }
            return this.values.get(fieldsIndex);
        } else {
            return AbstractRecord.noEntry;
        }
    }

    /**
     * INTERNAL:
     * Returns the row's field with the same name.
     */
    public DatabaseField getField(DatabaseField key) {
        // Optimize check.
        int index = key.index;
        if ((index >= 0) && (index < getFields().size())) {
            DatabaseField field = getFields().elementAt(index);
            if ((field == key) || field.equals(key)) {
                return field;
            }
        }
        for (index = 0; index < getFields().size(); index++) {
            DatabaseField field = getFields().elementAt(index);
            if ((field == key) || field.equals(key)) {
                return field;
            }
        }
        return null;
    }

    /**
     * INTERNAL:
     */
    public Vector<DatabaseField> getFields() {
        return fields;
    }

    /**
     * INTERNAL:
     */
    public Vector getValues() {
        return values;
    }

    /**
     * PUBLIC:
     * Return if the row is empty.
     */
    public boolean isEmpty() {
        return size() == 0;
    }

    /**
     * INTERNAL:
     * Return true if the AbstractRecord has been marked as valid
     * to check the update call cache with, false otherwise.
     */
    public boolean hasNullValueInFields() {
        return this.nullValueInFields;
    }

    /**
     * PUBLIC:
     * Returns an Enumeration of the DatabaseField objects.
     */
    public Enumeration keys() {
        return getFields().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 RecordKeyIterator();
        }
        public boolean contains(Object object) {
            return AbstractRecord.this.containsKey(object);
        }
        public boolean remove(Object object) {
            return AbstractRecord.this.remove(object) != null;
        }
    }

    /**
     * Defines the virtual valuesSet.
     */
    protected class ValuesSet extends EntrySet {
        public Iterator iterator() {
            return new RecordValuesIterator();
        }
        public boolean contains(Object object) {
            return AbstractRecord.this.contains(object);
        }
        public boolean remove(Object object) {
            int index = getValues().indexOf(object);
            if (index == -1) {
                return false;
            }
            AbstractRecord.this.remove(getFields().get(index));
            return true;
        }
    }

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

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

        RecordEntryIterator() {
            this.index = 0;
        }

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

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

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

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

    /**
     * Defines the virtual valuesSet iterator.
     */
    protected class RecordValuesIterator extends RecordEntryIterator {
        public Object next() {
            if (!hasNext()) {
                throw new NoSuchElementException();
            }
            this.index++;
            return getValues().get(this.index - 1);
        }
    }

    /**
     * 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));
        }
    }

    /**
     * INTERNAL:
     * Merge the provided row into this row.  Existing field values in this row will
     * be replaced with values from the provided row. Fields not in this row will be
     * added from provided row.  Values not in provided row will remain in this row.
     */

    public void mergeFrom(AbstractRecord row){
        for (int index = 0; index < row.size(); ++index){
            this.put(row.getFields().get(index), row.getValues().get(index));
        }
    }

    /**
     * PUBLIC:
     * Add the field-value pair to the row.
     */
    public Object put(Object key, Object value) throws ValidationException {
        if (key instanceof String) {
            return put((String)key, value);
        } else if (key instanceof DatabaseField) {
            return put((DatabaseField)key, value);
        } else {
            throw ValidationException.onlyFieldsAreValidKeysForDatabaseRows();
        }
    }

    /**
     * PUBLIC:
     * Add the field-value pair to the row.
     */
    public Object put(String key, Object value) {
        return put(new DatabaseField(key), value);
    }

    /**
     * INTERNAL:
     * Add the field-value pair to the row.
     */
    public Object put(DatabaseField key, Object value) {
        int index = this.fields.indexOf(key);
        if (index >= 0) {
            return this.values.set(index, value);
        } else {
            add(key, value);
        }

        return null;
    }

    /**
     * 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());
        }
    }

    /**
     * INTERNAL:
     * Remove the field key from the row.
     */
    public Object remove(Object key) {
        if (key instanceof String) {
            return remove((String)key);
        } else if (key instanceof DatabaseField) {
            return remove((DatabaseField)key);
        }
        return null;
    }

    /**
     * INTERNAL:
     * Remove the field key from the row.
     */
    public Object remove(String fieldName) {
        return remove(new DatabaseField(fieldName));
    }

    /**
     * INTERNAL:
     * Remove the field key from the row.
     */
    public Object remove(DatabaseField key) {
        int index = getFields().indexOf(key);
        if (index >= 0) {
            getFields().removeElementAt(index);
            Object value = getValues().elementAt(index);
            getValues().removeElementAt(index);
            resetSize();
            return value;
        }
        return null;
    }

    /**
     * INTERNAL:
     * replaces the value at index with value
     */
    public void replaceAt(Object value, int index) {
        this.values.set(index, value);
    }

    /**
     * INTERNAL:
     * replaces the value at field with value
     */
    public void replaceAt(Object value, DatabaseField key) {
        int index = key.index;
        if ((index >= 0) && (index < this.size)) {
            DatabaseField field = this.fields.get(index);
            if ((field == key) || field.equals(key)) {
                this.values.set(index, value);
            }
        }
        int fieldsIndex = this.fields.indexOf(key);
        if (fieldsIndex >= 0) {
            // PERF: If the fields index was not set, then set it.
            if (index == -1) {
                key.setIndex(fieldsIndex);
            }
            this.values.set(fieldsIndex, value);
        }
    }

    protected void setFields(Vector fields) {
        this.fields = fields;
        resetSize();
    }

    /**
     * INTERNAL:
     * Set the validForUpdateCallCacheCheck attribute to true if the row
     * does not contain nulls, false otherwise
     */
    public void setNullValueInFields(boolean nullValueInFields) {
        this.nullValueInFields = nullValueInFields;
    }

    protected void setValues(Vector values) {
        this.values = values;
    }

    /**
     * PUBLIC:
     * Return the number of field/value pairs in the row.
     */
    public int size() {
        return this.fields.size();
    }

    /**
     * INTERNAL:
     */
    public String toString() {
        StringWriter writer = new StringWriter();
        writer.write(Helper.getShortClassName(getClass()));
        writer.write("(");

        for (int index = 0; index < getFields().size(); index++) {
            writer.write(Helper.cr());
            writer.write("\t");
            writer.write(String.valueOf((getFields().elementAt(index))));
            writer.write(" => ");
            writer.write(String.valueOf((getValues().elementAt(index))));
        }
        if (this.sopObject != null) {
            writer.write(Helper.cr());
            writer.write(" sopObject = ");
            writer.write(this.sopObject.toString());
        }
        writer.write(")");

        return writer.toString();
    }

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

    /**
     * INTERNAL:
     */
    public boolean hasSopObject() {
        return this.sopObject != null;
    }

    /**
     * INTERNAL:
     */
    public Object getSopObject() {
        return this.sopObject;
    }

    /**
     * INTERNAL:
     */
    public void setSopObject(Object sopObject) {
        this.sopObject = sopObject;
    }
}
