/*
Copyright © 2013-2014, Silent Circle, LLC.
All rights reserved.

Redistribution and use in source and binary forms, with or without
modification, are permitted provided that the following conditions are met:
    * Any redistribution, use, or modification is done solely for personal 
      benefit and not for any commercial purpose or for monetary gain
    * Redistributions of source code must retain the above copyright
      notice, this list of conditions and the following disclaimer.
    * Redistributions in binary form must reproduce the above copyright
      notice, this list of conditions and the following disclaimer in the
      documentation and/or other materials provided with the distribution.
    * Neither the name Silent Circle nor the names of its contributors may 
      be used to endorse or promote products derived from this software 
      without specific prior written permission.

THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
DISCLAIMED. IN NO EVENT SHALL SILENT CIRCLE, LLC BE LIABLE FOR ANY
DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*/

/*
 * This  implementation is edited version of original Android sources.
 */

/*
 * Copyright (C) 2009 The Android Open Source Project
 *
 * Licensed 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 com.silentcircle.contacts.model;

import android.content.ContentProviderOperation;
import android.content.ContentProviderOperation.Builder;
import android.content.ContentValues;
import android.content.Context;
import android.net.Uri;
import android.os.Parcel;
import android.os.Parcelable;
import android.util.Log;

import com.silentcircle.contacts.model.dataitem.DataItem;
import com.silentcircle.silentcontacts.ScBaseColumns;
import com.silentcircle.silentcontacts.ScContactsContract.CommonDataKinds.Email;
import com.silentcircle.silentcontacts.ScContactsContract.CommonDataKinds.GroupMembership;
import com.silentcircle.silentcontacts.ScContactsContract.CommonDataKinds.Phone;
import com.silentcircle.silentcontacts.ScContactsContract.CommonDataKinds.Photo;
import com.silentcircle.silentcontacts.ScContactsContract.CommonDataKinds.StructuredName;
import com.silentcircle.silentcontacts.ScContactsContract.Data;
import com.silentcircle.silentcontacts.ScContactsContract.RawContacts;
import com.silentcircle.contacts.model.account.AccountType;

import com.google.common.collect.Lists;
import com.google.common.collect.Maps;
import com.google.common.collect.Sets;

import java.util.ArrayList;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Map;
import java.util.Set;
import java.util.Map.Entry;
/**
 * Contains a {@link RawContact} and records any modifications separately so the
 * original {@link RawContact} can be swapped out with a newer version and the
 * changes still cleanly applied.
 * <p>
 * One benefit of this approach is that we can build changes entirely on an
 * empty {@link RawContact}, which then becomes an insert {@link RawContacts} case.
 * <p>
 * When applying modifications over an {@link RawContact}, we try finding the
 * original {@link Data#_ID} rows where the modifications took place. If those
 * rows are missing from the new {@link RawContact}, we know the original data must
 * be deleted, but to preserve the user modifications we treat as an insert.
 */
public class RawContactDelta implements Parcelable {
    // TODO: optimize by using content values pool, since we allocate so many of them

    private static final String TAG = "EntityDelta";
    private static final boolean LOGV = true;  // false;

    /**
     * Direct values from {link Entity#getEntityValues()}.
     */
    private ValuesDelta mValues;

    /**
     * URI used for contacts queries, by default it is set to query raw contacts.
     * It can be set to query the profile's raw contact(s).
     */
    private Uri mContactsQueryUri = RawContacts.CONTENT_URI;

    /**
     * Internal map of children values from {link Entity#getSubValues()}, which
     * we store here sorted into {@link Data#MIMETYPE} bins.
     */
    private final HashMap<String, ArrayList<ValuesDelta>> mEntries = Maps.newHashMap();

    public RawContactDelta() {
    }

    public RawContactDelta(ValuesDelta values) {
        mValues = values;
    }

