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

import {
  AfterContentChecked,
  Component,
  ContentChildren,
  ElementRef,
  EventEmitter,
  Input,
  OnChanges,
  OnInit,
  Output,
  QueryList,
  SimpleChanges,
  ViewChildren,
} from '@angular/core';
import { BOSCH_BREAKPOINTS } from '@bci-web-core/core';
import {
  DisplayGrid,
  GridsterConfig,
  GridsterItem,
  GridsterItemComponent,
  GridsterItemComponentInterface,
  GridType,
} from 'angular-gridster2';
import isEqual from 'lodash-es/isEqual';
import { BehaviorSubject, distinctUntilChanged, skip, Subject } from 'rxjs';
import { Widget } from '../models/widget';
import { WidgetContentComponent } from '../models/widget-content-component';
import { WidgetDefinition } from '../models/widget-definition';
import { WidgetFilter } from '../models/widget-filter';
import { PinboardFilterBarComponent } from '../pinboard-filter-bar/pinboard-filter-bar.component';
import { PinboardI18nService } from '../services/pinboard-i18n.service';
import { WidgetComponent } from '../widget/widget.component';

@Component({
  selector: 'bci-pinboard-grid',
  templateUrl: './pinboard-grid.component.html',
  styleUrls: ['./pinboard-grid.component.scss'],
})
export class PinboardGridComponent implements OnInit, OnChanges, AfterContentChecked {
  private gridWidgets: Array<Widget> = [];
  private editModeActive = new BehaviorSubject<boolean>(false);
  editModeActive$ = this.editModeActive.asObservable();
  private editedWidget$ = new Subject<Widget | null>();
  public widgetsLoaded = false;
  public gridsterOptions: GridsterConfig = {};

  @ContentChildren(PinboardFilterBarComponent, { descendants: true })
  pinboardFilterBarComponents: QueryList<PinboardFilterBarComponent>;
  @ViewChildren(WidgetComponent) widgetComponents: QueryList<WidgetComponent>;
  @ViewChildren(GridsterItemComponent) gridsterItemComponents: QueryList<GridsterItemComponent>;
  @Input() private minCols = 12;
  @Input() private maxCols = 12;
  @Input() private minRows = 7;
  @Input() private maxRows = null;
  @Input() private gridType: GridType = GridType.VerticalFixed;
  @Input() set displayGrid(grid: DisplayGrid) {
    this.gridsterOptions.displayGrid = grid;
  }
  @Input() private margins = 16;
  @Input() private gridRowHeight = 115;
  @Input() private oneColResolutionPoint = BOSCH_BREAKPOINTS.S;
  @Input() readOnly = false;
  @Input() showLoader = false;
  @Input() widgetFilters: Array<WidgetFilter> = [];
  @Output() widgetEdit = new EventEmitter<Widget>();
  @Output() widgetSizeUpdated = new EventEmitter<Widget>();
  @Output() widgetsUpdated = new EventEmitter<Widget[]>();
  @Output() widgetRemoved = new EventEmitter<Widget>();

  /**
   * Invoked every time when widget is added to grid. Needed to register iframe widgets to handler
   */
  @Output() widgetsAddedToGrid = new EventEmitter<Widget[]>();

  
  constructor(public pinboardI18n: PinboardI18nService, private elementRef: ElementRef) {}

  /**
   * Finds the contentComponent of the given Widget model on the grid and calls the update method
   * @param widget model to update
   * @param resizeAfter optional, if true, the resize method will be automatically called after the update
   */
  public updateWidget(widget: Widget, resizeAfter?: boolean): void {
    const component = this.widgetComponents.find((w) => w.contentComponent.instance.widget === widget);
    if (component) {
      component.contentComponent.instance.update(resizeAfter);
    } else {
      console.warn("Couldn't update widget, because there was no widget component found on the grid!", widget);
    }
  }

  ngOnInit() {
    // Setup Gridster
    this.gridsterOptions = {
      minCols: this.minCols,
      minRows: this.minRows,
      maxCols: this.maxCols,
      maxRows: this.maxRows,
      margin: this.margins,
      gridType: this.gridType,
      fixedRowHeight: this.gridRowHeight,
      keepFixedHeightInMobile: true,
      mobileBreakpoint: this.oneColResolutionPoint,
      displayGrid: DisplayGrid.None,
      draggable: {
        delayStart: 150,
        enabled: false,
        stop: this.dragStop.bind(this),
      },
      copy: true,
      defaultItemCols: 1,
      defaultItemRows: 1,
      swap: true,
      itemResizeCallback: () => {
        setTimeout(this.resizeWidgets.bind(this), 400);
      },
    };
    this.setupStateListener();
  }

  ngAfterContentChecked(): void {
    if (this.pinboardFilterBarComponents.length > 0) {
      const contentHeight = this.pinboardFilterBarComponents.first.element.nativeElement.offsetHeight;
      if (contentHeight && contentHeight !== this.gridsterOptions.outerMarginTop) {
        this.gridsterOptions.outerMargin = true;
        this.gridsterOptions.outerMarginTop = this.pinboardFilterBarComponents.first.element.nativeElement.offsetHeight;
        this.updateGrid();
      }
    }
  }

