/*
 * 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 fr.paug.droidcon.sync.userdata;

import android.content.*;
import android.database.Cursor;
import android.net.Uri;
import android.util.Log;

import fr.paug.droidcon.appwidget.ScheduleWidgetProvider;
import fr.paug.droidcon.gcm.ServerUtilities;
import fr.paug.droidcon.provider.ScheduleContract;
import fr.paug.droidcon.provider.ScheduleContract.MySchedule;

import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;

import static fr.paug.droidcon.util.LogUtils.LOGD;
import static fr.paug.droidcon.util.LogUtils.LOGW;
import static fr.paug.droidcon.util.LogUtils.makeLogTag;


/**
 * Helper class that syncs starred sessions data in a Drive's AppData folder.
 *
 * Protocode:
 *
 *   // when user clicks on "star":
 *   session UI: run updateSession()
 *   this.updateSession():
 *     send addstar/removestar to contentProvider
 *     send broadcast to update any dependent UI
 *     save user actions as pending in shared preferences
 *
 *   // on sync
 *   syncadapter: call this.sync()
 *   this.sync():
 *     fetch remote content
 *     if pending actions:
 *       apply to content and update remote
 *     if modified content != last synced content:
 *       update contentProvider
 *       send broadcast to update any dependent UI
 *
 *
 */
public abstract class AbstractUserDataSyncHelper {
    private static final String TAG = makeLogTag(AbstractUserDataSyncHelper.class);

    protected Context mContext;
    protected String mAccountName;

    public AbstractUserDataSyncHelper(Context context, String accountName) {
        this.mContext = context;
        this.mAccountName = accountName;
    }

    protected abstract boolean syncImpl(List<UserAction> actions, boolean hasPendingLocalData);

    /**
     * Create a copy of current pending actions and delegate the
     * proper sync'ing to the concrete subclass on the method syncImpl.
     *
     */
    public boolean sync() {
        // get data pending sync:
        Cursor scheduleData = mContext.getContentResolver().query(
                MySchedule.buildMyScheduleUri(mContext, mAccountName), MyScheduleQuery.PROJECTION,
                null, null, null);

        if (scheduleData == null) {
            return false;
        }

        // Although we have a dirty flag per item, we need all schedule to sync, because it's all
        // sync'ed at once to a file on AppData folder. We only use the dirty flag to decide if
        // the local content was changed or not. If it was, we replace the remote content.
        boolean hasPendingLocalData = false;
        ArrayList<UserAction> actions = new ArrayList<UserAction>();
        while (scheduleData.moveToNext()) {

            UserAction userAction = new UserAction();
            userAction.sessionId = scheduleData.getString(MyScheduleQuery.SESSION_ID);
            Integer inSchedule = scheduleData.getInt(MyScheduleQuery.IN_SCHEDULE);
            if (inSchedule == 0) {
                userAction.type = UserAction.TYPE.REMOVE_STAR;
            } else {
                userAction.type = UserAction.TYPE.ADD_STAR;
            }
            userAction.requiresSync = scheduleData.getInt(MyScheduleQuery.DIRTY_FLAG) == 1;
            actions.add(userAction);
            if (!hasPendingLocalData && userAction.requiresSync) {
                hasPendingLocalData = true;
            }
        }
        scheduleData.close();

        Log.d(TAG, "Starting Drive AppData sync. hasPendingData = " + hasPendingLocalData);

        boolean dataChanged = syncImpl(actions, hasPendingLocalData);

        if (hasPendingLocalData) {
            resetDirtyFlag(actions);

            // Notify other devices via GCM
            ServerUtilities.notifyUserDataChanged(mContext);
        }
        if (dataChanged) {
            LOGD(TAG, "Notifying changes on paths related to user data on Content Resolver.");
            ContentResolver resolver = mContext.getContentResolver();
            for (String path : ScheduleContract.USER_DATA_RELATED_PATHS) {
                Uri uri = ScheduleContract.BASE_CONTENT_URI.buildUpon().appendPath(path).build();
                resolver.notifyChange(uri, null);
            }
            mContext.sendBroadcast(ScheduleWidgetProvider.getRefreshBroadcastIntent(mContext, false));
        }
        return dataChanged;
    }

    private void resetDirtyFlag(ArrayList<UserAction> actions) {
        ArrayList<ContentProviderOperation> ops = new ArrayList<ContentProviderOperation>();
        for (UserAction action: actions) {
            ContentProviderOperation op = ContentProviderOperation.newUpdate(
                    ScheduleContract.addCallerIsSyncAdapterParameter(
                            MySchedule.buildMyScheduleUri(mContext, mAccountName)))
                    .withSelection(MySchedule.SESSION_ID + "=? AND " +
                                    MySchedule.MY_SCHEDULE_IN_SCHEDULE + "=?",
                            new String[]{action.sessionId,
                                    action.type == UserAction.TYPE.ADD_STAR ? "1" : "0"})
                    .withValue(MySchedule.MY_SCHEDULE_DIRTY_FLAG, 0)
                    .build();
            LOGD(TAG, op.toString());
            ops.add(op);
        }
        try {
            ContentProviderResult[] result = mContext.getContentResolver().applyBatch(
                    ScheduleContract.CONTENT_AUTHORITY, ops);
            LOGD(TAG, "Result of cleaning dirty flags is "+ Arrays.toString(result));
        } catch (Exception ex) {
            LOGW(TAG, "Could not update dirty flags. Ignoring.", ex);
        }
    }

    private interface MyScheduleQuery {

        String[] PROJECTION = {
                MySchedule.SESSION_ID,
                MySchedule.MY_SCHEDULE_IN_SCHEDULE,
                MySchedule.MY_SCHEDULE_DIRTY_FLAG,
        };

        int SESSION_ID = 0;
        int IN_SCHEDULE= 1;
        int DIRTY_FLAG = 2;
    }

}