/*
 *  Copyright 2015 The WebRTC Project Authors. All rights reserved.
 *
 *  Use of this source code is governed by a BSD-style license
 *  that can be found in the LICENSE file in the root of the source
 *  tree. An additional intellectual property rights grant can be found
 *  in the file PATENTS.  All contributing project authors may
 *  be found in the AUTHORS file in the root of the source tree.
 */

package org.appspot.apprtc;

import android.annotation.TargetApi;
import android.content.Context;
import android.content.Intent;
import android.content.IntentFilter;
import android.os.BatteryManager;
import android.os.Build;
import android.os.SystemClock;
import android.util.Log;
import androidx.annotation.Nullable;
import java.io.BufferedReader;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.io.InputStreamReader;
import java.nio.charset.Charset;
import java.util.Arrays;
import java.util.Scanner;
import java.util.concurrent.Executors;
import java.util.concurrent.Future;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.TimeUnit;

/**
 * Simple CPU monitor.  The caller creates a CpuMonitor object which can then
 * be used via sampleCpuUtilization() to collect the percentual use of the
 * cumulative CPU capacity for all CPUs running at their nominal frequency.  3
 * values are generated: (1) getCpuCurrent() returns the use since the last
 * sampleCpuUtilization(), (2) getCpuAvg3() returns the use since 3 prior
 * calls, and (3) getCpuAvgAll() returns the use over all SAMPLE_SAVE_NUMBER
 * calls.
 *
 * <p>CPUs in Android are often "offline", and while this of course means 0 Hz
 * as current frequency, in this state we cannot even get their nominal
 * frequency.  We therefore tread carefully, and allow any CPU to be missing.
 * Missing CPUs are assumed to have the same nominal frequency as any close
 * lower-numbered CPU, but as soon as it is online, we'll get their proper
 * frequency and remember it.  (Since CPU 0 in practice always seem to be
 * online, this unidirectional frequency inheritance should be no problem in
 * practice.)
 *
 * <p>Caveats:
 *   o No provision made for zany "turbo" mode, common in the x86 world.
 *   o No provision made for ARM big.LITTLE; if CPU n can switch behind our
 *     back, we might get incorrect estimates.
 *   o This is not thread-safe.  To call asynchronously, create different
 *     CpuMonitor objects.
 *
 * <p>If we can gather enough info to generate a sensible result,
 * sampleCpuUtilization returns true.  It is designed to never throw an
 * exception.
 *
 * <p>sampleCpuUtilization should not be called too often in its present form,
 * since then deltas would be small and the percent values would fluctuate and
 * be unreadable. If it is desirable to call it more often than say once per
 * second, one would need to increase SAMPLE_SAVE_NUMBER and probably use
 * Queue<Integer> to avoid copying overhead.
 *
 * <p>Known problems:
 *   1. Nexus 7 devices running Kitkat have a kernel which often output an
 *      incorrect 'idle' field in /proc/stat.  The value is close to twice the
 *      correct value, and then returns to back to correct reading.  Both when
 *      jumping up and back down we might create faulty CPU load readings.
 */
class CpuMonitor {
  private static final String TAG = "CpuMonitor";
  private static final int MOVING_AVERAGE_SAMPLES = 5;

  private static final int CPU_STAT_SAMPLE_PERIOD_MS = 2000;
  private static final int CPU_STAT_LOG_PERIOD_MS = 6000;

  private final Context appContext;
  // User CPU usage at current frequency.
  private final MovingAverage userCpuUsage;
  // System CPU usage at current frequency.
  private final MovingAverage systemCpuUsage;
  // Total CPU usage relative to maximum frequency.
  private final MovingAverage totalCpuUsage;
  // CPU frequency in percentage from maximum.
  private final MovingAverage frequencyScale;