  ngOnChanges(changes: SimpleChanges): void {
    if (
      this.gridWidgets &&
      changes.widgetFilters &&
      JSON.stringify(changes.widgetFilters.currentValue) !== JSON.stringify(changes.widgetFilters.previousValue)
    ) {
      this.gridWidgets
        .filter((widget: Widget) => widget.isLinkedToFilter)
        .forEach((widget: Widget) => {
          this.setWidgetFilters(widget, changes.widgetFilters.currentValue);
        });
    }
  }

  dragStop(item: GridsterItem, itemComponent: GridsterItemComponentInterface) {
    // Preventing fire of resize event in case the grid item is just clicked and not actually moved.
    if (isEqual(item, itemComponent.$item)) {
      return;
    }
    setTimeout(() => {
      this.widgetsUpdated.emit(this.gridWidgets);
    }, 1);
  }

  /**
   * Update Gridster and resize Widgets
   */
  public updateGrid(): void {
    if (!this.gridsterOptions || !this.gridsterOptions.api) {
      return;
    }
    this.gridsterOptions.api.optionsChanged();
    this.gridsterOptions.api.resize();
  }

  /**
   * Checks for widget collisions and autopositions the widgets
   * @param widget - widget which have a collision
   * @returns - true if there (was) a collision
   */
  public checkForWidgetCollision(widget: Widget): boolean {
    const suspect = widget.gridsterItem;
    const collidedWidgets: Widget[] = [];
    let isCollision = false;

    // When a widget exceeds the maximum number of columns
    if (this.maxCols && suspect.x + suspect.cols > this.maxCols) {
      isCollision = true;
      collidedWidgets.push(widget);
    }

    for (const w of this.gridWidgets) {
      if (w === widget) {
        continue;
      }

      const gItem = w.gridsterItem;
      const gItemX1 = gItem.x;
      const gItemX2 = gItem.x + gItem.cols;
      const gItemY1 = gItem.y;
      const gItemY2 = gItem.y + gItem.rows;
      const suspectX1 = suspect.x;
      const suspectX2 = suspect.x + suspect.cols;
      const suspectY1 = suspect.y;
      const suspectY2 = suspect.y + suspect.rows;

      if (gItemX1 >= suspectX2 || gItemX2 <= suspectX1) {
        continue;
      }

      if (gItemY1 >= suspectY2 || gItemY2 <= suspectY1) {
        continue;
      }

      isCollision = true;
      collidedWidgets.push(w);
    }

    for (const cWi of collidedWidgets) {
      // https://github.com/tiberiuzuld/angular-gridster2/issues/215
      this.gridsterOptions.api.getNextPossiblePosition(cWi.gridsterItem);
      this.gridsterOptions.api.optionsChanged();
    }
    return isCollision;
  }

  public addWidgetsToGrid(widgetDefinitions: WidgetDefinition<WidgetContentComponent, any, any>[]): any {
    const widgetObjects: Widget[] = [];
    widgetDefinitions.forEach((def) => {
      widgetObjects.push(this.addWidgetToGrid(def));
    });
    this.widgetsUpdated.emit(this.gridWidgets);
    this.widgetsAddedToGrid.emit(widgetObjects);
  }

  /**
   * Generate a concrete widget out of a widget Definition and add it to the canvas
   * @returns created Widget based on given definition
   */
  public addWidgetToGrid(widgetDef: WidgetDefinition<WidgetContentComponent, any, any>): Widget {
    const randomSeed = '_' + Math.random().toString(36).substr(2, 9);
    const widgetObj: Widget = new Widget('newWidget' + randomSeed);
    widgetObj.widgetDefinition = widgetDef;
    widgetObj.title = !!widgetDef.defaultTitle ? widgetDef.defaultTitle : widgetDef.catalogConf.title;
    if (widgetObj.widgetDefinition.userSettings) {
      widgetObj.widgetDefinition.userSettings
        .filter((us) => us.defaultValue)
        .forEach((s) => widgetObj.setUserSettingValue(s, s.defaultValue));
    } else {
      console.warn("There aren't any usersettings for this widget, this could cause errors!", widgetObj);
    }
    // If configuration options are provided, take the default from that list
    const defaultConfig = this.getDefaultConfigurationFromWidgetDefinition(widgetDef);
    if (defaultConfig) {
      widgetObj.sizeArray = defaultConfig;
    }
    if (widgetObj.widgetDefinition.settings) {
      widgetObj.settings = JSON.parse(JSON.stringify(widgetObj.widgetDefinition.settings));
    }
    if (widgetObj.isLinkedToFilter) {
      this.setWidgetFilters(widgetObj, this.widgetFilters);
    }

    this.gridWidgets.push(widgetObj);
    return widgetObj;
  }

  /**
   * Gets the current Widgets of the grid
   * @returns current grid Widgets
   */
  public getWidgets(): Widget[] {
    return this.sortByOrderIndex(this.gridWidgets);
  }

  public handleWidgetSizeChange(widget: Widget) {
    this.checkForWidgetCollision(widget);
    this.resizeWidgets();
    this.updateGrid();
    this.widgetSizeUpdated.emit(widget);
  }

