;(function (root, factory) {
  if (typeof define === "function" && define.amd) {
    // AMD. Register as an anonymous module.
    define([], factory)
  } else if (typeof module === "object" && module.exports) {
    // Node. Does not work with strict CommonJS, but
    // only CommonJS-like environments that support module.exports,
    // like Node.
    module.exports = factory()
  } else {
    // Browser globals (root is window)
    root.diagrams_behavior = factory()
  }
})(this, function () {
  /**
   * @param [scrollingEnabled=true] {boolean} - Is the scrolling from a non-terminal usage to it's definition
   * enabled. it is enabled by default, but this flow is not relevant in all use cases (playground) and thus
   * it is parametrized.
   */
  function initDiagramsBehavior(scrollingEnabled) {
    if (scrollingEnabled === undefined) {
      scrollingEnabled = true
    }

    var diagramHeaders = toArr(document.getElementsByClassName("diagramHeader"))
    diagramHeaders.forEach(function (header) {
      header.addEventListener(
        "mouseover",
        toggleNonTerminalUsageAndDef_fromHeader
      )
      header.addEventListener(
        "mouseout",
        toggleNonTerminalUsageAndDef_fromHeader
      )
    })

    var noneTerminals = toArr(document.getElementsByClassName("non-terminal"))
    var noneTerminalsText = findDomChildrenByTagName(noneTerminals, "text")
    noneTerminalsText.forEach(function (nonTerminal) {
      nonTerminal.addEventListener(
        "mouseover",
        toggleNonTerminalUsageAndDef_fromNoneTerminal
      )
      nonTerminal.addEventListener(
        "mouseout",
        toggleNonTerminalUsageAndDef_fromNoneTerminal
      )

      if (scrollingEnabled) {
        nonTerminal.addEventListener("click", jumpToNoneTerminalDef)
      }
    })

    var terminals = toArr(document.getElementsByClassName("terminal"))
    var terminalsText = findDomChildrenByTagName(terminals, "text")
    terminalsText.forEach(function (terminal) {
      terminal.addEventListener("mouseover", toggleTerminalUsage)
      terminal.addEventListener("mouseout", toggleTerminalUsage)
    })
  }

  function toggleTerminalUsage(mouseEvent) {
    var terminalName = mouseEvent.target.getAttribute("label")
    var rects = getUsageSvgRect(terminalName, "terminal", "label")
    toggleClassForNodes(rects, "diagramRectUsage")
  }

  function toggleNonTerminalUsageAndDef_fromNoneTerminal(mouseEvent) {
    var rectsHeaderAndRuleName = getUsageRectAndDefHeader(mouseEvent.target)
    toggleClassForNodes(rectsHeaderAndRuleName.rects, "diagramRectUsage")
    toggleClass(rectsHeaderAndRuleName.header, "diagramHeaderDef")
  }

  function jumpToNoneTerminalDef(mouseEvent) {
    var header = findHeader(mouseEvent.target.getAttribute("rulename"))
    scrollToY(header.offsetTop, 666, "easeInOutQuint")
  }

  function toggleNonTerminalUsageAndDef_fromHeader(mouseEvent) {
    toggleClass(mouseEvent.target, "diagramHeaderDef")
    // this does not work on an svg DOM element so its ok to use innerHTML.
    var definitionName = mouseEvent.target.innerHTML
    var rects = getUsageSvgRect(definitionName, "non-terminal", "rulename")
    toggleClassForNodes(rects, "diagramRectUsage")
  }

  function getUsageSvgRect(definitionName, className, attributeName) {
    var classDomElements = toArr(document.getElementsByClassName(className))
    var rects = findDomChildrenByTagName(classDomElements, "rect")
    return rects.filter(function (currRect) {
      var textNode = currRect.parentNode.getElementsByTagName("text")[0]
      return textNode.getAttribute(attributeName) === definitionName
    })
  }

  function findHeader(headerName) {
    var headers = toArr(document.getElementsByClassName("diagramHeader"))
    var header = headers.find(function (currHeader) {
      // this works on H2 dom elements and not SVG elements so innerHTML usage is safe.
      return currHeader.innerHTML === headerName
    })
    return header
  }

  function getUsageRectAndDefHeader(target) {
    var headerName = target.getAttribute("rulename")
    var rects = getUsageSvgRect(headerName, "non-terminal", "rulename")
    var header = findHeader(headerName)
    return {
      rects: rects,
      header: header,
      ruleName: headerName
    }
  }

  // utils

  // IE 10/11 does not support this on svg elements.
  // I'm uncertain I really care... :)
  // https://developer.mozilla.org/en-US/docs/Web/API/Element/classList
  function toggleClass(domNode, className) {
    if (domNode.classList.contains(className)) {
      domNode.classList.remove(className)
    } else {
      domNode.classList.add(className)
    }
  }

  function toggleClassForNodes(domNodes, className) {
    domNodes.forEach(function (currDomNode) {
      toggleClass(currDomNode, className)
    })
  }

  function toArr(htmlCollection) {
    return Array.prototype.slice.call(htmlCollection)
  }

  // first add raf shim
  // http://www.paulirish.com/2011/requestanimationframe-for-smart-animating/
  var requestAnimFrame = (function () {
    return (
      window.requestAnimationFrame ||
      window.webkitRequestAnimationFrame ||
      window.mozRequestAnimationFrame ||
      function (callback) {
        window.setTimeout(callback, 1000 / 60)
      }
    )
  })()

  // https://stackoverflow.com/questions/8917921/cross-browser-javascript-not-jquery-scroll-to-top-animation
  function scrollToY(scrollTargetY, speed, easing) {
    // scrollTargetY: the target scrollY property of the window
    // speed: time in pixels per second
    // easing: easing equation to use

    var scrollY = window.scrollY,
      scrollTargetY = scrollTargetY || 0,
      speed = speed || 2000,
      easing = easing || "easeOutSine",
      currentTime = 0

    // min time .1, max time .8 seconds
    var time = Math.max(
      0.1,
      Math.min(Math.abs(scrollY - scrollTargetY) / speed, 0.8)
    )

    // easing equations from https://github.com/danro/easing-js/blob/master/easing.js
    var PI_D2 = Math.PI / 2,
      easingEquations = {
        easeOutSine: function (pos) {
          return Math.sin(pos * (Math.PI / 2))
        },
        easeInOutSine: function (pos) {
          return -0.5 * (Math.cos(Math.PI * pos) - 1)
        },
        easeInOutQuint: function (pos) {
          if ((pos /= 0.5) < 1) {
            return 0.5 * Math.pow(pos, 5)
          }
          return 0.5 * (Math.pow(pos - 2, 5) + 2)
        }
      }

    // add animation loop
    function tick() {
      currentTime += 1 / 60

      var p = currentTime / time
      var t = easingEquations[easing](p)

      if (p < 1) {
        requestAnimFrame(tick)

        window.scrollTo(0, scrollY + (scrollTargetY - scrollY) * t)
      } else {
        window.scrollTo(0, scrollTargetY)
      }
    }

    // call it once to get started
    tick()
  }

  function findDomChildrenByTagName(domElements, tagName) {
    var elemsFound = []
    domElements.forEach(function (currDomNode) {
      toArr(currDomNode.children).forEach(function (currChild) {
        if (currChild.tagName === tagName) {
          elemsFound.push(currChild)
        }
      })
    })
    return elemsFound
  }
  return {
    initDiagramsBehavior: initDiagramsBehavior
  }
})
