/*
 * Copyright 2014 Google Inc. All rights reserved.
 *
 * 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.google.samples.apps.iosched.service;

import android.util.Log;
import com.google.samples.apps.iosched.Config;
import com.google.samples.apps.iosched.R;
import com.google.samples.apps.iosched.provider.ScheduleContract;
import com.google.samples.apps.iosched.settings.SettingsUtils;
import com.google.samples.apps.iosched.util.AccountUtils;

import android.app.IntentService;
import android.content.ContentProviderOperation;
import android.content.ContentResolver;
import android.content.ContentValues;
import android.content.Intent;
import android.content.OperationApplicationException;
import android.database.Cursor;
import android.net.Uri;
import android.os.Bundle;
import android.os.RemoteException;
import android.provider.CalendarContract;
import android.text.TextUtils;

import java.util.ArrayList;

import static com.google.samples.apps.iosched.util.LogUtils.LOGE;
import static com.google.samples.apps.iosched.util.LogUtils.LOGW;
import static com.google.samples.apps.iosched.util.LogUtils.makeLogTag;

/**
 * Background {@link android.app.Service} that adds or removes session Calendar events through
 * the {@link CalendarContract} API available in Android 4.0 or above.
 */
public class SessionCalendarService extends IntentService {
    private static final String TAG = makeLogTag(SessionCalendarService.class);

    public static final String ACTION_ADD_SESSION_CALENDAR =
            "com.google.samples.apps.iosched.action.ADD_SESSION_CALENDAR";
    public static final String ACTION_REMOVE_SESSION_CALENDAR =
            "com.google.samples.apps.iosched.action.REMOVE_SESSION_CALENDAR";
    public static final String ACTION_UPDATE_ALL_SESSIONS_CALENDAR =
            "com.google.samples.apps.iosched.action.UPDATE_ALL_SESSIONS_CALENDAR";
    public static final String ACTION_UPDATE_ALL_SESSIONS_CALENDAR_COMPLETED =
            "com.google.samples.apps.iosched.action.UPDATE_CALENDAR_COMPLETED";
    public static final String ACTION_CLEAR_ALL_SESSIONS_CALENDAR =
            "com.google.samples.apps.iosched.action.CLEAR_ALL_SESSIONS_CALENDAR";
    public static final String EXTRA_ACCOUNT_NAME =
            "com.google.samples.apps.iosched.extra.ACCOUNT_NAME";
    public static final String EXTRA_SESSION_START =
            "com.google.samples.apps.iosched.extra.SESSION_BLOCK_START";
    public static final String EXTRA_SESSION_END =
            "com.google.samples.apps.iosched.extra.SESSION_BLOCK_END";
    public static final String EXTRA_SESSION_TITLE =
            "com.google.samples.apps.iosched.extra.SESSION_TITLE";
    public static final String EXTRA_SESSION_ROOM =
            "com.google.samples.apps.iosched.extra.SESSION_ROOM";

    private static final long INVALID_CALENDAR_ID = -1;

    // TODO: localize
    private static final String CALENDAR_CLEAR_SEARCH_LIKE_EXPRESSION =
            "%added by Google I/O Android app%";

    public SessionCalendarService() {
        super(TAG);
    }

