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

import {
  AfterViewInit,
  Component,
  ContentChild,
  ElementRef,
  EventEmitter,
  HostListener,
  Input,
  OnDestroy,
  OnInit,
  Output,
  Renderer2,
  TemplateRef,
  ViewChild,
} from '@angular/core';
import { BreakpointObserver } from '@angular/cdk/layout';
import { MatDialogRef } from '@angular/material/dialog';
import { MatSidenav } from '@angular/material/sidenav';
import { from, merge, Observable, of, Subject, Subscription } from 'rxjs';
import { filter, first, map, shareReplay, take, takeUntil, tap } from 'rxjs/operators';
import { ModalWindowService } from '../../core/modal-window/modal-window.service';
import { DetailViewComponent } from './detail/detail-view.component';
import { ResizeHandleSizeChangedEvent } from '../resize-handle/resize-handle.model';
import { BOSCH_MEDIA_QUERIES } from '../../core';
import { OverlayContainer } from '@angular/cdk/overlay';

export enum WidthUnit {
  percentage = '%',
  pixel = 'px'
}

/**
 * The MasterDetailComponent contains a {@link MasterViewComponent} and a {@link DetailViewComponent}.
 *
 * Warning: If you use routing for the details view you should remember adapting this component's 'closeTrigger' to use routing as well.
 * Otherwise you risk e.g. with close on escape in combination with the close-details.guard.
 *
 * In the {@link MasterViewComponent} typically a collection of objects is shown.
 * By click on one of these objects a {@link DetailViewComponent} is made visible and shows the details of the selected object.
 *
 *
 */
@Component({
  selector: 'bci-master-detail-view',
  templateUrl: './master-detail-view.component.html',
  styleUrls: ['./master-detail-view.component.scss'],
})
export class MasterDetailViewComponent implements OnInit, AfterViewInit, OnDestroy {
  /**
   * true when the details view is shown as dialog or as sidebar
   */
  public isDetailsShown: boolean;

  /**
   * The {@link DetailViewComponent} can be shown on the right side next to the {@link MasterViewComponent} or as a slideout/slideover above it.
   */
  private _useSlideoverSidebarMode: boolean;

  /**
   * references the {@link DetailViewComponent} shown in the sidebar
   */
  @ContentChild(DetailViewComponent, { static: true }) public detailViewComponent: DetailViewComponent;

  /**
   * when true the details view is shown as dialog
   * else it is shown as modal dialog
   */
  private _useDialog = false;

  /**
   * defines the min and max size of the resize handle
   */
  private _detailViewMinSize: number;
  private _detailViewMaxSize: number;
  private _detailViewSize: number;
  private _masterViewMinSize: number;

  /**
   * defines the padding of the mat-sidenav
   */
  private resizeHandlePaddedOffset = 64;

  private detailsDialogRef: MatDialogRef<DetailViewComponent>;

  /**
   * references the template containing the {@link DetailViewComponent} of this MasterDetailViewComponent
   */
  @ViewChild('detailView') private detailsView: TemplateRef<DetailViewComponent>;
  @ViewChild('sidenavcontent', { read: ElementRef, static: true }) private __sidenavcontent: ElementRef;
  @ViewChild('sidenav', { read: ElementRef }) private __sidenav: ElementRef;
  @ViewChild(MatSidenav) private _sidenav: MatSidenav;
  private sidenavOpenChangeSubscription: Subscription;
  private _defaultCloseTrigger = this.closeDetails;

  set sidenav(sidenav: MatSidenav) {
    this._sidenav = sidenav;
    if (this._sidenav && !this.sidenavOpenChangeSubscription) {
      this.sidenavOpenChangeSubscription = this._sidenav.openedChange
        .pipe(takeUntil(this.componentDestroyed$))
        .subscribe(() => window.dispatchEvent(new Event('resize')));
    }
  }

  private closed$: Subject<void> = new Subject();
  closed = this.closed$.asObservable();
  private componentDestroyed$ = new Subject<void>();

  /**
   * Whether to close dialog / sidebar by pressing escape while focusing details. The closeTrigger will be called in case.
   * @default: true
   */
  @Input()
  closeOnEscape = true;

  /**
   * Whether to close dialog / sidebar by clicking on backdrop. The closeTrigger will be called in case.
   * @default: true
   */
  @Input()
  closeOnBackdropClick = true;

  /**
   * Function to trigger the closing of the details.
   */
  @Input()
  closeTrigger: () => void = this._defaultCloseTrigger;

  /**
   * When true the details view can be resized by dragging the resize icon.
   * @default: false
   */
  @Input() useResizeHandle = false;

  /**
   * Emits the detail view size (in pixels) after it was resized using the resize handle
   */
  @Output()
  detailViewSizeChange = new EventEmitter<number>();

  /**
   * Get or set detail view size.
   * This value is ignored when `useSlideoverSidebarMode` is set to true.
   * Unit of measure used is pixels by default.
   */
  @Input()
  get detailViewSize() {
    return this._detailViewSize;
  }

