/* eslint-disable max-classes-per-file */
import {
  Inject,
  Injectable,
} from '@angular/core';
import {
  createSelector,
  Store,
} from '@ngrx/store';
import {
  combineLatest as observableCombineLatest,
  Observable,
  of,
} from 'rxjs';
import {
  filter,
  map,
  switchMap,
} from 'rxjs/operators';
import {
  APP_CONFIG,
  AppConfig,
} from 'src/config/app-config.interface';
import { v4 as uuidv4 } from 'uuid';

import { AppState } from '../app.reducer';
import { getCollectionPageRoute } from '../collection-page/collection-page-routing-paths';
import { getCommunityPageRoute } from '../community-page/community-page-routing-paths';
import { CollectionDataService } from '../core/data/collection-data.service';
import { CommunityDataService } from '../core/data/community-data.service';
import { FindListOptions } from '../core/data/find-list-options.model';
import {
  buildPaginatedList,
  PaginatedList,
} from '../core/data/paginated-list.model';
import { RemoteData } from '../core/data/remote-data';
import { Collection } from '../core/shared/collection.model';
import { Community } from '../core/shared/community.model';
import {
  getFirstCompletedRemoteData,
  getFirstSucceededRemoteData,
} from '../core/shared/operators';
import { PageInfo } from '../core/shared/page-info.model';
import {
  hasValue,
  isNotEmpty,
} from '../shared/empty.util';
import { followLink } from '../shared/utils/follow-link-config.model';
import { CommunityListSaveAction } from './community-list.actions';
import { CommunityListState } from './community-list.reducer';
import { FlatNode } from './flat-node.model';
import { ShowMoreFlatNode } from './show-more-flat-node.model';

// Helper method to combine and flatten an array of observables of flatNode arrays
export const combineAndFlatten = (obsList: Observable<FlatNode[]>[]): Observable<FlatNode[]> =>
  observableCombineLatest([...obsList]).pipe(
    map((matrix: any[][]) => [].concat(...matrix)),
    filter((arr: any[]) => arr.every((e) => hasValue(e))),
  );

/**
 * Creates a flatNode from a community or collection
 * @param c               The community or collection this flatNode represents
 * @param isExpandable    Whether or not this node is expandable (true if it has children)
 * @param level           Level indicating how deep in the tree this node should be rendered
 * @param isExpanded      Whether or not this node already is expanded
 * @param parent          Parent of this node (flatNode representing its parent community)
 */
export const toFlatNode = (
  c: Community | Collection,
  isExpandable: Observable<boolean>,
  level: number,
  isExpanded: boolean,
  parent?: FlatNode,
): FlatNode => ({
  isExpandable$: isExpandable,
  name: c.name,
  id: c.id,
  level: level,
  isExpanded,
  parent,
  payload: c,
  isShowMoreNode: false,
  route: c instanceof Community ? getCommunityPageRoute(c.id) : getCollectionPageRoute(c.id),
});

/**
 * Creates a show More flatnode where only the level and parent are of importance
 */
export const showMoreFlatNode = (
  id: string,
  level: number,
  parent: FlatNode,
): FlatNode => ({
  isExpandable$: of(false),
  name: 'Show More Flatnode',
  id: id,
  level: level,
  isExpanded: false,
  parent: parent,
  payload: new ShowMoreFlatNode(),
  isShowMoreNode: true,
});

// Selectors the get the communityList data out of the store
const communityListStateSelector = (state: AppState) => state.communityList;
const expandedNodesSelector = createSelector(communityListStateSelector, (communityList: CommunityListState) => communityList.expandedNodes);
const loadingNodeSelector = createSelector(communityListStateSelector, (communityList: CommunityListState) => communityList.loadingNode);

/**
 * Service class for the community list, responsible for the creating of the flat list used by communityList dataSource
 *  and connection to the store to retrieve and save the state of the community list
 */
@Injectable({ providedIn: 'root' })
export class CommunityListService {

  private pageSize: number;

  constructor(
    @Inject(APP_CONFIG) protected appConfig: AppConfig,
    private communityDataService: CommunityDataService,
    private collectionDataService: CollectionDataService,
    private store: Store<any>,
  ) {
    this.pageSize = appConfig.communityList.pageSize;
  }

  private configOnePage: FindListOptions = Object.assign(new FindListOptions(), {
    elementsPerPage: 1,
  });

