import fs from "fs";
import os from "os";
import { Logger } from "@perf-profiler/logger";
import {
  canIgnoreAwsTerminationError,
  cleanup,
  executeCommand,
  executeLongRunningProcess,
} from "../shell";
import {
  Measure,
  POLLING_INTERVAL,
  Profiler,
  ScreenRecorder,
  ThreadNames,
} from "@perf-profiler/types";
import { CpuMeasureAggregator } from "../cpu/CpuMeasureAggregator";
import { FrameTimeParser } from "../atrace/pollFpsUsage";
import { CppPerformanceMeasure, parseCppMeasure } from "../cppProfiler";
import { processOutput } from "../cpu/getCpuStatsByProcess";
import { processOutput as processRamOutput } from "../ram/pollRamUsage";

export const CppProfilerName = `BAMPerfProfiler`;

// Since Flipper uses esbuild, we copy the bin folder directly
// into the Flipper plugin directory
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-expect-error
const binaryFolder = global.Flipper
  ? `${__dirname}/bin`
  : `${__dirname}/../../..${__dirname.includes("dist") ? "/.." : ""}/cpp-profiler/bin`;

export abstract class UnixProfiler implements Profiler {
  stop(): void {
    throw new Error("Method not implemented.");
  }
  private hasInstalledProfiler = false;
  private cpuClockTick: number | undefined;
  private RAMPageSize: number | undefined;

  /**
   * Main setup function for the cpp profiler
   *
   * It will:
   * - install the C++ profiler for the correct architecture on the device
   * - Starts the atrace process (the c++ profiler will then starts another thread to read from it)
   * - Populate needed values like CPU clock tick and RAM page size
   *
   * This needs to be done before measures and can take a few seconds
   */
  public installProfilerOnDevice(): void {
    if (!this.hasInstalledProfiler) {
      this.assertSupported();
      this.installCppProfilerOnDevice();
      this.retrieveCpuClockTick();
      this.retrieveRAMPageSize();
    }
    this.hasInstalledProfiler = true;
  }

  private retrieveCpuClockTick() {
    this.cpuClockTick = parseInt(
      executeCommand(this.getDeviceCommand(`${this.getDeviceProfilerPath()} printCpuClockTick`)),
      10
    );
  }

  private retrieveRAMPageSize() {
    this.RAMPageSize = parseInt(
      executeCommand(this.getDeviceCommand(`${this.getDeviceProfilerPath()} printRAMPageSize`)),
      10
    );
  }

  getCpuClockTick(): number {
    this.installProfilerOnDevice();
    if (!this.cpuClockTick) {
      throw new Error("CPU clock tick not initialized");
    }
    return this.cpuClockTick;
  }

  getRAMPageSize(): number {
    this.installProfilerOnDevice();
    if (!this.RAMPageSize) {
      throw new Error("RAM Page size not initialized");
    }
    return this.RAMPageSize;
  }

  private installCppProfilerOnDevice(): void {
    const abi = this.getAbi();
    Logger.info(`Installing C++ profiler for ${abi} architecture`);

    const binaryPath = `${binaryFolder}/${CppProfilerName}-${abi}`;
    const binaryTmpPath = `${os.tmpdir()}/flashlight-${CppProfilerName}-${abi}`;

    // When running from standalone executable, we need to copy the binary to an actual file
    fs.copyFileSync(binaryPath, binaryTmpPath);

    this.pushExecutable(binaryTmpPath);
    Logger.success(`C++ Profiler installed in ${this.getDeviceProfilerPath()}`);
  }

