// 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 {exists} from '../../base/utils';
import {RecordingState, RecordingTarget, isAdbTarget} from './state';
import {
  extractDurationFromTraceConfig,
  extractTraceConfig,
} from './trace_config_utils';
import {Adb} from './adb_interfaces';
import {ReadBuffersResponse} from './consumer_port_types';
import {Consumer, RpcConsumerPort} from './record_controller_interfaces';

export enum AdbConnectionState {
  READY_TO_CONNECT,
  AUTH_IN_PROGRESS,
  CONNECTED,
  CLOSED,
}

interface Command {
  method: string;
  params: Uint8Array;
}

export abstract class AdbBaseConsumerPort extends RpcConsumerPort {
  // Contains the commands sent while the authentication is in progress. They
  // will all be executed afterwards. If the device disconnects, they are
  // removed.
  private commandQueue: Command[] = [];

  protected adb: Adb;
  protected state = AdbConnectionState.READY_TO_CONNECT;
  protected device?: USBDevice;
  protected recState: RecordingState;

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

  async handleCommand(method: string, params: Uint8Array) {
    try {
      if (method === 'FreeBuffers') {
        // When we finish tracing, we disconnect the adb device interface.
        // Otherwise, we will keep holding the device interface and won't allow
        // adb to access it. https://wicg.github.io/webusb/#abusing-a-device
        // "Lastly, since USB devices are unable to distinguish requests from
        // multiple sources, operating systems only allow a USB interface to
        // have a single owning user-space or kernel-space driver."
        this.state = AdbConnectionState.CLOSED;
        await this.adb.disconnect();
      } else if (method === 'EnableTracing') {
        this.state = AdbConnectionState.READY_TO_CONNECT;
      }

      if (this.state === AdbConnectionState.CLOSED) return;

      this.commandQueue.push({method, params});

      if (
        this.state === AdbConnectionState.READY_TO_CONNECT ||
        this.deviceDisconnected()
      ) {
        this.state = AdbConnectionState.AUTH_IN_PROGRESS;
        this.device = await this.findDevice(this.recState.recordingTarget);
        if (!this.device) {
          this.state = AdbConnectionState.READY_TO_CONNECT;
          const target = this.recState.recordingTarget;
          throw Error(
            `Device with serial ${
              isAdbTarget(target) ? target.serial : 'n/a'
            } not found.`,
          );
        }

        this.sendStatus(`Please allow USB debugging on device.
          If you press cancel, reload the page.`);

        await this.adb.connect(this.device);

        // During the authentication the device may have been disconnected.
        if (!this.recState.recordingInProgress || this.deviceDisconnected()) {
          throw Error('Recording not in progress after adb authorization.');
        }

        this.state = AdbConnectionState.CONNECTED;
        this.sendStatus('Device connected.');
      }

      if (this.state === AdbConnectionState.AUTH_IN_PROGRESS) return;

      console.assert(this.state === AdbConnectionState.CONNECTED);

      for (const cmd of this.commandQueue) this.invoke(cmd.method, cmd.params);

      this.commandQueue = [];
    } catch (e) {
      this.commandQueue = [];
      this.state = AdbConnectionState.READY_TO_CONNECT;
      this.sendErrorMessage(e.message);
    }
  }

  private deviceDisconnected() {
    return !this.device || !this.device.opened;
  }

  setDurationStatus(enableTracingProto: Uint8Array) {
    const traceConfigProto = extractTraceConfig(enableTracingProto);
    if (!traceConfigProto) return;
    const duration = extractDurationFromTraceConfig(traceConfigProto);
    this.sendStatus(
      `Recording in progress${
        exists(duration) ? ' for ' + duration.toString() + ' ms' : ''
      }...`,
    );
  }

  abstract invoke(method: string, argsProto: Uint8Array): void;

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

  async findDevice(
    connectedDevice: RecordingTarget,
  ): Promise<USBDevice | undefined> {
    if (!('usb' in navigator)) return undefined;
    if (!isAdbTarget(connectedDevice)) return undefined;
    const devices = await navigator.usb.getDevices();
    return devices.find((d) => d.serialNumber === connectedDevice.serial);
  }
}
