// Copyright (C) 2019 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.

import {_TextDecoder} from 'custom_utils';

import {base64Encode} from '../base/string_utils';
import {extractTraceConfig} from '../core/trace_config_utils';

import {AdbBaseConsumerPort, AdbConnectionState} from './adb_base_controller';
import {Adb, AdbStream} from './adb_interfaces';
import {ReadBuffersResponse} from './consumer_port_types';
import {Consumer} from './record_controller_interfaces';

enum AdbShellState {
  READY,
  RECORDING,
  FETCHING
}
const DEFAULT_DESTINATION_FILE = '/data/misc/perfetto-traces/trace-by-ui';
const textDecoder = new _TextDecoder();

export class AdbConsumerPort extends AdbBaseConsumerPort {
  traceDestFile = DEFAULT_DESTINATION_FILE;
  shellState: AdbShellState = AdbShellState.READY;
  private recordShell?: AdbStream;

  constructor(adb: Adb, consumer: Consumer) {
    super(adb, consumer);
    this.adb = adb;
  }

  async invoke(method: string, params: Uint8Array) {
    // ADB connection & authentication is handled by the superclass.
    console.assert(this.state === AdbConnectionState.CONNECTED);

    switch (method) {
      case 'EnableTracing':
        this.enableTracing(params);
        break;
      case 'ReadBuffers':
        this.readBuffers();
        break;
      case 'DisableTracing':
        this.disableTracing();
        break;
      case 'FreeBuffers':
        this.freeBuffers();
        break;
      case 'GetTraceStats':
        break;
      default:
        this.sendErrorMessage(`Method not recognized: ${method}`);
        break;
    }
  }

  async enableTracing(enableTracingProto: Uint8Array) {
    try {
      const traceConfigProto = extractTraceConfig(enableTracingProto);
      if (!traceConfigProto) {
        this.sendErrorMessage('Invalid config.');
        return;
      }

      await this.startRecording(traceConfigProto);
      this.setDurationStatus(enableTracingProto);
    } catch (e) {
      this.sendErrorMessage(e.message);
    }
  }

  async startRecording(configProto: Uint8Array) {
    this.shellState = AdbShellState.RECORDING;
    const recordCommand = this.generateStartTracingCommand(configProto);
    this.recordShell = await this.adb.shell(recordCommand);
    const output: string[] = [];
    this.recordShell.onData = (raw) => output.push(textDecoder.decode(raw));
    this.recordShell.onClose = () => {
      const response = output.join();
      if (!this.tracingEndedSuccessfully(response)) {
        this.sendErrorMessage(response);
        this.shellState = AdbShellState.READY;
        return;
      }
      this.sendStatus('Recording ended successfully. Fetching the trace..');
      this.sendMessage({type: 'EnableTracingResponse'});
      this.recordShell = undefined;
    };
  }

  tracingEndedSuccessfully(response: string): boolean {
    return !response.includes(' 0 ms') && response.includes('Wrote ');
  }

  async readBuffers() {
    console.assert(this.shellState === AdbShellState.RECORDING);
    this.shellState = AdbShellState.FETCHING;

    const readTraceShell =
        await this.adb.shell(this.generateReadTraceCommand());
    readTraceShell.onData = (raw) =>
        this.sendMessage(this.generateChunkReadResponse(raw));

    readTraceShell.onClose = () => {
      this.sendMessage(
          this.generateChunkReadResponse(new Uint8Array(), /* last */ true));
    };
  }

  async getPidFromShellAsString() {
    const pidStr =
        await this.adb.shellOutputAsString(`ps -u shell | grep perfetto`);
    // We used to use awk '{print $2}' but older phones/Go phones don't have
    // awk installed. Instead we implement similar functionality here.
    const awk = pidStr.split(' ').filter((str) => str !== '');
    if (awk.length < 1) {
      throw Error(`Unabled to find perfetto pid in string "${pidStr}"`);
    }
    return awk[1];
  }

  async disableTracing() {
    if (!this.recordShell) return;
    try {
      // We are not using 'pidof perfetto' so that we can use more filters. 'ps
      // -u shell' is meant to catch processes started from shell, so if there
      // are other ongoing tracing sessions started by others, we are not
      // killing them.
      const pid = await this.getPidFromShellAsString();

      if (pid.length === 0 || isNaN(Number(pid))) {
        throw Error(`Perfetto pid not found. Impossible to stop/cancel the
     recording. Command output: ${pid}`);
      }
      // Perfetto stops and finalizes the tracing session on SIGINT.
      const killOutput =
          await this.adb.shellOutputAsString(`kill -SIGINT ${pid}`);

      if (killOutput.length !== 0) {
        throw Error(`Unable to kill perfetto: ${killOutput}`);
      }
    } catch (e) {
      this.sendErrorMessage(e.message);
    }
  }

  freeBuffers() {
    this.shellState = AdbShellState.READY;
    if (this.recordShell) {
      this.recordShell.close();
      this.recordShell = undefined;
    }
  }

  generateChunkReadResponse(data: Uint8Array, last = false):
      ReadBuffersResponse {
    return {
      type: 'ReadBuffersResponse',
      slices: [{data, lastSliceForPacket: last}],
    };
  }

  generateReadTraceCommand(): string {
    // We attempt to delete the trace file after tracing. On a non-root shell,
    // this will fail (due to selinux denial), but perfetto cmd will be able to
    // override the file later. However, on a root shell, we need to clean up
    // the file since perfetto cmd might otherwise fail to override it in a
    // future session.
    return `gzip -c ${this.traceDestFile} && rm -f ${this.traceDestFile}`;
  }

  generateStartTracingCommand(tracingConfig: Uint8Array) {
    const configBase64 = base64Encode(tracingConfig);
    const perfettoCmd = `perfetto -c - -o ${this.traceDestFile}`;
    return `echo '${configBase64}' | base64 -d | ${perfettoCmd}`;
  }
}