    /**
     * Build an {@link RawContactDelta} using the given {@link RawContact} as a
     * starting point; the "before" snapshot.
     */
    public static RawContactDelta fromBefore(RawContact before) {
        final RawContactDelta rawContactDelta = new RawContactDelta();
        rawContactDelta.mValues = ValuesDelta.fromBefore(before.getValues());
        rawContactDelta.mValues.setIdColumn(RawContacts._ID);
        for (DataItem dataItem : before.getDataItems()) {
            rawContactDelta.addEntry(ValuesDelta.fromBefore(dataItem.getContentValues()));
        }
        return rawContactDelta;
    }

    /**
     * Merge the "after" values from the given {@link RawContactDelta} onto the
     * "before" state represented by this {@link RawContactDelta}, discarding any
     * existing "after" states. This is typically used when re-parenting changes
     * onto an updated {link Entity}.
     */
    public static RawContactDelta mergeAfter(RawContactDelta local, RawContactDelta remote) {
        // Bail early if trying to merge delete with missing local
        final ValuesDelta remoteValues = remote.mValues;
        if (local == null && (remoteValues.isDelete() || remoteValues.isTransient())) return null;

        // Create local version if none exists yet
        if (local == null) local = new RawContactDelta();

        if (LOGV) {
            final Long localVersion = (local.mValues == null) ? null : local.mValues
                    .getAsLong(RawContacts.VERSION);
            final Long remoteVersion = remote.mValues.getAsLong(RawContacts.VERSION);
            Log.d(TAG, "Re-parenting from original version " + remoteVersion + " to "
                    + localVersion);
        }

        // Create values if needed, and merge "after" changes
        local.mValues = ValuesDelta.mergeAfter(local.mValues, remote.mValues);

        // Find matching local entry for each remote values, or create
        for (ArrayList<ValuesDelta> mimeEntries : remote.mEntries.values()) {
            for (ValuesDelta remoteEntry : mimeEntries) {
                final Long childId = remoteEntry.getId();

                // Find or create local match and merge
                final ValuesDelta localEntry = local.getEntry(childId);
                final ValuesDelta merged = ValuesDelta.mergeAfter(localEntry, remoteEntry);

                if (localEntry == null && merged != null) {
                    // No local entry before, so insert
                    local.addEntry(merged);
                }
            }
        }
        return local;
    }

    public ValuesDelta getValues() {
        return mValues;
    }

    public boolean isContactInsert() {
        return mValues.isInsert();
    }

    public void setProfileEdit() {
        mValues.put(RawContacts.CONTACT_TYPE, RawContacts.CONTACT_TYPE_OWN);
    }

    /**
     * Get the {@link ValuesDelta} child marked as {@link Data#IS_PRIMARY},
     * which may return null when no entry exists.
     */
    public ValuesDelta getPrimaryEntry(String mimeType) {
        final ArrayList<ValuesDelta> mimeEntries = getMimeEntries(mimeType, false);
        if (mimeEntries == null) 
            return null;

        for (ValuesDelta entry : mimeEntries) {
            if (entry.isPrimary()) {
                return entry;
            }
        }
        // When no direct primary, return something
        return mimeEntries.size() > 0 ? mimeEntries.get(0) : null;
    }

    /**
     * calls {@link #getSuperPrimaryEntry(String, boolean)} with true
     * @see #getSuperPrimaryEntry(String, boolean)
     */
    public ValuesDelta getSuperPrimaryEntry(String mimeType) {
        return getSuperPrimaryEntry(mimeType, true);
    }

    /**
     * Returns the super-primary entry for the given mime type
     * @param forceSelection if true, will try to return some value even if a super-primary
     *     doesn't exist (may be a primary, or just a random item
     * @return
     */
//    @NeededForTesting
    public ValuesDelta getSuperPrimaryEntry(String mimeType, boolean forceSelection) {
        final ArrayList<ValuesDelta> mimeEntries = getMimeEntries(mimeType, false);
        if (mimeEntries == null) return null;

        ValuesDelta primary = null;
        for (ValuesDelta entry : mimeEntries) {
            if (entry.isSuperPrimary()) {
                return entry;
            } else if (entry.isPrimary()) {
                primary = entry;
            }
        }

        if (!forceSelection) {
            return null;
        }

        // When no direct super primary, return something
        if (primary != null) {
            return primary;
        }
        return mimeEntries.size() > 0 ? mimeEntries.get(0) : null;
    }