  saveCommunityListStateToStore(expandedNodes: FlatNode[], loadingNode: FlatNode): void {
    this.store.dispatch(new CommunityListSaveAction(expandedNodes, loadingNode));
  }

  getExpandedNodesFromStore(): Observable<FlatNode[]> {
    return this.store.select(expandedNodesSelector);
  }

  getLoadingNodeFromStore(): Observable<FlatNode> {
    return this.store.select(loadingNodeSelector);
  }

  /**
   * Gets all top communities, limited by page, and transforms this in a list of flatNodes.
   * @param findOptions       FindListOptions
   * @param expandedNodes     List of expanded nodes; if a node is not expanded its subCommunities and collections need
   *                            not be added to the list
   */
  loadCommunities(findOptions: FindListOptions, expandedNodes: FlatNode[]): Observable<FlatNode[]> {
    const currentPage = findOptions.currentPage;
    const topCommunities = [];
    for (let i = 1; i <= currentPage; i++) {
      const pagination: FindListOptions = Object.assign({}, findOptions, { currentPage: i });
      topCommunities.push(this.getTopCommunities(pagination));
    }
    const topComs$ = observableCombineLatest([...topCommunities]).pipe(
      map((coms: PaginatedList<Community>[]) => {
        const newPages: Community[][] = coms.map((unit: PaginatedList<Community>) => unit.page);
        const newPage: Community[] = [].concat(...newPages);
        let newPageInfo = new PageInfo();
        if (coms && coms.length > 0) {
          newPageInfo = Object.assign({}, coms[0].pageInfo, { currentPage });
        }
        return buildPaginatedList(newPageInfo, newPage);
      }),
    );
    return topComs$.pipe(
      switchMap((topComs: PaginatedList<Community>) => this.transformListOfCommunities(topComs, 0, null, expandedNodes)),
      // distinctUntilChanged((a: FlatNode[], b: FlatNode[]) => a.length === b.length)
    );
  }

  /**
   * Puts the initial top level communities in a list to be called upon
   */
  private getTopCommunities(options: FindListOptions): Observable<PaginatedList<Community>> {
    return this.communityDataService.findTop({
      currentPage: options.currentPage,
      elementsPerPage: this.pageSize,
      sort: {
        field: options.sort.field,
        direction: options.sort.direction,
      },
    },
    followLink('subcommunities', { findListOptions: this.configOnePage }),
    followLink('collections', { findListOptions: this.configOnePage }))
      .pipe(
        getFirstSucceededRemoteData(),
        map((results) => results.payload),
      );
  }

  /**
   * Transforms a list of communities to a list of FlatNodes according to the instructions detailed in transformCommunity
   * @param listOfPaginatedCommunities    Paginated list of communities to be transformed
   * @param level                         Level the tree is currently at
   * @param parent                        FlatNode of the parent of this list of communities
   * @param expandedNodes                 List of expanded nodes; if a node is not expanded its subcommunities and collections need not be added to the list
   */
  public transformListOfCommunities(listOfPaginatedCommunities: PaginatedList<Community>,
    level: number,
    parent: FlatNode,
    expandedNodes: FlatNode[]): Observable<FlatNode[]> {
    if (isNotEmpty(listOfPaginatedCommunities.page)) {
      let currentPage = listOfPaginatedCommunities.currentPage;
      if (isNotEmpty(parent)) {
        currentPage = expandedNodes.find((node: FlatNode) => node.id === parent.id).currentCommunityPage;
      }
      let obsList = listOfPaginatedCommunities.page
        .map((community: Community) => {
          return this.transformCommunity(community, level, parent, expandedNodes);
        });
      if (currentPage < listOfPaginatedCommunities.totalPages && currentPage === listOfPaginatedCommunities.currentPage) {
        obsList = [...obsList, of([showMoreFlatNode(`community-${uuidv4()}`, level, parent)])];
      }

      return combineAndFlatten(obsList);
    } else {
      return of([]);
    }
  }

