/**
 * Copyright (c) Facebook, Inc. and its affiliates.
 *
 * This source code is licensed under the MIT license found in the
 * LICENSE file in the root directory of this source tree.
 *
 * @format
 */

import stream from 'stream';
import {
  DeviceLogEntry,
  _SandyDevicePluginInstance,
  _SandyPluginDefinition,
  DeviceType,
  DeviceLogListener,
} from 'flipper-plugin';
import type {DevicePluginDefinition, DevicePluginMap} from '../plugin';
import {getFlipperLibImplementation} from '../utils/flipperLibImplementation';
import {DeviceSpec, OS as PluginOS} from 'flipper-plugin-lib';

export type DeviceShell = {
  stdout: stream.Readable;
  stderr: stream.Readable;
  stdin: stream.Writable;
};

export type OS = PluginOS | 'Windows' | 'MacOS' | 'JSWebApp';

export type DeviceExport = {
  os: OS;
  title: string;
  deviceType: DeviceType;
  serial: string;
  logs: Array<DeviceLogEntry>;
  pluginStates: Record<string, any>;
};

export default class BaseDevice {
  constructor(
    serial: string,
    deviceType: DeviceType,
    title: string,
    os: OS,
    specs: DeviceSpec[] = [],
  ) {
    this.serial = serial;
    this.title = title;
    this.deviceType = deviceType;
    this.os = os;
    this.specs = specs;
  }

  // operating system of this device
  os: OS;

  // human readable name for this device
  title: string;

  // type of this device
  deviceType: DeviceType;

  // serial number for this device
  serial: string;

  // additional device specs used for plugin compatibility checks
  specs: DeviceSpec[];

  // possible src of icon to display next to the device title
  icon: string | null | undefined;

  logListeners: Map<Symbol, DeviceLogListener> = new Map();
  logEntries: Array<DeviceLogEntry> = [];
  isArchived: boolean = false;
  // if imported, stores the original source location
  source = '';

  // sorted list of supported device plugins
  devicePlugins: string[] = [];

  sandyPluginStates: Map<string, _SandyDevicePluginInstance> = new Map<
    string,
    _SandyDevicePluginInstance
  >();

  supportsOS(os: OS) {
    return os.toLowerCase() === this.os.toLowerCase();
  }

  displayTitle(): string {
    return this.title;
  }

  toJSON(): DeviceExport {
    const pluginStates: Record<string, any> = {};

    for (const instance of this.sandyPluginStates.values()) {
      if (instance.isPersistable()) {
        pluginStates[instance.definition.id] = instance.exportState();
      }
    }

    return {
      os: this.os,
      title: this.title,
      deviceType: this.deviceType,
      serial: this.serial,
      logs: this.getLogs(),
      pluginStates,
    };
  }

  teardown() {
    for (const instance of this.sandyPluginStates.values()) {
      instance.destroy();
    }
  }

  addLogListener(callback: DeviceLogListener): Symbol {
    const id = Symbol();
    this.logListeners.set(id, callback);
    return id;
  }

  _notifyLogListeners(entry: DeviceLogEntry) {
    if (this.logListeners.size > 0) {
      this.logListeners.forEach((listener) => {
        // prevent breaking other listeners, if one listener doesn't work.
        try {
          listener(entry);
        } catch (e) {
          console.error(`Log listener exception:`, e);
        }
      });
    }
  }

  addLogEntry(entry: DeviceLogEntry) {
    this.logEntries.push(entry);
    this._notifyLogListeners(entry);
  }

  // TODO: remove getLogs T70688226
  getLogs(startDate: Date | null = null) {
    return startDate != null
      ? this.logEntries.filter((log) => {
          return log.date > startDate;
        })
      : this.logEntries;
  }

  clearLogs(): Promise<void> {
    // Only for device types that allow clearing.
    this.logEntries = [];
    return Promise.resolve();
  }

  removeLogListener(id: Symbol) {
    this.logListeners.delete(id);
  }

  navigateToLocation(_location: string) {
    throw new Error('unimplemented');
  }

  archive(): any | null | undefined {
    return null;
  }

  screenshot(): Promise<Buffer> {
    return Promise.reject(
      new Error('No screenshot support for current device'),
    );
  }

  async screenCaptureAvailable(): Promise<boolean> {
    return false;
  }

  async startScreenCapture(_destination: string): Promise<void> {
    throw new Error('startScreenCapture not implemented on BaseDevice ');
  }

  async stopScreenCapture(): Promise<string | null> {
    return null;
  }

  loadDevicePlugins(
    devicePlugins?: DevicePluginMap,
    pluginStates?: Record<string, any>,
  ) {
    if (!devicePlugins) {
      return;
    }
    const plugins = Array.from(devicePlugins.values());
    for (const plugin of plugins) {
      this.loadDevicePlugin(plugin, pluginStates?.[plugin.id]);
    }
  }

  loadDevicePlugin(plugin: DevicePluginDefinition, initialState?: any) {
    if (plugin instanceof _SandyPluginDefinition) {
      if (plugin.asDevicePluginModule().supportsDevice(this as any)) {
        this.devicePlugins.push(plugin.id);
        this.sandyPluginStates.set(
          plugin.id,
          new _SandyDevicePluginInstance(
            getFlipperLibImplementation(),
            plugin,
            this,
            initialState,
          ),
        );
      }
    } else {
      if (plugin.supportsDevice(this)) {
        this.devicePlugins.push(plugin.id);
      }
    }
  }

  unloadDevicePlugin(pluginId: string) {
    const instance = this.sandyPluginStates.get(pluginId);
    if (instance) {
      instance.destroy();
      this.sandyPluginStates.delete(pluginId);
    }
    this.devicePlugins.splice(this.devicePlugins.indexOf(pluginId), 1);
  }
}