    /**
     * Return the AccountType that this raw-contact belongs to.
     */
    public AccountType getRawContactAccountType(Context context) {
        return AccountTypeManager.getInstance(context).getAccountType();
    }

    public Long getRawContactId() {
        return getValues().getAsLong(RawContacts._ID);
    }


    public AccountType getAccountType(AccountTypeManager manager) {
        return manager.getAccountType();
    }

    public boolean isVisible() {
        return getValues().isVisible();
    }

    /**
     * Return the list of child {@link ValuesDelta} from our optimized map,
     * creating the list if requested.
     */
    private ArrayList<ValuesDelta> getMimeEntries(String mimeType, boolean lazyCreate) {
        ArrayList<ValuesDelta> mimeEntries = mEntries.get(mimeType);
        if (mimeEntries == null && lazyCreate) {
            mimeEntries = Lists.newArrayList();
            mEntries.put(mimeType, mimeEntries);
        }
        return mimeEntries;
    }

    public ArrayList<ValuesDelta> getMimeEntries(String mimeType) {
        return getMimeEntries(mimeType, false);
    }

    public int getMimeEntriesCount(String mimeType, boolean onlyVisible) {
        final ArrayList<ValuesDelta> mimeEntries = getMimeEntries(mimeType);
        if (mimeEntries == null) 
            return 0;

        int count = 0;
        for (ValuesDelta child : mimeEntries) {
            // Skip deleted items when requesting only visible
            if (onlyVisible && !child.isVisible()) continue;
            count++;
        }
        return count;
    }

    public boolean hasMimeEntries(String mimeType) {
        return mEntries.containsKey(mimeType);
    }

    public ValuesDelta addEntry(ValuesDelta entry) {
        final String mimeType = entry.getMimetype();
        getMimeEntries(mimeType, true).add(entry);
        return entry;
    }

    public ArrayList<ContentValues> getContentValues() {
        ArrayList<ContentValues> values = Lists.newArrayList();
        for (ArrayList<ValuesDelta> mimeEntries : mEntries.values()) {
            for (ValuesDelta entry : mimeEntries) {
                if (!entry.isDelete()) {
                    values.add(entry.getCompleteValues());
                }
            }
        }
        return values;
    }

    /**
     * Find entry with the given {@link ScBaseColumns#_ID} value.
     */
    public ValuesDelta getEntry(Long childId) {
        if (childId == null) {
            // Requesting an "insert" entry, which has no "before"
            return null;
        }

        // Search all children for requested entry
        for (ArrayList<ValuesDelta> mimeEntries : mEntries.values()) {
            for (ValuesDelta entry : mimeEntries) {
                if (childId.equals(entry.getId())) {
                    return entry;
                }
            }
        }
        return null;
    }

    /**
     * Return the total number of {@link ValuesDelta} contained.
     */
    public int getEntryCount(boolean onlyVisible) {
        int count = 0;
        for (String mimeType : mEntries.keySet()) {
            count += getMimeEntriesCount(mimeType, onlyVisible);
        }
        return count;
    }

    @Override
    public boolean equals(Object object) {
        if (object instanceof RawContactDelta) {
            final RawContactDelta other = (RawContactDelta)object;

            // Equality failed if parent values different
            if (!other.mValues.equals(mValues)) return false;

            for (ArrayList<ValuesDelta> mimeEntries : mEntries.values()) {
                for (ValuesDelta child : mimeEntries) {
                    // Equality failed if any children unmatched
                    if (!other.containsEntry(child)) return false;
                }
            }

            // Passed all tests, so equal
            return true;
        }
        return false;
    }

    private boolean containsEntry(ValuesDelta entry) {
        for (ArrayList<ValuesDelta> mimeEntries : mEntries.values()) {
            for (ValuesDelta child : mimeEntries) {
                // Contained if we find any child that matches
                if (child.equals(entry)) return true;
            }
        }
        return false;
    }