  @Nullable
  private ScheduledExecutorService executor;
  private long lastStatLogTimeMs;
  private long[] cpuFreqMax;
  private int cpusPresent;
  private int actualCpusPresent;
  private boolean initialized;
  private boolean cpuOveruse;
  private String[] maxPath;
  private String[] curPath;
  private double[] curFreqScales;
  @Nullable
  private ProcStat lastProcStat;

  private static class ProcStat {
    final long userTime;
    final long systemTime;
    final long idleTime;

    ProcStat(long userTime, long systemTime, long idleTime) {
      this.userTime = userTime;
      this.systemTime = systemTime;
      this.idleTime = idleTime;
    }
  }

  private static class MovingAverage {
    private final int size;
    private double sum;
    private double currentValue;
    private double[] circBuffer;
    private int circBufferIndex;

    public MovingAverage(int size) {
      if (size <= 0) {
        throw new AssertionError("Size value in MovingAverage ctor should be positive.");
      }
      this.size = size;
      circBuffer = new double[size];
    }

    public void reset() {
      Arrays.fill(circBuffer, 0);
      circBufferIndex = 0;
      sum = 0;
      currentValue = 0;
    }

    public void addValue(double value) {
      sum -= circBuffer[circBufferIndex];
      circBuffer[circBufferIndex++] = value;
      currentValue = value;
      sum += value;
      if (circBufferIndex >= size) {
        circBufferIndex = 0;
      }
    }

    public double getCurrent() {
      return currentValue;
    }

    public double getAverage() {
      return sum / (double) size;
    }
  }

  public static boolean isSupported() {
    return Build.VERSION.SDK_INT < Build.VERSION_CODES.N;
  }

  public CpuMonitor(Context context) {
    if (!isSupported()) {
      throw new RuntimeException("CpuMonitor is not supported on this Android version.");
    }

    Log.d(TAG, "CpuMonitor ctor.");
    appContext = context.getApplicationContext();
    userCpuUsage = new MovingAverage(MOVING_AVERAGE_SAMPLES);
    systemCpuUsage = new MovingAverage(MOVING_AVERAGE_SAMPLES);
    totalCpuUsage = new MovingAverage(MOVING_AVERAGE_SAMPLES);
    frequencyScale = new MovingAverage(MOVING_AVERAGE_SAMPLES);
    lastStatLogTimeMs = SystemClock.elapsedRealtime();

    scheduleCpuUtilizationTask();
  }

  public void pause() {
    if (executor != null) {
      Log.d(TAG, "pause");
      executor.shutdownNow();
      executor = null;
    }
  }

  public void resume() {
    Log.d(TAG, "resume");
    resetStat();
    scheduleCpuUtilizationTask();
  }

  // TODO(bugs.webrtc.org/8491): Remove NoSynchronizedMethodCheck suppression.
  @SuppressWarnings("NoSynchronizedMethodCheck")
  public synchronized void reset() {
    if (executor != null) {
      Log.d(TAG, "reset");
      resetStat();
      cpuOveruse = false;
    }
  }

  // TODO(bugs.webrtc.org/8491): Remove NoSynchronizedMethodCheck suppression.
  @SuppressWarnings("NoSynchronizedMethodCheck")
  public synchronized int getCpuUsageCurrent() {
    return doubleToPercent(userCpuUsage.getCurrent() + systemCpuUsage.getCurrent());
  }

  // TODO(bugs.webrtc.org/8491): Remove NoSynchronizedMethodCheck suppression.
  @SuppressWarnings("NoSynchronizedMethodCheck")
  public synchronized int getCpuUsageAverage() {
    return doubleToPercent(userCpuUsage.getAverage() + systemCpuUsage.getAverage());
  }

  // TODO(bugs.webrtc.org/8491): Remove NoSynchronizedMethodCheck suppression.
  @SuppressWarnings("NoSynchronizedMethodCheck")
  public synchronized int getFrequencyScaleAverage() {
    return doubleToPercent(frequencyScale.getAverage());
  }

