import { Signal, ISignal } from '@lumino/signaling';
import { PromiseDelegate } from '@lumino/coreutils';

import { ILabShell } from '@jupyterlab/application';
import { TranslationBundle } from '@jupyterlab/translation';

import { MainAreaWidget, VDomModel } from '@jupyterlab/apputils';
import { ISettingRegistry } from '@jupyterlab/settingregistry';

import { IVideoChatManager, DEFAULT_DOMAIN, CSS, DEBUG } from './tokens';

import type { JitsiMeetExternalAPIConstructor, JitsiMeetExternalAPI } from 'jitsi-meet';

import { Room, VideoChatConfig, IJitsiFactory } from './types';
import { Widget } from '@lumino/widgets';

/** A manager that can add, join, or create Video Chat rooms
 */
export class VideoChatManager extends VDomModel implements IVideoChatManager {
  private _rooms: Room[] = [];
  private _currentRoom: Room;
  private _isInitialized = false;
  private _initialized = new PromiseDelegate<void>();
  private _config: VideoChatConfig;
  private _meet: JitsiMeetExternalAPI;
  private _meetChanged: Signal<VideoChatManager, void>;
  private _settings: ISettingRegistry.ISettings;
  private _roomProviders = new Map<string, IVideoChatManager.IProviderOptions>();
  private _roomProvidedBy = new WeakMap<Room, string>();
  private _roomProvidersChanged: Signal<VideoChatManager, void>;
  private _currentRoomChanged: Signal<VideoChatManager, void>;
  private _trans: TranslationBundle;
  protected _mainWidget: MainAreaWidget;

  constructor(options?: VideoChatManager.IOptions) {
    super();
    this._trans = options.trans;
    this._meetChanged = new Signal(this);
    this._roomProvidersChanged = new Signal(this);
    this._currentRoomChanged = new Signal(this);
    this._roomProvidersChanged.connect(this.onRoomProvidersChanged, this);
  }

  __ = (msgid: string, ...args: string[]): string => {
    return this._trans.__(msgid, ...args);
  };

  /** all known rooms */
  get rooms(): Room[] {
    return this._rooms;
  }

  /** whether the manager is initialized */
  get isInitialized(): boolean {
    return this._isInitialized;
  }

  /** A `Promise` that resolves when fully initialized */
  get initialized(): Promise<void> {
    return this._initialized.promise;
  }

  /** the current room */
  get currentRoom(): Room {
    return this._currentRoom;
  }

  /**
   * set the current room, potentially scheduling a trip to the server for an id
   */
  set currentRoom(room: Room) {
    this._currentRoom = room;
    this.stateChanged.emit(void 0);
    this._currentRoomChanged.emit(void 0);
    if (room != null && room.id == null) {
      this.createRoom(room).catch(console.warn);
    }
  }

  /** A signal that emits when the current room changes. */
  get currentRoomChanged(): ISignal<IVideoChatManager, void> {
    return this._currentRoomChanged;
  }

  /** The configuration from the server/settings */
  get config(): VideoChatConfig {
    return this._config;
  }

  /** The current JitsiExternalAPI, as served by `<domain>/external_api.js` */
  get meet(): JitsiMeetExternalAPI {
    return this._meet;
  }

  /** Update the current meet */
  set meet(meet: JitsiMeetExternalAPI) {
    if (this._meet !== meet) {
      this._meet = meet;
      this._meetChanged.emit(void 0);
    }
  }

  /** A signal that emits when the current meet changes */
  get meetChanged(): Signal<IVideoChatManager, void> {
    return this._meetChanged;
  }

  /** A signal that emits when the available rooms change */
  get roomProvidersChanged(): Signal<IVideoChatManager, void> {
    return this._roomProvidersChanged;
  }

  /** The JupyterLab settings bundle */
  get settings(): ISettingRegistry.ISettings {
    return this._settings;
  }

  set settings(settings: ISettingRegistry.ISettings) {
    if (this._settings) {
      this._settings.changed.disconnect(this.onSettingsChanged, this);
    }
    this._settings = settings;
    if (this._settings) {
      this._settings.changed.connect(this.onSettingsChanged, this);
      if (!this.isInitialized) {
        this._isInitialized = true;
        this._initialized.resolve(void 0);
      }
    }
    this.stateChanged.emit(void 0);
  }

  get currentArea(): ILabShell.Area {
    return (this.settings?.composite['area'] || 'right') as ILabShell.Area;
  }

  set currentArea(currentArea: ILabShell.Area) {
    this.settings.set('area', currentArea).catch(void 0);
  }

  get mainWidget(): Promise<MainAreaWidget<Widget>> {
    return this.initialized.then(() => this._mainWidget);
  }

  setMainWidget(widget: MainAreaWidget): void {
    if (this._mainWidget) {
      console.error(this.__('Main Video Chat widget already set'));
      return;
    }
    this._mainWidget = widget;
  }

