/*  _____ _
 * |_   _| |_  _ _ ___ ___ _ __  __ _
 *   | | | ' \| '_/ -_) -_) '  \/ _` |_
 *   |_| |_||_|_| \___\___|_|_|_\__,_(_)
 *
 * Threema for Android
 * Copyright (c) 2017-2020 Threema GmbH
 *
 * This program is free software: you can redistribute it and/or modify
 * it under the terms of the GNU Affero General Public License, version 3,
 * as published by the Free Software Foundation.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
 * GNU Affero General Public License for more details.
 *
 * You should have received a copy of the GNU Affero General Public License
 * along with this program. If not, see <https://www.gnu.org/licenses/>.
 */

/*
 *  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 ch.threema.app.voip;

import android.content.Context;
import android.content.Intent;
import android.content.IntentFilter;
import android.os.BatteryManager;
import android.os.SystemClock;
import android.util.Log;

import java.io.BufferedReader;
import java.io.FileNotFoundException;
import java.io.FileReader;
import java.io.IOException;
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.
 */
public 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;

	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;
	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 CpuMonitor(Context context) {
		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();
	}

	public synchronized void reset() {
		if (executor != null) {
			Log.d(TAG, "reset");
			resetStat();
			cpuOveruse = false;
		}
	}

	public synchronized int getCpuUsageCurrent() {
		return doubleToPercent(userCpuUsage.getCurrent() + systemCpuUsage.getCurrent());
	}

	public synchronized int getCpuUsageAverage() {
		return doubleToPercent(userCpuUsage.getAverage() + systemCpuUsage.getAverage());
	}

	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 (FileReader fin = new FileReader("/sys/devices/system/cpu/present")) {
			try (BufferedReader reader = new BufferedReader(fin)) {
				try (Scanner scanner = new Scanner(reader).useDelimiter("[-\n]")) {
					scanner.nextInt(); // Skip leading number 0.
					cpusPresent = 1 + scanner.nextInt();
				}
			} catch (Exception e) {
				Log.e(TAG, "Cannot do CPU stats due to /sys/devices/system/cpu/present parsing problem");
			}
		} 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");
		}

		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 {
			BufferedReader reader = new BufferedReader(new FileReader(fileName));
			try {
				String line = reader.readLine();
				number = parseLong(line);
			} finally {
				reader.close();
			}
		} 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.
	 */
	private ProcStat readProcStat() {
		long userTime = 0;
		long systemTime = 0;
		long idleTime = 0;
		try {
			BufferedReader reader = new BufferedReader(new FileReader("/proc/stat"));
			try {
				// 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 (Exception e) {
				Log.e(TAG, "Problems parsing /proc/stat", e);
				return null;
			} finally {
				reader.close();
			}
		} catch (FileNotFoundException e) {
			Log.e(TAG, "Cannot open /proc/stat for reading", e);
			return null;
		} catch (IOException e) {
			Log.e(TAG, "Problems reading /proc/stat", e);
			return null;
		}
		return new ProcStat(userTime, systemTime, idleTime);
	}
}