  private void scheduleCpuUtilizationTask() {
    if (executor != null) {
      executor.shutdownNow();
      executor = null;
    }

    executor = Executors.newSingleThreadScheduledExecutor();
    @SuppressWarnings("unused") // Prevent downstream linter warnings.
    Future<?> possiblyIgnoredError = executor.scheduleAtFixedRate(new Runnable() {
      @Override
      public void run() {
        cpuUtilizationTask();
      }
    }, 0, CPU_STAT_SAMPLE_PERIOD_MS, TimeUnit.MILLISECONDS);
  }

  private void cpuUtilizationTask() {
    boolean cpuMonitorAvailable = sampleCpuUtilization();
    if (cpuMonitorAvailable
        && SystemClock.elapsedRealtime() - lastStatLogTimeMs >= CPU_STAT_LOG_PERIOD_MS) {
      lastStatLogTimeMs = SystemClock.elapsedRealtime();
      String statString = getStatString();
      Log.d(TAG, statString);
    }
  }

  private void init() {
    try (FileInputStream fin = new FileInputStream("/sys/devices/system/cpu/present");
         InputStreamReader streamReader = new InputStreamReader(fin, Charset.forName("UTF-8"));
         BufferedReader reader = new BufferedReader(streamReader);
         Scanner scanner = new Scanner(reader).useDelimiter("[-\n]");) {
      scanner.nextInt(); // Skip leading number 0.
      cpusPresent = 1 + scanner.nextInt();
      scanner.close();
    } catch (FileNotFoundException e) {
      Log.e(TAG, "Cannot do CPU stats since /sys/devices/system/cpu/present is missing");
    } catch (IOException e) {
      Log.e(TAG, "Error closing file");
    } catch (Exception e) {
      Log.e(TAG, "Cannot do CPU stats due to /sys/devices/system/cpu/present parsing problem");
    }

    cpuFreqMax = new long[cpusPresent];
    maxPath = new String[cpusPresent];
    curPath = new String[cpusPresent];
    curFreqScales = new double[cpusPresent];
    for (int i = 0; i < cpusPresent; i++) {
      cpuFreqMax[i] = 0; // Frequency "not yet determined".
      curFreqScales[i] = 0;
      maxPath[i] = "/sys/devices/system/cpu/cpu" + i + "/cpufreq/cpuinfo_max_freq";
      curPath[i] = "/sys/devices/system/cpu/cpu" + i + "/cpufreq/scaling_cur_freq";
    }

    lastProcStat = new ProcStat(0, 0, 0);
    resetStat();

    initialized = true;
  }

  private synchronized void resetStat() {
    userCpuUsage.reset();
    systemCpuUsage.reset();
    totalCpuUsage.reset();
    frequencyScale.reset();
    lastStatLogTimeMs = SystemClock.elapsedRealtime();
  }

  private int getBatteryLevel() {
    // Use sticky broadcast with null receiver to read battery level once only.
    Intent intent = appContext.registerReceiver(
        null /* receiver */, new IntentFilter(Intent.ACTION_BATTERY_CHANGED));

    int batteryLevel = 0;
    int batteryScale = intent.getIntExtra(BatteryManager.EXTRA_SCALE, 100);
    if (batteryScale > 0) {
      batteryLevel =
          (int) (100f * intent.getIntExtra(BatteryManager.EXTRA_LEVEL, 0) / batteryScale);
    }
    return batteryLevel;
  }

