import { Component, Input, Output, EventEmitter, forwardRef, ViewChild, ElementRef, ChangeDetectionStrategy, ChangeDetectorRef, Renderer2, HostListener, ViewChildren, QueryList } from '@angular/core';
import { AbstractControl, ControlValueAccessor, NG_VALIDATORS, NG_VALUE_ACCESSOR, ValidationErrors, Validator } from '@angular/forms';
import { DropDownOption } from '../../entity/dropDownOption';
import { NbContextMenuDirective, NbDynamicOverlay, NbDynamicOverlayHandler, NbTrigger, NbAdjustment, NbPosition, NbTooltipDirective } from '@nebular/theme';
import { filter, map, takeUntil } from 'rxjs/operators';
import { trigger, state, transition, animate, style } from '@angular/animations';
import { Subject } from 'rxjs';
import { DropDownMenuComponent } from './drop-down-menu/drop-down-menu.component';
import { UuidUtils } from '../../utilities/uuid-utils';
import { DropDownListService } from './drop-down-list.service';
import { StringUtils } from '../../utilities/string-utils';
import { DebounceTimer } from '../../utilities/timer-utils';
import { RbjTooltipService } from '../../services/rbj-tooltip.service';
import { TextareaUtils } from '../../utilities/textarea-utils';

const MAX_VISIBLE_ITEMS = 8;
const VISIBLE_ITEM_HEIGHT = 40;
const TEXT_CLEAR = "Clear The Field";

@Component({
  selector: 'rbj-drop-down',
  templateUrl: './drop-down-list.component.html',
  styleUrls: ['./drop-down-list.component.scss'],
  animations: [
    trigger('openClose', [
      // ...
      state('open', style({
        transform: 'rotate(180deg)'
      })),
      state('closed', style({
        transform: 'rotate(0deg)'
      })),
      transition('* => *', [
        animate(100)
      ]),
    ]),
  ],
  changeDetection: ChangeDetectionStrategy.OnPush,
  providers: [
    {
      provide: NG_VALUE_ACCESSOR,
      useExisting: forwardRef(() => DropDownListComponent),
      multi: true
    },
    {
      provide: NG_VALIDATORS,
      useExisting: forwardRef(() => DropDownListComponent),
      multi: true,
    },
    NbDynamicOverlayHandler,
    NbDynamicOverlay
  ]
})
export class DropDownListComponent implements ControlValueAccessor, Validator {

  @ViewChild('inputBox', { static: true }) input: ElementRef;
  @ViewChild('dropButton', { static: true }) button: ElementRef;
  @ViewChild(NbContextMenuDirective, { static: true }) contextMenu: NbContextMenuDirective;

  refreshMenuDebounceTimer = new DebounceTimer(20, () => { if (this.menuIsOpen) { this.setUpDropdownList(); } });
  constructor(
    private dropDownListService: DropDownListService,
    private cdRef: ChangeDetectorRef,
    private dynamicOverlayHandler: NbDynamicOverlayHandler,
    private elementRef: ElementRef,
    private renderer: Renderer2,
    private rbjTooltipService: RbjTooltipService,
  ) {
    this.tabindex = elementRef.nativeElement.getAttribute('tabindex');
  }

  private destroy$: Subject<void> = new Subject<void>();
  private _initialized: boolean;
  private _viewInitialized: boolean = false;
  private _value: any;
  private _text: string;
  private _textTyping: boolean = false;
  private dynamicOverlay: NbDynamicOverlay;
  private isFocused: boolean = false;
  private _propDisplay: string = 'value1';

  status = '';
  displayValue: string;
  selectedItem: DropDownOption;
  firstMatchOption: DropDownOption;
  uuid = 'rbjDropDown';
  items: DropDownOption[] = [];

  readonly: boolean = false;
  required: boolean = false;
  mustMatch: boolean = true;
  @Input() allowSearch: boolean = true;
  compact: boolean
  menuIsOpen: boolean = false;
  selectedIndex: number = -1;
  tabindex: string = null;
  focusIndex: number = -1;
  usingFocusIndex: boolean = false;

  public get tooltip(): string {
    return this.selectedItem ? this.selectedItem[this.propItemDisplay] || '' : '';
  }

