// 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 {HostOsByteStream} from '../host_os_byte_stream';
import {RecordingError} from '../recording_error_handling';
import {
  DataSource,
  HostOsTargetInfo,
  OnDisconnectCallback,
  OnTargetChangeCallback,
  RecordingTargetV2,
  TracingSession,
  TracingSessionListener,
} from '../recording_interfaces_v2';
import {
  isLinux,
  isMacOs,
  WEBSOCKET_CLOSED_ABNORMALLY_CODE,
} from '../recording_utils';
import {TracedTracingSession} from '../traced_tracing_session';

export class HostOsTarget implements RecordingTargetV2 {
  private readonly targetType: 'LINUX'|'MACOS';
  private readonly name: string;
  private websocket: WebSocket;
  private streams = new Set<HostOsByteStream>();
  private dataSources?: DataSource[];
  private onDisconnect: OnDisconnectCallback = (_) => {};

  constructor(
      websocketUrl: string,
      private maybeClearTarget: (target: HostOsTarget) => void,
      private onTargetChange: OnTargetChangeCallback) {
    if (isMacOs(navigator.userAgent)) {
      this.name = 'MacOS';
      this.targetType = 'MACOS';
    } else if (isLinux(navigator.userAgent)) {
      this.name = 'Linux';
      this.targetType = 'LINUX';
    } else {
      throw new RecordingError(
          'Host OS target created on an unsupported operating system.');
    }

    this.websocket = new WebSocket(websocketUrl);
    this.websocket.onclose = this.onClose.bind(this);
  }

  getInfo(): HostOsTargetInfo {
    return {
      targetType: this.targetType,
      name: this.name,
      dataSources: this.dataSources || [],
    };
  }

  canCreateTracingSession(): boolean {
    return true;
  }

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

    const osStream = await HostOsByteStream.create(this.getUrl());
    this.streams.add(osStream);
    const tracingSession =
        new TracedTracingSession(osStream, tracingSessionListener);
    await tracingSession.initConnection();

    if (!this.dataSources) {
      this.dataSources = await tracingSession.queryServiceState();
      this.onTargetChange();
    }
    return tracingSession;
  }

  // Starts a tracing session in order to fetch data sources from the
  // device. Then, it cancels the session.
  async fetchTargetInfo(tracingSessionListener: TracingSessionListener):
      Promise<void> {
    const tracingSession =
        await this.createTracingSession(tracingSessionListener);
    tracingSession.cancel();
  }

  async disconnect(): Promise<void> {
    if (this.websocket.readyState === this.websocket.OPEN) {
      this.websocket.close();
      // We remove the 'onclose' callback so the 'disconnect' method doesn't get
      // executed twice.
      this.websocket.onclose = null;
    }
    for (const stream of this.streams) {
      stream.close();
    }
    // We remove the existing target from the factory if present.
    this.maybeClearTarget(this);
    // We run the onDisconnect callback in case this target is used for tracing.
    this.onDisconnect();
  }

  // We can connect to the Host OS without taking the connection away from
  // another process.
  async canConnectWithoutContention(): Promise<boolean> {
    return true;
  }

  getUrl() {
    return this.websocket.url;
  }

  private onClose(ev: CloseEvent): void {
    if (ev.code === WEBSOCKET_CLOSED_ABNORMALLY_CODE) {
      console.info(
          `It's safe to ignore the 'WebSocket connection to ${
              this.getUrl()} error above, if present. It occurs when ` +
          'checking the connection to the local Websocket server.');
    }
    this.disconnect();
  }
}
