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

import android.car.builtin.util.Slogf;
import android.util.JsonReader;
import android.util.JsonWriter;

import com.android.car.systeminterface.SystemInterface;
import com.android.car.systeminterface.TimeInterface;
import com.android.internal.annotations.VisibleForTesting;

import java.io.File;
import java.io.FileReader;
import java.io.FileWriter;
import java.io.IOException;
import java.util.Objects;
import java.util.Optional;

/**
 * A class that can keep track of how long its instances are alive for.
 *
 * It can be used as a helper object to track the lifetime of system components, e.g.
 *
 * class InterestingService {
 *     private UptimeTracker mTracker;
 *
 *     public void onCreate() {
 *         mTracker = new UptimeTracker(
 *             "/storage/emulated/0/Android/data/interestingservice.uptime", 1 hour);
 *         mTracker.onCreate();
 *     }
 *
 *     public void onDestroy() {
 *         mTracker.onDestroy();
 *     }
 * }
 *
 * Now it's possible to know how long InterestingService has been alive in the system by querying
 * mTracker.getTotalUptime(). Because this data is stored to disk, the uptime is maintained across
 * process and system reboot boundaries. It is possible to configure periodic snapshot points to
 * ensure that crashes do not cause more than a certain amount of uptime to go untracked.
 */
public class UptimeTracker {
    /**
     * In order to prevent excessive wear-out of the storage, do not allow snapshots to happen
     * more frequently than this value
     */
    public static final long MINIMUM_SNAPSHOT_INTERVAL_MS = 60 * 60 * 1000;

    /**
     * The default snapshot interval if none is given
     */
    private static long DEFAULT_SNAPSHOT_INTERVAL_MS = 5 * 60 * 60 * 1000; // 5 hours

    private final Object mLock = new Object();

    /**
     * The file that uptime metrics are stored to
     */
    private File mUptimeFile;

    /**
     * The uptime value retrieved from mUptimeFile
     */
    private Optional<Long> mHistoricalUptime;

    /**
     * Last value of elapsedRealTime read from the system
     */
    private long mLastRealTimeSnapshot;

    /**
     * The source of real-time and scheduling
     */
    private TimeInterface mTimeInterface;

    public UptimeTracker(File file) {
        this(file, DEFAULT_SNAPSHOT_INTERVAL_MS);
    }

    public UptimeTracker(File file, long snapshotInterval) {
        this(file, snapshotInterval, new TimeInterface.DefaultImpl());
    }

    UptimeTracker(File file, long snapshotInterval, SystemInterface systemInterface) {
        this(file, snapshotInterval, systemInterface.getTimeInterface());
    }

    // This constructor allows one to replace the source of time-based truths with
    // a mock version. This is mostly useful for testing purposes.
    @VisibleForTesting
    UptimeTracker(File file,
            long snapShotIntervalMs,
            TimeInterface timeInterface) {
        long snapshotInterval = snapShotIntervalMs;
        snapshotInterval = Math.max(snapshotInterval, MINIMUM_SNAPSHOT_INTERVAL_MS);
        mUptimeFile = Objects.requireNonNull(file);
        mTimeInterface = timeInterface;
        mLastRealTimeSnapshot = mTimeInterface.getUptime(TimeInterface.EXCLUDE_DEEP_SLEEP_TIME);
        mHistoricalUptime = Optional.empty();

        mTimeInterface.scheduleAction(this::flushSnapshot, snapshotInterval);
    }

    void onDestroy() {
        synchronized (mLock) {
            if (mTimeInterface != null) {
                mTimeInterface.cancelAllActions();
            }
            flushSnapshot();
            mTimeInterface = null;
            mUptimeFile = null;
        }
    }

    /**
     * Return the total amount of uptime that has been observed, in milliseconds.
     *
     * This is the sum of the uptime stored on disk + the uptime seen since the last snapshot.
     */
    long getTotalUptime() {
        synchronized (mLock) {
            if (mTimeInterface == null) {
                return 0;
            }
            return getHistoricalUptimeLocked() + (
                    mTimeInterface.getUptime(TimeInterface.EXCLUDE_DEEP_SLEEP_TIME)
                            - mLastRealTimeSnapshot);
        }
    }

    private long getHistoricalUptimeLocked() {
        if (!mHistoricalUptime.isPresent() && mUptimeFile != null && mUptimeFile.exists()) {
            try {
                JsonReader reader = new JsonReader(new FileReader(mUptimeFile));
                reader.beginObject();
                if (!Objects.equals(reader.nextName(), "uptime")) {
                    throw new IllegalArgumentException(
                        mUptimeFile + " is not in a valid format");
                } else {
                    mHistoricalUptime = Optional.of(reader.nextLong());
                }
                reader.endObject();
                reader.close();
            } catch (IllegalArgumentException | IOException e) {
                Slogf.w(CarLog.TAG_SERVICE,  "unable to read historical uptime data", e);
                mHistoricalUptime = Optional.empty();
            }
        }
        return mHistoricalUptime.orElse(0L);
    }

    private void flushSnapshot() {
        synchronized (mLock) {
            if (mUptimeFile == null) {
                return;
            }
            try {
                long newUptime = getTotalUptime();
                mHistoricalUptime = Optional.of(newUptime);
                mLastRealTimeSnapshot = mTimeInterface.getUptime(
                        TimeInterface.EXCLUDE_DEEP_SLEEP_TIME);

                JsonWriter writer = new JsonWriter(new FileWriter(mUptimeFile));
                writer.beginObject();
                writer.name("uptime");
                writer.value(newUptime);
                writer.endObject();
                writer.close();
            } catch (IOException e) {
                Slogf.w(CarLog.TAG_SERVICE,  "unable to write historical uptime data", e);
            }
        }
    }
}