  public get value(): any {
    return this._value;
  }
  public set value(v: any) {
    if (this._initialized) {
      if (v != this._value) {
        this._value = v;
        this.onChange(this.value);
      }
    }
    else {
      this._initialized = true;
      this._value = v;
    }
    this.keyValueChangedByBinding();
  }

  @Input()
  public get text(): string {
    return this._text;
  }
  public set text(v: string) {
    this.input.nativeElement.value = v;
    if (v != this._text) {

      this._text = v;
      this.textChange.emit(this._text);
    }
  }

  @Input() public set source(v: DropDownOption[]) {
    if (this._source != v) {
      this._source = v;
      this.refreshMenuDebounceTimer.resetTimer();
      //if (this.menuIsOpen) { this.setUpDropdownList(); }
      this.keyValueChangedByBinding();
    }
  }
  private _source: DropDownOption[];
  public get source(): DropDownOption[] {
    return this._source;
  }

  public get availableOptions(): DropDownOption[] {
    if (this.source) {
      return this.source.filter(o => this.itemIsVisible(o) && !o[this.propDisabled]);
    }
    return [];
  }

  @Input() name?: string = ''
  @Input() id?: string = ''
  @Input() hideButton: boolean = false;
  @Input() inputClass = '';

  public get propDisplay(): string {
    return this._propDisplay;
  }

  /**
   * Display text of dropdown will displaying this property of dropdown item, defaults as `value1`, will set `propItemDisplay` also, if it's default.
   */
  @Input() public set propDisplay(v: string) {
    this._propDisplay = v;
    if (this.propItemDisplay == 'value1') this.propItemDisplay = v;
  }

  /**
   * Display text of dropdown context menu will displaying this property of dropdown item, support HTML code, defaults as `value1`
   */
  @Input() propItemDisplay = 'value1';
  /**
   * The `key` property name of dropdown item, defaults as `key`
   */
  @Input() propKey = 'key';

  /**
   * The `disabled` property name of dropdown item, defaults as `disabled`
   */
  @Input() propDisabled = 'disabled';
  /**
   * The `visible` property name of dropdown item, defaults as `visible`
   */
  @Input() propVisible = 'visible';
  @Input() matchWith = 'key';
  @Input() disabled = false;
  @Input() placeholder = '';
  @Input() maskExpression = null;
  @Input() clearOption = false;

  /**
   * Tooltip message for no matching found, default as "$inputText Not Matched", `$inputText` will replace with user input text
   */
  @Input() inputNotMatchedTooltip = "$inputText does not exist";

  @HostListener('focus')
  focusHandler() {
    this.focus();
  }

  @Input("mustMatch")
  public set input_mustMatch(value) {
    this.mustMatch = typeof value !== "undefined" && value !== false;
  }

  @Input("required")
  public set input_required(value) {
    this.required = typeof value !== "undefined" && value !== false;
  }
  @Input("readonly")
  public set input_readonly(value) {
    this.readonly = typeof value !== "undefined" && value !== false;
  }
  @Input("compact")
  public set input_compact(value) {
    this.compact = typeof value !== "undefined" && value !== false;
  }
  @Input("clearOption")
  public set input_clearOption(value) {
    this.clearOption = typeof value !== "undefined" && value !== false;
  }

  @Output() selectedChange = new EventEmitter<any>();
  @Output() selectedChangeFrom = new EventEmitter<{ previousValue: any, currentValue: any }>();
  @Output() menuItemSelected = new EventEmitter<any>();
  @Output() textChange = new EventEmitter<string>();
  @Output() blur: EventEmitter<any> = new EventEmitter();
  @Output() inputTextNotMatched = new EventEmitter<string>();

  /**
   * Triggering when dropdown option been change by user, when subscribe this will not trigger selectedChange any more
   */
  @Output() beforeChange = new EventEmitter<any>();

  onChange = (value: any) => { };
  onTouched = () => { };

  writeValue(value: any): void {
    this.value = value;
  }

  registerOnChange(fn: (value: any) => void): void {
    this.onChange = fn;
  }

  registerOnTouched(fn: any): void {
    this.onTouched = fn;
  }

  setDisabledState?(isDisabled: boolean): void {
    this.disabled = isDisabled;
  }

  validate(control: AbstractControl): ValidationErrors {
    if (this.required && (this.value == null)) {
      return { "dropdown": "" };
    }
    return null;
  }

  registerOnValidatorChange?(fn: () => void): void {
  }

