import {IDate} from '../common/models/date.model';
import {DomHelper} from '../common/services/dom-appender/dom-appender.service';
import {UtilsService} from '../common/services/utils/utils.service';
import {CalendarMode} from '../common/types/calendar-mode';
import {ECalendarMode} from '../common/types/calendar-mode-enum';
import {CalendarValue} from '../common/types/calendar-value';
import {ECalendarValue} from '../common/types/calendar-value-enum';
import {SingleCalendarValue} from '../common/types/single-calendar-value';
import {IDayCalendarConfig} from '../day-calendar/day-calendar-config.model';
import {DayCalendarComponent} from '../day-calendar/day-calendar.component';
import {DayCalendarService} from '../day-calendar/day-calendar.service';
import {IDayTimeCalendarConfig} from '../day-time-calendar/day-time-calendar-config.model';
import {DayTimeCalendarService} from '../day-time-calendar/day-time-calendar.service';
import {ITimeSelectConfig} from '../time-select/time-select-config.model';
import {TimeSelectComponent} from '../time-select/time-select.component';
import {TimeSelectService} from '../time-select/time-select.service';
import {IDatePickerConfig} from './date-picker-config.model';
import {IDpDayPickerApi} from './date-picker.api';
import {DatePickerService} from './date-picker.service';
import {
  AfterViewInit,
  Component,
  ElementRef,
  EventEmitter,
  forwardRef,
  HostBinding,
  HostListener,
  Input,
  OnChanges,
  OnDestroy,
  OnInit,
  Output,
  Renderer,
  SimpleChanges,
  ViewChild,
  ViewEncapsulation
} from '@angular/core';
import {
  ControlValueAccessor,
  FormControl,
  NG_VALIDATORS,
  NG_VALUE_ACCESSOR,
  ValidationErrors,
  Validator
} from '@angular/forms';
import * as moment from 'moment';
import {Moment, unitOfTime} from 'moment';

