/*
 * Copyright (C) 2015 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.server.notification;

import android.app.AlarmManager;
import android.app.PendingIntent;
import android.content.BroadcastReceiver;
import android.content.ComponentName;
import android.content.Context;
import android.content.Intent;
import android.content.IntentFilter;
import android.content.pm.PackageManager.NameNotFoundException;
import android.net.Uri;
import android.os.Handler;
import android.os.HandlerThread;
import android.os.Looper;
import android.os.Process;
import android.os.UserHandle;
import android.os.UserManager;
import android.service.notification.Condition;
import android.service.notification.IConditionProvider;
import android.service.notification.ZenModeConfig;
import android.service.notification.ZenModeConfig.EventInfo;
import android.util.ArraySet;
import android.util.Log;
import android.util.Slog;
import android.util.SparseArray;

import com.android.server.notification.CalendarTracker.CheckEventResult;
import com.android.server.notification.NotificationManagerService.DumpFilter;

import java.io.PrintWriter;

/**
 * Built-in zen condition provider for calendar event-based conditions.
 */
public class EventConditionProvider extends SystemConditionProviderService {
    private static final String TAG = "ConditionProviders.ECP";
    private static final boolean DEBUG = Log.isLoggable("ConditionProviders", Log.DEBUG);

    public static final ComponentName COMPONENT =
            new ComponentName("android", EventConditionProvider.class.getName());
    private static final String NOT_SHOWN = "...";
    private static final String SIMPLE_NAME = EventConditionProvider.class.getSimpleName();
    private static final String ACTION_EVALUATE = SIMPLE_NAME + ".EVALUATE";
    private static final int REQUEST_CODE_EVALUATE = 1;
    private static final String EXTRA_TIME = "time";
    private static final long CHANGE_DELAY = 2 * 1000;  // coalesce chatty calendar changes

    private final Context mContext = this;
    private final ArraySet<Uri> mSubscriptions = new ArraySet<Uri>();
    private final SparseArray<CalendarTracker> mTrackers = new SparseArray<>();
    private final Handler mWorker;
    private final HandlerThread mThread;

    private boolean mConnected;
    private boolean mRegistered;
    private boolean mBootComplete;  // don't hammer the calendar provider until boot completes.
    private long mNextAlarmTime;

    public EventConditionProvider() {
        if (DEBUG) Slog.d(TAG, "new " + SIMPLE_NAME + "()");
        mThread = new HandlerThread(TAG, Process.THREAD_PRIORITY_BACKGROUND);
        mThread.start();
        mWorker = new Handler(mThread.getLooper());
    }

    @Override
    public ComponentName getComponent() {
        return COMPONENT;
    }

    @Override
    public boolean isValidConditionId(Uri id) {
        return ZenModeConfig.isValidEventConditionId(id);
    }

    @Override
    public void dump(PrintWriter pw, DumpFilter filter) {
        pw.print("    "); pw.print(SIMPLE_NAME); pw.println(":");
        pw.print("      mConnected="); pw.println(mConnected);
        pw.print("      mRegistered="); pw.println(mRegistered);
        pw.print("      mBootComplete="); pw.println(mBootComplete);
        dumpUpcomingTime(pw, "mNextAlarmTime", mNextAlarmTime, System.currentTimeMillis());
        pw.println("      mSubscriptions=");
        for (Uri conditionId : mSubscriptions) {
            pw.print("        ");
            pw.println(conditionId);
        }
        pw.println("      mTrackers=");
        for (int i = 0; i < mTrackers.size(); i++) {
            pw.print("        user="); pw.println(mTrackers.keyAt(i));
            mTrackers.valueAt(i).dump("          ", pw);
        }
    }

    @Override
    public void onBootComplete() {
        if (DEBUG) Slog.d(TAG, "onBootComplete");
        if (mBootComplete) return;
        mBootComplete = true;
        final IntentFilter filter = new IntentFilter();
        filter.addAction(Intent.ACTION_MANAGED_PROFILE_ADDED);
        filter.addAction(Intent.ACTION_MANAGED_PROFILE_REMOVED);
        mContext.registerReceiver(new BroadcastReceiver() {
            @Override
            public void onReceive(Context context, Intent intent) {
                reloadTrackers();
            }
        }, filter);
        reloadTrackers();
    }