  /**
   * Re-measure CPU use.  Call this method at an interval of around 1/s.
   * This method returns true on success.  The fields
   * cpuCurrent, cpuAvg3, and cpuAvgAll are updated on success, and represents:
   * cpuCurrent: The CPU use since the last sampleCpuUtilization call.
   * cpuAvg3: The average CPU over the last 3 calls.
   * cpuAvgAll: The average CPU over the last SAMPLE_SAVE_NUMBER calls.
   */
  private synchronized boolean sampleCpuUtilization() {
    long lastSeenMaxFreq = 0;
    long cpuFreqCurSum = 0;
    long cpuFreqMaxSum = 0;

    if (!initialized) {
      init();
    }
    if (cpusPresent == 0) {
      return false;
    }

    actualCpusPresent = 0;
    for (int i = 0; i < cpusPresent; i++) {
      /*
       * For each CPU, attempt to first read its max frequency, then its
       * current frequency.  Once as the max frequency for a CPU is found,
       * save it in cpuFreqMax[].
       */

      curFreqScales[i] = 0;
      if (cpuFreqMax[i] == 0) {
        // We have never found this CPU's max frequency.  Attempt to read it.
        long cpufreqMax = readFreqFromFile(maxPath[i]);
        if (cpufreqMax > 0) {
          Log.d(TAG, "Core " + i + ". Max frequency: " + cpufreqMax);
          lastSeenMaxFreq = cpufreqMax;
          cpuFreqMax[i] = cpufreqMax;
          maxPath[i] = null; // Kill path to free its memory.
        }
      } else {
        lastSeenMaxFreq = cpuFreqMax[i]; // A valid, previously read value.
      }

      long cpuFreqCur = readFreqFromFile(curPath[i]);
      if (cpuFreqCur == 0 && lastSeenMaxFreq == 0) {
        // No current frequency information for this CPU core - ignore it.
        continue;
      }
      if (cpuFreqCur > 0) {
        actualCpusPresent++;
      }
      cpuFreqCurSum += cpuFreqCur;

      /* Here, lastSeenMaxFreq might come from
       * 1. cpuFreq[i], or
       * 2. a previous iteration, or
       * 3. a newly read value, or
       * 4. hypothetically from the pre-loop dummy.
       */
      cpuFreqMaxSum += lastSeenMaxFreq;
      if (lastSeenMaxFreq > 0) {
        curFreqScales[i] = (double) cpuFreqCur / lastSeenMaxFreq;
      }
    }

    if (cpuFreqCurSum == 0 || cpuFreqMaxSum == 0) {
      Log.e(TAG, "Could not read max or current frequency for any CPU");
      return false;
    }

    /*
     * Since the cycle counts are for the period between the last invocation
     * and this present one, we average the percentual CPU frequencies between
     * now and the beginning of the measurement period.  This is significantly
     * incorrect only if the frequencies have peeked or dropped in between the
     * invocations.
     */
    double currentFrequencyScale = cpuFreqCurSum / (double) cpuFreqMaxSum;
    if (frequencyScale.getCurrent() > 0) {
      currentFrequencyScale = (frequencyScale.getCurrent() + currentFrequencyScale) * 0.5;
    }

    ProcStat procStat = readProcStat();
    if (procStat == null) {
      return false;
    }

    long diffUserTime = procStat.userTime - lastProcStat.userTime;
    long diffSystemTime = procStat.systemTime - lastProcStat.systemTime;
    long diffIdleTime = procStat.idleTime - lastProcStat.idleTime;
    long allTime = diffUserTime + diffSystemTime + diffIdleTime;

    if (currentFrequencyScale == 0 || allTime == 0) {
      return false;
    }

    // Update statistics.
    frequencyScale.addValue(currentFrequencyScale);

    double currentUserCpuUsage = diffUserTime / (double) allTime;
    userCpuUsage.addValue(currentUserCpuUsage);

    double currentSystemCpuUsage = diffSystemTime / (double) allTime;
    systemCpuUsage.addValue(currentSystemCpuUsage);

    double currentTotalCpuUsage =
        (currentUserCpuUsage + currentSystemCpuUsage) * currentFrequencyScale;
    totalCpuUsage.addValue(currentTotalCpuUsage);

    // Save new measurements for next round's deltas.
    lastProcStat = procStat;

    return true;
  }

