import {
  Directive,
  ElementRef,
  EventEmitter,
  Inject,
  Injectable,
  Input,
  OnChanges,
  OnDestroy,
  OnInit,
  Output,
  PLATFORM_ID,
  SimpleChanges,
  NgZone
} from '@angular/core';
import { isPlatformBrowser } from '@angular/common';

@Directive({ selector: '[ucapClickOutside]' })
export class ClickOutsideDirective implements OnInit, OnChanges, OnDestroy {
  @Input()
  clickOutsideEnabled = true;

  @Input()
  attachOutsideOnClick = false;

  @Input()
  delayClickOutsideInit = false;

  @Input()
  emitOnBlur = false;

  @Input()
  exclude = '';

  @Input()
  excludeBeforeClick = false;

  @Input()
  clickOutsideEvents = '';

  @Output()
  ucapClickOutside: EventEmitter<Event> = new EventEmitter<Event>();

  private nodesExcluded: Array<HTMLElement> = [];
  private events: Array<string> = ['click', 'contextmenu'];

  constructor(
    private elementRef: ElementRef,
    private ngZone: NgZone,
    @Inject(PLATFORM_ID) private platformId: object
  ) {
    this._initOnClickBody = this._initOnClickBody.bind(this);
    this._onClickBody = this._onClickBody.bind(this);
    this._onWindowBlur = this._onWindowBlur.bind(this);
  }

  ngOnInit() {
    if (!isPlatformBrowser(this.platformId)) {
      return;
    }

    this._init();
  }

  ngOnDestroy() {
    if (!isPlatformBrowser(this.platformId)) {
      return;
    }

    this._removeClickOutsideListener();
    this._removeAttachOutsideOnClickListener();
    this._removeWindowBlurListener();
  }

  ngOnChanges(changes: SimpleChanges) {
    if (!isPlatformBrowser(this.platformId)) {
      return;
    }

    if (changes.attachOutsideOnClick || changes.exclude || changes.emitOnBlur) {
      this._init();
    }
  }

  private _init() {
    if (this.clickOutsideEvents !== '') {
      this.events = this.clickOutsideEvents.split(',').map(e => e.trim());
    }

    this._excludeCheck();

    if (this.attachOutsideOnClick) {
      this._initAttachOutsideOnClickListener();
    } else {
      this._initOnClickBody();
    }

    if (this.emitOnBlur) {
      this._initWindowBlurListener();
    }
  }

  private _initOnClickBody() {
    if (this.delayClickOutsideInit) {
      setTimeout(this._initClickOutsideListener.bind(this));
    } else {
      this._initClickOutsideListener();
    }
  }

  private _excludeCheck() {
    if (this.exclude) {
      try {
        const nodes = Array.from(
          document.querySelectorAll(this.exclude)
        ) as Array<HTMLElement>;
        if (nodes) {
          this.nodesExcluded = nodes;
        }
      } catch (err) {
        console.error(
          '[ng-click-outside] Check your exclude selector syntax.',
          err
        );
      }
    }
  }

  private _onClickBody(ev: Event) {
    if (!this.clickOutsideEnabled) {
      return;
    }

    if (this.excludeBeforeClick) {
      this._excludeCheck();
    }

    if (
      !this.elementRef.nativeElement.contains(ev.target) &&
      !this._shouldExclude(ev.target)
    ) {
      this._emit(ev);

      if (this.attachOutsideOnClick) {
        this._removeClickOutsideListener();
      }
    }
  }

  /**
   * Resolves problem with outside click on iframe
   * @see https://github.com/arkon/ng-click-outside/issues/32
   */
  private _onWindowBlur(ev: Event) {
    setTimeout(() => {
      if (!document.hidden) {
        this._emit(ev);
      }
    });
  }

  private _emit(ev: Event) {
    if (!this.clickOutsideEnabled) {
      return;
    }

    this.ngZone.run(() => this.ucapClickOutside.emit(ev));
  }

  private _shouldExclude(target): boolean {
    for (const excludedNode of this.nodesExcluded) {
      if (excludedNode.contains(target)) {
        return true;
      }
    }

    return false;
  }

  private _initClickOutsideListener() {
    this.ngZone.runOutsideAngular(() => {
      this.events.forEach(e =>
        document.body.addEventListener(e, this._onClickBody)
      );
    });
  }

  private _removeClickOutsideListener() {
    this.ngZone.runOutsideAngular(() => {
      this.events.forEach(e =>
        document.body.removeEventListener(e, this._onClickBody)
      );
    });
  }

  private _initAttachOutsideOnClickListener() {
    this.ngZone.runOutsideAngular(() => {
      this.events.forEach(e =>
        this.elementRef.nativeElement.addEventListener(e, this._initOnClickBody)
      );
    });
  }

  private _removeAttachOutsideOnClickListener() {
    this.ngZone.runOutsideAngular(() => {
      this.events.forEach(e =>
        this.elementRef.nativeElement.removeEventListener(
          e,
          this._initOnClickBody
        )
      );
    });
  }

  private _initWindowBlurListener() {
    this.ngZone.runOutsideAngular(() => {
      window.addEventListener('blur', this._onWindowBlur);
    });
  }

  private _removeWindowBlurListener() {
    this.ngZone.runOutsideAngular(() => {
      window.removeEventListener('blur', this._onWindowBlur);
    });
  }
}