    /**
     * Mark this entire object deleted, including any {@link ValuesDelta}.
     */
    public void markDeleted() {
        this.mValues.markDeleted();
        for (ArrayList<ValuesDelta> mimeEntries : mEntries.values()) {
            for (ValuesDelta child : mimeEntries) {
                child.markDeleted();
            }
        }
    }

    @Override
    public String toString() {
        final StringBuilder builder = new StringBuilder();
        builder.append("\n(");
        builder.append("Uri=");
        builder.append(mContactsQueryUri);
        builder.append(", Values=");
        builder.append(mValues != null ? mValues.toString() : "null");
        builder.append(", Entries={");
        for (ArrayList<ValuesDelta> mimeEntries : mEntries.values()) {
            for (ValuesDelta child : mimeEntries) {
                builder.append("\n\t");
                child.toString(builder);
            }
        }
        builder.append("\n})\n");
        return builder.toString();
    }

    /**
     * Consider building the given {@link ContentProviderOperation.Builder} and
     * appending it to the given list, which only happens if builder is valid.
     */
    private void possibleAdd(ArrayList<ContentProviderOperation> diff,
            ContentProviderOperation.Builder builder) {
        if (builder != null) {
            diff.add(builder.build());
        }
    }

    /**
     * Build a list of {@link ContentProviderOperation} that will assert any
     * "before" state hasn't changed. This is maintained separately so that all
     * asserts can take place before any updates occur.
     */
    public void buildAssert(ArrayList<ContentProviderOperation> buildInto) {
        final boolean isContactInsert = mValues.isInsert();
        if (!isContactInsert) {
            // Assert version is consistent while persisting changes
            final Long beforeId = mValues.getId();
            final Long beforeVersion = mValues.getAsLong(RawContacts.VERSION);
            if (beforeId == null || beforeVersion == null) return;

            final ContentProviderOperation.Builder builder = ContentProviderOperation
                    .newAssertQuery(mContactsQueryUri);
            builder.withSelection(RawContacts._ID + "=" + beforeId, null);
            builder.withValue(RawContacts.VERSION, beforeVersion);
            buildInto.add(builder.build());
        }
    }

    /**
     * Build a list of {@link ContentProviderOperation} that will transform the
     * current "before" {link Entity} state into the modified state which this
     * {@link RawContactDelta} represents.
     */
    public void buildDiff(ArrayList<ContentProviderOperation> buildInto) {
        final int firstIndex = buildInto.size();

        final boolean isContactInsert = mValues.isInsert();
        final boolean isContactDelete = mValues.isDelete();
        final boolean isContactUpdate = !isContactInsert && !isContactDelete;

        final Long beforeId = mValues.getId();

        Builder builder;

        // Build possible operation at Contact level
        builder = mValues.buildDiff(mContactsQueryUri);
        possibleAdd(buildInto, builder);

        // Build operations for all children
        for (ArrayList<ValuesDelta> mimeEntries : mEntries.values()) {
            for (ValuesDelta child : mimeEntries) {
                // Ignore children if parent was deleted
                if (isContactDelete)
                    continue;

                    builder = child.buildDiff(Data.CONTENT_URI);

                if (child.isInsert()) {
                    if (isContactInsert) {
                        // Parent is brand new insert, so back-reference _id
                        builder.withValueBackReference(Data.RAW_CONTACT_ID, firstIndex);
                    } else {
                        // Inserting under existing, so fill with known _id
                        builder.withValue(Data.RAW_CONTACT_ID, beforeId);
                    }
                } else if (isContactInsert && builder != null) {
                    // Child must be insert when Contact insert
                    throw new IllegalArgumentException("When parent insert, child must be also");
                }
                possibleAdd(buildInto, builder);
            }
        }

    }

    /** {@inheritDoc} */
    public int describeContents() {
        // Nothing special about this parcel
        return 0;
    }

    /** {@inheritDoc} */
    public void writeToParcel(Parcel dest, int flags) {
        final int size = this.getEntryCount(false);
        dest.writeInt(size);
        dest.writeParcelable(mValues, flags);
        dest.writeParcelable(mContactsQueryUri, flags);
        for (ArrayList<ValuesDelta> mimeEntries : mEntries.values()) {
            for (ValuesDelta child : mimeEntries) {
                dest.writeParcelable(child, flags);
            }
        }
    }

