//tabbable JS Lib (Wrapped in angular service)
//https://github.com/davidtheclark/tabbable

(function() {
    'use strict';

    function tabbableService() {

         var candidateSelectors = [
            'input',
            'select',
            'textarea',
            'a[href]',
            'button',
            '[tabindex]',
            'audio[controls]',
            'video[controls]',
            '[contenteditable]:not([contenteditable="false"])',
            'iframe[data-mce-style]'
          ];
          var candidateSelector = candidateSelectors.join(',');

          var matches = typeof Element === 'undefined'
            ? function () {}
            : Element.prototype.matches || Element.prototype.msMatchesSelector || Element.prototype.webkitMatchesSelector;

          function tabbable(el, options) {
            options = options || {};

            var elementDocument = el.ownerDocument || el;
            var regularTabbables = [];
            var orderedTabbables = [];

            var untouchabilityChecker = new UntouchabilityChecker(elementDocument);
            var candidates = el.querySelectorAll(candidateSelector);

            if (options.includeContainer) {
              if (matches.call(el, candidateSelector)) {
                candidates = Array.prototype.slice.apply(candidates);
                candidates.unshift(el);
              }
            }

            var i, candidate, candidateTabindex;
            for (i = 0; i < candidates.length; i++) {
              candidate = candidates[i];

              if (!isNodeMatchingSelectorTabbable(candidate, untouchabilityChecker)) continue;

              candidateTabindex = getTabindex(candidate);
              if (candidateTabindex === 0) {
                regularTabbables.push(candidate);
              } else {
                orderedTabbables.push({
                  documentOrder: i,
                  tabIndex: candidateTabindex,
                  node: candidate
                });
              }
            }

            var tabbableNodes = orderedTabbables
              .sort(sortOrderedTabbables)
              .map(function(a) { return a.node })
              .concat(regularTabbables);

            return tabbableNodes;
          }

          tabbable.isTabbable = isTabbable;
          tabbable.isFocusable = isFocusable;

          function isNodeMatchingSelectorTabbable(node, untouchabilityChecker) {
            if (
              !isNodeMatchingSelectorFocusable(node, untouchabilityChecker)
              || isNonTabbableRadio(node)
              || getTabindex(node) < 0
            ) {
              return false;
            }
            return true;
          }

          function isTabbable(node, untouchabilityChecker) {
            if (!node) throw new Error('No node provided');
            if (matches.call(node, candidateSelector) === false) return false;
            return isNodeMatchingSelectorTabbable(node, untouchabilityChecker);
          }

          function isNodeMatchingSelectorFocusable(node, untouchabilityChecker) {
            untouchabilityChecker = untouchabilityChecker || new UntouchabilityChecker(node.ownerDocument || node);
            if (
              node.disabled
              || isHiddenInput(node)
              || untouchabilityChecker.isUntouchable(node)
            ) {
              return false;
            }
            return true;
          }

          var focusableCandidateSelector = candidateSelectors.concat('iframe').join(',');
          function isFocusable(node, untouchabilityChecker) {
            if (!node) throw new Error('No node provided');
            if (matches.call(node, focusableCandidateSelector) === false) return false;
            return isNodeMatchingSelectorFocusable(node, untouchabilityChecker);
          }

          function getTabindex(node) {
            var tabindexAttr = parseInt(node.getAttribute('tabindex'), 10);
            if (!isNaN(tabindexAttr)) return tabindexAttr;
            // Browsers do not return `tabIndex` correctly for contentEditable nodes;
            // so if they don't have a tabindex attribute specifically set, assume it's 0.
            if (isContentEditable(node)) return 0;
            return node.tabIndex;
          }

          function sortOrderedTabbables(a, b) {
            return a.tabIndex === b.tabIndex ? a.documentOrder - b.documentOrder : a.tabIndex - b.tabIndex;
          }

          // Array.prototype.find not available in IE.
          function find(list, predicate) {
            for (var i = 0, length = list.length; i < length; i++) {
              if (predicate(list[i])) return list[i];
            }
          }

          function isContentEditable(node) {
            return node.contentEditable === 'true';
          }

          function isInput(node) {
            return node.tagName === 'INPUT';
          }

          function isHiddenInput(node) {
            return isInput(node) && node.type === 'hidden';
          }

          function isRadio(node) {
            return isInput(node) && node.type === 'radio';
          }

          function isNonTabbableRadio(node) {
            return isRadio(node) && !isTabbableRadio(node);
          }

          function getCheckedRadio(nodes) {
            for (var i = 0; i < nodes.length; i++) {
              if (nodes[i].checked) {
                return nodes[i];
              }
            }
          }

          function isTabbableRadio(node) {
            if (!node.name) return true;
            // This won't account for the edge case where you have radio groups with the same
            // in separate forms on the same page.
            var radioSet = node.ownerDocument.querySelectorAll('input[type="radio"][name="' + node.name + '"]');
            var checked = getCheckedRadio(radioSet);
            return !checked || checked === node;
          }

          // An element is "untouchable" if *it or one of its ancestors* has
          // `visibility: hidden` or `display: none`.
          function UntouchabilityChecker(elementDocument) {
            this.doc = elementDocument;
            // Node cache must be refreshed on every check, in case
            // the content of the element has changed. The cache contains tuples
            // mapping nodes to their boolean result.
            this.cache = [];
          }

          // getComputedStyle accurately reflects `visibility: hidden` of ancestors
          // but not `display: none`, so we need to recursively check parents.
          UntouchabilityChecker.prototype.hasDisplayNone = function hasDisplayNone(node, nodeComputedStyle) {
            if (node === this.doc.documentElement) return false;

              // Search for a cached result.
              var cached = find(this.cache, function(item) {
                return item === node;
              });
              if (cached) return cached[1];

              if (!nodeComputedStyle) {
                if (node instanceof DocumentFragment) {
                  return true;// though DocumentFragment doesn't directly have display 'none', we know that it will never be visible, and therefore we return true. (and do not cache this, cause it will change if appended to the DOM)
                } else {
                  nodeComputedStyle = this.doc.defaultView.getComputedStyle(node);
                }
              }

              var result = false;

              if (nodeComputedStyle.display === 'none') {
                result = true;
              } else if (node.parentNode) {
                result = this.hasDisplayNone(node.parentNode);
              }

              this.cache.push([node, result]);

              return result;
          }

          UntouchabilityChecker.prototype.isUntouchable = function isUntouchable(node) {
            if (node === this.doc.documentElement) return false;
            var computedStyle = this.doc.defaultView.getComputedStyle(node);
            if (this.hasDisplayNone(node, computedStyle)) return true;
            return computedStyle.visibility === 'hidden';
          }

          //module.exports = tabbable;

         ////////////

         var service = {
            tabbable: tabbable,
            isTabbable: isTabbable,
            isFocusable: isFocusable
         };

         return service;
    }

    angular.module('umbraco.services').factory('tabbableService', tabbableService);

 })();
