/*****************************************************************
 *   Licensed to the Apache Software Foundation (ASF) under one
 *  or more contributor license agreements.  See the NOTICE file
 *  distributed with this work for additional information
 *  regarding copyright ownership.  The ASF licenses this file
 *  to you under the Apache License, Version 2.0 (the
 *  "License"); you may not use this file except in compliance
 *  with the License.  You may obtain a copy of the License at
 *
 *    http://www.apache.org/licenses/LICENSE-2.0
 *
 *  Unless required by applicable law or agreed to in writing,
 *  software distributed under the License is distributed on an
 *  "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
 *  KIND, either express or implied.  See the License for the
 *  specific language governing permissions and limitations
 *  under the License.
 ****************************************************************/

package org.apache.cayenne;

import java.util.HashMap;
import java.util.Iterator;
import java.util.List;
import java.util.Map;

import org.apache.commons.lang.builder.ToStringBuilder;
import org.apache.cayenne.map.DbAttribute;
import org.apache.cayenne.map.DbEntity;
import org.apache.cayenne.map.DbRelationship;
import org.apache.cayenne.map.ObjEntity;
import org.apache.cayenne.util.Util;

/**
 * DataRow a map that holds values retrieved from the database for a given query row.
 * DataRows are used to cache raw database data and as a reference point for tracking
 * DataObject changes.
 * 
 * @author Andrus Adamchik
 * @since 1.1
 */
public class DataRow extends HashMap {

    // "volatile" is supposed to ensure consistency in read and increment operations;
    // is this universally true?

    // make sure the starting value is different from DataObject default version value
    private static volatile long currentVersion = DataObject.DEFAULT_VERSION + 1;

    protected long version = currentVersion++;
    protected long replacesVersion = DataObject.DEFAULT_VERSION;

    public DataRow(Map map) {
        super(map);
    }

    public DataRow(int initialCapacity) {
        super(initialCapacity);
    }

    public long getVersion() {
        return version;
    }

    public long getReplacesVersion() {
        return replacesVersion;
    }

    /**
     * Sets the version of DataRow replaced by this one in the store.
     */
    public void setReplacesVersion(long replacesVersion) {
        this.replacesVersion = replacesVersion;
    }

    /**
     * Builds a new DataRow, merging changes from <code>diff</code> parameter with data
     * contained in this DataRow.
     */
    public DataRow applyDiff(DataRow diff) {
        DataRow merged = new DataRow(this);

        Iterator it = diff.entrySet().iterator();
        while (it.hasNext()) {
            Map.Entry entry = (Map.Entry) it.next();
            merged.put(entry.getKey(), entry.getValue());
        }

        return merged;
    }

    /**
     * Creates a DataRow that contains only the keys that have values that differ between
     * this object and <code>row</code> parameter. Diff values are taken from the
     * <code>row</code> parameter. It is assumed that key sets are compatible in both
     * rows (e.g. they represent snapshots for the same entity). Returns null if no
     * differences are found.
     */
    public DataRow createDiff(DataRow row) {

        // build a diff...
        DataRow diff = null;

        Iterator entries = entrySet().iterator();
        while (entries.hasNext()) {
            Map.Entry entry = (Map.Entry) entries.next();

            Object key = entry.getKey();
            Object currentValue = entry.getValue();
            Object rowValue = row.get(key);

            if (!Util.nullSafeEquals(currentValue, rowValue)) {
                if (diff == null) {
                    diff = new DataRow(this.size());
                }
                diff.put(key, rowValue);
            }
        }

        return diff;
    }

    /**
     * Creates an ObjectId from the values in the snapshot. If needed attributes are
     * missing in a snapshot, CayenneRuntimeException is thrown.
     */
    // TODO: andrus, 5/25/2006 - deprecate this method - it is unused
    public ObjectId createObjectId(ObjEntity entity) {
        return createObjectId(entity.getName(), entity.getDbEntity());
    }

    /**
     * @deprecated since 1.2, as new portable ObjectIds can't store Java Class and store
     *             entity name instead. Now this method relies on default CayenneModeler
     *             naming convention to figure out entity name from class name. This may
     *             not work if the classes where mapped differently.
     */
    public ObjectId createObjectId(Class objectClass, DbEntity entity) {
        return createObjectId(ObjectId.entityNameFromClass(objectClass), entity);
    }

    /**
     * @deprecated since 1.2, as new portable ObjectIds can't store Java Class and store
     *             entity name instead. Now this method relies on default CayenneModeler
     *             naming convention to figure out entity name from class name. This may
     *             not work if the classes where mapped differently.
     */
    public ObjectId createTargetObjectId(Class targetClass, DbRelationship relationship) {
        return createTargetObjectId(
                ObjectId.entityNameFromClass(targetClass),
                relationship);
    }

    // TODO: andrus, 5/25/2006 - deprecate this method - it is unused
    public ObjectId createObjectId(String entityName, DbEntity entity) {
        return createObjectId(entityName, entity, null);
    }

    /**
     * Returns an ObjectId of an object on the other side of the to-one relationship, for
     * this DataRow representing a source of relationship. Returns null if snapshot FK
     * columns indicate a null to-one relationship.
     */
    public ObjectId createTargetObjectId(String entityName, DbRelationship relationship) {

        if (relationship.isToMany()) {
            throw new CayenneRuntimeException("Only 'to one' can have a target ObjectId.");
        }

        Map target = relationship.targetPkSnapshotWithSrcSnapshot(this);
        return (target != null) ? new ObjectId(entityName, target) : null;
    }

    /**
     * Extracts PK columns prefixed with some path. If namePrefix is null or empty, no
     * prefixing is done.
     * <p>
     * Prefixing is useful when extracting an ObjectId of a target row from a row obtained
     * via prefetching. namePrefix must omit the "db:" prefix and must end with ".", e.g.
     * "TO_ARTIST.PAINTING_ARRAY."
     * </p>
     * 
     * @since 1.2
     */
    // TODO: andrus, 5/25/2006 - deprecate this method - it is unused
    public ObjectId createObjectId(String entityName, DbEntity entity, String namePrefix) {

        boolean prefix = namePrefix != null && namePrefix.length() > 0;

        // ... handle special case - PK.size == 1
        // use some not-so-significant optimizations...

        List pk = entity.getPrimaryKey();
        if (pk.size() == 1) {
            DbAttribute attribute = (DbAttribute) pk.get(0);

            String key = (prefix) ? namePrefix + attribute.getName() : attribute
                    .getName();

            Object val = this.get(key);
            if (val == null) {
                throw new CayenneRuntimeException("Null value for '"
                        + key
                        + "'. Snapshot: "
                        + this
                        + ". Prefix: "
                        + namePrefix);
            }

            // PUT without a prefix
            return new ObjectId(entityName, attribute.getName(), val);
        }

        // ... handle generic case - PK.size > 1

        Map idMap = new HashMap(pk.size() * 2);
        Iterator it = pk.iterator();
        while (it.hasNext()) {
            DbAttribute attribute = (DbAttribute) it.next();

            String key = (prefix) ? namePrefix + attribute.getName() : attribute
                    .getName();

            Object val = this.get(key);
            if (val == null) {
                throw new CayenneRuntimeException("Null value for '"
                        + key
                        + "'. Snapshot: "
                        + this
                        + ". Prefix: "
                        + namePrefix);
            }

            // PUT without a prefix
            idMap.put(attribute.getName(), val);
        }

        return new ObjectId(entityName, idMap);
    }

    public String toString() {
        return new ToStringBuilder(this).append("values", super.toString()).append(
                " version",
                version).append(" replaces", replacesVersion).toString();
    }
}