  ngOnInit() {
    this.uuid += UuidUtils.generate();

    this.dropDownListService.onItemSelect
      .pipe(
        takeUntil(this.destroy$),
        filter(({ tag }) => tag === this.uuid),
        map(({ item }) => item),
      )
      .subscribe(item => {
        this.selectByMenu(item);
      });

    this.dynamicOverlayHandler
      .host(this.input)
      .componentType(DropDownMenuComponent);
  }

  ngAfterViewInit() {
    this._viewInitialized = true;
    this.setUpDropdownList();
    this.dynamicOverlay = this.configureDynamicOverlay().build();
    this.renderer.removeAttribute(this.elementRef.nativeElement, 'id')
  }
  ngOnChanges() {
    //this.dynamicOverlay = this.configureDynamicOverlay().rebuild();
  }

  ngOnDestroy() {
    this.destroy$.next();
    this.destroy$.complete();
    this.dynamicOverlayHandler.destroy();
  }

  private itemIsVisible(item: any) {
    if (item[this.propVisible] == undefined) {
      item[this.propVisible] = true;
    }

    return true === item[this.propVisible];
  }

  focusout() {
    this.isFocused = false;
  }

  onFocus() {
    this.isFocused = true;
  }

  onTextFocus() {
    this.isFocused = true;
    this._textTyping = true;
  }

  onTextFocusout() {
    this._textTyping = false;
    this.focusout();
  }

  focus() {
    this.input.nativeElement.focus();
  }

  /**
   * Remove `ng-invalid` class from input field, only calling this method for data been changed in background but UI doesn't refresh.
   */
  private removeInvalidClassFormInput() {
    this.renderer.removeClass(this.input.nativeElement, 'ng-invalid');
  }
  updateTextFromValue() {
    if (this.clearOption && StringUtils.isNullOrWhitespace(this.value)) {
      this.text = '';
      this.input.nativeElement.value = "";
    }
    else if (this.source) {
      let item: DropDownOption = null;

      if (null == this.value && this.mustMatch) {
        item = this.availableOptions.find(o => o.default);
      }
      else {
        item = this.availableOptions.find(o => o[this.propKey] == this.value);
      }

      this.selectedIndex = item ? this.source.indexOf(item) : -1;
      this._text = item ? item[this.propDisplay] : this.value;

      if (this.input) {
        this.input.nativeElement.value = this._text;
        this.removeInvalidClassFormInput();
      }
    }
    else {
      this.text = '';
    }
    this.cdRef.detectChanges();
  }


  clickSelectAllText() {
    if (this.allowSearch) {
      this.input.nativeElement.select();
    }
  }

  onTextFieldBlur() {

    //set timeout for avoiding menu been hided before click event triggered
    // setTimeout(() => {
    this.updateSelectedItemByText();
    this.onTouched();
    this.blur.next(this.text);
    // }, 200);
  }

  private setUpDropdownList() {
    this.items = [];
    if (this.source && this.source.length > 0) {
      if (this.clearOption) {
        this.items = [new DropDownOption('', '')];
        this.items[0][this.propKey] = "";
        this.items[0][this.propDisplay] = "";
        this.items[0][this.propItemDisplay] = TEXT_CLEAR;
      }

      if (this.menuIsOpen && this.allowSearch && this.input.nativeElement) {
        let filter = this.input.nativeElement.value.toLowerCase();
        this.items = this.items.concat(
          this.availableOptions.filter(s => s[this.propItemDisplay].toLowerCase().indexOf(filter) > -1
          ));

        //unique by this.propItemDisplay
        this.items = Array.from(new Map(this.items.map(item =>
          [item[this.propItemDisplay], item])).values());

        if (this.items.length > 0) {
          this.firstMatchOption = this.availableOptions.find(s => s[this.propKey] == this.items[0][this.propKey]);
          this.firstMatchOption = this.items[this.focusIndex];
        }
        else {
          this.firstMatchOption = null;
        }
      }
      else {
        this.items = this.items.concat(this.source)
      }

      if (this.selectedItem) {
        this.focusIndex = this.items.indexOf(this.selectedItem)
        this.focusIndex = this.focusIndex > -1 ? this.focusIndex : 0;
      }

      this.usingFocusIndex = false;
    }

    if (this._viewInitialized) {

      this.dynamicOverlay = this.configureDynamicOverlay()
        .rebuild();
    }
  }

