/**
 * Licensed to the Apache Software Foundation (ASF) under one
 * or more contributor license agreements.  See the NOTICE file
 * distributed with this work for additional information
 * regarding copyright ownership.  The ASF licenses this file
 * to you under the Apache License, Version 2.0 (the
 * "License"); you may not use this file except in compliance
 * with the License.  You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
import {
  Component,
  AfterContentInit,
  OnDestroy,
  ViewChild,
  ElementRef,
  Input,
  OnInit
} from '@angular/core';
import { ContextMenuService, ContextMenuConfigModel } from './context-menu.service';
import { fromEvent, Subject, merge } from 'rxjs';
import Popper from 'popper.js';
import { takeUntil, filter } from 'rxjs/operators';
import { DynamicMenuItem } from './dynamic-item.model';

@Component({
  selector: '[ctxMenu]',
  templateUrl: './context-menu.component.html',
  styleUrls: ['./context-menu.component.scss']
})
export class ContextMenuComponent implements OnInit, AfterContentInit, OnDestroy {

  @ViewChild('contextMenuDropDown') dropDown: ElementRef;
  @ViewChild('clickOutsideCanvas') outside: ElementRef;

  @Input() ctxMenuItems: { label: string, event: string }[];
  @Input() ctxMenuTitle: string;
  @Input() ctxMenuId: string;
  @Input() ctxMenuData: any;

  dynamicMenuItems: DynamicMenuItem[] = [];

  isEnabled = false;
  isOpen = false;

  private destroyed$: Subject<boolean> = new Subject<boolean>();

  private popper: Popper;

  private excludedDomElements = [
    'label', 'input', 'button'
  ];

  constructor(
    private contextMenuSvc: ContextMenuService,
    private host: ElementRef
    ) {}

  ngOnInit() {
    this.fetchContextMenuConfig();
  }

  ngAfterContentInit() {
    this.subscribeTo();
  }

  private fetchContextMenuConfig() {
    this.contextMenuSvc.getConfig()
      .pipe(filter(value => !!value))
      .subscribe((contextMenuConfigJSON: ContextMenuConfigModel) => {
        this.isEnabled = contextMenuConfigJSON.isEnabled;
        const currentConfig = contextMenuConfigJSON.config[this.ctxMenuId];

        if (!this.isEnabled || !currentConfig) {
          return;
        }

        this.dynamicMenuItems = currentConfig;
      });
  }

  private subscribeTo() {
    fromEvent(this.host.nativeElement, 'click')
      .pipe(takeUntil(this.destroyed$))
      .subscribe(this.toggle.bind(this));

    merge(
      fromEvent(this.host.nativeElement, 'mouseover'),
      fromEvent(this.host.nativeElement, 'mouseout'),
    )
      .pipe(takeUntil(this.destroyed$))
      .subscribe((event: MouseEvent) => {
        if (this.isOpen) {
          event.stopPropagation();
        }
      });
  }

  private toggle($event: MouseEvent) {
    if (this.isElementExcluded($event)) {
      return;
    }

    $event.stopPropagation();

    if (!this.isEnabled) {
      this.dispatchDefaultEvent($event);
      return;
    }

    if (this.isOpen) {
      if (this.popper) {
        this.popper.destroy();
      }
      this.isOpen = false;
      return;
    }

    const origin = this.getContextMenuOrigin($event);
    this.isOpen = true;

    let mutationObserver = new MutationObserver((mutations) => {
      if (document.body.contains(this.dropDown.nativeElement)) {
        mutationObserver.disconnect();
        mutationObserver = null;

        this.popper = new Popper(origin, this.dropDown.nativeElement, { placement: 'bottom-start' });
      }
    });
    mutationObserver.observe(document.body, {
      attributes: false,
      childList: true,
      characterData: false,
      subtree: true}
    );
  }

  private dispatchDefaultEvent($event: MouseEvent) {
    this.host.nativeElement.dispatchEvent(new Event(this.ctxMenuItems[0].event));
  }

  private isElementExcluded($event: MouseEvent) {
    return this.excludedDomElements.find(element => element === ($event.target as HTMLElement).nodeName.toLowerCase());
  }

  private getContextMenuOrigin($event: MouseEvent): HTMLElement {
    if (($event.currentTarget as HTMLElement).contains($event.target as Node)) {
      return $event.target as HTMLElement;
    } else {
      return $event.currentTarget as HTMLElement;
    }
  }

  onPredefinedItemClicked($event: MouseEvent, eventName: string) {
    this.host.nativeElement.dispatchEvent(new Event(eventName));
  }

  onDynamicItemClicked($event: MouseEvent, url: string) {
    window.open(this.parseUrlPattern(url, this.ctxMenuData));
  }

  private parseUrlPattern(url = '', data = {}, delimeter: RegExp = /{|}/): string {
    return url.replace('{}', `{${this.ctxMenuId}}`)
      .split(delimeter).map((urlSegment) => {
        return data[urlSegment] || urlSegment;
    }).join('');
  }

  ngOnDestroy() {
    this.destroyed$.next(true);
    this.destroyed$.complete();
  }
}
