import { Resource, getUrl, hasLink } from '@ngxp/rest';
import { isEqual, isNull } from 'lodash-es';
import { Observable, combineLatest, filter, startWith, tap } from 'rxjs';
import { ResourceStateService } from '../ngrx/state.service';
import { isNotNull } from '../tech.util';
import { ServiceConfig } from './resource.model';
import { mapToFirst, mapToResource } from './resource.rxjs.operator';
import {
  StateResource,
  createEmptyStateResource,
  isInvalidResourceCombination,
  isLoadingRequired,
  isStateResoureStable,
} from './resource.util';

export class ResourceLoader<B extends Resource, T extends Resource> {
  configResource: B = null;

  constructor(
    private config: ServiceConfig<B>,
    private resourceStateService: ResourceStateService<B, T>,
  ) {}

  public get(): Observable<StateResource<T>> {
    return combineLatest([
      this.resourceStateService.selectResource(),
      this.getConfigResource(),
    ]).pipe(
      tap(([stateResource, configResource]) =>
        this.handleResourceChanges(stateResource, configResource),
      ),
      filter(
        ([stateResource, configResource]) =>
          !isInvalidResourceCombination(stateResource, configResource),
      ),
      mapToFirst<T, B>(),
      startWith(createEmptyStateResource<T>(true)),
    );
  }

  private getConfigResource(): Observable<B> {
    return this.config.baseResource.pipe(filter(isStateResoureStable), mapToResource<B>());
  }

  handleResourceChanges(stateResource: StateResource<T>, configResource: B): void {
    if (!isEqual(this.configResource, configResource)) {
      this.handleConfigResourceChanges(stateResource, configResource);
    } else if (this.shouldLoadResource(stateResource, configResource)) {
      this.loadResource(configResource);
    }
  }

  handleConfigResourceChanges(stateResource: StateResource<T>, configResource: B) {
    this.configResource = configResource;
    if (isStateResoureStable(stateResource)) {
      this.updateStateResourceByConfigResource(stateResource, configResource);
    }
  }

  updateStateResourceByConfigResource(stateResource: StateResource<T>, configResource: B): void {
    if (this.shouldClearStateResource(stateResource, configResource)) {
      this.resourceStateService.clearResource();
    } else if (this.hasGetLink(configResource)) {
      this.loadResource(configResource);
    }
  }

  shouldClearStateResource(stateResource: StateResource<T>, configResource: B): boolean {
    return (
      (isNull(configResource) || this.hasNotGetLink(configResource)) &&
      !this.isStateResourceEmpty(stateResource)
    );
  }

  private hasNotGetLink(configResource: B) {
    return !this.hasGetLink(configResource);
  }

  hasGetLink(configResource: B): boolean {
    return isNotNull(configResource) && hasLink(configResource, this.config.getLinkRel);
  }

  private isStateResourceEmpty(stateResource: StateResource<T>): boolean {
    return isEqual(stateResource, createEmptyStateResource());
  }

  shouldLoadResource(stateResource: StateResource<T>, configResource: B): boolean {
    return (
      isNotNull(configResource) &&
      hasLink(configResource, this.config.getLinkRel) &&
      isLoadingRequired(stateResource)
    );
  }

  loadResource(configResource: B): void {
    this.resourceStateService.loadResource(this.getGetUrl(configResource));
  }

  private getGetUrl(configResource: B): string {
    return getUrl(configResource, this.config.getLinkRel);
  }
}