    @Override
    public void onConnected() {
        if (DEBUG) Slog.d(TAG, "onConnected");
        mConnected = true;
    }

    @Override
    public void onDestroy() {
        super.onDestroy();
        if (DEBUG) Slog.d(TAG, "onDestroy");
        mConnected = false;
    }

    @Override
    public void onSubscribe(Uri conditionId) {
        if (DEBUG) Slog.d(TAG, "onSubscribe " + conditionId);
        if (!ZenModeConfig.isValidEventConditionId(conditionId)) {
            notifyCondition(conditionId, Condition.STATE_FALSE, "badCondition");
            return;
        }
        if (mSubscriptions.add(conditionId)) {
            evaluateSubscriptions();
        }
    }

    @Override
    public void onUnsubscribe(Uri conditionId) {
        if (DEBUG) Slog.d(TAG, "onUnsubscribe " + conditionId);
        if (mSubscriptions.remove(conditionId)) {
            evaluateSubscriptions();
        }
    }

    @Override
    public void attachBase(Context base) {
        attachBaseContext(base);
    }

    @Override
    public IConditionProvider asInterface() {
        return (IConditionProvider) onBind(null);
    }

    private void reloadTrackers() {
        if (DEBUG) Slog.d(TAG, "reloadTrackers");
        for (int i = 0; i < mTrackers.size(); i++) {
            mTrackers.valueAt(i).setCallback(null);
        }
        mTrackers.clear();
        for (UserHandle user : UserManager.get(mContext).getUserProfiles()) {
            final Context context = user.isSystem() ? mContext : getContextForUser(mContext, user);
            if (context == null) {
                Slog.w(TAG, "Unable to create context for user " + user.getIdentifier());
                continue;
            }
            mTrackers.put(user.getIdentifier(), new CalendarTracker(mContext, context));
        }
        evaluateSubscriptions();
    }

    private void evaluateSubscriptions() {
        if (!mWorker.hasCallbacks(mEvaluateSubscriptionsW)) {
            mWorker.post(mEvaluateSubscriptionsW);
        }
    }

    private void evaluateSubscriptionsW() {
        if (DEBUG) Slog.d(TAG, "evaluateSubscriptions");
        if (!mBootComplete) {
            if (DEBUG) Slog.d(TAG, "Skipping evaluate before boot complete");
            return;
        }
        final long now = System.currentTimeMillis();
        for (int i = 0; i < mTrackers.size(); i++) {
            mTrackers.valueAt(i).setCallback(mSubscriptions.isEmpty() ? null : mTrackerCallback);
        }
        setRegistered(!mSubscriptions.isEmpty());
        long reevaluateAt = 0;
        for (Uri conditionId : mSubscriptions) {
            final EventInfo event = ZenModeConfig.tryParseEventConditionId(conditionId);
            if (event == null) {
                notifyCondition(conditionId, Condition.STATE_FALSE, "badConditionId");
                continue;
            }
            CheckEventResult result = null;
            if (event.calendar == null) { // any calendar
                // event could exist on any tracker
                for (int i = 0; i < mTrackers.size(); i++) {
                    final CalendarTracker tracker = mTrackers.valueAt(i);
                    final CheckEventResult r = tracker.checkEvent(event, now);
                    if (result == null) {
                        result = r;
                    } else {
                        result.inEvent |= r.inEvent;
                        result.recheckAt = Math.min(result.recheckAt, r.recheckAt);
                    }
                }
            } else {
                // event should exist on one tracker
                final int userId = EventInfo.resolveUserId(event.userId);
                final CalendarTracker tracker = mTrackers.get(userId);
                if (tracker == null) {
                    Slog.w(TAG, "No calendar tracker found for user " + userId);
                    notifyCondition(conditionId, Condition.STATE_FALSE, "badUserId");
                    continue;
                }
                result = tracker.checkEvent(event, now);
            }
            if (result.recheckAt != 0 && (reevaluateAt == 0 || result.recheckAt < reevaluateAt)) {
                reevaluateAt = result.recheckAt;
            }
            if (!result.inEvent) {
                notifyCondition(conditionId, Condition.STATE_FALSE, "!inEventNow");
                continue;
            }
            notifyCondition(conditionId, Condition.STATE_TRUE, "inEventNow");
        }
        rescheduleAlarm(now, reevaluateAt);
        if (DEBUG) Slog.d(TAG, "evaluateSubscriptions took " + (System.currentTimeMillis() - now));
    }

