import { Injectable } from '@angular/core';
import { createEffect, Actions, ofType, ROOT_EFFECTS_INIT } from '@ngrx/effects';
import { ROUTER_NAVIGATED, RouterNavigatedAction } from '@ngrx/router-store';
import { map, withLatestFrom, expand, switchMap, toArray, startWith, filter } from 'rxjs/operators';
import { SetThemeAction } from './theme.actions';
import { environment } from '../../../environments/environment';
import { ThemeConfig, themeFactory, Theme, } from '../../../config/theme.model';
import { hasValue, isNotEmpty, hasNoValue } from '../empty.util';
import { NoOpAction } from '../ngrx/no-op.action';
import { Store, select } from '@ngrx/store';
import { ThemeState } from './theme.reducer';
import { currentThemeSelector } from './theme.service';
import { of as observableOf, EMPTY, Observable } from 'rxjs';
import { ResolverActionTypes, ResolvedAction } from '../../core/resolving/resolver.actions';
import { followLink } from '../utils/follow-link-config.model';
import { RemoteData } from '../../core/data/remote-data';
import { DSpaceObject } from '../../core/shared/dspace-object.model';
import { getFirstCompletedRemoteData } from '../../core/shared/operators';
import { LinkService } from '../../core/cache/builders/link.service';
import { BASE_THEME_NAME } from './theme.constants';

export const DEFAULT_THEME_CONFIG = environment.themes.find((themeConfig: any) =>
  hasNoValue(themeConfig.regex) &&
  hasNoValue(themeConfig.handle) &&
  hasNoValue(themeConfig.uuid)
);

@Injectable()
export class ThemeEffects {
  /**
   * The list of configured themes
   */
  themes: Theme[];

  /**
   * True if at least one theme depends on the route
   */
  hasDynamicTheme: boolean;

  /**
   * Initialize with a theme that doesn't depend on the route.
   */
  initTheme$ = createEffect(() =>
    this.actions$.pipe(
      ofType(ROOT_EFFECTS_INIT),
      map(() => {
        if (hasValue(DEFAULT_THEME_CONFIG)) {
          return new SetThemeAction(DEFAULT_THEME_CONFIG.name);
        } else {
          return new SetThemeAction(BASE_THEME_NAME);
        }
      })
    )
  );

  /**
   * An effect that fires when a route change completes,
   * and determines whether or not the theme should change
   */
  updateThemeOnRouteChange$ = createEffect(() => this.actions$.pipe(
      // Listen for when a route change ends
      ofType(ROUTER_NAVIGATED),
      withLatestFrom(
        // Pull in the latest resolved action, or undefined if none was dispatched yet
        this.actions$.pipe(ofType(ResolverActionTypes.RESOLVED), startWith(undefined)),
        // and the current theme from the store
        this.store.pipe(select(currentThemeSelector))
      ),
      switchMap(([navigatedAction, resolvedAction, currentTheme]: [RouterNavigatedAction, ResolvedAction, string]) => {
        if (this.hasDynamicTheme === true && isNotEmpty(this.themes)) {
          const currentRouteUrl = navigatedAction.payload.routerState.url;
          // If resolvedAction exists, and deals with the current url
          if (hasValue(resolvedAction) && resolvedAction.payload.url === currentRouteUrl) {
            // Start with the resolved dso and go recursively through its parents until you reach the top-level community
            return observableOf(resolvedAction.payload.dso).pipe(
              this.getAncestorDSOs(),
              map((dsos: DSpaceObject[]) => {
                const dsoMatch =  this.matchThemeToDSOs(dsos, currentRouteUrl);
                return this.getActionForMatch(dsoMatch, currentTheme);
              })
            );
          }

          // check whether the route itself matches
          const routeMatch = this.themes.find((theme: Theme) => theme.matches(currentRouteUrl, undefined));

          return [this.getActionForMatch(routeMatch, currentTheme)];
        }

        // If there are no themes configured, do nothing
        return [new NoOpAction()];
      })
    )
  );

  /**
   * return the action to dispatch based on the given matching theme
   *
   * @param newTheme The theme to create an action for
   * @param currentThemeName The name of the currently active theme
   * @private
   */
  private getActionForMatch(newTheme: Theme, currentThemeName: string): SetThemeAction | NoOpAction {
    if (hasValue(newTheme) && newTheme.config.name !== currentThemeName) {
      // If we have a match, and it isn't already the active theme, set it as the new theme
      return new SetThemeAction(newTheme.config.name);
    } else {
      // Otherwise, do nothing
      return new NoOpAction();
    }
  }

  /**
   * Check the given DSpaceObjects in order to see if they match the configured themes in order.
   * If a match is found, the matching theme is returned
   *
   * @param dsos The DSpaceObjects to check
   * @param currentRouteUrl The url for the current route
   * @private
   */
  private matchThemeToDSOs(dsos: DSpaceObject[], currentRouteUrl: string): Theme {
    // iterate over the themes in order, and return the first one that matches
    return this.themes.find((theme: Theme) => {
      // iterate over the dsos's in order (most specific one first, so Item, Collection,
      // Community), and return the first one that matches the current theme
      const match = dsos.find((dso: DSpaceObject) => theme.matches(currentRouteUrl, dso));
      return hasValue(match);
    });

  }

  /**
   * An rxjs operator that will return an array of all the ancestors of the DSpaceObject used as
   * input. The initial DSpaceObject will be the first element of the output array, followed by
   * its parent, its grandparent etc
   *
   * @private
   */
  private getAncestorDSOs() {
    return (source: Observable<DSpaceObject>): Observable<DSpaceObject[]> =>
      source.pipe(
        expand((dso: DSpaceObject) => {
          // Check if the dso exists and has a parent link
          if (hasValue(dso) && typeof (dso as any).getParentLinkKey === 'function') {
            const linkName = (dso as any).getParentLinkKey();
            // If it does, retrieve it.
            return this.linkService.resolveLinkWithoutAttaching<DSpaceObject, DSpaceObject>(dso, followLink(linkName)).pipe(
              getFirstCompletedRemoteData(),
              map((rd: RemoteData<DSpaceObject>) => {
                if (hasValue(rd.payload)) {
                  // If there's a parent, use it for the next iteration
                  return rd.payload;
                } else {
                  // If there's no parent, or an error, return null, which will stop recursion
                  // in the next iteration
                  return null;
                }
              }),
            );
          }

          // The current dso has no value, or no parent. Return EMPTY to stop recursion
          return EMPTY;
        }),
        // only allow through DSOs that have a value
        filter((dso: DSpaceObject) => hasValue(dso)),
        // Wait for recursion to complete, and emit all results at once, in an array
        toArray()
      );
  }

  constructor(
    private actions$: Actions,
    private store: Store<ThemeState>,
    private linkService: LinkService,
  ) {
    // Create objects from the theme configs in the environment file
    this.themes = environment.themes.map((themeConfig: ThemeConfig) => themeFactory(themeConfig));
    this.hasDynamicTheme = environment.themes.some((themeConfig: any) =>
      hasValue(themeConfig.regex) ||
      hasValue(themeConfig.handle) ||
      hasValue(themeConfig.uuid)
    );
  }
}