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

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

import com.google.samples.apps.iosched.appwidget.ScheduleWidgetProvider;
import com.google.samples.apps.iosched.framework.QueryEnum;
import com.google.samples.apps.iosched.gcm.ServerUtilities;
import com.google.samples.apps.iosched.provider.ScheduleContract;
import com.google.samples.apps.iosched.provider.ScheduleContract.MySchedule;
import com.google.samples.apps.iosched.provider.ScheduleContract.MyFeedbackSubmitted;
import com.google.samples.apps.iosched.provider.ScheduleContract.MyViewedVideos;
import com.google.samples.apps.iosched.provider.ScheduleContractHelper;

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

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


/**
 * Helper class that syncs user 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 settings_prefs
 *
 *   // 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() {
        // Although we have a dirty flag per item, we need all schedule/viewed videos 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<>();

        // Get schedule data pending sync.
        Cursor scheduleData = mContext.getContentResolver().query(
                MySchedule.buildMyScheduleUri(mAccountName),
                UserDataQueryEnum.MY_SCHEDULE.getProjection(), null, null, null);

        if (scheduleData != null) {
            while (scheduleData.moveToNext()) {
                UserAction userAction = new UserAction();
                userAction.sessionId = scheduleData.getString(
                        scheduleData.getColumnIndex(MySchedule.SESSION_ID));
                Integer inSchedule = scheduleData.getInt(
                        scheduleData.getColumnIndex(MySchedule.MY_SCHEDULE_IN_SCHEDULE));
                if (inSchedule == 0) {
                    userAction.type = UserAction.TYPE.REMOVE_STAR;
                } else {
                    userAction.type = UserAction.TYPE.ADD_STAR;
                }
                userAction.requiresSync = scheduleData.getInt(
                        scheduleData.getColumnIndex(MySchedule.MY_SCHEDULE_DIRTY_FLAG)) == 1;
                actions.add(userAction);
                if (!hasPendingLocalData && userAction.requiresSync) {
                    hasPendingLocalData = true;
                }
            }
            scheduleData.close();
        }

        // Get video viewed data pending sync.
        Cursor videoViewed = mContext.getContentResolver().query(
                ScheduleContract.MyViewedVideos.buildMyViewedVideosUri(mAccountName),
                UserDataQueryEnum.MY_VIEWED_VIDEO.getProjection(), null, null, null);

        if (videoViewed != null) {
            while (videoViewed.moveToNext()) {
                UserAction userAction = new UserAction();
                userAction.videoId = videoViewed.getString(
                        videoViewed.getColumnIndex(MyViewedVideos.VIDEO_ID));
                userAction.type = UserAction.TYPE.VIEW_VIDEO;
                userAction.requiresSync = videoViewed.getInt(
                        videoViewed.getColumnIndex(
                                MyViewedVideos.MY_VIEWED_VIDEOS_DIRTY_FLAG)) == 1;
                actions.add(userAction);
                if (!hasPendingLocalData && userAction.requiresSync) {
                    hasPendingLocalData = true;
                }
            }
            videoViewed.close();
        }

        // Get feedback submitted data pending sync.
        Cursor feedbackSubmitted = mContext.getContentResolver().query(
                MyFeedbackSubmitted.buildMyFeedbackSubmittedUri(mAccountName),
                UserDataQueryEnum.MY_FEEDBACK_SUBMITTED.getProjection(), null, null, null);

        if (feedbackSubmitted != null) {
            while (feedbackSubmitted.moveToNext()) {
                UserAction userAction = new UserAction();
                userAction.sessionId = feedbackSubmitted.getString(
                        feedbackSubmitted.getColumnIndex(MyFeedbackSubmitted.SESSION_ID));
                userAction.type = UserAction.TYPE.VIEW_VIDEO;
                userAction.requiresSync = feedbackSubmitted.getInt(
                        feedbackSubmitted.getColumnIndex(
                                MyFeedbackSubmitted.MY_FEEDBACK_SUBMITTED_DIRTY_FLAG)) == 1;
                actions.add(userAction);
                if (!hasPendingLocalData && userAction.requiresSync) {
                    hasPendingLocalData = true;
                }
            }
            feedbackSubmitted.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<>();
        for (UserAction action : actions) {

            Uri baseUri;
            String with;
            String[] withSelectionValue;
            String dirtyField;

            if (action.type == UserAction.TYPE.VIEW_VIDEO) {
                baseUri = MyViewedVideos.buildMyViewedVideosUri(mAccountName);
                with = MyViewedVideos.VIDEO_ID + "=?";
                withSelectionValue = new String[]{action.videoId};
                dirtyField = MyViewedVideos.MY_VIEWED_VIDEOS_DIRTY_FLAG;
            } else if (action.type == UserAction.TYPE.SUBMIT_FEEDBACK) {
                baseUri = MyFeedbackSubmitted.buildMyFeedbackSubmittedUri(mAccountName);
                with = MyFeedbackSubmitted.SESSION_ID + "=?";
                withSelectionValue = new String[]{action.sessionId};
                dirtyField = MyFeedbackSubmitted.MY_FEEDBACK_SUBMITTED_DIRTY_FLAG;
            } else {
                baseUri = MySchedule.buildMyScheduleUri(mAccountName);
                with = MySchedule.SESSION_ID + "=? AND "
                        + MySchedule.MY_SCHEDULE_IN_SCHEDULE + "=?";
                withSelectionValue = new String[]{action.sessionId,
                        action.type == UserAction.TYPE.ADD_STAR ? "1" : "0"};
                dirtyField = MySchedule.MY_SCHEDULE_DIRTY_FLAG;
            }

            ContentProviderOperation op = ContentProviderOperation.newUpdate(
                    ScheduleContractHelper.setUriAsCalledFromSyncAdapter(baseUri))
                    .withSelection(with, withSelectionValue)
                    .withValue(dirtyField, 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 enum UserDataQueryEnum implements QueryEnum {
        MY_SCHEDULE(0, new String[]{MySchedule.SESSION_ID, MySchedule.MY_SCHEDULE_IN_SCHEDULE,
                MySchedule.MY_SCHEDULE_DIRTY_FLAG}),

        MY_FEEDBACK_SUBMITTED(0, new String[]{MyFeedbackSubmitted.SESSION_ID,
                MyFeedbackSubmitted.MY_FEEDBACK_SUBMITTED_DIRTY_FLAG}),

        MY_VIEWED_VIDEO(0, new String[]{MyViewedVideos.VIDEO_ID,
                MyViewedVideos.MY_VIEWED_VIDEOS_DIRTY_FLAG});

        private int id;

        private String[] projection;

        UserDataQueryEnum(int id, String[] projection) {
            this.id = id;
            this.projection = projection;
        }

        @Override
        public int getId() {
            return id;
        }

        @Override
        public String[] getProjection() {
            return projection;
        }

    }
}