  private configureDynamicOverlay() {
    let visibleItemsCount = this.items.filter(i => i.visible).length;
    visibleItemsCount = visibleItemsCount < MAX_VISIBLE_ITEMS ? visibleItemsCount : MAX_VISIBLE_ITEMS;
    var windowHeight = window.innerHeight;
    //include input box height
    var itemHeight = (visibleItemsCount + 1) * VISIBLE_ITEM_HEIGHT;
    var y = this.input.nativeElement ? this.input.nativeElement.getBoundingClientRect().y : 0;
    let menuOnBottom = (y + itemHeight) < windowHeight;

    return this.dynamicOverlayHandler
      .position(menuOnBottom ? NbPosition.BOTTOM_END : NbPosition.TOP_END)
      .trigger(NbTrigger.NOOP)
      .offset(menuOnBottom ? 2 : -40)
      .adjustment(NbAdjustment.NOOP)
      .context({
        position: menuOnBottom ? NbPosition.BOTTOM : NbPosition.TOP,
        items: this.items,
        tag: this.uuid,
        class: 'text-left',
        width: this.input.nativeElement.parentElement.clientWidth,
        propDisplay: this.propItemDisplay,
        propDisabled: this.propDisabled,
        focusIndex: this.focusIndex,
        hide: () => { this.hide(); },
        focus: () => { this.isFocused = true; }
      });
  }

  private updateSelectedItemInMenu() {
    this.items.forEach(item => {
      item.selected = item[this.propKey] == this.value;
    });
  }

  private show() {
    this.menuIsOpen = true;
    this.dynamicOverlay.show();
    this.cdRef.detectChanges();
  }

  private hide() {
    this.menuIsOpen = false;
    if (this.dynamicOverlay && !this.cdRef['destroyed']) {
      this.dynamicOverlay.hide();
      this.cdRef.detectChanges();
    }
  }

  toggle(selectAllText: boolean = true) {
    if (!this.disabled && !this.readonly) {
      if (false == this.menuIsOpen) {
        this.setUpDropdownList();
      }
      this.dynamicOverlay.toggle();
      if (this.dynamicOverlay.isAttached) {
        this.updateSelectedItemInMenu();

        this.scrollingMenuByIndex(this.selectedIndex);

        this.menuIsOpen = true;
        if (selectAllText) {
          this.clickSelectAllText();
        }
      }
      else {
        this.menuIsOpen = false;
      }
    }
  }

  private scrollingMenuByIndex(i: number) {
    if (this.dynamicOverlay && this.dynamicOverlay.isAttached) {
      if (this.items.length > MAX_VISIBLE_ITEMS) {
        const el = document.getElementsByClassName(this.uuid);
        el[0].scrollTop = (i - MAX_VISIBLE_ITEMS / 2) * VISIBLE_ITEM_HEIGHT;
      }
    }
  }

  //Triggered when user clicks option from dropdown menu
  private selectByMenu(item: DropDownOption) {
    if (this.beforeChange.observers.length == 0) {
      this.menuItemSelected.emit(item[this.propKey]);

      if (this.value != item[this.propKey]) {
        const previousValue = this.value;
        this.value = item[this.propKey]
        this._text = item[this.propDisplay] == TEXT_CLEAR ? '' : item[this.propDisplay];

        //this line is for triggering html input field validating without triggering whole textChange process
        this.removeInvalidClassFormInput();
        this.selectedIndex = this.items.indexOf(item);
        this.blur.next(this.text);
        this.textChange.next(this.text);
        this.onChange(this.value);
        this.selectedChange.emit(this.value);
        this.selectedChangeFrom.emit({ previousValue, currentValue: this.value });

        if (this.hideButton) {
          this.input.nativeElement.focus();
        }
        else {
          this.input.nativeElement.focus();
          this.clickSelectAllText();
        }

        this.onTouched();
        this.updateSelectedItemInMenu();
      }
    }
    else {
      this.beforeChange.emit(item[this.propKey]);
    }
  }

