package com.android.contacts.interactions;

import com.google.common.base.Preconditions;

import com.android.contacts.common.util.PermissionsUtil;

import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.HashSet;
import java.util.List;
import java.util.Set;

import android.Manifest.permission;
import android.content.AsyncTaskLoader;
import android.content.ContentValues;
import android.content.Context;
import android.database.Cursor;
import android.database.DatabaseUtils;
import android.provider.CalendarContract;
import android.provider.CalendarContract.Calendars;
import android.util.Log;


/**
 * Loads a list of calendar interactions showing shared calendar events with everyone passed in
 * {@param emailAddresses}.
 *
 * Note: the calendar provider treats mailing lists as atomic email addresses.
 */
public class CalendarInteractionsLoader extends AsyncTaskLoader<List<ContactInteraction>> {
    private static final String TAG = CalendarInteractionsLoader.class.getSimpleName();

    private List<String> mEmailAddresses;
    private int mMaxFutureToRetrieve;
    private int mMaxPastToRetrieve;
    private long mNumberFutureMillisecondToSearchLocalCalendar;
    private long mNumberPastMillisecondToSearchLocalCalendar;
    private List<ContactInteraction> mData;


    /**
     * @param maxFutureToRetrieve The maximum number of future events to retrieve
     * @param maxPastToRetrieve The maximum number of past events to retrieve
     */
    public CalendarInteractionsLoader(Context context, List<String> emailAddresses,
            int maxFutureToRetrieve, int maxPastToRetrieve,
            long numberFutureMillisecondToSearchLocalCalendar,
            long numberPastMillisecondToSearchLocalCalendar) {
        super(context);
        mEmailAddresses = emailAddresses;
        mMaxFutureToRetrieve = maxFutureToRetrieve;
        mMaxPastToRetrieve = maxPastToRetrieve;
        mNumberFutureMillisecondToSearchLocalCalendar =
                numberFutureMillisecondToSearchLocalCalendar;
        mNumberPastMillisecondToSearchLocalCalendar = numberPastMillisecondToSearchLocalCalendar;
    }

    @Override
    public List<ContactInteraction> loadInBackground() {
        if (!PermissionsUtil.hasPermission(getContext(), permission.READ_CALENDAR)
                || mEmailAddresses == null || mEmailAddresses.size() < 1) {
            return Collections.emptyList();
        }
        // Perform separate calendar queries for events in the past and future.
        Cursor cursor = getSharedEventsCursor(/* isFuture= */ true, mMaxFutureToRetrieve);
        List<ContactInteraction> interactions = getInteractionsFromEventsCursor(cursor);
        cursor = getSharedEventsCursor(/* isFuture= */ false, mMaxPastToRetrieve);
        List<ContactInteraction> interactions2 = getInteractionsFromEventsCursor(cursor);

        ArrayList<ContactInteraction> allInteractions = new ArrayList<ContactInteraction>(
                interactions.size() + interactions2.size());
        allInteractions.addAll(interactions);
        allInteractions.addAll(interactions2);

        Log.v(TAG, "# ContactInteraction Loaded: " + allInteractions.size());
        return allInteractions;
    }

    /**
     * @return events inside phone owners' calendars, that are shared with people inside mEmails
     */
    private Cursor getSharedEventsCursor(boolean isFuture, int limit) {
        List<String> calendarIds = getOwnedCalendarIds();
        if (calendarIds == null) {
            return null;
        }
        long timeMillis = System.currentTimeMillis();

        List<String> selectionArgs = new ArrayList<>();
        selectionArgs.addAll(mEmailAddresses);
        selectionArgs.addAll(calendarIds);

        // Add time constraints to selectionArgs
        String timeOperator = isFuture ? " > " : " < ";
        long pastTimeCutoff = timeMillis - mNumberPastMillisecondToSearchLocalCalendar;
        long futureTimeCutoff = timeMillis
                + mNumberFutureMillisecondToSearchLocalCalendar;
        String[] timeArguments = {String.valueOf(timeMillis), String.valueOf(pastTimeCutoff),
                String.valueOf(futureTimeCutoff)};
        selectionArgs.addAll(Arrays.asList(timeArguments));

        // When LAST_SYNCED = 1, the event is not a real event. We should ignore all such events.
        String IS_NOT_TEMPORARY_COPY_OF_LOCAL_EVENT
                = CalendarContract.Attendees.LAST_SYNCED + " = 0";

        String orderBy = CalendarContract.Attendees.DTSTART + (isFuture ? " ASC " : " DESC ");
        String selection = caseAndDotInsensitiveEmailComparisonClause(mEmailAddresses.size())
                + " AND " + CalendarContract.Attendees.CALENDAR_ID
                + " IN " + ContactInteractionUtil.questionMarks(calendarIds.size())
                + " AND " + CalendarContract.Attendees.DTSTART + timeOperator + " ? "
                + " AND " + CalendarContract.Attendees.DTSTART + " > ? "
                + " AND " + CalendarContract.Attendees.DTSTART + " < ? "
                + " AND " + IS_NOT_TEMPORARY_COPY_OF_LOCAL_EVENT;

        return getContext().getContentResolver().query(CalendarContract.Attendees.CONTENT_URI,
                /* projection = */ null, selection,
                selectionArgs.toArray(new String[selectionArgs.size()]),
                orderBy + " LIMIT " + limit);
    }

