/*
 * Copyright (C) 2020 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.alarm;

import static com.android.server.alarm.AlarmManagerService.dumpAlarmList;
import static com.android.server.alarm.AlarmManagerService.isTimeTickAlarm;

import android.app.AlarmManager;
import android.util.IndentingPrintWriter;
import android.util.Slog;
import android.util.proto.ProtoOutputStream;

import com.android.internal.annotations.VisibleForTesting;
import com.android.internal.util.StatLogger;

import java.text.SimpleDateFormat;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Comparator;
import java.util.function.Predicate;

/**
 * Lazy implementation of an alarm store.
 * This keeps the alarms in a sorted list, and only batches them at the time of delivery.
 */
public class LazyAlarmStore implements AlarmStore {
    @VisibleForTesting
    static final String TAG = LazyAlarmStore.class.getSimpleName();
    private static final long ALARM_DEADLINE_SLOP = 500;

    private final ArrayList<Alarm> mAlarms = new ArrayList<>();
    private Runnable mOnAlarmClockRemoved;

    interface Stats {
        int GET_NEXT_DELIVERY_TIME = 0;
        int GET_NEXT_WAKEUP_DELIVERY_TIME = 1;
        int GET_COUNT = 2;
    }

    final StatLogger mStatLogger = new StatLogger(TAG + " stats", new String[]{
            "GET_NEXT_DELIVERY_TIME",
            "GET_NEXT_WAKEUP_DELIVERY_TIME",
            "GET_COUNT",
    });

    // Decreasing time order because it is more efficient to remove from the tail of an array list.
    private static final Comparator<Alarm> sDecreasingTimeOrder = Comparator.comparingLong(
            Alarm::getWhenElapsed).reversed();

    @Override
    public void add(Alarm a) {
        int index = Collections.binarySearch(mAlarms, a, sDecreasingTimeOrder);
        if (index < 0) {
            index = 0 - index - 1;
        }
        mAlarms.add(index, a);
    }

    @Override
    public void addAll(ArrayList<Alarm> alarms) {
        if (alarms == null) {
            return;
        }
        mAlarms.addAll(alarms);
        Collections.sort(mAlarms, sDecreasingTimeOrder);
    }

    @Override
    public ArrayList<Alarm> remove(Predicate<Alarm> whichAlarms) {
        final ArrayList<Alarm> removedAlarms = new ArrayList<>();
        for (int i = mAlarms.size() - 1; i >= 0; i--) {
            if (whichAlarms.test(mAlarms.get(i))) {
                final Alarm removed = mAlarms.remove(i);
                if (removed.alarmClock != null && mOnAlarmClockRemoved != null) {
                    mOnAlarmClockRemoved.run();
                }
                if (isTimeTickAlarm(removed)) {
                    // This code path is not invoked when delivering alarms, only when removing
                    // alarms due to the caller cancelling it or getting uninstalled, etc.
                    Slog.wtf(TAG, "Removed TIME_TICK alarm");
                }
                removedAlarms.add(removed);
            }
        }
        return removedAlarms;
    }

    @Override
    public void setAlarmClockRemovalListener(Runnable listener) {
        mOnAlarmClockRemoved = listener;
    }

    @Override
    public Alarm getNextWakeFromIdleAlarm() {
        for (int i = mAlarms.size() - 1; i >= 0; i--) {
            final Alarm alarm = mAlarms.get(i);
            if ((alarm.flags & AlarmManager.FLAG_WAKE_FROM_IDLE) != 0) {
                return alarm;
            }
        }
        return null;
    }

    @Override
    public int size() {
        return mAlarms.size();
    }

    @Override
    public long getNextWakeupDeliveryTime() {
        final long start = mStatLogger.getTime();
        long nextWakeup = 0;
        for (int i = mAlarms.size() - 1; i >= 0; i--) {
            final Alarm a = mAlarms.get(i);
            if (!a.wakeup) {
                continue;
            }
            if (nextWakeup == 0) {
                nextWakeup = a.getMaxWhenElapsed();
            } else {
                if (a.getWhenElapsed() > nextWakeup) {
                    break;
                }
                nextWakeup = Math.min(nextWakeup, a.getMaxWhenElapsed());
            }
        }
        mStatLogger.logDurationStat(Stats.GET_NEXT_WAKEUP_DELIVERY_TIME, start);
        return nextWakeup;
    }

