/*
 * Copyright (C) 2010 The Android Open Source Project
 *
 * 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.android.calendar;

import com.android.calendar.AsyncQueryService.Operation;

import android.app.IntentService;
import android.content.ContentProviderOperation;
import android.content.ContentResolver;
import android.content.ContentValues;
import android.content.Context;
import android.content.Intent;
import android.content.OperationApplicationException;
import android.database.Cursor;
import android.net.Uri;
import android.os.Handler;
import android.os.Message;
import android.os.RemoteException;
import android.os.SystemClock;
import android.util.Log;

import java.util.ArrayList;
import java.util.Arrays;
import java.util.Iterator;
import java.util.PriorityQueue;
import java.util.concurrent.Delayed;
import java.util.concurrent.TimeUnit;

public class AsyncQueryServiceHelper extends IntentService {
    private static final String TAG = "AsyncQuery";

    private static final PriorityQueue<OperationInfo> sWorkQueue =
        new PriorityQueue<OperationInfo>();

    protected Class<AsyncQueryService> mService = AsyncQueryService.class;

    protected static class OperationInfo implements Delayed{
        public int token; // Used for cancel
        public int op;
        public ContentResolver resolver;
        public Uri uri;
        public String authority;
        public Handler handler;
        public String[] projection;
        public String selection;
        public String[] selectionArgs;
        public String orderBy;
        public Object result;
        public Object cookie;
        public ContentValues values;
        public ArrayList<ContentProviderOperation> cpo;

        /**
         * delayMillis is relative time e.g. 10,000 milliseconds
         */
        public long delayMillis;

        /**
         * scheduleTimeMillis is the time scheduled for this to be processed.
         * e.g. SystemClock.elapsedRealtime() + 10,000 milliseconds Based on
         * {@link android.os.SystemClock#elapsedRealtime }
         */
        private long mScheduledTimeMillis = 0;

        // @VisibleForTesting
        void calculateScheduledTime() {
            mScheduledTimeMillis = SystemClock.elapsedRealtime() + delayMillis;
        }

        // @Override // Uncomment with Java6
        public long getDelay(TimeUnit unit) {
            return unit.convert(mScheduledTimeMillis - SystemClock.elapsedRealtime(),
                    TimeUnit.MILLISECONDS);
        }

        // @Override // Uncomment with Java6
        public int compareTo(Delayed another) {
            OperationInfo anotherArgs = (OperationInfo) another;
            if (this.mScheduledTimeMillis == anotherArgs.mScheduledTimeMillis) {
                return 0;
            } else if (this.mScheduledTimeMillis < anotherArgs.mScheduledTimeMillis) {
                return -1;
            } else {
                return 1;
            }
        }

        @Override
        public String toString() {
            StringBuilder builder = new StringBuilder();
            builder.append("OperationInfo [\n\t token= ");
            builder.append(token);
            builder.append(",\n\t op= ");
            builder.append(Operation.opToChar(op));
            builder.append(",\n\t uri= ");
            builder.append(uri);
            builder.append(",\n\t authority= ");
            builder.append(authority);
            builder.append(",\n\t delayMillis= ");
            builder.append(delayMillis);
            builder.append(",\n\t mScheduledTimeMillis= ");
            builder.append(mScheduledTimeMillis);
            builder.append(",\n\t resolver= ");
            builder.append(resolver);
            builder.append(",\n\t handler= ");
            builder.append(handler);
            builder.append(",\n\t projection= ");
            builder.append(Arrays.toString(projection));
            builder.append(",\n\t selection= ");
            builder.append(selection);
            builder.append(",\n\t selectionArgs= ");
            builder.append(Arrays.toString(selectionArgs));
            builder.append(",\n\t orderBy= ");
            builder.append(orderBy);
            builder.append(",\n\t result= ");
            builder.append(result);
            builder.append(",\n\t cookie= ");
            builder.append(cookie);
            builder.append(",\n\t values= ");
            builder.append(values);
            builder.append(",\n\t cpo= ");
            builder.append(cpo);
            builder.append("\n]");
            return builder.toString();
        }

        /**
         * Compares an user-visible operation to this private OperationInfo
         * object
         *
         * @param o operation to be compared
         * @return true if logically equivalent
         */
        public boolean equivalent(Operation o) {
            return o.token == this.token && o.op == this.op;
        }
    }

    /**
     * Queues the operation for execution
     *
     * @param context
     * @param args OperationInfo object describing the operation
     */
    static public void queueOperation(Context context, OperationInfo args) {
        // Set the schedule time for execution based on the desired delay.
        args.calculateScheduledTime();

        synchronized (sWorkQueue) {
            sWorkQueue.add(args);
            sWorkQueue.notify();
        }

        context.startService(new Intent(context, AsyncQueryServiceHelper.class));
    }

    /**
     * Gets the last delayed operation. It is typically used for canceling.
     *
     * @return Operation object which contains of the last cancelable operation
     */
    static public Operation getLastCancelableOperation() {
        long lastScheduleTime = Long.MIN_VALUE;
        Operation op = null;

        synchronized (sWorkQueue) {
            // Unknown order even for a PriorityQueue
            Iterator<OperationInfo> it = sWorkQueue.iterator();
            while (it.hasNext()) {
                OperationInfo info = it.next();
                if (info.delayMillis > 0 && lastScheduleTime < info.mScheduledTimeMillis) {
                    if (op == null) {
                        op = new Operation();
                    }

                    op.token = info.token;
                    op.op = info.op;
                    op.scheduledExecutionTime = info.mScheduledTimeMillis;

                    lastScheduleTime = info.mScheduledTimeMillis;
                }
            }
        }

        if (AsyncQueryService.localLOGV) {
            Log.d(TAG, "getLastCancelableOperation -> Operation:" + Operation.opToChar(op.op)
                    + " token:" + op.token);
        }
        return op;
    }

    /**
     * Attempts to cancel operation that has not already started. Note that
     * there is no guarantee that the operation will be canceled. They still may
     * result in a call to on[Query/Insert/Update/Delete/Batch]Complete after
     * this call has completed.
     *
     * @param token The token representing the operation to be canceled. If
     *            multiple operations have the same token they will all be
     *            canceled.
     */
    static public int cancelOperation(int token) {
        int canceled = 0;
        synchronized (sWorkQueue) {
            Iterator<OperationInfo> it = sWorkQueue.iterator();
            while (it.hasNext()) {
                if (it.next().token == token) {
                    it.remove();
                    ++canceled;
                }
            }
        }

        if (AsyncQueryService.localLOGV) {
            Log.d(TAG, "cancelOperation(" + token + ") -> " + canceled);
        }
        return canceled;
    }

    public AsyncQueryServiceHelper(String name) {
        super(name);
    }

    public AsyncQueryServiceHelper() {
        super("AsyncQueryServiceHelper");
    }

    @Override
    protected void onHandleIntent(Intent intent) {
        OperationInfo args;

        if (AsyncQueryService.localLOGV) {
            Log.d(TAG, "onHandleIntent: queue size=" + sWorkQueue.size());
        }
        synchronized (sWorkQueue) {
            while (true) {
                /*
                 * This method can be called with no work because of
                 * cancellations
                 */
                if (sWorkQueue.size() == 0) {
                    return;
                } else if (sWorkQueue.size() == 1) {
                    OperationInfo first = sWorkQueue.peek();
                    long waitTime = first.mScheduledTimeMillis - SystemClock.elapsedRealtime();
                    if (waitTime > 0) {
                        try {
                            sWorkQueue.wait(waitTime);
                        } catch (InterruptedException e) {
                        }
                    }
                }

                args = sWorkQueue.poll();
                if (args != null) {
                    // Got work to do. Break out of waiting loop
                    break;
                }
            }
        }

        if (AsyncQueryService.localLOGV) {
            Log.d(TAG, "onHandleIntent: " + args);
        }

        ContentResolver resolver = args.resolver;
        if (resolver != null) {

            switch (args.op) {
                case Operation.EVENT_ARG_QUERY:
                    Cursor cursor;
                    try {
                        cursor = resolver.query(args.uri, args.projection, args.selection,
                                args.selectionArgs, args.orderBy);
                        /*
                         * Calling getCount() causes the cursor window to be
                         * filled, which will make the first access on the main
                         * thread a lot faster
                         */
                        if (cursor != null) {
                            cursor.getCount();
                        }
                    } catch (Exception e) {
                        Log.w(TAG, e.toString());
                        cursor = null;
                    }

                    args.result = cursor;
                    break;

                case Operation.EVENT_ARG_INSERT:
                    args.result = resolver.insert(args.uri, args.values);
                    break;

                case Operation.EVENT_ARG_UPDATE:
                    args.result = resolver.update(args.uri, args.values, args.selection,
                            args.selectionArgs);
                    break;

                case Operation.EVENT_ARG_DELETE:
                    try {
                        args.result = resolver.delete(args.uri, args.selection, args.selectionArgs);
                    } catch (IllegalArgumentException e) {
                        Log.w(TAG, "Delete failed.");
                        Log.w(TAG, e.toString());
                        args.result = 0;
                    }

                    break;

                case Operation.EVENT_ARG_BATCH:
                    try {
                        args.result = resolver.applyBatch(args.authority, args.cpo);
                    } catch (RemoteException e) {
                        Log.e(TAG, e.toString());
                        args.result = null;
                    } catch (OperationApplicationException e) {
                        Log.e(TAG, e.toString());
                        args.result = null;
                    }
                    break;
            }

            /*
             * passing the original token value back to the caller on top of the
             * event values in arg1.
             */
            Message reply = args.handler.obtainMessage(args.token);
            reply.obj = args;
            reply.arg1 = args.op;

            if (AsyncQueryService.localLOGV) {
                Log.d(TAG, "onHandleIntent: op=" + Operation.opToChar(args.op) + ", token="
                        + reply.what);
            }

            reply.sendToTarget();
        }
    }

    @Override
    public void onStart(Intent intent, int startId) {
        if (AsyncQueryService.localLOGV) {
            Log.d(TAG, "onStart startId=" + startId);
        }
        super.onStart(intent, startId);
    }

    @Override
    public void onCreate() {
        if (AsyncQueryService.localLOGV) {
            Log.d(TAG, "onCreate");
        }
        super.onCreate();
    }

    @Override
    public void onDestroy() {
        if (AsyncQueryService.localLOGV) {
            Log.d(TAG, "onDestroy");
        }
        super.onDestroy();
    }
}