  set detailViewSize(value: number) {
    if (this.useSlideoverSidebarMode) {
      return;
    }
    this._detailViewSize = value;
    this.resizeDetailView();
    this.detailViewSizeChange.emit(value);
  }

  /**
   * detail view size unit
   */
  @Input()
  detailViewSizeUnit: string = WidthUnit.pixel;

  /**
   * When this value is set then master view cannot have a smaller size than the one provided.
   * If not provided, then master view size is not restricted.
   * Unit of measure used is pixels.
   * This property has effect only if `useResizeHandle` is set to true.
   */
  @Input()
  set masterViewMinSize(value: number) {
    // remember the value if set before AfterViewInit lifecycle, it will be set once AfterViewInit hits
    this._masterViewMinSize = value;
    if (this.__sidenavcontent === undefined) {
      return;
    }
    this.setDetailViewMaxSize();
  }

  /**
   * When this value is set then detail view cannot have a smaller size (in pixels) than the one provided.
   * If not provided, then detail view size is not restricted.
   * Unit of measure used is pixels.
   * This property has effect only if `useResizeHandle` is set to true.
   */
  @Input()
  set detailViewMinSize(value: number) {
    this._detailViewMinSize = value;
  }

  /**
   * Dynamically react to changes of the sidebar size independent of resizing the window (e.g. switching between differently sized tabs).
   * Could decrease performance, but is required for resizing dynamically sized sidebars correctly.
   * This option does not have an effect if `detailViewSize` is given.
   * Default: true
   */
  @Input()
  sidebarAutosize = true;

  constructor(
    private modalWindowService: ModalWindowService,
    private breakpointObserver: BreakpointObserver,
    private renderer: Renderer2,
    private overlayContainer: OverlayContainer
  ) {}

  get useSlideoverSidebarMode() {
    return this._useSlideoverSidebarMode;
  }

  @Input() set useSlideoverSidebarMode(use: boolean) {
    const setSlideoverSidebarMode = () => {
      this._useSlideoverSidebarMode = use;

      // make sure detail view is rendered correctly
      this.resizeDetailView();
    };

    // fix main content width calculation when responsively switching from over to side sidebar mode
    if (this.isDetailsShown && this._sidenav && this.useSlideoverSidebarMode && !use) {
      this.reopen(setSlideoverSidebarMode);
    } else {
      setSlideoverSidebarMode();
    }
  }

  get useDialog() {
    return this._useDialog;
  }

  @Input() set useDialog(use: boolean) {
    if (this.isDetailsShown) {
      this.reopen(() => (this._useDialog = use));
    } else {
      this._useDialog = use;
    }
  }

  ngOnDestroy() {
    this.componentDestroyed$.next();
    this.componentDestroyed$.complete();
  }

  ngOnInit() {
    if (this.useSlideoverSidebarMode === false) {
      // this BehaviorSubject emits changes to the viewport (and initially the current viewport state)
      this.breakpointObserver
        .observe([BOSCH_MEDIA_QUERIES.mobile, BOSCH_MEDIA_QUERIES.tablet])
        .pipe(takeUntil(this.componentDestroyed$))
        .subscribe(() => {
          this.useSlideoverSidebarMode = this.breakpointObserver.isMatched([BOSCH_MEDIA_QUERIES.mobile, BOSCH_MEDIA_QUERIES.tablet]);
        });
    }
  }

  ngAfterViewInit() {
    this.setDetailViewMaxSize();
    this.resizeDetailView();

    if (
      !!this.detailViewComponent &&
      this.detailViewComponent.hasRouterOutlet &&
      this.closeTrigger === this._defaultCloseTrigger
    ) {
      alert(
        "Seems like you're using routing for details view. Please replace the master detail's closeTrigger accordingly."
      );
    }
  }

  /**
   * Hide the {@link DetailViewComponent} on click of 'ESC' key event on master detail
   */
  @HostListener('window:keydown', ['$event'])
  onWindowKeydown(event: KeyboardEvent) {
    const hasAnyOverlayPaneOpen = this.overlayContainer.getContainerElement().children; // To check the angular overlay
    const hasStencilOverlayOpen = document.querySelector('active-overlay'); // To check for web components overlay
    if (event.key === 'Escape' && this.closeOnEscape && hasAnyOverlayPaneOpen.length === 0 && !hasStencilOverlayOpen && this.isDetailsShown) {
      this.closeTrigger();
    }
  }

  /**
   * Make the {@link DetailViewComponent} visible and show the details of the object identified by id
   */
   openDetails() {
    if (!this.isDetailsShown) {
      this.isDetailsShown = true;

      if (this.useDialog) {
        /*
         * Currently, there are several github issues on creating views in the ngAfterViewInit which isn't possible without error in Dev-Mode
         * https://github.com/angular/angular/issues/15634
         * https://github.com/angular/angular/issues/10131
         * https://github.com/angular/angular/issues/17572
         * ...
         * TODO: This solution is one workaround for this issue, but should be fixed as soon as possible since its hacky!
         */
        setTimeout(() => {
          this.openDetailsModal();
        });
      } else {
        this.resizeDetailView();
      }
    }
  }