  private int doubleToPercent(double d) {
    return (int) (d * 100 + 0.5);
  }

  private synchronized String getStatString() {
    StringBuilder stat = new StringBuilder();
    stat.append("CPU User: ")
        .append(doubleToPercent(userCpuUsage.getCurrent()))
        .append("/")
        .append(doubleToPercent(userCpuUsage.getAverage()))
        .append(". System: ")
        .append(doubleToPercent(systemCpuUsage.getCurrent()))
        .append("/")
        .append(doubleToPercent(systemCpuUsage.getAverage()))
        .append(". Freq: ")
        .append(doubleToPercent(frequencyScale.getCurrent()))
        .append("/")
        .append(doubleToPercent(frequencyScale.getAverage()))
        .append(". Total usage: ")
        .append(doubleToPercent(totalCpuUsage.getCurrent()))
        .append("/")
        .append(doubleToPercent(totalCpuUsage.getAverage()))
        .append(". Cores: ")
        .append(actualCpusPresent);
    stat.append("( ");
    for (int i = 0; i < cpusPresent; i++) {
      stat.append(doubleToPercent(curFreqScales[i])).append(" ");
    }
    stat.append("). Battery: ").append(getBatteryLevel());
    if (cpuOveruse) {
      stat.append(". Overuse.");
    }
    return stat.toString();
  }

  /**
   * Read a single integer value from the named file.  Return the read value
   * or if an error occurs return 0.
   */
  private long readFreqFromFile(String fileName) {
    long number = 0;
    try (FileInputStream stream = new FileInputStream(fileName);
         InputStreamReader streamReader = new InputStreamReader(stream, Charset.forName("UTF-8"));
         BufferedReader reader = new BufferedReader(streamReader)) {
      String line = reader.readLine();
      number = parseLong(line);
    } catch (FileNotFoundException e) {
      // CPU core is off, so file with its scaling frequency .../cpufreq/scaling_cur_freq
      // is not present. This is not an error.
    } catch (IOException e) {
      // CPU core is off, so file with its scaling frequency .../cpufreq/scaling_cur_freq
      // is empty. This is not an error.
    }
    return number;
  }

  private static long parseLong(String value) {
    long number = 0;
    try {
      number = Long.parseLong(value);
    } catch (NumberFormatException e) {
      Log.e(TAG, "parseLong error.", e);
    }
    return number;
  }

  /*
   * Read the current utilization of all CPUs using the cumulative first line
   * of /proc/stat.
   */
  @SuppressWarnings("StringSplitter")
  private @Nullable ProcStat readProcStat() {
    long userTime = 0;
    long systemTime = 0;
    long idleTime = 0;
    try (FileInputStream stream = new FileInputStream("/proc/stat");
         InputStreamReader streamReader = new InputStreamReader(stream, Charset.forName("UTF-8"));
         BufferedReader reader = new BufferedReader(streamReader)) {
      // line should contain something like this:
      // cpu  5093818 271838 3512830 165934119 101374 447076 272086 0 0 0
      //       user    nice  system     idle   iowait  irq   softirq
      String line = reader.readLine();
      String[] lines = line.split("\\s+");
      int length = lines.length;
      if (length >= 5) {
        userTime = parseLong(lines[1]); // user
        userTime += parseLong(lines[2]); // nice
        systemTime = parseLong(lines[3]); // system
        idleTime = parseLong(lines[4]); // idle
      }
      if (length >= 8) {
        userTime += parseLong(lines[5]); // iowait
        systemTime += parseLong(lines[6]); // irq
        systemTime += parseLong(lines[7]); // softirq
      }
    } catch (FileNotFoundException e) {
      Log.e(TAG, "Cannot open /proc/stat for reading", e);
      return null;
    } catch (Exception e) {
      Log.e(TAG, "Problems parsing /proc/stat", e);
      return null;
    }
    return new ProcStat(userTime, systemTime, idleTime);
  }
}