    public void readFromParcel(Parcel source) {
        final ClassLoader loader = ((Object)this).getClass().getClassLoader();
        final int size = source.readInt();
        mValues = source.<ValuesDelta> readParcelable(loader);
        mContactsQueryUri = source.<Uri> readParcelable(loader);
        for (int i = 0; i < size; i++) {
            final ValuesDelta child = source.<ValuesDelta> readParcelable(loader);
            this.addEntry(child);
        }
    }

    public static final Parcelable.Creator<RawContactDelta> CREATOR =
            new Parcelable.Creator<RawContactDelta>() {
        public RawContactDelta createFromParcel(Parcel in) {
            final RawContactDelta state = new RawContactDelta();
            state.readFromParcel(in);
            return state;
        }

        public RawContactDelta[] newArray(int size) {
            return new RawContactDelta[size];
        }
    };

    /**
     * Type of {@link ContentValues} that maintains both an original state and a
     * modified version of that state. This allows us to build insert, update,
     * or delete operations based on a "before" {link Entity} snapshot.
     */
    public static class ValuesDelta implements Parcelable {
        protected ContentValues mBefore;
        protected ContentValues mAfter;
        protected String mIdColumn = ScBaseColumns._ID;
        private boolean mFromTemplate;

        /**
         * Next value to assign to {@link #mIdColumn} when building an insert
         * operation through {@link #fromAfter(ContentValues)}. This is used so
         * we can concretely reference this {@link ValuesDelta} before it has
         * been persisted.
         */
        protected static int sNextInsertId = -1;

        protected ValuesDelta() {
        }

        /**
         * Create {@link ValuesDelta}, using the given object as the
         * "before" state, usually from an {link Entity}.
         */
        public static ValuesDelta fromBefore(ContentValues before) {
            final ValuesDelta entry = new ValuesDelta();
            entry.mBefore = before;
            entry.mAfter = new ContentValues();
            return entry;
        }

        /**
         * Create {@link ValuesDelta}, using the given object as the "after"
         * state, usually when we are inserting a row instead of updating.
         */
        public static ValuesDelta fromAfter(ContentValues after) {
            final ValuesDelta entry = new ValuesDelta();
            entry.mBefore = null;
            entry.mAfter = after;

            // Assign temporary id which is dropped before insert.
            entry.mAfter.put(entry.mIdColumn, sNextInsertId--);
            return entry;
        }

//        @NeededForTesting
        public ContentValues getAfter() {
            return mAfter;
        }

        public boolean containsKey(String key) {
            return ((mAfter != null && mAfter.containsKey(key)) ||
                    (mBefore != null && mBefore.containsKey(key)));
        }

        public String getAsString(String key) {
            if (mAfter != null && mAfter.containsKey(key)) {
                return mAfter.getAsString(key);
            } else if (mBefore != null && mBefore.containsKey(key)) {
                return mBefore.getAsString(key);
            } else {
                return null;
            }
        }

        public byte[] getAsByteArray(String key) {
            if (mAfter != null && mAfter.containsKey(key)) {
                return mAfter.getAsByteArray(key);
            } else if (mBefore != null && mBefore.containsKey(key)) {
                return mBefore.getAsByteArray(key);
            } else {
                return null;
            }
        }

        public Long getAsLong(String key) {
            if (mAfter != null && mAfter.containsKey(key)) {
                return mAfter.getAsLong(key);
            } else if (mBefore != null && mBefore.containsKey(key)) {
                return mBefore.getAsLong(key);
            } else {
                return null;
            }
        }

        public Integer getAsInteger(String key) {
            return getAsInteger(key, null);
        }

        public Integer getAsInteger(String key, Integer defaultValue) {
            if (mAfter != null && mAfter.containsKey(key)) {
                return mAfter.getAsInteger(key);
            } else if (mBefore != null && mBefore.containsKey(key)) {
                return mBefore.getAsInteger(key);
            } else {
                return defaultValue;
            }
        }