    @Override
    protected void onHandleIntent(Intent intent) {
        final String action = intent.getAction();
        Log.d(TAG, "Received intent: " + action);

        final ContentResolver resolver = getContentResolver();

        boolean isAddEvent = false;

        if (ACTION_ADD_SESSION_CALENDAR.equals(action)) {
            isAddEvent = true;

        } else if (ACTION_REMOVE_SESSION_CALENDAR.equals(action)) {
            isAddEvent = false;

        } else if (ACTION_UPDATE_ALL_SESSIONS_CALENDAR.equals(action) &&
                SettingsUtils.shouldSyncCalendar(this)) {
            try {
                getContentResolver().applyBatch(CalendarContract.AUTHORITY,
                        processAllSessionsCalendar(resolver, getCalendarId(intent)));
                sendBroadcast(new Intent(
                        SessionCalendarService.ACTION_UPDATE_ALL_SESSIONS_CALENDAR_COMPLETED));
            } catch (RemoteException e) {
                LOGE(TAG, "Error adding all sessions to Google Calendar", e);
            } catch (OperationApplicationException e) {
                LOGE(TAG, "Error adding all sessions to Google Calendar", e);
            }

        } else if (ACTION_CLEAR_ALL_SESSIONS_CALENDAR.equals(action)) {
            try {
                getContentResolver().applyBatch(CalendarContract.AUTHORITY,
                        processClearAllSessions(resolver, getCalendarId(intent)));
            } catch (RemoteException e) {
                LOGE(TAG, "Error clearing all sessions from Google Calendar", e);
            } catch (OperationApplicationException e) {
                LOGE(TAG, "Error clearing all sessions from Google Calendar", e);
            }

        } else {
            return;
        }

        final Uri uri = intent.getData();
        final Bundle extras = intent.getExtras();
        if (uri == null || extras == null || !SettingsUtils.shouldSyncCalendar(this)) {
            return;
        }

        try {
            resolver.applyBatch(CalendarContract.AUTHORITY,
                    processSessionCalendar(resolver, getCalendarId(intent), isAddEvent, uri,
                            extras.getLong(EXTRA_SESSION_START),
                            extras.getLong(EXTRA_SESSION_END),
                            extras.getString(EXTRA_SESSION_TITLE),
                            extras.getString(EXTRA_SESSION_ROOM)));
        } catch (RemoteException e) {
            LOGE(TAG, "Error adding session to Google Calendar", e);
        } catch (OperationApplicationException e) {
            LOGE(TAG, "Error adding session to Google Calendar", e);
        }
    }

    /**
     * Gets the currently-logged in user's Google Calendar, or the Google Calendar for the user
     * specified in the given intent's {@link #EXTRA_ACCOUNT_NAME}.
     */
    private long getCalendarId(Intent intent) {
        final String accountName;
        if (intent != null && intent.hasExtra(EXTRA_ACCOUNT_NAME)) {
            accountName = intent.getStringExtra(EXTRA_ACCOUNT_NAME);
        } else {
            accountName = AccountUtils.getActiveAccountName(this);
        }

        if (TextUtils.isEmpty(accountName)) {
            return INVALID_CALENDAR_ID;
        }

        // TODO: The calendar ID should be stored in shared settings_prefs upon choosing an account.
        Cursor calendarsCursor = getContentResolver().query(
                CalendarContract.Calendars.CONTENT_URI,
                new String[]{"_id"},
                // TODO: What if the calendar is not displayed or not sync'd?
                "account_name = ownerAccount and account_name = ?",
                new String[]{accountName},
                null);

        long calendarId = INVALID_CALENDAR_ID;
        if (calendarsCursor != null && calendarsCursor.moveToFirst()) {
            calendarId = calendarsCursor.getLong(0);
            calendarsCursor.close();
        }

        return calendarId;
    }

    private String makeCalendarEventTitle(String sessionTitle) {
        return sessionTitle + getResources().getString(R.string.session_calendar_suffix);
    }

    /**
     * Processes all sessions in the
     * {@link com.google.samples.apps.iosched.provider.ScheduleProvider}, adding or removing
     * calendar events to/from the specified Google Calendar depending on whether a session is
     * in the user's schedule or not.
     */
    private ArrayList<ContentProviderOperation> processAllSessionsCalendar(ContentResolver resolver,
            final long calendarId) {

        ArrayList<ContentProviderOperation> batch = new ArrayList<ContentProviderOperation>();

        // Unable to find the Calendar associated with the user. Stop here.
        if (calendarId == INVALID_CALENDAR_ID) {
            return batch;
        }

        // Retrieves all sessions. For each session, add to Calendar if starred and attempt to
        // remove from Calendar if unstarred.
        Cursor cursor = resolver.query(
                ScheduleContract.Sessions.CONTENT_URI,
                SessionsQuery.PROJECTION,
                null, null, null);

        if (cursor != null) {
            while (cursor.moveToNext()) {
                Uri uri = ScheduleContract.Sessions.buildSessionUri(
                        Long.valueOf(cursor.getLong(0)).toString());
                boolean isAddEvent = (cursor.getInt(SessionsQuery.SESSION_IN_MY_SCHEDULE) == 1);
                if (isAddEvent) {
                    batch.addAll(processSessionCalendar(resolver,
                            calendarId, isAddEvent, uri,
                            cursor.getLong(SessionsQuery.SESSION_START),
                            cursor.getLong(SessionsQuery.SESSION_END),
                            cursor.getString(SessionsQuery.SESSION_TITLE),
                            cursor.getString(SessionsQuery.ROOM_NAME)));
                }
            }
            cursor.close();
        }

        return batch;
    }