    private void rescheduleAlarm(long now, long time) {
        mNextAlarmTime = time;
        final AlarmManager alarms = (AlarmManager) mContext.getSystemService(Context.ALARM_SERVICE);
        final PendingIntent pendingIntent = PendingIntent.getBroadcast(mContext,
                REQUEST_CODE_EVALUATE,
                new Intent(ACTION_EVALUATE)
                        .addFlags(Intent.FLAG_RECEIVER_FOREGROUND)
                        .putExtra(EXTRA_TIME, time),
                PendingIntent.FLAG_UPDATE_CURRENT);
        alarms.cancel(pendingIntent);
        if (time == 0 || time < now) {
            if (DEBUG) Slog.d(TAG, "Not scheduling evaluate: " + (time == 0 ? "no time specified"
                    : "specified time in the past"));
            return;
        }
        if (DEBUG) Slog.d(TAG, String.format("Scheduling evaluate for %s, in %s, now=%s",
                ts(time), formatDuration(time - now), ts(now)));
        alarms.setExact(AlarmManager.RTC_WAKEUP, time, pendingIntent);
    }

    private void notifyCondition(Uri conditionId, int state, String reason) {
        if (DEBUG) Slog.d(TAG, "notifyCondition " + conditionId + " "
                + Condition.stateToString(state) + " reason=" + reason);
        notifyCondition(createCondition(conditionId, state));
    }

    private Condition createCondition(Uri id, int state) {
        final String summary = NOT_SHOWN;
        final String line1 = NOT_SHOWN;
        final String line2 = NOT_SHOWN;
        return new Condition(id, summary, line1, line2, 0, state, Condition.FLAG_RELEVANT_ALWAYS);
    }

    private void setRegistered(boolean registered) {
        if (mRegistered == registered) return;
        if (DEBUG) Slog.d(TAG, "setRegistered " + registered);
        mRegistered = registered;
        if (mRegistered) {
            final IntentFilter filter = new IntentFilter();
            filter.addAction(Intent.ACTION_TIME_CHANGED);
            filter.addAction(Intent.ACTION_TIMEZONE_CHANGED);
            filter.addAction(ACTION_EVALUATE);
            registerReceiver(mReceiver, filter);
        } else {
            unregisterReceiver(mReceiver);
        }
    }

    private static Context getContextForUser(Context context, UserHandle user) {
        try {
            return context.createPackageContextAsUser(context.getPackageName(), 0, user);
        } catch (NameNotFoundException e) {
            return null;
        }
    }

    private final CalendarTracker.Callback mTrackerCallback = new CalendarTracker.Callback() {
        @Override
        public void onChanged() {
            if (DEBUG) Slog.d(TAG, "mTrackerCallback.onChanged");
            mWorker.removeCallbacks(mEvaluateSubscriptionsW);
            mWorker.postDelayed(mEvaluateSubscriptionsW, CHANGE_DELAY);
        }
    };

    private final BroadcastReceiver mReceiver = new BroadcastReceiver() {
        @Override
        public void onReceive(Context context, Intent intent) {
            if (DEBUG) Slog.d(TAG, "onReceive " + intent.getAction());
            evaluateSubscriptions();
        }
    };

    private final Runnable mEvaluateSubscriptionsW = new Runnable() {
        @Override
        public void run() {
            evaluateSubscriptionsW();
        }
    };
}
