/*
 * Copyright (C) 2012 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.android.contacts.common.model;

import android.content.ContentValues;
import android.content.Context;
import android.content.Entity;
import android.net.Uri;
import android.os.Parcel;
import android.os.Parcelable;
import android.provider.ContactsContract.Contacts;
import android.provider.ContactsContract.Data;
import android.provider.ContactsContract.RawContacts;

import com.android.contacts.common.model.AccountTypeManager;
import com.android.contacts.common.model.account.AccountType;
import com.android.contacts.common.model.account.AccountWithDataSet;
import com.android.contacts.common.model.dataitem.DataItem;
import com.google.common.base.Objects;
import com.google.common.collect.Lists;

import java.util.ArrayList;
import java.util.List;

/**
 * RawContact represents a single raw contact in the raw contacts database.
 * It has specialized getters/setters for raw contact
 * items, and also contains a collection of DataItem objects.  A RawContact contains the information
 * from a single account.
 *
 * This allows RawContact objects to be thought of as a class with raw contact
 * fields (like account type, name, data set, sync state, etc.) and a list of
 * DataItem objects that represent contact information elements (like phone
 * numbers, email, address, etc.).
 */
final public class RawContact implements Parcelable {

    private AccountTypeManager mAccountTypeManager;
    private final ContentValues mValues;
    private final ArrayList<NamedDataItem> mDataItems;

    final public static class NamedDataItem implements Parcelable {
        public final Uri mUri;

        // This use to be a DataItem. DataItem creation is now delayed until the point of request
        // since there is no benefit to storing them here due to the multiple inheritance.
        // Eventually instanceof still has to be used anyways to determine which sub-class of
        // DataItem it is. And having parent DataItem's here makes it very difficult to serialize or
        // parcelable.
        //
        // Instead of having a common DataItem super class, we should refactor this to be a generic
        // Object where the object is a concrete class that no longer relies on ContentValues.
        // (this will also make the classes easier to use).
        // Since instanceof is used later anyways, having a list of Objects won't hurt and is no
        // worse than having a DataItem.
        public final ContentValues mContentValues;

        public NamedDataItem(Uri uri, ContentValues values) {
            this.mUri = uri;
            this.mContentValues = values;
        }

        public NamedDataItem(Parcel parcel) {
            this.mUri = parcel.readParcelable(Uri.class.getClassLoader());
            this.mContentValues = parcel.readParcelable(ContentValues.class.getClassLoader());
        }

        @Override
        public int describeContents() {
            return 0;
        }

        @Override
        public void writeToParcel(Parcel parcel, int i) {
            parcel.writeParcelable(mUri, i);
            parcel.writeParcelable(mContentValues, i);
        }

        public static final Parcelable.Creator<NamedDataItem> CREATOR
                = new Parcelable.Creator<NamedDataItem>() {

            @Override
            public NamedDataItem createFromParcel(Parcel parcel) {
                return new NamedDataItem(parcel);
            }

            @Override
            public NamedDataItem[] newArray(int i) {
                return new NamedDataItem[i];
            }
        };

        @Override
        public int hashCode() {
            return Objects.hashCode(mUri, mContentValues);
        }

        @Override
        public boolean equals(Object obj) {
            if (obj == null) return false;
            if (getClass() != obj.getClass()) return false;

            final NamedDataItem other = (NamedDataItem) obj;
            return Objects.equal(mUri, other.mUri) &&
                    Objects.equal(mContentValues, other.mContentValues);
        }
    }

    public static RawContact createFrom(Entity entity) {
        final ContentValues values = entity.getEntityValues();
        final ArrayList<Entity.NamedContentValues> subValues = entity.getSubValues();

        RawContact rawContact = new RawContact(values);
        for (Entity.NamedContentValues subValue : subValues) {
            rawContact.addNamedDataItemValues(subValue.uri, subValue.values);
        }
        return rawContact;
    }

    /**
     * A RawContact object can be created with or without a context.
     */
    public RawContact() {
        this(new ContentValues());
    }

    public RawContact(ContentValues values) {
        mValues = values;
        mDataItems = new ArrayList<NamedDataItem>();
    }

    /**
     * Constructor for the parcelable.
     *
     * @param parcel The parcel to de-serialize from.
     */
    private RawContact(Parcel parcel) {
        mValues = parcel.readParcelable(ContentValues.class.getClassLoader());
        mDataItems = Lists.newArrayList();
        parcel.readTypedList(mDataItems, NamedDataItem.CREATOR);
    }

    @Override
    public int describeContents() {
        return 0;
    }

    @Override
    public void writeToParcel(Parcel parcel, int i) {
        parcel.writeParcelable(mValues, i);
        parcel.writeTypedList(mDataItems);
    }

    /**
     * Create for building the parcelable.
     */
    public static final Parcelable.Creator<RawContact> CREATOR
            = new Parcelable.Creator<RawContact>() {

        @Override
        public RawContact createFromParcel(Parcel parcel) {
            return new RawContact(parcel);
        }

        @Override
        public RawContact[] newArray(int i) {
            return new RawContact[i];
        }
    };

    public AccountTypeManager getAccountTypeManager(Context context) {
        if (mAccountTypeManager == null) {
            mAccountTypeManager = AccountTypeManager.getInstance(context);
        }
        return mAccountTypeManager;
    }