@Component({
  selector: 'dp-date-picker',
  template: '<div [ngClass]="{\'dp-open\': areCalendarsShown}">   <div *ngIf="!componentConfig.hideInputContainer" class="dp-input-container">     <input type="text"            class="dp-picker-input"            [placeholder]="placeholder"            [ngModel]="inputElementValue"            (ngModelChange)="onViewDateChange($event)"            (focus)="inputFocused()"            [readonly]="componentConfig.disableKeypress"            [disabled]="disabled"/>      <button type="button"             class="dp-current-location-btn"             *ngIf="shouldShowGoToCurrent()"             (click)="moveToCurrent()"             [hidden]="!_areCalendarsShown">     </button>   </div>   <div #container>     <div class="dp-popup {{theme}}"          [ngSwitch]="mode"          [hidden]="!_areCalendarsShown">       <dp-day-calendar #dayCalendar                        *ngSwitchCase="\'day\'"                        [config]="dayCalendarConfig"                        [ngModel]="_selected"                        [displayDate]="currentDateView"                        (onSelect)="dateSelected($event, \'day\')"                        [theme]="theme">       </dp-day-calendar>        <dp-month-calendar #monthCalendar                          *ngSwitchCase="\'month\'"                          [config]="dayCalendarConfig"                          [ngModel]="_selected"                          [displayDate]="currentDateView"                          (onSelect)="dateSelected($event, \'month\')"                          [theme]="theme">       </dp-month-calendar>        <dp-time-select #timeSelect                       *ngSwitchCase="\'time\'"                       [config]="timeSelectConfig"                       [ngModel]="_selected && _selected[0]"                       (onChange)="dateSelected($event, \'second\', true)"                       [theme]="theme">       </dp-time-select>        <dp-day-time-calendar #daytimeCalendar                             *ngSwitchCase="\'daytime\'"                             [config]="dayTimeCalendarConfig"                             [displayDate]="currentDateView"                             [ngModel]="_selected && _selected[0]"                             (onChange)="dateSelected($event, \'second\', true)"                             [theme]="theme">       </dp-day-time-calendar>     </div>   </div> </div> ',
  styles: ['dp-date-picker {  display: inline-block;}dp-date-picker.dp-material .dp-picker-input {  box-sizing: border-box;  height: 30px;  width: 213px;  font-size: 13px;  outline: none;}dp-date-picker.dp-material .dp-current-location-btn {  top: calc(50% - 9px);  right: 5px;  height: 18px;  width: 18px;  border: 2px solid rgba(0, 0, 0, 0.6);}dp-date-picker .dp-input-container {  position: relative;}dp-date-picker .dp-selected {  background: #106CC8;  color: #FFFFFF;}dp-date-picker .dp-current-location-btn {  position: absolute;  top: calc(50% - 7px);  right: 5px;  height: 14px;  width: 13px;  background: rgba(0, 0, 0, 0.6);  border: 1px solid rgba(0, 0, 0, 0.6);  outline: none;  border-radius: 50%;  box-shadow: inset 0 0 0 3px #FFFFFF;  cursor: pointer;}dp-date-picker .dp-current-location-btn:hover {  background: #000000;}.dp-popup {  position: relative;  background: #FFFFFF;  box-shadow: 1px 1px 5px 0 rgba(0, 0, 0, 0.1);  border-left: 1px solid rgba(0, 0, 0, 0.1);  border-right: 1px solid rgba(0, 0, 0, 0.1);  border-bottom: 1px solid rgba(0, 0, 0, 0.1);  z-index: 9999;  white-space: nowrap;}'],
  encapsulation: ViewEncapsulation.None,
  providers: [
    DatePickerService,
    DayTimeCalendarService,
    DayCalendarService,
    TimeSelectService,
    {
      provide: NG_VALUE_ACCESSOR,
      useExisting: forwardRef(() => DatePickerComponent),
      multi: true
    },
    {
      provide: NG_VALIDATORS,
      useExisting: forwardRef(() => DatePickerComponent),
      multi: true
    }
  ]
})
export class DatePickerComponent implements OnChanges,
                                            OnInit,
                                            AfterViewInit,
                                            ControlValueAccessor,
                                            Validator,
                                            OnDestroy {
  isInitialized: boolean = false;
  @Input() config: IDatePickerConfig;
  @Input() mode: CalendarMode = 'day';
  @Input() placeholder: string = '';
  @Input() disabled: boolean = false;
  @Input() displayDate: SingleCalendarValue;
  @HostBinding('class') @Input() theme: string;
  @Input() minDate: Moment | string;
  @Input() maxDate: Moment | string;
  @Input() minTime: Moment | string;
  @Input() maxTime: Moment | string;

  @Output() open = new EventEmitter<void>();
  @Output() close = new EventEmitter<void>();

  @ViewChild('container') calendarContainer: ElementRef;
  @ViewChild('dayCalendar') dayCalendarRef: DayCalendarComponent;
  @ViewChild('monthCalendar') monthCalendarRef: DayCalendarComponent;
  @ViewChild('timeSelect') timeSelectRef: TimeSelectComponent;

  componentConfig: IDatePickerConfig;
  dayCalendarConfig: IDayCalendarConfig;
  dayTimeCalendarConfig: IDayTimeCalendarConfig;
  timeSelectConfig: ITimeSelectConfig;
  _areCalendarsShown: boolean = false;
  hideStateHelper: boolean = false;
  _selected: Moment[] = [];
  inputValue: CalendarValue;
  inputValueType: ECalendarValue;
  isFocusedTrigger: boolean = false;
  currentDateView: Moment;
  inputElementValue: string;
  calendarWrapper: HTMLElement;
  appendToElement: HTMLElement;
  inputElementContainer: HTMLElement;
  popupElem: HTMLElement;
  handleInnerElementClickUnlisteners: Function[] = [];
  globalListnersUnlisteners: Function[] = [];
  validateFn: (inputVal: CalendarValue) => {[key: string]: any};
  api: IDpDayPickerApi = {
    open: this.showCalendars.bind(this),
    close: this.hideCalendar.bind(this)
  };

  set selected(selected: Moment[]) {
    this._selected = selected;
    this.inputElementValue = (<string[]>this.utilsService
      .convertFromMomentArray(this.componentConfig.format, selected, ECalendarValue.StringArr))
      .join(', ');
    this.onChangeCallback(this.processOnChangeCallback(selected));
  }

  get selected(): Moment[] {
    return this._selected;
  }

  get areCalendarsShown(): boolean {
    return this._areCalendarsShown;
  }

  set areCalendarsShown(value: boolean) {
    if (value) {
      this.startGlobalListeners();
      this.domHelper.appendElementToPosition({
        container: this.appendToElement,
        element: this.calendarWrapper,
        anchor: this.inputElementContainer,
        dimElem: this.popupElem,
        drops: this.componentConfig.drops,
        opens: this.componentConfig.opens
      });
    } else {
      this.stopGlobalListeners();
      this.dayPickerService.pickerClosed();
    }

    this._areCalendarsShown = value;
  }

  constructor(private dayPickerService: DatePickerService,
              private domHelper: DomHelper,
              private elemRef: ElementRef,
              private renderer: Renderer,
              private utilsService: UtilsService) {
  }

  @HostListener('click')
  onClick() {
    if (!this.isFocusedTrigger && !this.disabled) {
      this.hideStateHelper = true;
      if (!this.areCalendarsShown) {
        this.showCalendars();
      }
    }
  }

  @HostListener('document:click')
  onBodyClick() {
    if (!this.hideStateHelper && this.areCalendarsShown) {
      this.hideCalendar();
    }

    this.hideStateHelper = false;
  }

  @HostListener('document:scroll')
  @HostListener('window:resize')
  onScroll() {
    if (this.areCalendarsShown) {
      this.domHelper.setElementPosition({
        container: this.appendToElement,
        element: this.calendarWrapper,
        anchor: this.inputElementContainer,
        dimElem: this.popupElem,
        drops: this.componentConfig.drops,
        opens: this.componentConfig.opens
      });
    }
  }

  writeValue(value: CalendarValue): void {
    this.inputValue = value;

    if (value) {
      this.selected = this.utilsService
        .convertToMomentArray(value, this.componentConfig.format, this.componentConfig.allowMultiSelect);
      this.init();
    } else {
      this.selected = [];
    }
  }

  registerOnChange(fn: any): void {
    this.onChangeCallback = fn;
  }

  onChangeCallback(_: any) {
  };

  registerOnTouched(fn: any): void {
  }

  validate(formControl: FormControl): ValidationErrors | any {
    if (this.minDate || this.maxDate || this.minTime || this.maxTime) {
      return this.validateFn(formControl.value);
    } else {
      return () => null;
    }
  }

  processOnChangeCallback(selected: Moment[]): CalendarValue {
    return this.utilsService.convertFromMomentArray(this.componentConfig.format, selected, this.inputValueType);
  }

  initValidators() {
    this.validateFn = this.utilsService.createValidator(
      {
        minDate: this.minDate,
        maxDate: this.maxDate,
        minTime: this.minTime,
        maxTime: this.maxTime
      }, this.componentConfig.format, this.mode);
    this.onChangeCallback(this.processOnChangeCallback(this.selected));
  }

  ngOnInit() {
    this.isInitialized = true;
    this.init();
    this.initValidators();
  }

  ngOnChanges(changes: SimpleChanges) {
    if (this.isInitialized) {
      const {minDate, maxDate, minTime, maxTime} = changes;
      this.init();

      if (minDate || maxDate || minTime || maxTime) {
        this.initValidators();
      }
    }
  }

  ngAfterViewInit() {
    this.setElementPositionInDom();
  }

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

  setElementPositionInDom() {
    this.calendarWrapper = <HTMLElement> this.calendarContainer.nativeElement;
    this.setInputElementContainer();
    this.popupElem = this.elemRef.nativeElement.querySelector('.dp-popup');
    this.handleInnerElementClick(this.popupElem);

    const {appendTo} = this.componentConfig;
    if (appendTo) {
      if (typeof appendTo === 'string') {
        this.appendToElement = <HTMLElement>document.querySelector(<string>appendTo);
      } else {
        this.appendToElement = <HTMLElement>appendTo;
      }
    } else {
      this.appendToElement = this.elemRef.nativeElement;
    }

    this.appendToElement.appendChild(this.calendarWrapper);
  }

  setInputElementContainer() {
    this.inputElementContainer = this.componentConfig.inputElementContainer
      || this.elemRef.nativeElement.querySelector('.dp-input-container')
      || document.body;
  }

  handleInnerElementClick(element: HTMLElement) {
    this.handleInnerElementClickUnlisteners.push(
      this.renderer.listen(element, 'click', () => {
        this.hideStateHelper = true;
      })
    );
  }

  init() {
    this.componentConfig = this.dayPickerService.getConfig(this.config, this.mode);
    this.currentDateView = this.displayDate
      ? this.utilsService.convertToMoment(this.displayDate, this.componentConfig.format).clone()
      : this.utilsService
        .getDefaultDisplayDate(this.currentDateView, this.selected, this.componentConfig.allowMultiSelect);
    this.inputValueType = this.utilsService.getInputType(this.inputValue, this.componentConfig.allowMultiSelect);
    this.dayCalendarConfig = this.dayPickerService.getDayConfigService(this.componentConfig);
    this.dayTimeCalendarConfig = this.dayPickerService.getDayTimeConfigService(this.componentConfig);
    this.timeSelectConfig = this.dayPickerService.getTimeConfigService(this.componentConfig);
  }

  inputFocused() {
    this.isFocusedTrigger = true;
    setTimeout(() => {
      this.hideStateHelper = false;

      if (!this.areCalendarsShown) {
        this.showCalendars();
      }

      this.isFocusedTrigger = false;
    }, this.componentConfig.onOpenDelay);
  }

  showCalendars() {
    this.hideStateHelper = true;
    this.areCalendarsShown = true;

    if (this.timeSelectRef) {
      this.timeSelectRef.api.triggerChange();
    }

    this.open.emit();
  }

  hideCalendar() {
    this.areCalendarsShown = false;

    if (this.dayCalendarRef) {
      this.dayCalendarRef.api.toggleCalendar(ECalendarMode.Day);
    }

    this.close.emit();
  }

  onViewDateChange(value: string) {
    if (this.dayPickerService.isValidInputDateValue(value, this.componentConfig)) {
      this.selected = this.dayPickerService.convertInputValueToMomentArray(value, this.componentConfig);
      this.currentDateView = this.selected.length
        ? this.utilsService.getDefaultDisplayDate(null, this.selected, this.componentConfig.allowMultiSelect)
        : this.currentDateView;
    }
  }

  shouldShowGoToCurrent(): boolean {
    return this.componentConfig.showGoToCurrent && this.mode !== 'time';
  }

  moveToCurrent() {
    this.currentDateView = moment();
  }

  dateSelected(date: IDate, granularity: unitOfTime.Base, ignoreClose?: boolean) {
    this.selected = this.utilsService
      .updateSelected(this.componentConfig.allowMultiSelect, this.selected, date, granularity);
    if (!ignoreClose) {
      this.onDateClick();
    }
  }

  onDateClick() {
    if (this.componentConfig.closeOnSelect) {
      setTimeout(this.hideCalendar.bind(this), this.componentConfig.closeOnSelectDelay);
    }
  }

  onKeyPress(event: KeyboardEvent) {
    switch (event.keyCode) {
      case (9):
      case (27):
        this.hideCalendar();
        break;
    }
  }

  startGlobalListeners() {
    this.globalListnersUnlisteners.push(
      this.renderer.listen(document, 'keydown', (e: KeyboardEvent) => {
        this.onKeyPress(e);
      }));
  }

  stopGlobalListeners() {
    this.globalListnersUnlisteners.forEach((ul) => ul());
    this.globalListnersUnlisteners = [];
  }

  ngOnDestroy() {
    this.handleInnerElementClickUnlisteners.forEach(ul => ul());
    this.appendToElement.removeChild(this.calendarWrapper);
  }
}