        public boolean isChanged(String key) {
            if (mAfter == null || !mAfter.containsKey(key)) {
                return false;
            }

            Object newValue = mAfter.get(key);
            Object oldValue = mBefore.get(key);

            if (oldValue == null) {
                return newValue != null;
            }

            return !oldValue.equals(newValue);
        }

        public String getMimetype() {
            return getAsString(Data.MIMETYPE);
        }

        public Long getId() {
            return getAsLong(mIdColumn);
        }

        public void setIdColumn(String idColumn) {
            mIdColumn = idColumn;
        }

        public boolean isPrimary() {
            final Long isPrimary = getAsLong(Data.IS_PRIMARY);
            return isPrimary == null ? false : isPrimary != 0;
        }

        public void setFromTemplate(boolean isFromTemplate) {
            mFromTemplate = isFromTemplate;
        }

        public boolean isFromTemplate() {
            return mFromTemplate;
        }

        public boolean isSuperPrimary() {
            final Long isSuperPrimary = getAsLong(Data.IS_SUPER_PRIMARY);
            return isSuperPrimary == null ? false : isSuperPrimary != 0;
        }

        public boolean beforeExists() {
            return (mBefore != null && mBefore.containsKey(mIdColumn));
        }

        /**
         * When "after" is present, then visible
         */
        public boolean isVisible() {
            return (mAfter != null);
        }

        /**
         * When "after" is wiped, action is "delete"
         */
        public boolean isDelete() {
            return beforeExists() && (mAfter == null);
        }

        /**
         * When no "before" or "after", is transient
         */
        public boolean isTransient() {
            return (mBefore == null) && (mAfter == null);
        }

        /**
         * When "after" has some changes, action is "update"
         */
        public boolean isUpdate() {
            if (!beforeExists() || mAfter == null || mAfter.size() == 0) {
                return false;
            }
            Set<Entry<String, Object>> valueEntries = mAfter.valueSet();
            for (Entry<String, Object> entry : valueEntries) {
//            for (String key : mAfter.keySet()) {
                Object newValue = mAfter.get(entry.getKey());
                Object oldValue = mBefore.get(entry.getKey());
                if (oldValue == null) {
                    if (newValue != null) {
                        return true;
                    }
                } 
                else if (!oldValue.equals(newValue)) {
                    return true;
                }
            }
            return false;
        }

        /**
         * When "after" has no changes, action is no-op
         */
        public boolean isNoop() {
            return beforeExists() && (mAfter != null && mAfter.size() == 0);
        }

        /**
         * When no "before" id, and has "after", action is "insert"
         */
        public boolean isInsert() {
            return !beforeExists() && (mAfter != null);
        }

        public void markDeleted() {
            mAfter = null;
        }

        /**
         * Ensure that our internal structure is ready for storing updates.
         */
        private void ensureUpdate() {
            if (mAfter == null) {
                mAfter = new ContentValues();
            }
        }

        public void put(String key, String value) {
            ensureUpdate();
            mAfter.put(key, value);
        }

        public void put(String key, byte[] value) {
            ensureUpdate();
            mAfter.put(key, value);
        }

        public void put(String key, int value) {
            ensureUpdate();
            mAfter.put(key, value);
        }

        public void put(String key, long value) {
            ensureUpdate();
            mAfter.put(key, value);
        }

        public void putNull(String key) {
            ensureUpdate();
            mAfter.putNull(key);
        }

        public void copyStringFrom(ValuesDelta from, String key) {
            ensureUpdate();
            put(key, from.getAsString(key));
        }

        /**
         * Return set of all keys defined through this object.
         */
        public Set<String> keySet() {
            final HashSet<String> keys = Sets.newHashSet();

            if (mBefore != null) {
                for (Map.Entry<String, Object> entry : mBefore.valueSet()) {
                    keys.add(entry.getKey());
                }
            }

            if (mAfter != null) {
                for (Map.Entry<String, Object> entry : mAfter.valueSet()) {
                    keys.add(entry.getKey());
                }
            }

            return keys;
        }

