/*
 * Copyright (C) 2018 Robert Bosch GmbH Copyright (C) 2018 Robert Bosch Manufacturing Solutions GmbH, Germany. All rights reserved.
 */

import { Injectable, OnDestroy } from '@angular/core';

import { ActivatedRoute, ActivatedRouteSnapshot, NavigationEnd, NavigationStart, Router } from '@angular/router';
import { BehaviorSubject, Observable, Subject } from 'rxjs';
import { filter, takeUntil } from 'rxjs/operators';
import { Breadcrumb } from '../shared/breadcrumbs/breadcrumb/breadcrumb.model';
// noinspection TypeScriptPreferShortImport due to cyclic dependency
import { SidebarNavItem } from './layout/bci-sidebar/sidebar-nav-item.model';
import { LoggerService } from './logger/logger.service';

/**
 * Provides list of breadcrumbs for the currently active route.
 * HOW TO USE:
 * Import this module in app.module with "BreadcrumbsModule".
 * Position it on the page using the html tag <bci-breadcrumbs>.
 * Define custom titles and temporary titles, which will be cleared upon navigation.
 */
@Injectable({
  providedIn: 'root',
})
export class BreadcrumbsService implements OnDestroy {
  /**
   * Always show "home"-route in breadcrumbs (default: false)
   */
  public stickyRootBreadcrumb = false;
  private breadcrumbs$: BehaviorSubject<Breadcrumb[]> = new BehaviorSubject<Breadcrumb[]>([]);
  /**
   * Observable providing the list of Breadcrumb (current as well as future lists)
   */
  public breadcrumbs: Observable<Breadcrumb[]> = this.breadcrumbs$.asObservable();
  public showOrHideBreadcrumbs = true;
  private pathToBreadcrumbMap = new Map<string, Breadcrumb>();
  private temporaryPathToBreadcrumbMap = new Map<string, Breadcrumb>();

  private ngUnsubscribe = new Subject<void>();

  constructor(private router: Router, private activatedRoute: ActivatedRoute, private logger: LoggerService) {
    // update breadcrumbs after navgiation
    this.router.events
      .pipe(
        takeUntil(this.ngUnsubscribe),
        filter((event) => event instanceof NavigationEnd)
      )
      .subscribe(() => this.refresh());

    // clean up temporary path-to-title mappings upon start of navigation
    this.router.events
      .pipe(
        takeUntil(this.ngUnsubscribe),
        filter((event) => event instanceof NavigationStart)
      )
      .subscribe((event) => {
        const oldUrl = this.normalizePath((event as NavigationStart).url);
        Array.from(this.temporaryPathToBreadcrumbMap.keys(), (path) => {
          if (!oldUrl.startsWith(path)) {
            this.temporaryPathToBreadcrumbMap.delete(path);
          }
        });
      });
  }

  ngOnDestroy(): void {
    this.ngUnsubscribe.next();
    this.ngUnsubscribe.complete();
  }

  public setNavigationItems(navItems: SidebarNavItem[]) {
    this.processNavigationItems(navItems);
    this.refresh();
  }

  /**
   * Set title for a path, like setTitle('a/b/c', 'C');
   * @param path relative path
   * @param title breadcrumb title for the given path
   */
  public setTitle(path: string, title: string): void {
    this.setBreadcrumb(this.normalizePath(path), {
      title: title,
      url: path,
    });
    this.refresh();
  }

  /**
   * Create a temporary breacrumb for a path, like 'C' for 'a/b/c'
   * @param path relative path
   * @param title breadcrumb title for the given path
   */
  public setTemporaryTitle(path: string, title: string): void {
    this.temporaryPathToBreadcrumbMap.set(this.normalizePath(path), {
      title: title,
      url: path,
    });
    this.refresh();
  }

  /**
   * Create a temporary breacrumb for a route's url
   * @param route the matched route
   * @param title the desired title
   */
  public setTemporaryTitleForRoute(route: ActivatedRouteSnapshot, title: string) {
    this.setTemporaryTitle(this.routeToPathFromRoot(route), title);
  }

