/*
 * Copyright © 2013 – 2015 Ricki Hirner (bitfire web engineering).
 * All rights reserved. This program and the accompanying materials
 * are made available under the terms of the GNU Public License v3.0
 * which accompanies this distribution, and is available at
 * http://www.gnu.org/licenses/gpl.html
 */
package at.bitfire.davdroid.resource;

import android.accounts.Account;
import android.content.ContentProviderClient;
import android.content.ContentUris;
import android.content.ContentValues;
import android.database.Cursor;
import android.os.Bundle;
import android.os.Parcel;
import android.os.RemoteException;
import android.provider.ContactsContract;
import android.provider.ContactsContract.CommonDataKinds.GroupMembership;
import android.provider.ContactsContract.Data;
import android.provider.ContactsContract.Groups;
import android.provider.ContactsContract.RawContacts;

import java.util.LinkedList;
import java.util.List;

import at.bitfire.davdroid.App;
import at.bitfire.vcard4android.AndroidAddressBook;
import at.bitfire.vcard4android.AndroidContact;
import at.bitfire.vcard4android.AndroidGroupFactory;
import at.bitfire.vcard4android.ContactsStorageException;
import lombok.Cleanup;


public class LocalAddressBook extends AndroidAddressBook implements LocalCollection {

    protected static final String
            SYNC_STATE_CTAG = "ctag",
            SYNC_STATE_URL = "url";

    private final Bundle syncState = new Bundle();


    public LocalAddressBook(Account account, ContentProviderClient provider) {
        super(account, provider, AndroidGroupFactory.INSTANCE, LocalContact.Factory.INSTANCE);
    }


    /**
     * Returns an array of local contacts, excluding those which have been modified locally (and not uploaded yet).
     */
    @Override
    public LocalContact[] getAll() throws ContactsStorageException {
        return (LocalContact[])queryContacts(null, null);
    }

    /**
     * Returns an array of local contacts which have been deleted locally. (DELETED != 0).
     */
    @Override
    public LocalContact[] getDeleted() throws ContactsStorageException {
        return (LocalContact[])queryContacts(RawContacts.DELETED + "!=0", null);
    }

    /**
     * Returns an array of local contacts which have been changed locally (DIRTY != 0).
     */
    @Override
    public LocalContact[] getDirty() throws ContactsStorageException {
        return (LocalContact[])queryContacts(RawContacts.DIRTY + "!=0", null);
    }

    /**
     * Returns an array of local contacts which don't have a file name yet.
     */
    @Override
    public LocalContact[] getWithoutFileName() throws ContactsStorageException {
        return (LocalContact[])queryContacts(AndroidContact.COLUMN_FILENAME + " IS NULL", null);
    }

    public void deleteAll()  throws ContactsStorageException {
        try {
            provider.delete(syncAdapterURI(RawContacts.CONTENT_URI), null, null);
        } catch (RemoteException e) {
            throw new ContactsStorageException("Couldn't delete all local contacts", e);
        }
    }


    // GROUPS

    /**
     * Finds the first group with the given title.
     * @param displayName   title of the group to look for
     * @return              group with given title, or null if none
     */
    @SuppressWarnings("Recycle")
    public LocalGroup findGroupByTitle(String displayName) throws ContactsStorageException {
        try {
            @Cleanup Cursor cursor = provider.query(syncAdapterURI(Groups.CONTENT_URI),
                    new String[] { Groups._ID },
                    ContactsContract.Groups.TITLE + "=?", new String[] { displayName }, null);
            if (cursor != null && cursor.moveToNext())
                return new LocalGroup(this, cursor.getLong(0));
        } catch (RemoteException e) {
            throw new ContactsStorageException("Couldn't find local contact group", e);
        }
        return null;
    }