    /**
     * Adds or removes a single session to/from the specified Google Calendar.
     */
    private ArrayList<ContentProviderOperation> processSessionCalendar(
            final ContentResolver resolver,
            final long calendarId, final boolean isAddEvent,
            final Uri sessionUri, final long sessionBlockStart, final long sessionBlockEnd,
            final String sessionTitle, final String sessionRoom) {
        ArrayList<ContentProviderOperation> batch = new ArrayList<ContentProviderOperation>();

        // Unable to find the Calendar associated with the user. Stop here.
        if (calendarId == INVALID_CALENDAR_ID) {
            return batch;
        }

        final String calendarEventTitle = makeCalendarEventTitle(sessionTitle);

        Cursor cursor;
        ContentValues values = new ContentValues();

        // Add Calendar event.
        if (isAddEvent) {
            if (sessionBlockStart == 0L || sessionBlockEnd == 0L || sessionTitle == null) {
                LOGW(TAG, "Unable to add a Calendar event due to insufficient input parameters.");
                return batch;
            }

            // Check if the calendar event exists first.  If it does, we don't want to add a
            // duplicate one.
            cursor = resolver.query(
                    CalendarContract.Events.CONTENT_URI,                       // URI
                    new String[] {CalendarContract.Events._ID},                // Projection
                    CalendarContract.Events.CALENDAR_ID + "=? and "            // Selection
                            + CalendarContract.Events.TITLE + "=? and "
                            + CalendarContract.Events.DTSTART + ">=? and "
                            + CalendarContract.Events.DTEND + "<=?",
                    new String[]{                                              // Selection args
                            Long.valueOf(calendarId).toString(),
                            calendarEventTitle,
                            Long.toString(Config.CONFERENCE_START_MILLIS),
                            Long.toString(Config.CONFERENCE_END_MILLIS)
                    },
                    null);

            long newEventId = -1;

            if (cursor != null && cursor.moveToFirst()) {
                // Calendar event already exists for this session.
                newEventId = cursor.getLong(0);
                cursor.close();

                // Data fix (workaround):
                batch.add(
                        ContentProviderOperation.newUpdate(CalendarContract.Events.CONTENT_URI)
                                .withValue(CalendarContract.Events.EVENT_TIMEZONE,
                                        Config.CONFERENCE_TIMEZONE.getID())
                                .withSelection(CalendarContract.Events._ID + "=?",
                                        new String[]{Long.valueOf(newEventId).toString()})
                                .build()
                );
                // End data fix.

            } else {
                // Calendar event doesn't exist, create it.

                // NOTE: we can't use batch processing here because we need the result of
                // the insert.
                values.clear();
                values.put(CalendarContract.Events.DTSTART, sessionBlockStart);
                values.put(CalendarContract.Events.DTEND, sessionBlockEnd);
                values.put(CalendarContract.Events.EVENT_LOCATION, sessionRoom);
                values.put(CalendarContract.Events.TITLE, calendarEventTitle);
                values.put(CalendarContract.Events.CALENDAR_ID, calendarId);
                values.put(CalendarContract.Events.EVENT_TIMEZONE,
                        Config.CONFERENCE_TIMEZONE.getID());
                Uri eventUri = resolver.insert(CalendarContract.Events.CONTENT_URI, values);
                String eventId = eventUri.getLastPathSegment();
                if (eventId == null) {
                    return batch; // Should be empty at this point
                }

                newEventId = Long.valueOf(eventId);
                // Since we're adding session reminder to system notification, we're not creating
                // Calendar event reminders.  If we were to create Calendar event reminders, this
                // is how we would do it.
                //values.put(CalendarContract.Reminders.EVENT_ID, Integer.valueOf(eventId));
                //values.put(CalendarContract.Reminders.MINUTES, 10);
                //values.put(CalendarContract.Reminders.METHOD,
                //        CalendarContract.Reminders.METHOD_ALERT); // Or default?
                //cr.insert(CalendarContract.Reminders.CONTENT_URI, values);
                //values.clear();
            }

            // Update the session in our own provider with the newly created calendar event ID.
            values.clear();
            values.put(ScheduleContract.Sessions.SESSION_CAL_EVENT_ID, newEventId);
            resolver.update(sessionUri, values, null, null);

        } else {
            // Remove Calendar event, if exists.

            // Get the event calendar id.
            cursor = resolver.query(sessionUri,
                    new String[] {ScheduleContract.Sessions.SESSION_CAL_EVENT_ID},
                    null, null, null);
            long calendarEventId = -1;
            if (cursor != null && cursor.moveToFirst()) {
                calendarEventId = cursor.getLong(0);
                cursor.close();
            }

            // Try to remove the Calendar Event based on key.  If successful, move on;
            // otherwise, remove the event based on Event title.
            int affectedRows = 0;
            if (calendarEventId != -1) {
                affectedRows = resolver.delete(
                        CalendarContract.Events.CONTENT_URI,
                        CalendarContract.Events._ID + "=?",
                        new String[]{Long.valueOf(calendarEventId).toString()});
            }

            if (affectedRows == 0) {
                resolver.delete(CalendarContract.Events.CONTENT_URI,
                        String.format("%s=? and %s=? and %s=? and %s=?",
                                CalendarContract.Events.CALENDAR_ID,
                                CalendarContract.Events.TITLE,
                                CalendarContract.Events.DTSTART,
                                CalendarContract.Events.DTEND),
                        new String[]{Long.valueOf(calendarId).toString(),
                                calendarEventTitle,
                                Long.valueOf(sessionBlockStart).toString(),
                                Long.valueOf(sessionBlockEnd).toString()});
            }

            // Remove the session and calendar event association.
            values.clear();
            values.put(ScheduleContract.Sessions.SESSION_CAL_EVENT_ID, (Long) null);
            resolver.update(sessionUri, values, null, null);
        }

        return batch;
    }

