/**
 * @module tgam-patterns/modal-dialog
 */
import extend from "lodash/extend";
import tabbable from "tabbable";
import ee from "./event-emitter";
import { toArray } from "./utils";

const eventNamespace = "tgam-modal-dialog";

const defaults = {
  dialogClass: "c-modal-dialog",
  innerSelector: ".c-modal-dialog__inner",
  contentSelector: ".c-modal-dialog__contents",
  closeSelector: ".c-modal-dialog__toolbar .c-button"
};

let previouslyFocusedDialog;
let previouslyFocusedDialogElement;

export default {

  getRootElement: function() {
    return this.rootElement;
  },

  getOpenButtonThatSpawnedDialog: function() {
    return this.openButtonThatSpawnedDialog;
  },

  setLabel: function(newLabel) {
    if (this.label) {
      this.label.innerHTML = newLabel;
    }
  },

  init: function(openButtons, dialog, options) {
    this.options = extend(defaults, options);
    this.rootElement = dialog;
    this.innerElement = dialog.querySelector(this.options.innerSelector);
    this.bodyElement = dialog.querySelector(this.options.contentSelector);
    this.closeButton = dialog.querySelector(this.options.closeSelector);
    this.openButtons = toArray(openButtons);
    this.openButtonThatSpawnedDialog = null;
    this.lastFocusedElement = null;
    this.tabbableNodesInDialog = null;
    this.label = document.getElementById(dialog.getAttribute("aria-labelledby")) || null;
    this.screenPositionX = null;
    this.screenPositionY = null;

    // Ensure that the "this" keyword always references the correct
    // object when the following methods are used as event handlers
    this.boundOnOpenButtonClick = this.onOpenButtonClick.bind(this);
    this.boundOnKeyPress = this.onKeyPress.bind(this);
    this.boundOnFocusChange = this.onFocusChange.bind(this);
    this.boundOpenButtonMouseDown = this.openButtonMouseDown.bind(this);
    this.boundOpenButtonMouseUp = this.openButtonMouseUp.bind(this);

    if (this.rootElement && this.openButtons.length && this.closeButton) {
      this.initOpenButtons();

      this.closeButton.addEventListener("click", () => {
        this.close();
      });
      this.rootElement.addEventListener("animationend", () => {
        this.onDialogAnimationEnd();
      });
      // if inner modal is present, then do lightbox - click to close
      if (this.innerElement) {
        this.innerElement.addEventListener("click", (e) => {
          e.stopPropagation();
        });
        this.rootElement.addEventListener("click", () => {
          this.close();
        });
      }
    }
  },

  initOpenButtons: function(newOpenButtons = null) {
    if (newOpenButtons) {
      // An additional set of open buttons has been supplied, so we need to
      // add them to the existing array, dedupe the array, then initialize them
      const newButtons = toArray(newOpenButtons);

      this.openButtons = this.openButtons.concat(newButtons).filter((button, i, arr) => {
        return arr.filter(item => item === button).length < 2;
      });
    }
    this.openButtons.forEach(openButton => {
      if (this.options.supportDragEvents) {
        openButton.addEventListener("mousedown", this.boundOpenButtonMouseDown);
        openButton.addEventListener("mouseup", this.boundOpenButtonMouseUp);
      } else {
        openButton.addEventListener("click", this.boundOnOpenButtonClick);
      }
    });
  },

  inject: function(html) {
    this.bodyElement.innerHTML = html;
  },

  clear: function() {
    this.bodyElement.innerHTML = "";
  },

  isOpen: function() {
    return this.rootElement.dataset.open === "true";
  },

  open: function() {
    this.rootElement.dataset.open = true;
    this.rootElement.classList.remove(`${this.options.dialogClass}--hiding`);
    this.rootElement.classList.add(`${this.options.dialogClass}--revealing`);
    this.rootElement.removeAttribute("hidden");
    ee.emitEvent(`${eventNamespace}:opening`, [this]);
  },

  close: function() {
    this.rootElement.dataset.open = false;
    this.rootElement.classList.remove(`${this.options.dialogClass}--revealing`);
    this.rootElement.classList.add(`${this.options.dialogClass}--hiding`);
    this.removeKeyPressListener();
    this.deactivateKeyboardTrap();
    ee.emitEvent(`${eventNamespace}:closing`, [this]);
    // We can't set focus back on the trigger button until after the "closing"
    // event has fired, because we need to ensure that the ScrollManager has
    // released its scroll lock first
    if (this.openButtonThatSpawnedDialog) {
      this.openButtonThatSpawnedDialog.focus();
      this.openButtonThatSpawnedDialog = null;
    } else {
      this.openButtons[0].focus();
    }
  },

  onDialogAnimationEnd: function() {
    // Using classList.remove() with multiple arguments fails in IE11,
    // so we're removing classes separately
    this.rootElement.classList.remove(`${this.options.dialogClass}--hiding`);
    this.rootElement.classList.remove(`${this.options.dialogClass}--revealing`);
    if (this.rootElement.dataset.open === "false") {
      this.rootElement.setAttribute("hidden", "hidden");
      ee.emitEvent(`${eventNamespace}:closed`, [this]);
    } else {
      this.addKeyPressListener();
      this.activateKeyboardTrap();
      this.closeButton.focus();
      ee.emitEvent(`${eventNamespace}:open`, [this]);
    }
  },

  onOpenButtonClick: function(event) {
    event.preventDefault();
    let openButton;
    // dataset is not supported in IE11 for svg path elements
    // See https://developer.mozilla.org/en-US/docs/Web/API/SVGElement/dataset
    const dataPopup = event.target.nodeName.toLowerCase() === "path" ? event.target.getAttribute("data-popup") : event.target.dataset.popup;
    if (dataPopup === this.rootElement.id) {
      openButton = event.target;
    } else {
      openButton = event.target.closest(`[data-popup=${this.rootElement.id}]`);
    }
    if (openButton) {
      this.openButtonThatSpawnedDialog = openButton;
      this.open();
    }
  },

  openButtonMouseDown: function(event) {
    this.screenPositionX = event.screenX;
    this.screenPositionY = event.screenY;
  },

  openButtonMouseUp: function(event) {
    // Allow 20px room for clicking
    if (((event.screenX <= this.screenPositionX && event.screenX + 20 >= this.screenPositionX) ||
      (event.screenX >= this.screenPositionX && event.screenX - 20 <= this.screenPositionX)) &&
      ((event.screenY <= this.screenPositionY && event.screenY + 20 >= this.screenPositionY) ||
      (event.screenY >= this.screenPositionY && event.screenY - 20 <= this.screenPositionY))) {
      this.onOpenButtonClick(event);
    }
  },

  addKeyPressListener: function() {
    document.addEventListener("keydown", this.boundOnKeyPress, false);
  },

  removeKeyPressListener: function() {
    document.removeEventListener("keydown", this.boundOnKeyPress);
  },

  onKeyPress: function(event) {
    // keyCode 27 is the Escape key
    if (this.isOpen() && event.keyCode === 27 && previouslyFocusedDialog === this.rootElement) {
      this.close();
    }
  },

  activateKeyboardTrap: function() {
    if (!this.tabbableNodesInDialog) {
      this.tabbableNodesInDialog = tabbable(this.rootElement);
    }
    document.addEventListener("focus", this.boundOnFocusChange, true);
  },

  deactivateKeyboardTrap: function() {
    this.tabbableNodesInDialog = null;
    document.removeEventListener("focus", this.boundOnFocusChange);
  },

  // Note: document.activeElement is the element that's ABOUT TO receive focus
  onFocusChange: function() {
    // Keep track of which dialog last had focus (prototype properties
    // because they can be shared between linked object "instances")

    // User is about to focus an element inside of the dialog
    if (this.isDescendantOfThisDialog(document.activeElement)) {
      previouslyFocusedDialog = this.rootElement;
      previouslyFocusedDialogElement = document.activeElement;
    }
    // User is about to focus an element outside of the dialog while the dialog is still open
    if (this.isOpen() && !this.isDescendantOfThisDialog(document.activeElement) && previouslyFocusedDialog === this.rootElement) {
      if (previouslyFocusedDialogElement === this.closeButton) {
        // Focus last tabbable node in dialog
        this.tabbableNodesInDialog[this.tabbableNodesInDialog.length - 1].focus();
      } else {
        // Focus first tabbable node in dialog
        this.closeButton.focus();
      }
    }
  },

  isDescendantOfAnyDialog: function(child) {
    let node = child.parentNode;
    while (node != null) {
      if (node.classList != null && node.classList.contains(this.options.dialogClass)) {
        return true;
      }
      node = node.parentNode;
    }
    return false;
  },

  isDescendantOfThisDialog: function(child) {
    return this.isDescendant(this.rootElement, child);
  },

  isDescendant: function(parent, child) {
    let node = child.parentNode;
    while (node != null) {
      if (node === parent) {
        return true;
      }
      node = node.parentNode;
    }
    return false;
  }

};
