package com.foxx.androidcast.diagnostics;

/*********************************************************************
 * CpuLoadSampler.java
 * Created at: Tue 19 May 2026 12:23:39 +0200
 * Updated at: Wed 20 May 2026 15:17:13 +0200 by Anton Afanasyeu <a.afanasieff@gmail.com>
 * Commit: 5d8e82d2e60a21fff3138d2a394ee4e8b4c6dcb8
 * class CpuLoadSampler
 * Contributors:
 *    - Anton Afanasyeu <a.afanasieff@gmail.com> (2 commits, 241 lines)
 *    - Cursor Agent (project assistant)
 * Digest: SHA256	b012c6212636a1af22f16b9365b19616caff8d74b49746602b55735115a7f34c
**********************************************************************/
import java.io.BufferedReader;
import java.io.FileReader;
import java.io.IOException;
import java.util.ArrayList;
import java.util.List;
import java.util.Locale;

/** Samples device or process CPU load (0–100% smoothed). */
public final class CpuLoadSampler {
    private static final long MIN_INTERVAL_MS = 200;
    private static final int CORE_BUSY_THRESHOLD_PERCENT = 12;
    private static final int USER_HZ = 100;

    private static final Object LOCK = new Object();
    private static long[] prevTotals;
    private static long[][] prevPerCpu;
    private static long prevSelfJiffies;
    private static long lastSampleMs;
    private static boolean procStatReadable = true;
    private static Sample cached = new Sample(0, Runtime.getRuntime().availableProcessors(), -1);

    public static final class Sample {
        public final int loadPercent;
        public final int totalCores;
        /** Cores above {@link #CORE_BUSY_THRESHOLD_PERCENT} usage; {@code -1} if unavailable. */
        public final int activeCores;

        Sample(int loadPercent, int totalCores, int activeCores) {
            this.loadPercent = Math.max(0, Math.min(100, loadPercent));
            this.totalCores = Math.max(1, totalCores);
            this.activeCores = activeCores;
        }
    }

    private CpuLoadSampler() {}

    /** Prime baseline so the first displayed sample is not stuck at 0%. */
    public static void warmUp() {
        synchronized (LOCK) {
            lastSampleMs = 0;
            prevTotals = null;
            prevPerCpu = null;
            prevSelfJiffies = 0;
        }
        sample();
        try {
            Thread.sleep(MIN_INTERVAL_MS + 50);
        } catch (InterruptedException e) {
            Thread.currentThread().interrupt();
        }
        sample();
    }

    public static Sample getCached() {
        synchronized (LOCK) {
            return cached;
        }
    }

    public static Sample sample() {
        synchronized (LOCK) {
            long now = System.currentTimeMillis();
            if (now - lastSampleMs < MIN_INTERVAL_MS && prevTotals != null) {
                return cached;
            }
            int totalCores = Math.max(1, cached.totalCores);
            int deviceLoad = -1;
            int active = -1;

            if (procStatReadable) {
                ProcStat current = readProcStat();
                if (current != null) {
                    totalCores = Math.max(1, current.perCpu.size());
                    if (prevTotals != null) {
                        deviceLoad = percentDelta(prevTotals, current.totals);
                        active = countActiveCores(prevPerCpu, current.perCpu, totalCores);
                    }
                    prevTotals = current.totals;
                    prevPerCpu = copyPerCpu(current.perCpu);
                } else {
                    procStatReadable = false;
                }
            }

            int processLoad = sampleProcessLoad(now);
            int load = deviceLoad >= 0 ? deviceLoad : processLoad;
            if (deviceLoad >= 0 && processLoad > load) {
                load = processLoad;
            }

            if (prevTotals != null || prevSelfJiffies > 0) {
                cached = new Sample(load, totalCores, active);
            } else if (processLoad > 0) {
                cached = new Sample(processLoad, totalCores, -1);
            }
            lastSampleMs = now;
            return cached;
        }
    }

