import {
  Component,
  ViewChild,
  ViewContainerRef,
  ComponentRef,
  SimpleChanges,
  OnInit,
  OnDestroy,
  ComponentFactoryResolver,
  ChangeDetectorRef,
  OnChanges
} from '@angular/core';
import { hasValue, isNotEmpty } from '../empty.util';
import { Subscription } from 'rxjs';
import { ThemeService } from './theme.service';
import { fromPromise } from 'rxjs/internal-compatibility';
import { catchError, switchMap, map } from 'rxjs/operators';
import { GenericConstructor } from '../../core/shared/generic-constructor';

@Component({
  selector: 'ds-themed',
  styleUrls: ['./themed.component.scss'],
  templateUrl: './themed.component.html',
})
export abstract class ThemedComponent<T> implements OnInit, OnDestroy, OnChanges {
  @ViewChild('vcr', { read: ViewContainerRef }) vcr: ViewContainerRef;
  protected compRef: ComponentRef<T>;

  protected lazyLoadSub: Subscription;
  protected themeSub: Subscription;

  protected inAndOutputNames: (keyof T & keyof this)[] = [];

  constructor(
    protected resolver: ComponentFactoryResolver,
    protected cdr: ChangeDetectorRef,
    protected themeService: ThemeService
  ) {
  }

  protected abstract getComponentName(): string;

  protected abstract importThemedComponent(themeName: string): Promise<any>;
  protected abstract importUnthemedComponent(): Promise<any>;

  ngOnChanges(changes: SimpleChanges): void {
    // if an input or output has changed
    if (this.inAndOutputNames.some((name: any) => hasValue(changes[name]))) {
      this.connectInputsAndOutputs();
    }
  }

  ngOnInit(): void {
    this.destroyComponentInstance();
    this.themeSub = this.themeService.getThemeName$().subscribe(() => {
      this.renderComponentInstance();
    });
  }

  ngOnDestroy(): void {
    [this.themeSub, this.lazyLoadSub].filter((sub) => hasValue(sub)).forEach((sub) => sub.unsubscribe());
    this.destroyComponentInstance();
  }

  protected renderComponentInstance(): void {
    this.destroyComponentInstance();

    if (hasValue(this.lazyLoadSub)) {
      this.lazyLoadSub.unsubscribe();
    }

    this.lazyLoadSub =
      fromPromise(this.importThemedComponent(this.themeService.getThemeName())).pipe(
        // if there is no themed version of the component an exception is thrown,
        // catch it and return null instead
        catchError(() => [null]),
        switchMap((themedFile: any) => {
          if (hasValue(themedFile) && hasValue(themedFile[this.getComponentName()])) {
            // if the file is not null, and exports a component with the specified name,
            // return that component
            return [themedFile[this.getComponentName()]];
          } else {
            // otherwise import and return the default component
            return fromPromise(this.importUnthemedComponent()).pipe(
              map((unthemedFile: any) => {
                return unthemedFile[this.getComponentName()];
              })
            );
          }
        }),
      ).subscribe((constructor: GenericConstructor<T>) => {
        const factory = this.resolver.resolveComponentFactory(constructor);
        this.compRef = this.vcr.createComponent(factory);
        this.connectInputsAndOutputs();
        this.cdr.markForCheck();
      });
  }

  protected destroyComponentInstance(): void {
    if (hasValue(this.compRef)) {
      this.compRef.destroy();
      this.compRef = null;
    }
    if (hasValue(this.vcr)) {
      this.vcr.clear();
    }
  }

  protected connectInputsAndOutputs(): void {
    if (isNotEmpty(this.inAndOutputNames) && hasValue(this.compRef) && hasValue(this.compRef.instance)) {
      this.inAndOutputNames.forEach((name: any) => {
        this.compRef.instance[name] = this[name];
      });
    }
  }
}