    @SuppressWarnings("Recycle")
    public LocalGroup[] getDeletedGroups() throws ContactsStorageException {
        List<LocalGroup> groups = new LinkedList<>();
        try {
            @Cleanup Cursor cursor = provider.query(syncAdapterURI(Groups.CONTENT_URI),
                    new String[] { Groups._ID },
                    Groups.DELETED + "!=0", null, null);
            while (cursor != null && cursor.moveToNext())
                groups.add(new LocalGroup(this, cursor.getLong(0)));
        } catch (RemoteException e) {
            throw new ContactsStorageException("Couldn't query deleted groups", e);
        }
        return groups.toArray(new LocalGroup[groups.size()]);
    }

    @SuppressWarnings("Recycle")
    public LocalGroup[] getDirtyGroups() throws ContactsStorageException {
        List<LocalGroup> groups = new LinkedList<>();
        try {
            @Cleanup Cursor cursor = provider.query(syncAdapterURI(Groups.CONTENT_URI),
                    new String[] { Groups._ID },
                    Groups.DIRTY + "!=0", null, null);
            while (cursor != null && cursor.moveToNext())
                groups.add(new LocalGroup(this, cursor.getLong(0)));
        } catch (RemoteException e) {
            throw new ContactsStorageException("Couldn't query dirty groups", e);
        }
        return groups.toArray(new LocalGroup[groups.size()]);
    }

    @SuppressWarnings("Recycle")
    public void markMembersDirty(long groupId) throws ContactsStorageException {
        ContentValues dirty = new ContentValues(1);
        dirty.put(RawContacts.DIRTY, 1);
        try {
            // query all GroupMemberships of this groupId, mark every corresponding raw contact as DIRTY
            @Cleanup Cursor cursor = provider.query(syncAdapterURI(Data.CONTENT_URI),
                    new String[] { GroupMembership.RAW_CONTACT_ID },
                    Data.MIMETYPE + "=? AND " + GroupMembership.GROUP_ROW_ID + "=?",
                    new String[] { GroupMembership.CONTENT_ITEM_TYPE, String.valueOf(groupId) }, null);
            while (cursor != null && cursor.moveToNext()) {
                long id = cursor.getLong(0);
                App.log.fine("Marking raw contact #" + id + " as dirty");
                provider.update(syncAdapterURI(ContentUris.withAppendedId(RawContacts.CONTENT_URI, id)), dirty, null, null);
            }
        } catch (RemoteException e) {
            throw new ContactsStorageException("Couldn't query dirty groups", e);
        }
    }


    // SYNC STATE

    @SuppressWarnings("Recycle")
    protected void readSyncState() throws ContactsStorageException {
        @Cleanup("recycle") Parcel parcel = Parcel.obtain();
        byte[] raw = getSyncState();
        syncState.clear();
        if (raw != null) {
            parcel.unmarshall(raw, 0, raw.length);
            parcel.setDataPosition(0);
            syncState.putAll(parcel.readBundle());
        }
    }

    @SuppressWarnings("Recycle")
    protected void writeSyncState() throws ContactsStorageException {
        @Cleanup("recycle") Parcel parcel = Parcel.obtain();
        parcel.writeBundle(syncState);
        setSyncState(parcel.marshall());
    }

    public String getURL() throws ContactsStorageException {
        synchronized (syncState) {
            readSyncState();
            return syncState.getString(SYNC_STATE_URL);
        }
    }

    public void setURL(String url) throws ContactsStorageException {
        synchronized (syncState) {
            readSyncState();
            syncState.putString(SYNC_STATE_URL, url);
            writeSyncState();
        }
    }

    @Override
    public String getCTag() throws ContactsStorageException {
        synchronized (syncState) {
            readSyncState();
            return syncState.getString(SYNC_STATE_CTAG);
        }
    }

    @Override
    public void setCTag(String cTag) throws ContactsStorageException {
        synchronized (syncState) {
            readSyncState();
            syncState.putString(SYNC_STATE_CTAG, cTag);
            writeSyncState();
        }
    }

}