        /**
         * Return complete set of "before" and "after" values mixed together,
         * giving full state regardless of edits.
         */
        public ContentValues getCompleteValues() {
            final ContentValues values = new ContentValues();
            if (mBefore != null) {
                values.putAll(mBefore);
            }
            if (mAfter != null) {
                values.putAll(mAfter);
            }
            if (values.containsKey(GroupMembership.GROUP_ROW_ID)) {
                // Clear to avoid double-definitions, and prefer rows
                values.remove(GroupMembership.GROUP_SOURCE_ID);
            }

            return values;
        }

        /**
         * Merge the "after" values from the given {@link ValuesDelta},
         * discarding any existing "after" state. This is typically used when
         * re-parenting changes onto an updated {link Entity}.
         */
        public static ValuesDelta mergeAfter(ValuesDelta local, ValuesDelta remote) {
            // Bail early if trying to merge delete with missing local
            if (local == null && (remote.isDelete() || remote.isTransient())) return null;

            // Create local version if none exists yet
            if (local == null) local = new ValuesDelta();

            if (!local.beforeExists()) {
                // Any "before" record is missing, so take all values as "insert"
                local.mAfter = remote.getCompleteValues();
            } else {
                // Existing "update" with only "after" values
                local.mAfter = remote.mAfter;
            }

            return local;
        }

        @Override
        public boolean equals(Object object) {
            if (object instanceof ValuesDelta) {
                // Only exactly equal with both are identical subsets
                final ValuesDelta other = (ValuesDelta)object;
                return this.subsetEquals(other) && other.subsetEquals(this);
            }
            return false;
        }

        @Override
        public String toString() {
            final StringBuilder builder = new StringBuilder();
            toString(builder);
            return builder.toString();
        }

        /**
         * Helper for building string representation, leveraging the given
         * {@link StringBuilder} to minimize allocations.
         */
        public void toString(StringBuilder builder) {
            builder.append("{ ");
            builder.append("IdColumn=");
            builder.append(mIdColumn);
            builder.append(", FromTemplate=");
            builder.append(mFromTemplate);
            builder.append(", ");
            for (String key : this.keySet()) {
                builder.append(key);
                builder.append("=");
                builder.append(this.getAsString(key));
                builder.append(", ");
            }
            builder.append("}");
        }

        /**
         * Check if the given {@link ValuesDelta} is both a subset of this
         * object, and any defined keys have equal values.
         */
        public boolean subsetEquals(ValuesDelta other) {
            for (String key : this.keySet()) {
                final String ourValue = this.getAsString(key);
                final String theirValue = other.getAsString(key);
                if (ourValue == null) {
                    // If they have value when we're null, no match
                    if (theirValue != null) return false;
                } else {
                    // If both values defined and aren't equal, no match
                    if (!ourValue.equals(theirValue)) return false;
                }
            }
            // All values compared and matched
            return true;
        }

        /**
         * Build a {@link ContentProviderOperation} that will transform our
         * "before" state into our "after" state, using insert, update, or
         * delete as needed.
         */
        public ContentProviderOperation.Builder buildDiff(Uri targetUri) {
            Builder builder = null;
            if (isInsert()) {
                // Changed values are "insert" back-referenced to Contact
                mAfter.remove(mIdColumn);
                builder = ContentProviderOperation.newInsert(targetUri);
                builder.withValues(mAfter);
            } else if (isDelete()) {
                // When marked for deletion and "before" exists, then "delete"
                builder = ContentProviderOperation.newDelete(targetUri);
                builder.withSelection(mIdColumn + "=" + getId(), null);
            } else if (isUpdate()) {
                // When has changes and "before" exists, then "update"
                builder = ContentProviderOperation.newUpdate(targetUri);
                builder.withSelection(mIdColumn + "=" + getId(), null);
                builder.withValues(mAfter);
            }
            return builder;
        }

        /** {@inheritDoc} */
        public int describeContents() {
            // Nothing special about this parcel
            return 0;
        }

        /** {@inheritDoc} */
        public void writeToParcel(Parcel dest, int flags) {
            dest.writeParcelable(mBefore, flags);
            dest.writeParcelable(mAfter, flags);
            dest.writeString(mIdColumn);
        }