    private static int sampleProcessLoad(long now) {
        long jiffies = readSelfCpuJiffies();
        if (jiffies < 0) {
            return 0;
        }
        int load = 0;
        if (prevSelfJiffies > 0 && lastSampleMs > 0) {
            long wallMs = now - lastSampleMs;
            if (wallMs > 0) {
                long delta = jiffies - prevSelfJiffies;
                float pct = delta * 1000f * 100f / (USER_HZ * wallMs * cached.totalCores);
                load = Math.round(Math.max(0f, Math.min(100f, pct)));
            }
        }
        prevSelfJiffies = jiffies;
        return load;
    }

    private static long readSelfCpuJiffies() {
        try (BufferedReader reader = new BufferedReader(new FileReader("/proc/self/stat"))) {
            String line = reader.readLine();
            if (line == null) {
                return -1;
            }
            int close = line.lastIndexOf(')');
            if (close < 0 || close + 2 >= line.length()) {
                return -1;
            }
            String[] parts = line.substring(close + 2).split("\\s+");
            if (parts.length < 2) {
                return -1;
            }
            return Long.parseLong(parts[0]) + Long.parseLong(parts[1]);
        } catch (IOException | NumberFormatException ignored) {
            return -1;
        }
    }

    private static int countActiveCores(long[][] prev, List<long[]> curr, int totalCores) {
        if (prev == null || curr.isEmpty() || prev.length != curr.size()) {
            return -1;
        }
        int active = 0;
        for (int i = 0; i < curr.size(); i++) {
            if (percentDelta(prev[i], curr.get(i)) >= CORE_BUSY_THRESHOLD_PERCENT) {
                active++;
            }
        }
        return Math.min(active, totalCores);
    }

    private static int percentDelta(long[] prev, long[] curr) {
        long prevIdle = prev[3] + prev[4];
        long currIdle = curr[3] + curr[4];
        long prevTotal = sumTimes(prev);
        long currTotal = sumTimes(curr);
        long totalDelta = currTotal - prevTotal;
        if (totalDelta <= 0) {
            return 0;
        }
        long idleDelta = currIdle - prevIdle;
        float busy = (totalDelta - idleDelta) * 100f / totalDelta;
        return Math.round(Math.max(0f, Math.min(100f, busy)));
    }

    private static long sumTimes(long[] times) {
        long sum = 0;
        for (long t : times) {
            sum += t;
        }
        return sum;
    }

    private static long[][] copyPerCpu(List<long[]> list) {
        long[][] out = new long[list.size()][];
        for (int i = 0; i < list.size(); i++) {
            out[i] = list.get(i).clone();
        }
        return out;
    }

    private static ProcStat readProcStat() {
        try (BufferedReader reader = new BufferedReader(new FileReader("/proc/stat"))) {
            ProcStat stat = new ProcStat();
            String line;
            while ((line = reader.readLine()) != null) {
                if (line.startsWith("cpu ")) {
                    stat.totals = parseCpuLine(line);
                } else if (line.startsWith("cpu") && line.length() > 3 && Character.isDigit(line.charAt(3))) {
                    long[] cpu = parseCpuLine(line);
                    if (cpu != null) {
                        stat.perCpu.add(cpu);
                    }
                } else if (!line.startsWith("cpu")) {
                    break;
                }
            }
            return stat.totals != null ? stat : null;
        } catch (IOException ignored) {
            return null;
        }
    }

    private static long[] parseCpuLine(String line) {
        String[] parts = line.trim().split("\\s+");
        if (parts.length < 5) {
            return null;
        }
        long[] times = new long[Math.min(10, parts.length - 1)];
        for (int i = 0; i < times.length; i++) {
            try {
                times[i] = Long.parseLong(parts[i + 1]);
            } catch (NumberFormatException e) {
                return null;
            }
        }
        return times;
    }

    private static final class ProcStat {
        long[] totals;
        final List<long[]> perCpu = new ArrayList<>();
    }

    /** HTML suffix: {@code (2/8)} or {@code (8 cores)}. */
    public static String formatCoreSuffix(Sample sample) {
        if (sample.activeCores >= 0 && sample.activeCores <= sample.totalCores) {
            return String.format(Locale.US, "(%d/%d)", sample.activeCores, sample.totalCores);
        }
        return String.format(Locale.US, "(%d cores)", sample.totalCores);
    }
}
