import { isPlatformBrowser } from '@angular/common';
import { Inject, Injectable, InjectionToken, NgZone, Optional, PLATFORM_ID } from '@angular/core';
import { ɵPromiseProxy, ɵapplyMixins, ɵcacheInstance, ɵlazySDKProxy } from '@angular/fire/compat';
import { FirebaseApp } from '@angular/fire/compat';
import firebase from 'firebase/compat/app';
import { EMPTY, Observable, Subscription, of } from 'rxjs';
import { map, shareReplay, switchMap, tap } from 'rxjs/operators';
import { proxyPolyfillCompat } from './base';

export const INSTRUMENTATION_ENABLED = new InjectionToken<boolean>('angularfire2.performance.instrumentationEnabled');
export const DATA_COLLECTION_ENABLED = new InjectionToken<boolean>('angularfire2.performance.dataCollectionEnabled');

// eslint-disable-next-line @typescript-eslint/no-empty-interface
export interface AngularFirePerformance extends ɵPromiseProxy<firebase.performance.Performance> {
}

@Injectable({
  providedIn: 'any'
})
export class AngularFirePerformance {

  private readonly performance: Observable<firebase.performance.Performance>;

  constructor(
    app: FirebaseApp,
    @Optional() @Inject(INSTRUMENTATION_ENABLED) instrumentationEnabled: boolean | null,
    @Optional() @Inject(DATA_COLLECTION_ENABLED) dataCollectionEnabled: boolean | null,
    private zone: NgZone,
    // eslint-disable-next-line @typescript-eslint/ban-types
    @Inject(PLATFORM_ID) platformId: Object
  ) {

    this.performance = of(undefined).pipe(
      switchMap(() => isPlatformBrowser(platformId) ? zone.runOutsideAngular(() => import('firebase/compat/performance')) : EMPTY),
      map(() => ɵcacheInstance(`performance`, 'AngularFirePerformance', app.name, () => {
        const performance = zone.runOutsideAngular(() => app.performance());
        if (instrumentationEnabled === false) {
          performance.instrumentationEnabled = false;
        }
        if (dataCollectionEnabled === false) {
          performance.dataCollectionEnabled = false;
        }
        return performance;
      }, [instrumentationEnabled, dataCollectionEnabled])),
      shareReplay({ bufferSize: 1, refCount: false })
    );

    return ɵlazySDKProxy(this, this.performance, zone);

  }

}

const trace$ = (traceId: string) => {
  // eslint-disable-next-line @typescript-eslint/prefer-optional-chain
  if (typeof window !== 'undefined' && window.performance?.mark) {
    const entries = window.performance.getEntriesByName(traceId, 'measure') || [];
    const startMarkName = `_${traceId}Start[${entries.length}]`;
    const endMarkName = `_${traceId}End[${entries.length}]`;
    return new Observable<void>(emitter => {
      window.performance.mark(startMarkName);
      emitter.next();
      return {
        unsubscribe: () => {
          window.performance.mark(endMarkName);
          window.performance.measure(traceId, startMarkName, endMarkName);
        }
      };
    });
  } else {
    return EMPTY;
  }
};

export const traceUntil = <T = any>(
  name: string,
  test: (a: T) => boolean,
  options?: { orComplete?: boolean }
) => (source$: Observable<T>) => new Observable<T>(subscriber => {
  const traceSubscription = trace$(name).subscribe();
  return source$.pipe(
    tap(
      a => test(a) && traceSubscription.unsubscribe(),
      () => undefined,
      () => options && options.orComplete && traceSubscription.unsubscribe()
    )
  ).subscribe(subscriber);
});

export const traceWhile = <T = any>(
  name: string,
  test: (a: T) => boolean,
  options?: { orComplete?: boolean }
) => (source$: Observable<T>) => new Observable<T>(subscriber => {
  let traceSubscription: Subscription | undefined;
  return source$.pipe(
    tap(
      a => {
        if (test(a)) {
          traceSubscription = traceSubscription || trace$(name).subscribe();
        } else {
          if (traceSubscription) {
            traceSubscription.unsubscribe();
          }

          traceSubscription = undefined;
        }
      },
      () => undefined,
      () => options && options.orComplete && traceSubscription && traceSubscription.unsubscribe()
    )
  ).subscribe(subscriber);
});

export const traceUntilComplete = <T = any>(name: string) => (source$: Observable<T>) => new Observable<T>(subscriber => {
  const traceSubscription = trace$(name).subscribe();
  return source$.pipe(
    tap(
      () => undefined,
      () => undefined,
      () => traceSubscription.unsubscribe()
    )
  ).subscribe(subscriber);
});

export const traceUntilFirst = <T = any>(name: string) => (source$: Observable<T>) => new Observable<T>(subscriber => {
  const traceSubscription = trace$(name).subscribe();
  return source$.pipe(
    tap(
      () => traceSubscription.unsubscribe(),
      () => undefined,
      () => undefined    )
  ).subscribe(subscriber);
});

export const trace = <T = any>(name: string) => (source$: Observable<T>) => new Observable<T>(subscriber => {
  const traceSubscription = trace$(name).subscribe();
  return source$.pipe(
    tap(
      () => traceSubscription.unsubscribe(),
      () => undefined,
      () => traceSubscription.unsubscribe()
    )
  ).subscribe(subscriber);
});

ɵapplyMixins(AngularFirePerformance, [proxyPolyfillCompat]);
