// Copyright (C) 2022 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 {fetchWithTimeout} from '../../../base/http_utils';
import {exists} from '../../../base/utils';
import {VERSION} from '../../../gen/perfetto_version';
import {AdbConnectionImpl} from '../adb_connection_impl';
import {
  DataSource,
  OnTargetChangeCallback,
  RecordingTargetV2,
  TargetInfo,
  TracingSession,
  TracingSessionListener,
} from '../recording_interfaces_v2';
import {
  CUSTOM_TRACED_CONSUMER_SOCKET_PATH,
  DEFAULT_TRACED_CONSUMER_SOCKET_PATH,
  TRACEBOX_DEVICE_PATH,
  TRACEBOX_FETCH_TIMEOUT,
} from '../recording_utils';
import {TracedTracingSession} from '../traced_tracing_session';

export abstract class AndroidTarget implements RecordingTargetV2 {
  private consumerSocketPath = DEFAULT_TRACED_CONSUMER_SOCKET_PATH;
  protected androidApiLevel?: number;
  protected dataSources?: DataSource[];

  protected constructor(
      private adbConnection: AdbConnectionImpl,
      private onTargetChange: OnTargetChangeCallback) {}

  abstract getInfo(): TargetInfo;

  // This is called when a usb USBConnectionEvent of type 'disconnect' event is
  // emitted. This event is emitted when the USB connection is lost (example:
  // when the user unplugged the connecting cable).
  async disconnect(disconnectMessage?: string): Promise<void> {
    await this.adbConnection.disconnect(disconnectMessage);
  }

  // Starts a tracing session in order to fetch information such as apiLevel
  // and dataSources from the device. Then, it cancels the session.
  async fetchTargetInfo(listener: TracingSessionListener): Promise<void> {
    const tracingSession = await this.createTracingSession(listener);
    tracingSession.cancel();
  }

  // We do not support long tracing on Android.
  canCreateTracingSession(recordingMode: string): boolean {
    return recordingMode !== 'LONG_TRACE';
  }

  async createTracingSession(tracingSessionListener: TracingSessionListener):
      Promise<TracingSession> {
    this.adbConnection.onStatus = tracingSessionListener.onStatus;
    this.adbConnection.onDisconnect = tracingSessionListener.onDisconnect;

    if (!exists(this.androidApiLevel)) {
      // 1. Fetch the API version from the device.
      const version = await this.adbConnection.shellAndGetOutput(
          'getprop ro.build.version.sdk');
      this.androidApiLevel = Number(version);

      this.onTargetChange();

      // 2. For older OS versions we push the tracebox binary.
      if (this.androidApiLevel < 29) {
        await this.pushTracebox();
        this.consumerSocketPath = CUSTOM_TRACED_CONSUMER_SOCKET_PATH;

        await this.adbConnection.shellAndWaitCompletion(
            this.composeTraceboxCommand('traced'));
        await this.adbConnection.shellAndWaitCompletion(
            this.composeTraceboxCommand('traced_probes'));
      }
    }

    const adbStream =
        await this.adbConnection.connectSocket(this.consumerSocketPath);

    // 3. Start a tracing session.
    const tracingSession =
        new TracedTracingSession(adbStream, tracingSessionListener);
    await tracingSession.initConnection();

    if (!this.dataSources) {
      // 4. Fetch dataSources from QueryServiceState.
      this.dataSources = await tracingSession.queryServiceState();

      this.onTargetChange();
    }
    return tracingSession;
  }

  async pushTracebox() {
    const arch = await this.fetchArchitecture();
    const shortVersion = VERSION.split('-')[0];
    const requestUrl =
        `https://commondatastorage.googleapis.com/perfetto-luci-artifacts/${
            shortVersion}/${arch}/tracebox`;
    const fetchResponse = await fetchWithTimeout(
        requestUrl, {method: 'get'}, TRACEBOX_FETCH_TIMEOUT);
    const traceboxBin = await fetchResponse.arrayBuffer();
    await this.adbConnection.push(
        new Uint8Array(traceboxBin), TRACEBOX_DEVICE_PATH);

    // We explicitly set the tracebox permissions because adb does not reliably
    // set permissions when uploading the binary.
    await this.adbConnection.shellAndWaitCompletion(
        `chmod 755 ${TRACEBOX_DEVICE_PATH}`);
  }

  async fetchArchitecture() {
    const abiList = await this.adbConnection.shellAndGetOutput(
        'getprop ro.vendor.product.cpu.abilist');
    // If multiple ABIs are allowed, the 64bit ones should have higher priority.
    if (abiList.includes('arm64-v8a')) {
      return 'android-arm64';
    } else if (abiList.includes('x86')) {
      return 'android-x86';
    } else if (abiList.includes('armeabi-v7a') || abiList.includes('armeabi')) {
      return 'android-arm';
    } else if (abiList.includes('x86_64')) {
      return 'android-x64';
    }
    // Most devices have arm64 architectures, so we should return this if
    // nothing else is found.
    return 'android-arm64';
  }

  canConnectWithoutContention(): Promise<boolean> {
    return this.adbConnection.canConnectWithoutContention();
  }

  composeTraceboxCommand(applet: string) {
    // 1. Set the consumer socket.
    return 'PERFETTO_CONSUMER_SOCK_NAME=@traced_consumer ' +
        // 2. Set the producer socket.
        'PERFETTO_PRODUCER_SOCK_NAME=@traced_producer ' +
        // 3. Start the applet in the background.
        `/data/local/tmp/tracebox ${applet} --background`;
  }
}
