/**
 * @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",
  innerClass: "c-modal-dialog__inner",
  contentClass: "c-modal-dialog__contents",
  closeClass: "c-modal-dialog__close-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.getElementsByClassName(this.options.innerClass).item(0);
    this.bodyElement = dialog.getElementsByClassName(this.options.contentClass).item(0);
    this.closeButton = dialog.getElementsByClassName(this.options.closeClass).item(0);
    this.openButtons = toArray(openButtons);
    // this.openButtonThatSpawnedDialog;
    this.lastFocusedElement = null;
    // this.tabbableNodesInDialog;
    this.label = document.getElementById(dialog.getAttribute("aria-labelledby")) || 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);

    if (this.rootElement && this.openButtons.length && this.closeButton) {
      this.openButtons.map(openButton => {
        openButton.addEventListener("click", this.boundOnOpenButtonClick);
      });
      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();
        });
      }
    }
  },

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

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

  isOpen: function() {
    return this.openButtons[0].getAttribute("aria-expanded") === "true";
  },

  open: function() {
    this.openButtons.map(openButton => {
      openButton.setAttribute("aria-expanded", 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.openButtons.map(openButton => {
      openButton.setAttribute("aria-expanded", 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();
      window.scrollTo(0, Math.round(this.openButtonThatSpawnedDialog.getBoundingClientRect().top + window.pageYOffset));
      this.openButtonThatSpawnedDialog = null;
    } else {
      this.openButtons[0].focus();
      window.scrollTo(0, Math.round(this.openButtons[0].getBoundingClientRect().top + window.pageYOffset));
    }
  },

  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.openButtons[0].getAttribute("aria-expanded") === "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;
    if (event.target.getAttribute("aria-controls") === this.rootElement.id) {
      openButton = event.target;
    } else {
      openButton = event.target.closest(`[aria-controls=${this.rootElement.id}]`);
    }
    if (openButton) {
      this.openButtonThatSpawnedDialog = openButton;
      this.open();
    }
  },

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

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

  onKeyPress: function(event) {
    // 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")
    if (this.isDescendantOfThisDialog(document.activeElement)) {
      previouslyFocusedDialog = this.rootElement;
      previouslyFocusedDialogElement = document.activeElement;
    }
    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 {
        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;
  }

};