  /**
   * Create a temporary breadcrumb for a route superpath containing all required segments.
   * E.g. a breadcrumb with the url [...]/a/b will be created for a route path /:A/:B/:C and the required segments [':A', ':B'] or [':B']
   * @param route the matched route
   * @param requiredPathSegments the required path segments, parameters prefixed with ':'
   * @param title the desired title
   */
  public setTemporaryTitleForPartialRoute(
    route: ActivatedRouteSnapshot,
    requiredPathSegments: string[],
    title: string
  ) {
    const missingPathSegments = new Set(requiredPathSegments);
    const baseUrl = this.routeToPathFromRoot(route.parent);
    const pathSegments = this.getPathSegments(route);
    const relativeUrlSegments: string[] = [];
    for (let i = 0; i < pathSegments.length; i++) {
      const pathSegment = pathSegments[i].path;
      const urlSegment = route.url[i].path;
      missingPathSegments.delete(pathSegment);
      relativeUrlSegments.push(urlSegment);

      if (missingPathSegments.size === 0) {
        break;
      }
    }

    if (missingPathSegments.size > 0) {
      this.logger.warn(
        'Required path segments missing, no new breadcrumb has been defined: ' + Array.from(missingPathSegments)
      );
      return;
    }

    const relativeUrl =
      relativeUrlSegments.length > 0 ? (baseUrl === '/' ? '' : '/') + relativeUrlSegments.join('/') : '';
    this.setTemporaryTitle(baseUrl + relativeUrl, title);
  }

  /**
   * Build URL string from root to the given route (including).
   * @param route last route part of the url.
   * @returns URL from root to route.
   */
  public routeToPathFromRoot(route: ActivatedRouteSnapshot): string {
    return (
      '/' +
      route.pathFromRoot
        .map((r) =>
          r.url
            .map((urlSegment) => urlSegment.path)
            .filter((s) => !!s.length)
            .join('/')
        )
        .filter((s) => !!s.length)
        .join('/')
    );
  }

  /**
   * Recompute breadcrumbs for currently activated route
   */
  public refresh(): void {
    this.breadcrumbs$.next(this.parseActivatedRoute());
  }

  public setShowOrHideBreadcrumbs(value: boolean) : void {
    this.showOrHideBreadcrumbs = value;
  }

  public getShowOrHideBreadcrumbs() : boolean {
    return this.showOrHideBreadcrumbs;
  }

  private getPathSegments(route: ActivatedRouteSnapshot) {
    if (!route.routeConfig || !route.routeConfig.path) {
      return route.url;
    }
    const urlTree = this.router.parseUrl(route.routeConfig.path);
    if (urlTree.root && urlTree.root.children && urlTree.root.children.primary) {
      return urlTree.root.children.primary.segments;
    } else {
      return [];
    }
  }

  private processNavigationItems(navigationItems: SidebarNavItem[]) {
    this.flattenNavigationItems(navigationItems).forEach(({ url, title }) =>
      this.setBreadcrumb(url, {
        title: title,
        url: url,
      })
    );
  }

  private flattenNavigationItems(navItems: SidebarNavItem[]): SidebarNavItem[] {
    return navItems
      .map((navItem) => {
        if (navItem.items && navItem.items.length > 0) {
          return this.flattenNavigationItems(navItem.items).concat(navItem);
        }
        return [navItem];
      })
      .reduce((prev, cur) => prev.concat(cur));
  }

  private parseActivatedRoute(): Breadcrumb[] {
    const breadcrumbs: Breadcrumb[] = [];
    const pathSegments: string[] = [];

    // get path segments
    for (let route = this.activatedRoute.snapshot; route; route = route.firstChild) {
      pathSegments.push(...route.url.map((segment) => segment.path));
    }

    // add "/" ("Home") to breadcrumbs
    if (this.stickyRootBreadcrumb || pathSegments.length === 0) {
      pathSegments.unshift('');
    }

    // look for breadcrumbs matching superpaths of the current path starting from the root
    const superpathSegments = [];
    for (const pathSegment of pathSegments) {
      superpathSegments.push(pathSegment);
      const breadcrumbElement: Breadcrumb = this.getBreadcrumb(this.toPath(superpathSegments));
      if (!!breadcrumbElement) {
        breadcrumbs.push(breadcrumbElement);
      }
    }

    if (!!breadcrumbs.length) {
      const lastBreadcrumb = breadcrumbs[breadcrumbs.length - 1];
      if (lastBreadcrumb.url && this.normalizePath(lastBreadcrumb.url) === this.toPath(pathSegments)) {
        // do not link breadcrumb for currently activated route
        breadcrumbs[breadcrumbs.length - 1] = {
          ...lastBreadcrumb,
          url: undefined,
        };
      }
    }

    return breadcrumbs;
  }

  private getBreadcrumb(path: string): Breadcrumb | undefined {
    return this.temporaryPathToBreadcrumbMap.get(path) || this.pathToBreadcrumbMap.get(path);
  }

  private setBreadcrumb(path: string, breadcrumb: Breadcrumb): void {
    this.pathToBreadcrumbMap.set(this.normalizePath(path), breadcrumb);
  }

  private normalizePath(path: string) {
    if (path === undefined) {
      return '';
    }
    const tempPath = path.startsWith('/') ? path : '/' + path;
    return tempPath.endsWith('/') ? tempPath : tempPath + '/';
  }

  private toPath(pathSegments: string[]) {
    return this.normalizePath(pathSegments.join('/'));
  }
}