  //Key value changed from binding, will update the displaying text and selected item in menu.
  private keyValueChangedByBinding() {
    let defaultOption = null;
    if (this.source) {

      let comparison = this._value;
      this.selectedItem = this.availableOptions.find(o => o[this.propKey] == comparison);
      defaultOption = this.availableOptions.find(o => o.default);
    }
    if (!this.selectedItem) {
      if (this.mustMatch) {
        if (defaultOption) {
          this._value = defaultOption[this.propKey];
          this.selectedItem = defaultOption;
        }
        else {
          this._value = null;
          this.selectedItem = new DropDownOption('', '');
        }
      }
      this.onChange(this.value);
    }

    //this.text = this.selectedItem[this.propDisplay];
    this.updateTextFromValue();
  }
  private updateSelectedItemByText() {
    this._text = this.input.nativeElement.value;
    let originSelectedItem = this.selectedItem;
    if (!this.readonly && this.source && this.allowSearch) {
      var newValue = null;
      var inputText = StringUtils.toUpperString(this.text);
      var foundByInput = this.availableOptions.find(o => StringUtils.toUpperString(o[this.propDisplay]) == inputText);

      //Find again with Key Value
      if (!foundByInput) {
        foundByInput = this.availableOptions.find(o => StringUtils.toUpperString(o[this.matchWith]) == inputText);
      }

      if (this.usingFocusIndex || (!foundByInput && this.menuIsOpen && this.items.length > 0)) {
        foundByInput = this.items[this.focusIndex];
        //this.firstMatchOption = null;
      }

      if (!foundByInput) {
        if (this.mustMatch) {
          //Reset text to current value if mustMatch is true and no result
          this.textInputNoMatched(this.text);
          inputText = this.selectedItem[this.propDisplay];
        }
        else {
          inputText = this.text;
          newValue = inputText;
          //Set inputText as option to selected item
          this.selectedItem = new DropDownOption(newValue, inputText);
          this.selectedIndex = -1;
        }
      }
      else {
        //Have matched option with text input
        this.selectedItem = foundByInput;
        this.selectedIndex = this.source.indexOf(this.selectedItem);
        newValue = this.selectedItem[this.propKey];
        inputText = this.selectedItem[this.propDisplay];
      }

      if ((newValue != null || false == this.mustMatch) && newValue != this.value) {

        if (this.beforeChange.observers.length == 0) {

          let previousValue = this.value;
          this.value = newValue;
          this.selectedChange.emit(this.value);
          this.selectedChangeFrom.emit({ previousValue, currentValue: this.value });
        }
        else {
          this.beforeChange.emit(newValue);
        }
      }

      if (this.beforeChange.observers.length == 0) {
        if (!this.clearOption || inputText != TEXT_CLEAR) {
          this.text = inputText;
        }
      }
      else {
        this.selectedItem = originSelectedItem;
        this.text = this.selectedItem[this.propDisplay];
      }
    }
  }

  private textInputNoMatched(inputValue: string) {
    this.inputTextNotMatched.next(this.text);
    if (false == StringUtils.isNullOrWhitespace(this.text)) {
      let tempMsg = this.inputNotMatchedTooltip.replace("$inputText", inputValue.toUpperCase());
      //this.tooltips.first.content = `"${inputValue}" does not exist`;
      this.rbjTooltipService.show(this.input, tempMsg, this.uuid, 3, NbPosition.TOP_START);

    }
  }

  public onKeyDown(e: KeyboardEvent): void {
    switch (e.key) {
      case 'Enter':
        this.updateSelectedItemByText();
        this.hide();
        return;
        break;

      case 'ArrowDown':
      case 'ArrowUp':
        if (this.menuIsOpen) {
          if (e.key == "ArrowUp") {
            this.focusIndex = this.focusIndex > 0 ? this.focusIndex - 1 : this.focusIndex;
          }
          else {
            this.focusIndex = this.focusIndex == this.items.length - 1 ? this.focusIndex : this.focusIndex + 1
          }

          this.dropDownListService.setFocusIndex(this.uuid, this.focusIndex);
          this.scrollingMenuByIndex(this.focusIndex);
          this.usingFocusIndex = true;
        }
        else {
          this.toggle();
        }
        return;
        break;

      case 'Tab':
        this.isFocused = false;
        //use time out to let tab out support select the last index user choose from menu
        setTimeout(() => {
          this.hide();
        }, 100);
        return;

        break;

      default:
        break;
    }
    if (TextareaUtils.isEditingKeyPress(e)) {
      if (this.allowSearch) {
        if (this.isFocused && false == this.menuIsOpen) {
          this.toggle(false);
        }
      }
      this.refreshMenuDebounceTimer.resetTimer();
    }
  }

}