    /**
     * Removes all calendar entries associated with Google I/O 2013.
     */
    private ArrayList<ContentProviderOperation> processClearAllSessions(
            ContentResolver resolver, long calendarId) {

        ArrayList<ContentProviderOperation> batch = new ArrayList<ContentProviderOperation>();

        // Unable to find the Calendar associated with the user. Stop here.
        if (calendarId == INVALID_CALENDAR_ID) {
            Log.e(TAG, "Unable to find Calendar for user");
            return batch;
        }

        // Delete all calendar entries matching the given title within the given time period
        batch.add(ContentProviderOperation
                .newDelete(CalendarContract.Events.CONTENT_URI)
                .withSelection(
                        CalendarContract.Events.CALENDAR_ID + " = ? and "
                                + CalendarContract.Events.TITLE + " LIKE ? and "
                                + CalendarContract.Events.DTSTART + ">= ? and "
                                + CalendarContract.Events.DTEND + "<= ?",
                        new String[]{
                                Long.toString(calendarId),
                                CALENDAR_CLEAR_SEARCH_LIKE_EXPRESSION,
                                Long.toString(Config.CONFERENCE_START_MILLIS),
                                Long.toString(Config.CONFERENCE_END_MILLIS)
                        }
                )
                .build());

        return batch;
    }

    private interface SessionsQuery {
        String[] PROJECTION = {
                ScheduleContract.Sessions._ID,
                ScheduleContract.Sessions.SESSION_START,
                ScheduleContract.Sessions.SESSION_END,
                ScheduleContract.Sessions.SESSION_TITLE,
                ScheduleContract.Sessions.ROOM_NAME,
                ScheduleContract.Sessions.SESSION_IN_MY_SCHEDULE,
        };

        int _ID = 0;
        int SESSION_START = 1;
        int SESSION_END = 2;
        int SESSION_TITLE = 3;
        int ROOM_NAME = 4;
        int SESSION_IN_MY_SCHEDULE = 5;
    }
}