  /**
   * Hide the {@link DetailViewComponent}
   * @returns an Observable that emits a single true when the details is finished closing
   */
  closeDetails(): Observable<undefined> {
    if (!this.isDetailsShown || (this.useDialog && !this.detailsDialogRef)) {
      return of();
    }

    const closedObservable = (
      this.useDialog
        ? this.detailsDialogRef.afterClosed()
        : from(this._sidenav.close()).pipe(
            filter((result) => result !== 'open'),
            map(() => void 0)
          )
    ).pipe(
      first(),
      takeUntil(this.componentDestroyed$),
      tap(() => this.closed$.next()),
      tap(() => (this.isDetailsShown = false)),
      shareReplay()
    );

    closedObservable.subscribe();
    if (this.useDialog) {
      this.detailsDialogRef.close();
    }

    return closedObservable;
  }

  /**
   * detailsClosed() is called when the  {@link DetailViewComponent} is closed.
   * either when the details dialog is closed or when the sidebar has disappeared
   */
  public detailsClosed() {
    this.isDetailsShown = false;
    this.closed$.next();
  }

  private openDetailsModal() {
    this.detailsDialogRef = this.modalWindowService.openDialogWithComponent(this.detailsView, {
      disableClose: true,
      closeOnNavigation: false,
    });
    merge(
      this.detailsDialogRef.backdropClick().pipe(filter(() => this.closeOnBackdropClick)),
      this.detailsDialogRef.keydownEvents().pipe(
        filter((event) => event.code === 'Escape'),
        filter(() => this.closeOnEscape)
      )
    )
      .pipe(takeUntil(this.componentDestroyed$))
      .subscribe(() => {
        this.closeTrigger();
      });
    this.detailsDialogRef
      .afterClosed()
      .pipe(takeUntil(this.componentDestroyed$))
      .subscribe(() => {
        this.detailsClosed();
      });
  }

  private reopen(beforeOpening: (x: undefined) => void) {
    this.closeDetails()
      .pipe(takeUntil(this.componentDestroyed$), take(1), tap(beforeOpening))
      .subscribe(() => {
        this.openDetails();
      });
  }

  get resizeHandleOffset() {
    return this.detailViewComponent.isPadded ? this.resizeHandlePaddedOffset : 0;
  }

  get resizeHandleMinSize() {
    return this._detailViewMinSize || this.resizeHandleOffset;
  }

  get resizeHandleMaxSize() {
    return this._detailViewMaxSize || this.__sidenavcontent.nativeElement.parentElement.offsetWidth;
  }

  private setDetailViewMaxSize() {
    if (this._masterViewMinSize !== undefined) {
      this._detailViewMaxSize = this.__sidenavcontent.nativeElement.parentElement.offsetWidth - this._masterViewMinSize;
    } else {
      this._detailViewMaxSize = undefined;
    }
  }

  private resizeDetailView() {
    if (!this.isDetailsShown) {
      // do not resize if not shown
      return;
    }
    if (this.__sidenav && this.__sidenavcontent && this.detailViewSize === undefined && !this.useSlideoverSidebarMode) {
      this.renderer.removeStyle(this.__sidenav.nativeElement, 'width');
      this.renderer.removeStyle(this.__sidenavcontent.nativeElement, 'margin-right');
    } else if (this.__sidenav && this.__sidenavcontent && this.detailViewSize && !this.useSlideoverSidebarMode) {
      // render components base on new size, but only when no useSlideoverSidebarMode
      this.renderer.setStyle(this.__sidenav.nativeElement, 'width', `${this.detailViewSize + this.detailViewSizeUnit}`);
      this.renderer.setStyle(this.__sidenavcontent.nativeElement, 'margin-right', `${this.detailViewSize + this.detailViewSizeUnit}`);
    } else if (this.__sidenav && this.useSlideoverSidebarMode) {
      // make sure that when useSlideoverSidebarMode is false, side nav does not have width set, so that side nav can fill parent size
      this.renderer.setStyle(this.__sidenav.nativeElement, 'width', 'initial');
      this.renderer.removeStyle(this.__sidenavcontent.nativeElement, 'margin-right');
    }
  }

  resizeHandleSizeChange($event: ResizeHandleSizeChangedEvent) {
    //Changes for setting the detail view size if the unit is in percentage
    if(this.detailViewSizeUnit == WidthUnit.percentage) {
      this.detailViewSize = Math.floor(($event.value / this.__sidenavcontent.nativeElement.parentElement.offsetWidth) * 100);
    }
    else  this.detailViewSize = $event.value;
  }
}