    public ContentValues getValues() {
        return mValues;
    }

    /**
     * Returns the id of the raw contact.
     */
    public Long getId() {
        return getValues().getAsLong(RawContacts._ID);
    }

    /**
     * Returns the account name of the raw contact.
     */
    public String getAccountName() {
        return getValues().getAsString(RawContacts.ACCOUNT_NAME);
    }

    /**
     * Returns the account type of the raw contact.
     */
    public String getAccountTypeString() {
        return getValues().getAsString(RawContacts.ACCOUNT_TYPE);
    }

    /**
     * Returns the data set of the raw contact.
     */
    public String getDataSet() {
        return getValues().getAsString(RawContacts.DATA_SET);
    }

    public boolean isDirty() {
        return getValues().getAsBoolean(RawContacts.DIRTY);
    }

    public String getSourceId() {
        return getValues().getAsString(RawContacts.SOURCE_ID);
    }

    public String getSync1() {
        return getValues().getAsString(RawContacts.SYNC1);
    }

    public String getSync2() {
        return getValues().getAsString(RawContacts.SYNC2);
    }

    public String getSync3() {
        return getValues().getAsString(RawContacts.SYNC3);
    }

    public String getSync4() {
        return getValues().getAsString(RawContacts.SYNC4);
    }

    public boolean isDeleted() {
        return getValues().getAsBoolean(RawContacts.DELETED);
    }

    public long getContactId() {
        return getValues().getAsLong(Contacts.Entity.CONTACT_ID);
    }

    public boolean isStarred() {
        return getValues().getAsBoolean(Contacts.STARRED);
    }

    public AccountType getAccountType(Context context) {
        return getAccountTypeManager(context).getAccountType(getAccountTypeString(), getDataSet());
    }

    /**
     * Sets the account name, account type, and data set strings.
     * Valid combinations for account-name, account-type, data-set
     * 1) null, null, null (local account)
     * 2) non-null, non-null, null (valid account without data-set)
     * 3) non-null, non-null, non-null (valid account with data-set)
     */
    private void setAccount(String accountName, String accountType, String dataSet) {
        final ContentValues values = getValues();
        if (accountName == null) {
            if (accountType == null && dataSet == null) {
                // This is a local account
                values.putNull(RawContacts.ACCOUNT_NAME);
                values.putNull(RawContacts.ACCOUNT_TYPE);
                values.putNull(RawContacts.DATA_SET);
                return;
            }
        } else {
            if (accountType != null) {
                // This is a valid account, either with or without a dataSet.
                values.put(RawContacts.ACCOUNT_NAME, accountName);
                values.put(RawContacts.ACCOUNT_TYPE, accountType);
                if (dataSet == null) {
                    values.putNull(RawContacts.DATA_SET);
                } else {
                    values.put(RawContacts.DATA_SET, dataSet);
                }
                return;
            }
        }
        throw new IllegalArgumentException(
                "Not a valid combination of account name, type, and data set.");
    }

    public void setAccount(AccountWithDataSet accountWithDataSet) {
        if (accountWithDataSet != null) {
            setAccount(accountWithDataSet.name, accountWithDataSet.type,
                    accountWithDataSet.dataSet);
        } else {
            setAccount(null, null, null);
        }
    }

    public void setAccountToLocal() {
        setAccount(null, null, null);
    }

    /**
     * Creates and inserts a DataItem object that wraps the content values, and returns it.
     */
    public void addDataItemValues(ContentValues values) {
        addNamedDataItemValues(Data.CONTENT_URI, values);
    }

    public NamedDataItem addNamedDataItemValues(Uri uri, ContentValues values) {
        final NamedDataItem namedItem = new NamedDataItem(uri, values);
        mDataItems.add(namedItem);
        return namedItem;
    }

    public ArrayList<ContentValues> getContentValues() {
        final ArrayList<ContentValues> list = Lists.newArrayListWithCapacity(mDataItems.size());
        for (NamedDataItem dataItem : mDataItems) {
            if (Data.CONTENT_URI.equals(dataItem.mUri)) {
                list.add(dataItem.mContentValues);
            }
        }
        return list;
    }

    public List<DataItem> getDataItems() {
        final ArrayList<DataItem> list = Lists.newArrayListWithCapacity(mDataItems.size());
        for (NamedDataItem dataItem : mDataItems) {
            if (Data.CONTENT_URI.equals(dataItem.mUri)) {
                list.add(DataItem.createFrom(dataItem.mContentValues));
            }
        }
        return list;
    }

    public String toString() {
        final StringBuilder sb = new StringBuilder();
        sb.append("RawContact: ").append(mValues);
        for (RawContact.NamedDataItem namedDataItem : mDataItems) {
            sb.append("\n  ").append(namedDataItem.mUri);
            sb.append("\n  -> ").append(namedDataItem.mContentValues);
        }
        return sb.toString();
    }

    @Override
    public int hashCode() {
        return Objects.hashCode(mValues, mDataItems);
    }

    @Override
    public boolean equals(Object obj) {
        if (obj == null) return false;
        if (getClass() != obj.getClass()) return false;

        RawContact other = (RawContact) obj;
        return Objects.equal(mValues, other.mValues) &&
                Objects.equal(mDataItems, other.mDataItems);
    }
}
