/*
    BEEM is a videoconference application on the Android Platform.

    Copyright (C) 2009 by Frederic-Charles Barthelery,
                          Jean-Manuel Da Silva,
                          Nikita Kozlov,
                          Philippe Lago,
                          Jean Baptiste Vergely,
                          Vincent Veronis.

    This file is part of BEEM.

    BEEM is free software: you can redistribute it and/or modify
    it under the terms of the GNU General Public License as published by
    the Free Software Foundation, either version 3 of the License, or
    (at your option) any later version.

    BEEM is distributed in the hope that it will be useful,
    but WITHOUT ANY WARRANTY; without even the implied warranty of
    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
    GNU General Public License for more details.

    You should have received a copy of the GNU General Public License
    along with BEEM.  If not, see <http://www.gnu.org/licenses/>.

    Please send bug reports with examples or suggestions to
    contact@beem-project.com or http://dev.beem-project.com/

    Epitech, hereby disclaims all copyright interest in the program "Beem"
    written by Frederic-Charles Barthelery,
               Jean-Manuel Da Silva,
               Nikita Kozlov,
               Philippe Lago,
               Jean Baptiste Vergely,
               Vincent Veronis.

    Nicolas Sadirac, November 26, 2009
    President of Epitech.

    Flavien Astraud, November 26, 2009
    Head of the EIP Laboratory.

 */

package com.beem.project.beem.account;

import java.util.ArrayList;

import org.jivesoftware.smack.Roster;
import org.jivesoftware.smack.RosterEntry;
import org.jivesoftware.smack.RosterGroup;
import org.jivesoftware.smack.XMPPConnection;
import org.jivesoftware.smack.XMPPException;
import org.jivesoftware.smack.packet.Presence;

import android.accounts.Account;
import android.accounts.OperationCanceledException;
import android.app.Service;
import android.content.ContentProviderClient;
import android.content.ContentProviderOperation;
import android.content.ContentResolver;
import android.content.ContentUris;
import android.content.ContentValues;
import android.content.Context;
import android.content.Intent;
import android.content.OperationApplicationException;
import android.content.SyncResult;
import android.database.Cursor;
import android.net.Uri;
import android.os.Bundle;
import android.os.IBinder;
import android.os.RemoteException;
import android.provider.ContactsContract;
import android.util.Log;

import com.beem.project.beem.BeemConnection;
import com.beem.project.beem.R;
import com.beem.project.beem.utils.Status;

/**
 * Class to integrate beem in android's account.
 * @author marseille
 */
public class SyncAdapterService extends Service {

    private static final int NB_DB_OPERATION = 50;
    private static final String TAG = "SynAcapterService";
    private static SyncAdapter mSyncAdapter = null;
    private static ContentResolver mContentResolver = null;
    private static Context mContext;

