import { Observable, ReplaySubject, Subject, firstValueFrom, map, scan, startWith } from 'rxjs';

import { ExtensionsLog, log } from '../logs/log';
import { deepFreeze } from '../utils';

export const MSG_CANNOT_REGISTER_READ_ONLY = 'Cannot register to a read-only registry';

export type PluginExtensionConfigs<T> = {
  pluginId: string;
  configs: T[];
};

export type RegistryType<T> = Record<string | symbol, T>;

// This is the base-class used by the separate specific registries.
export abstract class Registry<TRegistryValue, TMapType> {
  // Used in cases when we would like to pass a read-only registry to plugin.
  // In these cases we are passing in the `registrySubject` to the constructor.
  // (If TRUE `initialState` is ignored.)
  private isReadOnly: boolean;
  // This is the subject that receives extension configs for a loaded plugin.
  private resultSubject: Subject<PluginExtensionConfigs<TMapType>>;
  protected logger: ExtensionsLog;
  // This is the subject that we expose.
  // (It will buffer the last value on the stream - the registry - and emit it to new subscribers immediately.)
  protected registrySubject: ReplaySubject<RegistryType<TRegistryValue>>;

  constructor(options: {
    registrySubject?: ReplaySubject<RegistryType<TRegistryValue>>;
    initialState?: RegistryType<TRegistryValue>;
    log?: ExtensionsLog;
  }) {
    this.resultSubject = new Subject<PluginExtensionConfigs<TMapType>>();
    this.logger = options.log ?? log;
    this.isReadOnly = false;

    // If the registry subject (observable) is provided, it means that all the registry updates are taken care of outside of this class -> it is read-only.
    if (options.registrySubject) {
      this.registrySubject = options.registrySubject;
      this.isReadOnly = true;

      return;
    }

    this.registrySubject = new ReplaySubject<RegistryType<TRegistryValue>>(1);
    this.resultSubject
      .pipe(
        scan(this.mapToRegistry.bind(this), options.initialState ?? {}),
        // Emit an empty registry to start the stream (it is only going to do it once during construction, and then just passes down the values)
        startWith(options.initialState ?? {}),
        map((registry) => deepFreeze(registry))
      )
      // Emitting the new registry to `this.registrySubject`
      .subscribe(this.registrySubject);
  }

  abstract mapToRegistry(
    registry: RegistryType<TRegistryValue>,
    item: PluginExtensionConfigs<TMapType>
  ): RegistryType<TRegistryValue>;

  register(result: PluginExtensionConfigs<TMapType>): void {
    if (this.isReadOnly) {
      throw new Error(MSG_CANNOT_REGISTER_READ_ONLY);
    }

    this.resultSubject.next(result);
  }

  asObservable(): Observable<RegistryType<TRegistryValue>> {
    return this.registrySubject.asObservable();
  }

  getState(): Promise<RegistryType<TRegistryValue>> {
    return firstValueFrom(this.asObservable());
  }
}