  /** A scoped handler for connecting to the settings Signal  */
  protected onSettingsChanged = (): void => {
    this.stateChanged.emit(void 0);
  };

  /**
   * Add a new room provider.
   */
  registerRoomProvider(options: IVideoChatManager.IProviderOptions): void {
    this._roomProviders.set(options.id, options);

    const { stateChanged } = options.provider;

    if (stateChanged) {
      stateChanged.connect(
        async () => await Promise.all([this.updateConfig(), this.updateRooms()])
      );
    }

    this._roomProvidersChanged.emit(void 0);
  }

  providerForRoom = (room: Room): IVideoChatManager.IProviderOptions => {
    const key = this._roomProvidedBy.get(room) || null;
    if (key) {
      return this._roomProviders.get(key);
    }
    return null;
  };

  /**
   * Handle room providers changing
   */
  protected async onRoomProvidersChanged(): Promise<void> {
    try {
      await Promise.all([this.updateConfig(), this.updateRooms()]);
    } catch (err) {
      console.warn(err);
    }
    this.stateChanged.emit(void 0);
  }

  get rankedProviders(): IVideoChatManager.IProviderOptions[] {
    const providers = [...this._roomProviders.values()];
    providers.sort((a, b) => a.rank - b.rank);
    return providers;
  }

  /**
   * Fetch all config from all providers
   */
  async updateConfig(): Promise<VideoChatConfig> {
    let config: VideoChatConfig = { jitsiServer: DEFAULT_DOMAIN };
    for (const { provider, id } of this.rankedProviders) {
      try {
        config = { ...config, ...(await provider.updateConfig()) };
      } catch (err) {
        console.warn(this.__(`Failed to load config from %1`, id));
        console.trace(err);
      }
    }
    this._config = config;
    this.stateChanged.emit(void 0);
    return config;
  }

  /**
   * Fetch all rooms from all providers
   */
  async updateRooms(): Promise<Room[]> {
    let rooms: Room[] = [];
    let providerRooms: Room[];
    for (const { provider, id } of this.rankedProviders) {
      try {
        providerRooms = await provider.updateRooms();
        for (const room of providerRooms) {
          this._roomProvidedBy.set(room, id);
        }
        rooms = [...rooms, ...providerRooms];
      } catch (err) {
        console.warn(this.__(`Failed to load rooms from %1`, id));
        console.trace(err);
      }
    }
    this._rooms = rooms;
    this.stateChanged.emit(void 0);
    return rooms;
  }

  async createRoom(room: Partial<Room>): Promise<Room | null> {
    let newRoom: Room | null = null;
    for (const { provider, id } of this.rankedProviders) {
      if (!provider.canCreateRooms) {
        continue;
      }
      try {
        newRoom = await provider.createRoom(room);
        break;
      } catch (err) {
        console.warn(this.__(`Failed to create room from %1`, id));
      }
    }

    this.currentRoom = newRoom;

    return newRoom;
  }

  get canCreateRooms(): boolean {
    for (const { provider } of this.rankedProviders) {
      if (provider.canCreateRooms) {
        return true;
      }
    }
    return false;
  }

  /** Lazily get the JitiExternalAPI script, as loaded from the jitsi server */
  getJitsiAPI(): IJitsiFactory {
    return () => {
      if (Private.api) {
        return Private.api;
      } else if (this.config != null) {
        const domain = this.config?.jitsiServer
          ? this.config.jitsiServer
          : DEFAULT_DOMAIN;
        const url = `https://${domain}/external_api.js`;
        Private.ensureExternalAPI(url)
          .then(() => this.stateChanged.emit(void 0))
          .catch(console.warn);
      }
      return null;
    };
  }
}

/** A namespace for video chat manager extras */
export namespace VideoChatManager {
  /** placeholder options for video chat manager */
  export interface IOptions extends IVideoChatManager.IOptions {
    trans: TranslationBundle;
  }
}

/** a private namespace for the singleton jitsi script tag */
namespace Private {
  export let api: JitsiMeetExternalAPIConstructor;

  let _scriptElement: HTMLScriptElement;
  let _loadPromise: PromiseDelegate<JitsiMeetExternalAPIConstructor>;

  /** return a promise that resolves when the Jitsi external JS API is available */
  export async function ensureExternalAPI(
    url: string
  ): Promise<JitsiMeetExternalAPIConstructor> {
    if (_loadPromise == null) {
      DEBUG && console.warn('loading...');
      _loadPromise = new PromiseDelegate();
      _scriptElement = document.createElement('script');
      _scriptElement.id = `id-${CSS}-external-api`;
      _scriptElement.src = url;
      _scriptElement.async = true;
      _scriptElement.type = 'text/javascript';
      document.body.appendChild(_scriptElement);
      _scriptElement.onload = () => {
        api = (window as any).JitsiMeetExternalAPI;
        DEBUG && console.warn('loaded...');
        _loadPromise.resolve(api);
      };
    }
    return _loadPromise.promise;
  }
}