    @Override
    public long getNextDeliveryTime() {
        final long start = mStatLogger.getTime();
        final int n = mAlarms.size();
        if (n == 0) {
            return 0;
        }
        long nextDelivery = mAlarms.get(n - 1).getMaxWhenElapsed();
        for (int i = n - 2; i >= 0; i--) {
            final Alarm a = mAlarms.get(i);
            if (a.getWhenElapsed() > nextDelivery) {
                break;
            }
            nextDelivery = Math.min(nextDelivery, a.getMaxWhenElapsed());
        }
        mStatLogger.logDurationStat(Stats.GET_NEXT_DELIVERY_TIME, start);
        return nextDelivery;
    }

    @Override
    public ArrayList<Alarm> removePendingAlarms(long nowElapsed) {
        final ArrayList<Alarm> pending = new ArrayList<>();

        // Only send wake-up alarms if this is the absolutely latest time we can evaluate
        // for at least one wakeup alarm. This prevents sending other non-wakeup alarms when the
        // screen is off but the CPU is awake for some reason.
        boolean sendWakeups = false;

        // If any alarm with FLAG_STANDALONE is present, we cannot send any alarms without that flag
        // in the present batch.
        boolean standalonesOnly = false;

        for (int i = mAlarms.size() - 1; i >= 0; i--) {
            final Alarm alarm = mAlarms.get(i);
            if (alarm.getWhenElapsed() > nowElapsed) {
                break;
            }
            mAlarms.remove(i);
            pending.add(alarm);
            if (alarm.wakeup && alarm.getMaxWhenElapsed() <= nowElapsed + ALARM_DEADLINE_SLOP) {
                // Using some slop as it is better to send the wakeup alarm now, rather than
                // waking up again a short time later, just to send it.
                sendWakeups = true;
            }
            if ((alarm.flags & AlarmManager.FLAG_STANDALONE) != 0) {
                standalonesOnly = true;
            }
        }
        final ArrayList<Alarm> toSend = new ArrayList<>();
        for (int i = pending.size() - 1; i >= 0; i--) {
            final Alarm pendingAlarm = pending.get(i);
            if (!sendWakeups && pendingAlarm.wakeup) {
                continue;
            }
            if (standalonesOnly && (pendingAlarm.flags & AlarmManager.FLAG_STANDALONE) == 0) {
                continue;
            }
            pending.remove(i);
            toSend.add(pendingAlarm);
        }
        // Perhaps some alarms could not be sent right now. Adding them back for later.
        addAll(pending);
        return toSend;
    }

    @Override
    public boolean updateAlarmDeliveries(AlarmDeliveryCalculator deliveryCalculator) {
        boolean changed = false;
        for (final Alarm alarm : mAlarms) {
            changed |= deliveryCalculator.updateAlarmDelivery(alarm);
        }
        if (changed) {
            Collections.sort(mAlarms, sDecreasingTimeOrder);
        }
        return changed;
    }

    @Override
    public ArrayList<Alarm> asList() {
        final ArrayList<Alarm> copy = new ArrayList<>(mAlarms);
        Collections.reverse(copy);
        return copy;
    }

    @Override
    public void dump(IndentingPrintWriter ipw, long nowElapsed, SimpleDateFormat sdf) {
        ipw.println(mAlarms.size() + " pending alarms: ");
        ipw.increaseIndent();
        dumpAlarmList(ipw, mAlarms, nowElapsed, sdf);
        ipw.decreaseIndent();
        mStatLogger.dump(ipw);
    }

    @Override
    public void dumpProto(ProtoOutputStream pos, long nowElapsed) {
        for (final Alarm a : mAlarms) {
            a.dumpDebug(pos, AlarmManagerServiceDumpProto.PENDING_ALARMS, nowElapsed);
        }
    }

    @Override
    public String getName() {
        return TAG;
    }

    @Override
    public int getCount(Predicate<Alarm> condition) {
        long start = mStatLogger.getTime();

        int count = 0;
        for (final Alarm a : mAlarms) {
            if (condition.test(a)) {
                count++;
            }
        }
        mStatLogger.logDurationStat(Stats.GET_COUNT, start);
        return count;
    }
}
