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

import {
  Component,
  ComponentFactoryResolver,
  ComponentRef,
  EventEmitter,
  HostListener,
  Input,
  OnDestroy,
  OnInit,
  Output,
  ViewChild,
  ViewContainerRef,
} from '@angular/core';
import { FormArray, FormBuilder, FormControl, FormGroup } from '@angular/forms';
import { cloneDeep, isEqual } from 'lodash-es';
import { Observable, of, Subject } from 'rxjs';
import { debounceTime, takeUntil } from 'rxjs/operators';
import { Category } from '../../models/category';
import { CustomFormComponent } from '../../models/custom-form-component';
import { DataSourceValue } from '../../models/data-source-value';
import { WidgetTypes } from '../../models/definitions';
import { InputType } from '../../models/input-type.enum';
import { UserSettingChangeEvent } from '../../models/user-setting-change-event';
import { UserSetting } from '../../models/userSetting';
import { Widget } from '../../models/widget';
import { CategoryService } from '../../services/category.service';
import { PinboardI18nService } from '../../services/pinboard-i18n.service';
import { PinboardInteractionService } from '../../services/pinboard-interaction.service';
import { SelectBoxDataSourceService } from '../../services/select-box-data-source.service';
import { Configuration } from '../../models/configuration';

@Component({
  selector: 'bci-widget-settings-form',
  templateUrl: './widget-settings-form.component.html',
  styleUrls: ['./widget-settings-form.component.scss'],
})
export class WidgetSettingsFormComponent implements OnInit, OnDestroy {
  public formGroup: FormGroup = new FormGroup({});
  public inputType = InputType;
  public isSaveDisabled = true;
  public widget: Widget;
  public configuration: Configuration = {
    availableWidths: [],
    availableHeights: [],
  };
  public customFormType: any;
  @Output() settingsChange = new EventEmitter<UserSettingChangeEvent>();
  @Input() private settingsChangeDebounceTime = 0;
  @Output() private cancel = new EventEmitter<Widget>();
  @Output() private save = new EventEmitter<Widget>();
  @ViewChild('customFormContainer', { read: ViewContainerRef, static: true })
  private customFormContainer: ViewContainerRef;
  private settingsChangeDebouncer: Subject<UserSettingChangeEvent> = new Subject<UserSettingChangeEvent>();
  private backupFormValue: { [name: string]: any } = {};
  private cachedWidgetPositions = [];
  private ngUnsubscribe = new Subject<void>();
  private customFormComponent: ComponentRef<CustomFormComponent>;
  private originalForm: FormGroup;
  private allWidgets: Widget[] = [];

  constructor(
    private formBuilder: FormBuilder,
    private categoryService: CategoryService,
    private pinboardInteraction: PinboardInteractionService,
    private cmpFctReslv: ComponentFactoryResolver,
    public pinboardI18n: PinboardI18nService,
    private readonly selectBoxDataSourceService: SelectBoxDataSourceService
  ) {}

  ngOnInit() {
    if (this.settingsChangeDebounceTime === 0) {
      this.settingsChangeDebouncer.pipe(takeUntil(this.ngUnsubscribe)).subscribe((settingChangeEvent) => {
        this.settingsChange.emit(settingChangeEvent);
      });
    } else {
      this.settingsChangeDebouncer
        .pipe(takeUntil(this.ngUnsubscribe), debounceTime(this.settingsChangeDebounceTime))
        .subscribe((settingChangeEvent) => {
          this.settingsChange.emit(settingChangeEvent);
        });
    }
    this.pinboardInteraction.widgets$.subscribe((widgets) => (this.allWidgets = widgets));
  }

  /**
   * Unsubscribes formControls and emits save event
   */
  public saveEdit(): void {
    const [oldWidgetRows, oldWidgetCols] = this.widget.sizeArray;
    this.widget.title = this.formGroup.get('title').value;
    this.widget.isLinkedToFilter = this.formGroup.get('isLinkedToFilter')?.value ?? false;
    this.widget.showWidgetTitle = this.formGroup.get('showWidgetTitle')?.value ?? false;
    const widgetCols = this.formGroup.get('width') ? this.formGroup.get('width').value : this.widget.sizeArray[1];
    const widgetRows = this.formGroup.get('height') ? this.formGroup.get('height').value : this.widget.sizeArray[0];
    this.widget.gridsterItem.cols = widgetCols;
    this.widget.gridsterItem.rows = widgetRows;
    this.widget.sizeArray = [widgetRows, widgetCols];
    if (oldWidgetRows !== widgetRows || oldWidgetCols !== widgetCols) {
      // resizing the grid, when widget width and height has been resized
      this.pinboardInteraction.handleWidgetResize(this.widget);
    }
    this.save.emit(this.widget);
    this.widget.saveWidget();
  }