  /**
   * sorts widgets by their orderIndex calculation
   * @param widgets
   */
  private sortByOrderIndex(widgets: Widget[]): Widget[] {
    return widgets.sort((a, b) => this.calcOrderIndex(a) - this.calcOrderIndex(b));
  }

  /**
   * Returns the current WidgetComponents on the grid
   * @returns the current WidgetComponents on the grid
   */
  public getWidgetComponents(): WidgetComponent[] {
    return this.widgetComponents.toArray();
  }

  /**
   * Replace the current Widgets on the grid with the passed Widgets array
   * @param widgets - New set of the Widgets
   */
  public setGridWidgets(widgets: Widget[]): void {
    // setting default size array
    widgets.forEach((widget) => {
      const defaultConfig = this.getDefaultConfigurationFromWidgetDefinition(widget.widgetDefinition);
      if (!widget.sizeArray && defaultConfig) {
        widget.sizeArray = defaultConfig;
      }
      if (widget.isLinkedToFilter) {
        this.setWidgetFilters(widget, this.widgetFilters);
      }
    });
    this.widgetsLoaded = true;
    this.gridWidgets = widgets;
    this.resizeWidgets();
  }

  /**
   * Removes a Widget from the grid and resizes all
   */
  public removeWidget(widget: Widget): void {
    const idx = this.gridWidgets.indexOf(widget);
    this.gridWidgets.splice(idx, 1);
    this.widgetsUpdated.emit(this.gridWidgets);
    if (!this.gridsterOptions.draggable.enabled) {
      this.widgetRemoved.emit(widget);
    }
    this.resizeWidgets();
  }

  /**
   * Calls the resize and update method of Widget(-Component)
   * @param widget - optional: specify one widget, or trigger all on grid
   */
  public resizeWidgets(widget?: Widget): void {
    if (!this.widgetComponents) {
      return;
    }
    this.widgetComponents.forEach((item) => {
      if (widget) {
        if (widget.id === item.widget.id) {
          item.contentComponent.instance.update(true);
          return;
        }
      } else if (item.contentComponent) {
        item.contentComponent.instance.update(true);
      }
    });
  }

  public startGridEditMode(): void {
    if (this.readOnly) {
      return;
    }
    this.editModeActive.next(true);
  }

  public stopGridEditMode(): void {
    if (!this.gridsterOptions.draggable) {
      return;
    }
    this.editModeActive.next(false);
  }

  public startWidgetEditMode(widgetRef: Widget) {
    this.editedWidget$.next(widgetRef);
  }

  public leaveWidgetEditMode() {
    this.editedWidget$.next(null);
  }

  public linkToFilter(widget: Widget) {
    if (widget.isLinkedToFilter) {
      widget.widgetFilters = [...this.widgetFilters];
      widget.widgetFiltersChangedSubject.next();
    }
    this.widgetsUpdated.emit(this.gridWidgets);
  }

  private setupStateListener() {
    this.editModeActive$.pipe(skip(1), distinctUntilChanged()).subscribe((active) => {
      const { displayGrid, draggable } = active
        ? { displayGrid: DisplayGrid.Always, draggable: true }
        : { displayGrid: DisplayGrid.None, draggable: false };

      this.gridsterOptions.displayGrid = displayGrid;
      this.gridsterOptions.draggable.enabled = draggable;
      this.gridsterOptions = { ...this.gridsterOptions };
      this.widgetComponents.forEach((widget) => (widget.draggable = draggable));
    });
    this.editedWidget$.pipe(distinctUntilChanged()).subscribe((widgetRef) => {
      const { draggable } = widgetRef !== null ? { draggable: false } : { draggable: this.editModeActive.value };
      this.gridsterOptions.draggable.enabled = draggable;
      this.widgetComponents.forEach((widget: WidgetComponent) => {
        widget.editModeActiveFor = widgetRef;
      });
      this.pinboardFilterBarComponents.first.editMode = !!widgetRef || draggable;
    });
  }

  /** calculates an index of ordering */
  private calcOrderIndex(widget: Widget): number {
    const gridstarItem: GridsterItem = widget.gridsterItem;
    return gridstarItem.y * this.maxCols + gridstarItem.x + 1;
  }

  private setWidgetFilters(widget: Widget, widgetFilters: WidgetFilter[]) {
    widget.widgetFilters = [...widgetFilters];
    setTimeout(() => {
      widget.widgetFiltersChangedSubject.next();
    });
  }

  private getDefaultConfigurationFromWidgetDefinition(
    widgetDef: WidgetDefinition<WidgetContentComponent, any, any>
  ): [number, number] {
    if (
      widgetDef.configuration &&
      widgetDef.configuration.availableHeights.length > 0 &&
      widgetDef.configuration.availableWidths.length > 0
    ) {
      const { availableWidths, availableHeights } = widgetDef.configuration;
      const smallestWidth = Math.min(...availableWidths);
      const smallestHeight = Math.min(...availableHeights);
      return [smallestHeight, smallestWidth];
    }
    return null;
  }
}