        public void readFromParcel(Parcel source) {
            final ClassLoader loader = ((Object)this).getClass().getClassLoader();
            mBefore = source.<ContentValues> readParcelable(loader);
            mAfter = source.<ContentValues> readParcelable(loader);
            mIdColumn = source.readString();
        }

        public static final Parcelable.Creator<ValuesDelta> CREATOR = new Parcelable.Creator<ValuesDelta>() {
            public ValuesDelta createFromParcel(Parcel in) {
                final ValuesDelta values = new ValuesDelta();
                values.readFromParcel(in);
                return values;
            }

            public ValuesDelta[] newArray(int size) {
                return new ValuesDelta[size];
            }
        };

        public void setGroupRowId(long groupId) {
            put(GroupMembership.GROUP_ROW_ID, groupId);
        }

        public Long getGroupRowId() {
            return getAsLong(GroupMembership.GROUP_ROW_ID);
        }

        public void setPhoto(byte[] value) {
            put(Photo.PHOTO, value);
        }

        public byte[] getPhoto() {
            return getAsByteArray(Photo.PHOTO);
        }

        public void setSuperPrimary(boolean val) {
            if (val) {
                put(Data.IS_SUPER_PRIMARY, 1);
            } else {
                put(Data.IS_SUPER_PRIMARY, 0);
            }
        }

        public void setPhoneticFamilyName(String value) {
            put(StructuredName.PHONETIC_FAMILY_NAME, value);
        }

        public void setPhoneticMiddleName(String value) {
            put(StructuredName.PHONETIC_MIDDLE_NAME, value);
        }

        public void setPhoneticGivenName(String value) {
            put(StructuredName.PHONETIC_GIVEN_NAME, value);
        }

        public String getPhoneticFamilyName() {
            return getAsString(StructuredName.PHONETIC_FAMILY_NAME);
        }

        public String getPhoneticMiddleName() {
            return getAsString(StructuredName.PHONETIC_MIDDLE_NAME);
        }

        public String getPhoneticGivenName() {
            return getAsString(StructuredName.PHONETIC_GIVEN_NAME);
        }

        public String getDisplayName() {
            return getAsString(StructuredName.DISPLAY_NAME);
        }

        public void setDisplayName(String name) {
            if (name == null) {
                putNull(StructuredName.DISPLAY_NAME);
            } else {
                put(StructuredName.DISPLAY_NAME, name);
            }
        }

        public void copyStructuredNameFieldsFrom(ValuesDelta name) {
            copyStringFrom(name, StructuredName.DISPLAY_NAME);

            copyStringFrom(name, StructuredName.GIVEN_NAME);
            copyStringFrom(name, StructuredName.FAMILY_NAME);
            copyStringFrom(name, StructuredName.PREFIX);
            copyStringFrom(name, StructuredName.MIDDLE_NAME);
            copyStringFrom(name, StructuredName.SUFFIX);

            copyStringFrom(name, StructuredName.PHONETIC_GIVEN_NAME);
            copyStringFrom(name, StructuredName.PHONETIC_MIDDLE_NAME);
            copyStringFrom(name, StructuredName.PHONETIC_FAMILY_NAME);

            copyStringFrom(name, StructuredName.FULL_NAME_STYLE);
            copyStringFrom(name, StructuredName.PHONETIC_NAME_STYLE);
        }

        public String getPhoneNumber() {
            return getAsString(Phone.NUMBER);
        }

        public String getPhoneNormalizedNumber() {
            return getAsString(Phone.NORMALIZED_NUMBER);
        }

        public boolean phoneHasType() {
            return containsKey(Phone.TYPE);
        }

        public int getPhoneType() {
            return getAsInteger(Phone.TYPE);
        }

        public String getPhoneLabel() {
            return getAsString(Phone.LABEL);
        }

        public String getEmailData() {
            return getAsString(Email.DATA);
        }

        public boolean emailHasType() {
            return containsKey(Email.TYPE);
        }

        public int getEmailType() {
            return getAsInteger(Email.TYPE);
        }

        public String getEmailLabel() {
            return getAsString(Email.LABEL);
        }
    }
}