    /**
     * Returns a clause that checks whether an attendee's email is equal to one of
     * {@param count} values. The comparison is insensitive to dots and case.
     *
     * NOTE #1: This function is only needed for supporting non google accounts. For calendars
     * synced by a google account, attendee email values will be be modified by the server to ensure
     * they match an entry in contacts.google.com.
     *
     * NOTE #2: This comparison clause can result in false positives. Ex#1, test@gmail.com will
     * match test@gmailco.m. Ex#2, a.2@exchange.com will match a2@exchange.com (exchange addresses
     * should be dot sensitive). This probably isn't a large concern.
     */
    private String caseAndDotInsensitiveEmailComparisonClause(int count) {
        Preconditions.checkArgument(count > 0, "Count needs to be positive");
        final String COMPARISON
                = " REPLACE(" + CalendarContract.Attendees.ATTENDEE_EMAIL
                + ", '.', '') = REPLACE(?, '.', '') COLLATE NOCASE";
        StringBuilder sb = new StringBuilder("( " + COMPARISON);
        for (int i = 1; i < count; i++) {
            sb.append(" OR " + COMPARISON);
        }
        return sb.append(")").toString();
    }

    /**
     * @return A list with upto one Card. The Card contains events from {@param Cursor}.
     * Only returns unique events.
     */
    private List<ContactInteraction> getInteractionsFromEventsCursor(Cursor cursor) {
        try {
            if (cursor == null || cursor.getCount() == 0) {
                return Collections.emptyList();
            }
            Set<String> uniqueUris = new HashSet<String>();
            ArrayList<ContactInteraction> interactions = new ArrayList<ContactInteraction>();
            while (cursor.moveToNext()) {
                ContentValues values = new ContentValues();
                DatabaseUtils.cursorRowToContentValues(cursor, values);
                CalendarInteraction calendarInteraction = new CalendarInteraction(values);
                if (!uniqueUris.contains(calendarInteraction.getIntent().getData().toString())) {
                    uniqueUris.add(calendarInteraction.getIntent().getData().toString());
                    interactions.add(calendarInteraction);
                }
            }

            return interactions;
        } finally {
            if (cursor != null) {
                cursor.close();
            }
        }
    }

    /**
     * @return the Ids of calendars that are owned by accounts on the phone.
     */
    private List<String> getOwnedCalendarIds() {
        String[] projection = new String[] {Calendars._ID, Calendars.CALENDAR_ACCESS_LEVEL};
        Cursor cursor = getContext().getContentResolver().query(Calendars.CONTENT_URI, projection,
                Calendars.VISIBLE + " = 1 AND " + Calendars.CALENDAR_ACCESS_LEVEL + " = ? ",
                new String[] {String.valueOf(Calendars.CAL_ACCESS_OWNER)}, null);
        try {
            if (cursor == null || cursor.getCount() < 1) {
                return null;
            }
            cursor.moveToPosition(-1);
            List<String> calendarIds = new ArrayList<>(cursor.getCount());
            while (cursor.moveToNext()) {
                calendarIds.add(String.valueOf(cursor.getInt(0)));
            }
            return calendarIds;
        } finally {
            if (cursor != null) {
                cursor.close();
            }
        }
    }

    @Override
    protected void onStartLoading() {
        super.onStartLoading();

        if (mData != null) {
            deliverResult(mData);
        }

        if (takeContentChanged() || mData == null) {
            forceLoad();
        }
    }

    @Override
    protected void onStopLoading() {
        // Attempt to cancel the current load task if possible.
        cancelLoad();
    }

    @Override
    protected void onReset() {
        super.onReset();

        // Ensure the loader is stopped
        onStopLoading();
        if (mData != null) {
            mData.clear();
        }
    }

    @Override
    public void deliverResult(List<ContactInteraction> data) {
        mData = data;
        if (isStarted()) {
            super.deliverResult(data);
        }
    }
}