  /**
   * Transforms a community in a list of FlatNodes containing firstly a flatnode of the community itself,
   *      followed by flatNodes of its possible subcommunities and collection
   * It gets called recursively for each subcommunity to add its subcommunities and collections to the list
   * Number of subcommunities and collections added, is dependent on the current page the parent is at for respectively subcommunities and collections.
   * @param community         Community being transformed
   * @param level             Depth of the community in the list, subcommunities and collections go one level deeper
   * @param parent            Flatnode of the parent community
   * @param expandedNodes     List of nodes which are expanded, if node is not expanded, it need not add its page-limited subcommunities or collections
   */
  public transformCommunity(community: Community, level: number, parent: FlatNode, expandedNodes: FlatNode[]): Observable<FlatNode[]> {
    let isExpanded = false;
    if (isNotEmpty(expandedNodes)) {
      isExpanded = hasValue(expandedNodes.find((node) => (node.id === community.id)));
    }

    const isExpandable$ = this.getIsExpandable(community);

    const communityFlatNode = toFlatNode(community, isExpandable$, level, isExpanded, parent);

    let obsList = [of([communityFlatNode])];

    if (isExpanded) {
      const currentCommunityPage = expandedNodes.find((node: FlatNode) => node.id === community.id).currentCommunityPage;
      let subcoms = [];
      for (let i = 1; i <= currentCommunityPage; i++) {
        const nextSetOfSubcommunitiesPage = this.communityDataService.findByParent(community.uuid, {
          elementsPerPage: this.pageSize,
          currentPage: i,
        },
        followLink('subcommunities', { findListOptions: this.configOnePage }),
        followLink('collections', { findListOptions: this.configOnePage }))
          .pipe(
            getFirstCompletedRemoteData(),
            switchMap((rd: RemoteData<PaginatedList<Community>>) => {
              if (hasValue(rd) && hasValue(rd.payload)) {
                return this.transformListOfCommunities(rd.payload, level + 1, communityFlatNode, expandedNodes);
              } else {
                return of([]);
              }
            }),
          );

        subcoms = [...subcoms, nextSetOfSubcommunitiesPage];
      }

      obsList = [...obsList, combineAndFlatten(subcoms)];

      const currentCollectionPage = expandedNodes.find((node: FlatNode) => node.id === community.id).currentCollectionPage;
      let collections = [];
      for (let i = 1; i <= currentCollectionPage; i++) {
        const nextSetOfCollectionsPage = this.collectionDataService.findByParent(community.uuid, {
          elementsPerPage: this.pageSize,
          currentPage: i,
        })
          .pipe(
            getFirstCompletedRemoteData(),
            map((rd: RemoteData<PaginatedList<Collection>>) => {
              if (hasValue(rd) && hasValue(rd.payload)) {
                let nodes = rd.payload.page
                  .map((collection: Collection) => toFlatNode(collection, of(false), level + 1, false, communityFlatNode));
                if (currentCollectionPage < rd.payload.totalPages && currentCollectionPage === rd.payload.currentPage) {
                  nodes = [...nodes, showMoreFlatNode(`collection-${uuidv4()}`, level + 1, communityFlatNode)];
                }
                return nodes;
              } else {
                return [];
              }
            }),
          );
        collections = [...collections, nextSetOfCollectionsPage];
      }
      obsList = [...obsList, combineAndFlatten(collections)];
    }

    return combineAndFlatten(obsList);
  }

  /**
   * Checks if a community has subcommunities or collections by querying the respective services with a pageSize = 0
   *      Returns an observable that combines the result.payload.totalElements of the observables that the
   *          respective services return when queried
   * @param community     Community being checked whether it is expandable (if it has subcommunities or collections)
   */
  public getIsExpandable(community: Community): Observable<boolean> {
    const hasSubcoms$ = this.communityDataService.findByParent(community.uuid, this.configOnePage)
      .pipe(
        map((rd: RemoteData<PaginatedList<Community>>) => {
          if (hasValue(rd) && hasValue(rd.payload)) {
            return rd.payload.totalElements > 0;
          } else {
            return false;
          }
        }),
      );

    const hasColls$ = this.collectionDataService.findByParent(community.uuid, this.configOnePage)
      .pipe(
        map((rd: RemoteData<PaginatedList<Collection>>) => {
          if (hasValue(rd) && hasValue(rd.payload)) {
            return rd.payload.totalElements > 0;
          } else {
            return false;
          }
        }),
      );

    return observableCombineLatest(hasSubcoms$, hasColls$).pipe(
      map(([hasSubcoms, hasColls]: [boolean, boolean]) => hasSubcoms || hasColls),
    );
  }

}