  pollPerformanceMeasures(
    bundleId: string,
    {
      onMeasure,
      onStartMeasuring = () => {
        // noop by default
      },
    }: {
      onMeasure: (measure: Measure) => void;
      onStartMeasuring?: () => void;
    }
  ) {
    let initialTime: number | null = null;
    let previousTime: number | null = null;

    let cpuMeasuresAggregator = new CpuMeasureAggregator(this.getCpuClockTick());
    let frameTimeParser = new FrameTimeParser();

    const reset = () => {
      initialTime = null;
      previousTime = null;
      cpuMeasuresAggregator = new CpuMeasureAggregator(this.getCpuClockTick());
      frameTimeParser = new FrameTimeParser();
    };

    return this.pollPerformanceMeasuresWeirdSubfunction(
      bundleId,
      ({ pid, cpu, ram: ramStr, atrace, timestamp }) => {
        if (!atrace) {
          Logger.debug("NO ATRACE OUTPUT, if the app is idle, that is normal");
        }
        const subProcessesStats = processOutput(cpu, pid);

        const ram = processRamOutput(ramStr, this.getRAMPageSize());
        const { frameTimes, interval: atraceInterval } = frameTimeParser.getFrameTimes(atrace, pid);

        if (!initialTime) {
          initialTime = timestamp;
        }

        if (previousTime) {
          const interval = timestamp - previousTime;

          const cpuMeasures = cpuMeasuresAggregator.process(subProcessesStats, interval);

          const fps = FrameTimeParser.getFps(
            frameTimes,
            atraceInterval,
            Math.max(
              cpuMeasures.perName[ThreadNames.ANDROID.UI] || 0,
              // Hack for Flutter apps - if this thread is heavy app will be laggy
              cpuMeasures.perName[ThreadNames.FLUTTER.UI] || 0
            )
          );

          onMeasure(
            this.supportFPS()
              ? {
                  cpu: cpuMeasures,
                  fps,
                  ram,
                  time: timestamp - initialTime,
                }
              : {
                  cpu: cpuMeasures,
                  ram,
                  time: timestamp - initialTime,
                }
          );
        } else {
          onStartMeasuring();
          cpuMeasuresAggregator.initStats(subProcessesStats);
        }
        previousTime = timestamp;
      },
      () => {
        Logger.warn("Process id has changed, ignoring measures until now");
        reset();
      }
    );
  }

  pollPerformanceMeasuresWeirdSubfunction = (
    pid: string,
    onData: (measure: CppPerformanceMeasure) => void,
    onPidChanged?: (pid: string) => void
  ) => {
    this.installProfilerOnDevice();

    const DELIMITER = "=STOP MEASURE=";

    const process = executeLongRunningProcess(
      this.getDeviceCommand(
        `${this.getDeviceProfilerPath()} pollPerformanceMeasures ${pid} ${POLLING_INTERVAL}`
      ),
      DELIMITER,
      (data: string) => {
        onData(parseCppMeasure(data));
      }
    );

    process.stderr?.on("data", (data) => {
      const log = data.toString();

      // Ignore errors, it might be that the thread is dead and we can't read stats anymore
      if (log.includes("CPP_ERROR_CANNOT_OPEN_FILE")) {
        Logger.debug(log);
      } else if (log.includes("CPP_ERROR_MAIN_PID_CLOSED")) {
        onPidChanged?.(pid);
      } else {
        if (!canIgnoreAwsTerminationError(log)) Logger.error(log);
      }
    });

    return {
      stop: () => {
        process.kill("SIGINT");
        this.stop();
      },
    };
  };

  // Disabling the warning because the method isn't implemented
  // eslint-disable-next-line @typescript-eslint/no-unused-vars
  public getScreenRecorder(videoPath: string): ScreenRecorder | undefined {
    return undefined;
  }

  // Disabling the warning because the method isn't implemented
  // eslint-disable-next-line @typescript-eslint/no-unused-vars
  public async stopApp(bundleId: string) {
    throw new Error("Method not implemented.");
  }

  public cleanup() {
    cleanup();
  }

  public abstract getDeviceCommand(command: string): string;
  protected abstract getAbi(): string;
  protected abstract pushExecutable(binaryTmpPath: string): void;
  protected abstract assertSupported(): void;
  public abstract getDeviceProfilerPath(): string;
  public abstract detectCurrentBundleId(): string;
  public abstract supportFPS(): boolean;
}