  /**
   * Resets the widget to backup values and emits the cancel event
   * Unsubscribes formControls
   * Resets all grid widgets to origin positions
   */
  public cancelEdit(): void {
    for (const backupSettingId in this.backupFormValue) {
      // eslint-disable-next-line no-prototype-builtins
      if (this.backupFormValue.hasOwnProperty(backupSettingId)) {
        const ctrl = this.formGroup.get(backupSettingId);
        if (ctrl) {
          ctrl.reset(this.backupFormValue[backupSettingId], { onlySelf: false, emitEvent: true });
        }
      }
    }
    this.applyCachedWidgetPositions();
    this.cancel.emit(this.widget);
  }

  /**
   * Use this method to init the setting form!
   * Builds for each category a form group and adds form controls for each user setting
   */
  public buildFormsForWidget(widget: Widget): void {
    this.widget = widget;
    this.cacheWidgetPositions();
    this.configuration = this.widget.widgetDefinition.configuration;
    this.formGroup = null;
    this.createForms();
    this.customFormType = WidgetTypes.find((def) =>
      this.compareCaseInsensitive(def.widgetType, this.widget.widgetDefinition.widgetType)
    ).customSettingFormType;

    if (this.customFormType) {
      if (this.customFormComponent) {
        this.customFormComponent.destroy();
      }
      const customFormFactory = this.cmpFctReslv.resolveComponentFactory(this.customFormType);
      this.customFormComponent = this.customFormContainer.createComponent(
        customFormFactory
      ) as ComponentRef<CustomFormComponent>;
      this.customFormComponent.instance.formGroup = this.formGroup;
      this.customFormComponent.instance.widget = this.widget;
    }

    setTimeout(() => {
      this.formGroup.patchValue(widget.getUserSettingsObject(), { emitEvent: false });
      this.originalForm = cloneDeep(this.formGroup);
      Object.keys(this.formGroup.controls).forEach((key) => {
        const control = this.formGroup.controls[key] as FormControl | FormArray;
        this.backupFormValue[key] = Array.isArray(control.value)
          ? control.value.filter((item) => !!item)
          : control.value;
        this.addChangeEmitter(control, key);
      });
    }, 1);
  }

  public getIfWidgetFormValueChanged(): boolean {
    return !isEqual(this.originalForm?.value, this.formGroup.value);
  }

  private addChangeEmitter(ctrl: FormControl | FormArray, setting: UserSetting | string): void {
    ctrl.valueChanges.pipe(takeUntil(this.ngUnsubscribe)).subscribe((c) => {
      const settingConfig = typeof setting === 'string' ? { id: setting } : setting;
      this.settingsChangeDebouncer.next({ widget: this.widget, newValue: c, setting: settingConfig });
      this.widget.setUserSettingValue(settingConfig, c);
      this.formGroup.updateValueAndValidity();
      this.isSaveDisabled = !this.formGroup.valid || !this.getIfWidgetFormValueChanged();
    });
  }

  /**
   * Returns a distinct array of the current widgets UserSettings Categories
   * @returns a distinct array of the current widgets UserSettings Categories
   */
  public getAvailableCategories(): Category[] {
    const userSettings = this.widget.widgetDefinition.userSettings;
    if (!userSettings) {
      console.error('No usersettings for this widget!');
      return;
    }
    const catIds = userSettings.map((setting: UserSetting) => setting.categoryId);
    let catObjs = [];

    catIds.forEach((catId) => {
      const catObjIds = catObjs.map((cat: Category) => cat.id);
      if (catObjIds.indexOf(catId) === -1) {
        catObjs.push(this.categoryService.getCategory(catId));
      }
    });

    catObjs = catObjs.filter((cat: Category) => {
      const settings = this.getUserSettingsOfCategory(cat);
      if (settings.length > 0 && settings.filter((x) => x.inputType !== InputType.None).length > 0) {
        return cat;
      }
    });

    // No categories needed, when theres is a customFormComponent
    if (this.customFormType) {
      catObjs = [];
    }

    catObjs.unshift(this.categoryService.getCategory('set_general'));
    return catObjs;
  }

