/*
 * 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.net.Uri;
import android.os.Bundle;
import android.os.Parcel;
import android.os.RemoteException;
import android.provider.ContactsContract.Groups;
import android.provider.ContactsContract.RawContacts;
import android.support.annotation.NonNull;

import java.io.FileNotFoundException;
import java.util.Collections;
import java.util.LinkedList;
import java.util.List;

import at.bitfire.vcard4android.AndroidAddressBook;
import at.bitfire.vcard4android.AndroidContact;
import at.bitfire.vcard4android.AndroidGroup;
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();

    /**
     * Whether contact groups (LocalGroup resources) are included in query results for
     * {@link #getAll()}, {@link #getDeleted()}, {@link #getDirty()} and
     * {@link #getWithoutFileName()}.
     */
    public boolean includeGroups = true;


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

    public LocalContact findContactByUID(String uid) throws ContactsStorageException, FileNotFoundException {
        LocalContact[] contacts = (LocalContact[])queryContacts(LocalContact.COLUMN_UID + "=?", new String[] { uid });
        if (contacts.length == 0)
            throw new FileNotFoundException();
        return contacts[0];
    }

    @Override
    public LocalResource[] getAll() throws ContactsStorageException {
        List<LocalResource> all = new LinkedList<>();
        Collections.addAll(all, (LocalResource[])queryContacts(null, null));
        if (includeGroups)
            Collections.addAll(all, (LocalResource[])queryGroups(null, null));
        return all.toArray(new LocalResource[all.size()]);
    }

    /**
     * Returns an array of local contacts/groups which have been deleted locally. (DELETED != 0).
     */
    @Override
    public LocalResource[] getDeleted() throws ContactsStorageException {
        List<LocalResource> deleted = new LinkedList<>();
        Collections.addAll(deleted, getDeletedContacts());
        if (includeGroups)
            Collections.addAll(deleted, getDeletedGroups());
        return deleted.toArray(new LocalResource[deleted.size()]);
    }

    /**
     * Returns an array of local contacts/groups which have been changed locally (DIRTY != 0).
     */
    @Override
    public LocalResource[] getDirty() throws ContactsStorageException {
        List<LocalResource> dirty = new LinkedList<>();
        Collections.addAll(dirty, getDirtyContacts());
        if (includeGroups)
            Collections.addAll(dirty, getDirtyGroups());
        return dirty.toArray(new LocalResource[dirty.size()]);
    }

    /**
     * Returns an array of local contacts which don't have a file name yet.
     */
    @Override
    public LocalResource[] getWithoutFileName() throws ContactsStorageException {
        List<LocalResource> nameless = new LinkedList<>();
        Collections.addAll(nameless, (LocalContact[])queryContacts(AndroidContact.COLUMN_FILENAME + " IS NULL", null));
        if (includeGroups)
            Collections.addAll(nameless, (LocalGroup[])queryGroups(AndroidGroup.COLUMN_FILENAME + " IS NULL", null));
        return nameless.toArray(new LocalResource[nameless.size()]);
    }

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


    public LocalContact[] getDeletedContacts() throws ContactsStorageException {
        return (LocalContact[])queryContacts(RawContacts.DELETED + "!= 0", null);
    }

    public LocalContact[] getDirtyContacts() throws ContactsStorageException {
        return (LocalContact[])queryContacts(RawContacts.DIRTY + "!= 0", null);
    }

    public LocalGroup[] getDeletedGroups() throws ContactsStorageException {
        return (LocalGroup[])queryGroups(Groups.DELETED + "!= 0", null);
    }

    public LocalGroup[] getDirtyGroups() throws ContactsStorageException {
        return (LocalGroup[])queryGroups(Groups.DIRTY + "!= 0", null);
    }


    /**
     * Finds the first group with the given title. If there is no group with this
     * title, a new group is created.
     * @param title     title of the group to look for
     * @return          id of the group with given title
     * @throws ContactsStorageException on contact provider errors
     */
    public long findOrCreateGroup(@NonNull String title) throws ContactsStorageException {
        try {
            @Cleanup Cursor cursor = provider.query(syncAdapterURI(Groups.CONTENT_URI),
                    new String[] { Groups._ID  },
                    Groups.TITLE + "=?", new String[] { title },
                    null);
            if (cursor != null && cursor.moveToNext())
                return cursor.getLong(0);

            ContentValues values = new ContentValues();
            values.put(Groups.TITLE, title);
            Uri uri = provider.insert(syncAdapterURI(Groups.CONTENT_URI), values);
            return ContentUris.parseId(uri);
        } catch(RemoteException e) {
            throw new ContactsStorageException("Couldn't find local contact group", e);
        }
    }

    public void removeEmptyGroups() throws ContactsStorageException {
        // find groups without members
        /** should be done using {@link Groups.SUMMARY_COUNT}, but it's not implemented in Android yet */
        for (LocalGroup group : (LocalGroup[])queryGroups(null, null))
            if (group.getMembers().length == 0)
                group.delete();
    }

    public void removeGroups() throws ContactsStorageException {
        try {
            provider.delete(syncAdapterURI(Groups.CONTENT_URI), null, null);
        } catch(RemoteException e) {
            throw new ContactsStorageException("Couldn't remove all groups", e);
        }
    }


    // SYNC STATE

    @SuppressWarnings("ParcelClassLoader,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();
        }
    }

}