    /**
     * Constructor.
     */
    public SyncAdapterService() {
	super();
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public void onCreate() {
	mContext = SyncAdapterService.this;
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public IBinder onBind(Intent intent) {
	IBinder ret = null;
	ret = getSyncAdapter().getSyncAdapterBinder();
	return ret;

    }

    /**
     * Get syncAdapter instance.
     * @return sync adapter
     */
    private SyncAdapter getSyncAdapter() {
	if (mSyncAdapter == null)
	    mSyncAdapter = new SyncAdapter(this, true);
	return mSyncAdapter;
    }

    /**
     * Method to sync Beem roster with Android account.
     * @param context context
     * @param account account
     * @param extras extras
     * @param authority authority
     * @param provider provider
     * @param syncResult syncResult
     * @throws OperationCanceledException OperationCanceledException
     */
    public static void performSync(Context context, final Account account, Bundle extras, String authority,
	ContentProviderClient provider, SyncResult syncResult) throws OperationCanceledException {
	mContentResolver = context.getContentResolver();
	Log.i(TAG, "performSync: " + account.toString());

	//TODO: Get BeemService connectino support
	//TODO: Get resource information
	BeemConnection beemco = new BeemConnection(mContext.getSharedPreferences(account.name, MODE_PRIVATE), null);
	//if (!BeemService.getIsLaunch())
	//beemco.setNoPresence();
	XMPPConnection con = new XMPPConnection(beemco.getConnectionConfiguration());
	Roster roster = null;
	try {
	    con.connect();
	    //SharedPreferences sp = context.getSharedPreferences(account.name, MODE_PRIVATE);	    
	    con.login(beemco.getLogin(), beemco.getPassword(), "beem sync adapter");
	    roster = con.getRoster();
	} catch (XMPPException e) {
	    Log.e(TAG, "Error while connecting with syncAdapter", e);
	} catch (IllegalStateException e) {
	    Log.e(TAG, "Not connected to server", e);
	}
	if (roster != null)
	    manageRoster(roster, account);
	con.disconnect();
    }

    /**
     * Method to execute content provider operation.
     * @param ops
     */
    private static void executeOperation(final ArrayList<ContentProviderOperation> ops) {
	try {
	    mContentResolver.applyBatch(ContactsContract.AUTHORITY, ops);
	} catch (RemoteException e) {
	    Log.d(TAG, "Error during sync of contact", e);
	} catch (OperationApplicationException e) {
	    Log.d(TAG, "Error during sync of contact", e);
	}
	ops.clear();
    }

    /**
     * Roster sync method.
     * @param r The roster to sync
     * @param a The account related
     */
    private static void manageRoster(final Roster r, final Account a) {
	ArrayList<ContentProviderOperation> ops = new ArrayList<ContentProviderOperation>();
	for (RosterGroup group : r.getGroups()) {
	    if (group != null) {
		manageGroup(ops, a, group);
	    }
	    if (ops.size() > NB_DB_OPERATION)
		executeOperation(ops);	    
	}
	if (ops.size() > 0)
	    executeOperation(ops);
	for (RosterEntry entry : r.getEntries()) {
	    if (entry != null) {
		long rawContactID = manageEntry(ops, a, entry);
		addUpdateStatus(ops, entry, r.getPresence(entry.getUser()), rawContactID);
	    }
	    if (ops.size() > NB_DB_OPERATION)
		executeOperation(ops);
	}
	if (ops.size() > 0)
	    executeOperation(ops);
    }

    private static void manageGroup(ArrayList<ContentProviderOperation> ops, Account account, RosterGroup group) {
	Log.i(TAG, "Sync group : " + group.getName() + " " + group.getEntryCount());
	long rawGroupID = getRawGroupID(account.name, group.getName());
	if (rawGroupID == -1) {
	    ContentProviderOperation.Builder builder = ContentProviderOperation
		.newInsert(ContactsContract.Groups.CONTENT_URI);
	    builder.withValue(ContactsContract.Groups.ACCOUNT_NAME, account.name);
	    builder.withValue(ContactsContract.Groups.ACCOUNT_TYPE, account.type);
	    builder.withValue(ContactsContract.Groups.TITLE, group.getName());
	    ops.add(builder.build());
	}
    }

    /**
     * RosterEntry sync method.
     * @param ops The content provider operation
     * @param account The account related
     * @param entry The roster entry to sync
     * @return The raw contact ID
     */
    private static long manageEntry(ArrayList<ContentProviderOperation> ops, Account account, RosterEntry entry) {
	long rawContactID = getRawContactID(account.name, entry.getUser());
	Log.i(TAG, "Sync Contact : " + entry.getUser() + " RawContactID : " + rawContactID);
	if (rawContactID == -1) { // Not found in database, add new
	    ContentValues values = new ContentValues();
	    values.put(ContactsContract.RawContacts.ACCOUNT_TYPE, account.type);
	    values.put(ContactsContract.RawContacts.ACCOUNT_NAME, account.name);
	    values.put(ContactsContract.RawContacts.SOURCE_ID, entry.getUser());
	    Uri rawContactUri = mContentResolver.insert(ContactsContract.RawContacts.CONTENT_URI, values);
	    rawContactID = ContentUris.parseId(rawContactUri);
	    values.clear();
	    ContentProviderOperation.Builder builder = addUpdateStructuredName(entry, rawContactID, true);
	    ops.add(builder.build());
	    for (RosterGroup group : entry.getGroups()) {
		builder = addUpdateGroup(entry, rawContactID, getRawGroupID(account.name, group.getName()), true);
		ops.add(builder.build());
	    }
	    builder = createProfile(entry, rawContactID, account);
	    ops.add(builder.build());
	} else { // Found, update	   
	    ContentProviderOperation.Builder builder = addUpdateStructuredName(entry, rawContactID, false);
	    ops.add(builder.build());
	}
	return rawContactID;
    }

    /**
     * Method to insert or update structured name informations.
     * @param entry The roster entry to sync
     * @param rawContactID The contact ID in the android database
     * @param isInsert Insert boolean
     * @return
     */
    private static ContentProviderOperation.Builder addUpdateStructuredName(RosterEntry entry, long rawContactID,
	boolean isInsert) {
	String displayName = entry.getName() != null ? entry.getName() : entry.getUser();
	ContentProviderOperation.Builder builder;
	if (isInsert) {
	    builder = ContentProviderOperation.newInsert(ContactsContract.Data.CONTENT_URI);
	    builder.withValue(ContactsContract.Data.RAW_CONTACT_ID, rawContactID);
	    builder.withValue(ContactsContract.Data.MIMETYPE,
		ContactsContract.CommonDataKinds.StructuredName.CONTENT_ITEM_TYPE);
	    builder.withValue(ContactsContract.CommonDataKinds.StructuredName.RAW_CONTACT_ID, rawContactID);
	} else {
	    builder = ContentProviderOperation.newUpdate(ContactsContract.Data.CONTENT_URI);
	    builder.withSelection(
		ContactsContract.CommonDataKinds.StructuredName.RAW_CONTACT_ID + " =? AND "
		    + ContactsContract.Data.MIMETYPE + " = '" + ContactsContract.CommonDataKinds.Im.CONTENT_ITEM_TYPE
		    + "'", new String[] { String.valueOf(rawContactID) });
	}
	builder.withValue(ContactsContract.CommonDataKinds.StructuredName.DISPLAY_NAME, displayName);
	builder.withValue(ContactsContract.CommonDataKinds.StructuredName.GIVEN_NAME, displayName);
	builder.withValue(ContactsContract.CommonDataKinds.StructuredName.FAMILY_NAME, displayName);
	return builder;
    }

    private static ContentProviderOperation.Builder addUpdateGroup(RosterEntry entry, long rawContactID,
	long rawGroupID, boolean isInsert) {
	String displayName = entry.getName() != null ? entry.getName() : entry.getUser();
	Log.e(TAG + "UPDATE GROUP", "Contact : " + displayName + " GroupID :" + rawGroupID);
	ContentProviderOperation.Builder builder = null;
	if (isInsert) {
	    builder = ContentProviderOperation.newInsert(ContactsContract.Data.CONTENT_URI);
	    builder.withValue(ContactsContract.Data.RAW_CONTACT_ID, rawContactID);
	    builder.withValue(ContactsContract.Data.MIMETYPE,
		ContactsContract.CommonDataKinds.GroupMembership.CONTENT_ITEM_TYPE);
	    builder.withValue(ContactsContract.CommonDataKinds.GroupMembership.GROUP_ROW_ID, rawGroupID);
	}
	//TODO: delete - contact doesnt appear anymore in this group 
	return builder;
    }

    /**
     * Method to insert or update IM informations.
     * @param entry The roster entry to sync
     * @param rawContactID The contact ID in the android database
     * @param isInsert Insert boolean
     * @return
     */
    private static ContentProviderOperation.Builder createProfile(RosterEntry entry, long rawContactID, Account account) {
	String displayName = entry.getName() != null ? entry.getName() : entry.getUser();
	ContentProviderOperation.Builder builder;
	builder = ContentProviderOperation.newInsert(ContactsContract.Data.CONTENT_URI);
	builder.withValue(ContactsContract.Data.RAW_CONTACT_ID, rawContactID);
	builder.withValue(ContactsContract.Data.MIMETYPE, ContactsContract.CommonDataKinds.Im.CONTENT_ITEM_TYPE);
	builder.withValue(ContactsContract.CommonDataKinds.Im.RAW_CONTACT_ID, rawContactID);
	builder.withValue(ContactsContract.CommonDataKinds.Im.DATA1, displayName);
	builder.withValue(ContactsContract.CommonDataKinds.Im.PROTOCOL,
	    ContactsContract.CommonDataKinds.Im.PROTOCOL_JABBER);
	return builder;
    }

    /**
     * Method to insert or update IM informations.
     * @param entry The roster entry to sync
     * @param rawContactID The contact ID in the android database
     * @param isInsert Insert boolean
     * @return
     */
    private static void addUpdateStatus(ArrayList<ContentProviderOperation> ops, RosterEntry entry, Presence p,
	long rawContactID) {
	String displayName = entry.getName() != null ? entry.getName() : entry.getUser();
	Log.i(TAG + "UPDATESTATUS", "Contact : " + displayName + " Presence status : " + p.getStatus()
	    + " Presence status state : " + Status.getStatusFromPresence(p));
	ContentProviderOperation.Builder builder;
	builder = ContentProviderOperation.newInsert(ContactsContract.StatusUpdates.CONTENT_URI);
	builder.withValue(ContactsContract.StatusUpdates.PROTOCOL, ContactsContract.CommonDataKinds.Im.PROTOCOL_JABBER);
	builder.withValue(ContactsContract.StatusUpdates.IM_HANDLE, displayName);
	//TODO: Get account name
	builder.withValue(ContactsContract.StatusUpdates.IM_ACCOUNT, "beem@elyzion.net");
	builder.withValue(ContactsContract.StatusUpdates.STATUS, p.getStatus());
	builder.withValue(ContactsContract.StatusUpdates.STATUS_RES_PACKAGE, "com.beem.project.beem");
	builder.withValue(ContactsContract.StatusUpdates.STATUS_LABEL, R.string.app_name);
	//TODO: Get status icon
	builder.withValue(ContactsContract.StatusUpdates.STATUS_ICON, R.drawable.beem_status_icon);
	//TODO: Pb presence ... 2 appear on 3 raw .... random appear
	builder.withValue(ContactsContract.StatusUpdates.PRESENCE, Status.getStatusFromPresence(p));
	ops.add(builder.build());
    }

    /**
     * Get contact ID from android database.
     * @param account The account related
     * @param jid The jid related
     * @return ID in the database of the jid
     */
    private static long getRawContactID(String account, String jid) {
	long authorId = -1;
	final Cursor c = mContentResolver.query(ContactsContract.RawContacts.CONTENT_URI, new String[] {
	    ContactsContract.RawContacts._ID, ContactsContract.RawContacts.SOURCE_ID },
	    ContactsContract.RawContacts.ACCOUNT_NAME + "=? AND " + ContactsContract.RawContacts.SOURCE_ID + "=?",
	    new String[] { account, jid }, null);
	try {
	    if (c.moveToFirst())
		authorId = c.getInt(c.getColumnIndex(ContactsContract.RawContacts._ID));
	} finally {
	    if (c != null)
		c.close();
	}
	return authorId;
    }

    private static long getRawGroupID(String account, String group) {
	long authorId = -1;
	final Cursor c = mContentResolver.query(ContactsContract.Groups.CONTENT_URI,
	    new String[] { ContactsContract.Groups._ID }, ContactsContract.Groups.ACCOUNT_NAME + "=? AND "
		+ ContactsContract.Groups.TITLE + "=?", new String[] { account, group }, null);
	try {
	    if (c.moveToFirst())
		authorId = c.getInt(c.getColumnIndex(ContactsContract.Groups._ID));
	} finally {
	    if (c != null)
		c.close();
	}
	return authorId;
    }

}