  getSelectionValues(userSetting: UserSetting): Observable<DataSourceValue[]> {
    if (userSetting.values) {
      return of(userSetting.values);
    }
    return this.selectBoxDataSourceService.getDataSourceValues(userSetting.dataSource);
  }

  /**
   * Creates the forms from the userSettings
   */
  private createForms(): void {
    const userSettings = this.widget.widgetDefinition.userSettings;
    const controlsConfig = {};
    userSettings.forEach((setting) => {
      const ctrl = new FormControl(this.widget.getUserSettingValue(setting.id));
      this.assignValueToControl(ctrl, setting);
      controlsConfig[setting.id] = ctrl;
      this.addChangeEmitter(ctrl, setting);
    });

    this.formGroup = this.formBuilder.group(controlsConfig);
    this.addGeneralCategory();
  }

  private assignValueToControl(ctrl: FormControl, setting: UserSetting): void {
    if (setting.inputType === this.inputType.CheckBox) {
      ctrl.setValue(!!ctrl.value);
    }
  }

  /**
   * Returns all UserSettings which belong to the passed category
   * @returns UserSettings of cat
   */
  public getUserSettingsOfCategory(cat: Category): UserSetting[] {
    return this.widget.widgetDefinition.userSettings.filter((setting: UserSetting) => setting.categoryId === cat.id);
  }

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

  /**
   * Adds the formControls for the 'set_general' category
   */
  private addGeneralCategory(): void {
    // add title formControl
    const titleControl = new FormControl(this.widget.title);
    const showWidgetTitleControl = new FormControl(this.widget.showWidgetTitle);
    let widthControl = new FormControl();
    let heightControl = new FormControl();
    if (this.configuration.availableWidths.includes(this.widget.gridsterItem.cols)) {
      widthControl = new FormControl(this.widget.gridsterItem.cols);
    }
    if (this.configuration.availableHeights.includes(this.widget.gridsterItem.rows)) {
      heightControl = new FormControl(this.widget.gridsterItem.rows);
    }
    this.formGroup.addControl('showWidgetTitle', showWidgetTitleControl);
    this.formGroup.addControl('title', titleControl);
    this.formGroup.addControl('width', widthControl);
    this.formGroup.addControl('height', heightControl);
    // add respondToFilter formControl
    if (this.widget.widgetDefinition.canLinkToFilter) {
      const canLinkToFilterControl = new FormControl(this.widget.isLinkedToFilter);
      this.formGroup.addControl('isLinkedToFilter', canLinkToFilterControl);
    }
    // resizing the grid when the pop up opens
    this.pinboardInteraction.handleWidgetResize(this.widget);
  }

  /**
   * Cache all widget positions (of all widgets on grid),
   * to apply the cached positions use applyCachedWidgetPositions()
   */
  private cacheWidgetPositions(): void {
    this.allWidgets.forEach((w) => {
      const pos = {
        id: w.id,
        x: w.gridsterItem.x,
        y: w.gridsterItem.y,
      };

      this.cachedWidgetPositions.push({ ...pos });
    });
  }

  /**
   * Applies the already cached widget positions to all widgets
   */
  private applyCachedWidgetPositions(): void {
    this.cachedWidgetPositions.forEach((pos) => {
      const widget = this.allWidgets.find((w) => w.id === pos.id);
      if (!widget) {
        return;
      }
      widget.gridsterItem.x = pos.x;
      widget.gridsterItem.y = pos.y;
    });
  }

  private compareCaseInsensitive(a: string, b: string): boolean {
    return a.localeCompare(b, undefined, { sensitivity: 'base' }) === 0;
  }

  @HostListener('document:keydown.escape', ['$event']) onKeydownHandler() {
    this.cancelEdit();
  }
}
