
/*
 * BackgroundCheck
 * http://kennethcachia.com/background-check
 *
 * v1.2.2
 */

(function (root, factory) {

    if (typeof define === 'function' && define.amd) {
      define(factory);
    } else {
      root.BackgroundCheck = factory(root);
    }
  
  }(this, function () {
  
    'use strict';
  
    var resizeEvent = window.orientation !== undefined ? 'orientationchange' : 'resize';
    var supported;
    var canvas;
    var context;
    var throttleDelay;
    var viewport;
    var attrs = {};
  
  
    /*
     * Initializer
     */
    function init(a) {
  
      if (a === undefined || a.targets === undefined) {
        throw 'Missing attributes';
      }
  
      // Default values
      attrs.debug         = checkAttr(a.debug, false);
      attrs.debugOverlay  = checkAttr(a.debugOverlay, false);
      attrs.targets       = getElements(a.targets);
      attrs.images        = getElements(a.images || 'img', true);
      attrs.changeParent  = checkAttr(a.changeParent, false);
      attrs.threshold     = checkAttr(a.threshold, 50);
      attrs.minComplexity = checkAttr(a.minComplexity, 30);
      attrs.minOverlap    = checkAttr(a.minOverlap, 50);
      attrs.windowEvents  = checkAttr(a.windowEvents, true);
      attrs.maxDuration   = checkAttr(a.maxDuration, 500);
  
      attrs.mask = checkAttr(a.mask, {
        r: 0,
        g: 255,
        b: 0
      });
  
      attrs.classes = checkAttr(a.classes, {
        dark: 'background--dark',
        light: 'background--light',
        complex: 'background--complex'
      });
  
      if (supported === undefined) {
        checkSupport();
  
        if (supported) {
          canvas.style.position = 'fixed';
          canvas.style.top = '0px';
          canvas.style.left = '0px';
          canvas.style.width = '100%';
          canvas.style.height = '100%';
  
          window.addEventListener(resizeEvent, throttle.bind(null, function () {
            resizeCanvas();
            check();
          }));
  
          window.addEventListener('scroll', throttle.bind(null, check));
  
          resizeCanvas();
          check();
        }
      }
    }
  
  
    /*
     * Destructor
     */
    function destroy() {
      supported = null;
      canvas = null;
      context = null;
      attrs = {};
  
      if (throttleDelay) {
        clearTimeout(throttleDelay);
      }
    }
  
  
    /*
     * Output debug logs
     */
    function log(msg) {
  
      if (get('debug')) {
        console.log(msg);
      }
    }
  
  
    /*
     * Get attribute value, use a default
     * when undefined
     */
    function checkAttr(value, def) {
      checkType(value, typeof def);
      return (value === undefined) ? def : value;
    }
  
  
    /*
     * Reject unwanted types
     */
    function checkType(value, type) {
  
      if (value !== undefined && typeof value !== type) {
        throw 'Incorrect attribute type';
      }
    }
  
  
    /*
     * Convert elements with background-image
     * to Images
     */
    function checkForCSSImages(els) {
      var el;
      var url;
      var list = [];
  
      for (var e = 0; e < els.length; e++) {
        el = els[e];
        list.push(el);
  
        if (el.tagName !== 'IMG') {
          url = window.getComputedStyle(el).backgroundImage;
  
          // Ignore multiple backgrounds
          if (url.split(/,url|, url/).length > 1) {
            throw 'Multiple backgrounds are not supported';
          }
  
          if (url && url !== 'none') {
            list[e] = {
              img: new Image(),
              el: list[e]
            };
  
            url = url.slice(4, -1);
            url = url.replace(/"/g, '');
  
            list[e].img.src = url;
            log('CSS Image - ' + url);
          } else {
            throw 'Element is not an <img> but does not have a background-image';
          }
        }
      }
  
      return list;
    }
  
  
    /*
     * Check for String, Element or NodeList
     */
    function getElements(selector, convertToImages) {
      var els = selector;
  
      if (typeof selector === 'string') {
        els = document.querySelectorAll(selector);
      } else if (selector && selector.nodeType === 1) {
        els = [selector];
      }
  
      if (!els || els.length === 0 || els.length === undefined) {
        throw 'Elements not found';
      } else {
  
        if (convertToImages) {
          els = checkForCSSImages(els);
        }
  
        els = Array.prototype.slice.call(els);
      }
  
      return els;
    }
  
  
    /*
     * Check if browser supports <canvas>
     */
    function checkSupport() {
      canvas = document.createElement('canvas');
  
      if (canvas && canvas.getContext) {
        context = canvas.getContext('2d');
        supported = true;
      } else {
        supported = false;
      }
  
      showDebugOverlay();
    }
  
  
    /*
     * Show <canvas> on top of page
     */
    function showDebugOverlay() {
  
      if (get('debugOverlay')) {
        canvas.style.opacity = 0.5;
        canvas.style.pointerEvents = 'none';
        document.body.appendChild(canvas);
      } else {
  
        // Check if it was previously added
        if (canvas.parentNode) {
          canvas.parentNode.removeChild(canvas);
        }
      }
    }
  
  
    /*
     * Stop if it's slow
     */
    function kill(start) {
      var duration = new Date().getTime() - start;
      
      log('Duration: ' + duration + 'ms');
  
      if (duration > get('maxDuration')) {
        // Log a message even when debug is false
        console.log('BackgroundCheck - Killed');
        removeClasses();
        destroy();
      }
    }
  
  
    /*
     * Set width and height of <canvas>
     */
    function resizeCanvas() {
      viewport = {
        left: 0,
        top: 0,
        right: document.body.clientWidth,
        bottom: window.innerHeight
      };
  
      canvas.width = document.body.clientWidth;
      canvas.height = window.innerHeight;
    }
  
  
    /*
     * Process px and %, discard anything else
     */
    function getValue(css, parent, delta) {
      var value;
      var percentage;
  
      if (css.indexOf('px') !== -1) {
        value = parseFloat(css);
      } else if (css.indexOf('%') !== -1) {
        value = parseFloat(css);
        percentage = value / 100;
        value = percentage * parent;
  
        if (delta) {
          value -= delta * percentage;
        }
      } else {
        value = parent;
      }
  
      return value;
    }
  
  
    /*
     * Calculate top, left, width and height
     * using the object's CSS
     */
    function calculateAreaFromCSS(obj) {
      var css = window.getComputedStyle(obj.el);
  
      // Force no-repeat and padding-box
      obj.el.style.backgroundRepeat = 'no-repeat';
      obj.el.style.backgroundOrigin = 'padding-box';
  
      // Background Size
      var size = css.backgroundSize.split(' ');
      var width = size[0];
      var height = size[1] === undefined ? 'auto' : size[1];
  
      var parentRatio = obj.el.clientWidth / obj.el.clientHeight;
      var imgRatio = obj.img.naturalWidth / obj.img.naturalHeight;
  
      if (width === 'cover') {
  
        if (parentRatio >= imgRatio) {
          width = '100%';
          height = 'auto';
        } else {
          width = 'auto';
          size[0] = 'auto';
          height = '100%';
        }
  
      } else if (width === 'contain') {
  
        if (1 / parentRatio < 1 / imgRatio) {
          width = 'auto';
          size[0] = 'auto';
          height = '100%';
        } else {
          width = '100%';
          height = 'auto';
        }
      }
  
      if (width === 'auto') {
        width = obj.img.naturalWidth;
      } else {
        width = getValue(width, obj.el.clientWidth);
      }
  
      if (height === 'auto') {
        height = (width / obj.img.naturalWidth) * obj.img.naturalHeight;
      } else {
        height = getValue(height, obj.el.clientHeight);
      }
  
      if (size[0] === 'auto' && size[1] !== 'auto') {
        width = (height / obj.img.naturalHeight) * obj.img.naturalWidth;
      }
  
      var position = css.backgroundPosition;
  
      // Fix inconsistencies between browsers
      if (position === 'top') {
        position = '50% 0%';
      } else if (position === 'left') {
        position = '0% 50%';
      } else if (position === 'right') {
        position = '100% 50%';
      } else if (position === 'bottom') {
        position = '50% 100%';
      } else if (position === 'center') {
        position = '50% 50%';
      }
  
      position = position.split(' ');
  
      var x;
      var y;
  
      // Two-value syntax vs Four-value syntax
      if (position.length === 4) {
        x = position[1];
        y = position[3];
      } else {
        x = position[0];
        y = position[1];
      }
  
      // Use a default value
      y = y || '50%';
  
      // Background Position
      x = getValue(x, obj.el.clientWidth, width);
      y = getValue(y, obj.el.clientHeight, height);
  
      // Take care of ex: background-position: right 20px bottom 20px;
      if (position.length === 4) {
  
        if (position[0] === 'right') {
          x = obj.el.clientWidth - obj.img.naturalWidth - x;
        }
  
        if (position[2] === 'bottom') {
          y = obj.el.clientHeight - obj.img.naturalHeight - y;
        }
      }
  
      x += obj.el.getBoundingClientRect().left;
      y += obj.el.getBoundingClientRect().top;
  
      return {
        left: Math.floor(x),
        right: Math.floor(x + width),
        top: Math.floor(y),
        bottom: Math.floor(y + height),
        width: Math.floor(width),
        height: Math.floor(height)
      };
    }
  
  
    /*
     * Get Bounding Client Rect
     */
    function getArea(obj) {
      var area;
      var image;
      var parent;
  
      if (obj.nodeType) {
        var rect = obj.getBoundingClientRect();
  
        // Clone ClientRect for modification purposes
        area = {
          left: rect.left,
          right: rect.right,
          top: rect.top,
          bottom: rect.bottom,
          width: rect.width,
          height: rect.height
        };
  
        parent = obj.parentNode;
        image = obj;
      } else {
        area = calculateAreaFromCSS(obj);
        parent = obj.el;
        image = obj.img;
      }
  
      parent = parent.getBoundingClientRect();
  
      area.imageTop = 0;
      area.imageLeft = 0;
      area.imageWidth = image.naturalWidth;
      area.imageHeight = image.naturalHeight;
  
      var ratio = area.imageHeight / area.height;
      var delta;
  
      // Stay within the parent's boundary
      if (area.top < parent.top) {
        delta = parent.top - area.top;
        area.imageTop = ratio * delta;
        area.imageHeight -= ratio * delta;
        area.top += delta;
        area.height -= delta;
      }
  
      if (area.left < parent.left) {
        delta = parent.left - area.left;
        area.imageLeft += ratio * delta;
        area.imageWidth -= ratio * delta;
        area.width -= delta;
        area.left += delta;
      }
  
      if (area.bottom > parent.bottom) {
        delta = area.bottom - parent.bottom;
        area.imageHeight -= ratio * delta;
        area.height -= delta;
      }
  
      if (area.right > parent.right) {
        delta = area.right - parent.right;
        area.imageWidth -= ratio * delta;
        area.width -= delta;
      }
  
      area.imageTop = Math.floor(area.imageTop);
      area.imageLeft = Math.floor(area.imageLeft);
      area.imageHeight = Math.floor(area.imageHeight);
      area.imageWidth = Math.floor(area.imageWidth);
  
      return area;
    }
  
  
    /*
     * Render image on canvas
     */
    function drawImage(image) {
      var area = getArea(image);
  
      image = image.nodeType ? image : image.img;
  
      if (area.imageWidth > 0 && area.imageHeight > 0 && area.width > 0 && area.height > 0) {
        context.drawImage(image,
                          area.imageLeft, area.imageTop, area.imageWidth, area.imageHeight,
                          area.left, area.top, area.width, area.height);
      } else {
        log('Skipping image - ' + image.src + ' - area too small');
      }
    }
  
  
    /*
     * Add/remove classes
     */
    function classList(node, name, mode) {
      var className = node.className;
  
      switch (mode) {
      case 'add':
        className += ' ' + name;
        break;
      case 'remove':
        var pattern = new RegExp('(?:^|\\s)' + name + '(?!\\S)', 'g');
        className = className.replace(pattern, '');
        break;
      }
  
      node.className = className.trim();
    }
  
  
    /*
     * Remove classes from element or
     * their parents, depending on checkParent
     */
    function removeClasses(el) {
      var targets = el ? [el] : get('targets');
      var target;
  
      for (var t = 0; t < targets.length; t++) {
        target = targets[t];
        target = get('changeParent') ? target.parentNode : target;
        
        classList(target, get('classes').light, 'remove');
        classList(target, get('classes').dark, 'remove');
        classList(target, get('classes').complex, 'remove');
      }
    }
  
  
    /*
     * Calculate average pixel brightness of a region 
     * and add 'light' or 'dark' accordingly
     */
    function calculatePixelBrightness(target) {
      var dims = target.getBoundingClientRect();
      var brightness;
      var data;
      var pixels = 0;
      var delta;
      var deltaSqr = 0;
      var mean = 0;
      var variance;
      var minOverlap = 0;
      var mask = get('mask');
  
      if (dims.width > 0 && dims.height > 0) {
        removeClasses(target);
  
        target = get('changeParent') ? target.parentNode : target;
        data = context.getImageData(dims.left, dims.top, dims.width, dims.height).data;
  
        for (var p = 0; p < data.length; p += 4) {
  
          if (data[p] === mask.r && data[p + 1] === mask.g && data[p + 2] === mask.b) {
            minOverlap++;
          } else {
            pixels++;
            brightness = (0.2126 * data[p]) + (0.7152 * data[p + 1]) + (0.0722 * data[p + 2]);
            delta = brightness - mean;
            deltaSqr += delta * delta;
            mean = mean + delta / pixels;
          }
        }
  
        if (minOverlap <= (data.length / 4) * (1 - (get('minOverlap') / 100))) {
          variance = Math.sqrt(deltaSqr / pixels) / 255;
          mean = mean / 255;
          log('Target: ' + target.className +  ' lum: ' + mean + ' var: ' + variance);
          classList(target, mean <= (get('threshold') / 100) ? get('classes').dark : get('classes').light, 'add');
  
          if (variance > get('minComplexity') / 100) {
            classList(target, get('classes').complex, 'add');
          }
        }
      }
    }
  
  
    /*
     * Test if a is within b's boundary
     */
    function isInside(a, b) {
      a = (a.nodeType ? a : a.el).getBoundingClientRect();
      b = b === viewport ? b : (b.nodeType ? b : b.el).getBoundingClientRect();
  
      return !(a.right < b.left || a.left > b.right || a.top > b.bottom || a.bottom < b.top);
    }
  
  
    /*
     * Process all targets (checkTarget is undefined)
     * or a single target (checkTarget is a previously set target)
     *
     * When not all images are loaded, checkTarget is an image
     * to avoid processing all targets multiple times
     */
    function processTargets(checkTarget) {
      var start = new Date().getTime();
      var mode = (checkTarget && (checkTarget.tagName === 'IMG' || checkTarget.img)) ? 'image' : 'targets';
      var found = checkTarget ? false : true;
      var total = get('targets').length;
      var target;
  
      for (var t = 0; t < total; t++) {
        target = get('targets')[t];
  
        if (isInside(target, viewport)) {
          if (mode === 'targets' && (!checkTarget || checkTarget === target)) {
            found = true;
            calculatePixelBrightness(target);
          } else if (mode === 'image' && isInside(target, checkTarget)) {
            calculatePixelBrightness(target);
          }
        }
      }
  
      if (mode === 'targets' && !found) {
        throw checkTarget + ' is not a target';
      }
  
      kill(start);
    }
  
  
    /*
     * Find the element's zIndex. Also checks
     * the zIndex of its parent
     */
    function getZIndex(el) {
      var calculate = function (el) {
        var zindex = 0;
  
        if (window.getComputedStyle(el).position !== 'static') {
          zindex = parseInt(window.getComputedStyle(el).zIndex, 10) || 0;
  
          // Reserve zindex = 0 for elements with position: static;
          if (zindex >= 0) {
            zindex++;
          }
        }
  
        return zindex;
      };
  
      var parent = el.parentNode;
      var zIndexParent = parent ? calculate(parent) : 0;
      var zIndexEl = calculate(el);
  
      return (zIndexParent * 100000) + zIndexEl;
    }
  
  
    /*
     * Check zIndexes
     */
    function sortImagesByZIndex(images) {
      var sorted = false;
  
      images.sort(function (a, b) {
        a = a.nodeType ? a : a.el;
        b = b.nodeType ? b : b.el;
  
        var pos = a.compareDocumentPosition(b);
        var reverse = 0;
  
        a = getZIndex(a);
        b = getZIndex(b);
  
        if (a > b) {
          sorted = true;
        }
  
        // Reposition if zIndex is the same but the elements are not
        // sorted according to their document position
        if (a === b && pos === 2) {
          reverse = 1;
        } else if (a === b && pos === 4) {
          reverse = -1;
        }
  
        return reverse || a - b;
      });
  
      log('Sorted: ' + sorted);
  
      if (sorted) {
        log(images);
      }
  
      return sorted;
    }
  
  
    /*
     * Main function
     */
    function check(target, avoidClear, imageLoaded) {
  
      if (supported) {
        var mask = get('mask');
  
        log('--- BackgroundCheck ---');
        log('onLoad event: ' + (imageLoaded && imageLoaded.src));
  
        if (avoidClear !== true) {
          context.clearRect(0, 0, canvas.width, canvas.height);
          context.fillStyle = 'rgb(' + mask.r + ', ' + mask.g + ', ' + mask.b + ')';
          context.fillRect(0, 0, canvas.width, canvas.height);
        }
  
        var processImages = imageLoaded ? [imageLoaded] : get('images');
        var sorted = sortImagesByZIndex(processImages);
  
        var image;
        var imageNode;
        var loading = false;
  
        for (var i = 0; i < processImages.length; i++) {
          image = processImages[i];
  
          if (isInside(image, viewport)) {
            imageNode = image.nodeType ? image : image.img;
  
            if (imageNode.naturalWidth === 0) {
              loading = true;
              log('Loading... ' + image.src);
  
              imageNode.removeEventListener('load', check);
  
              if (sorted) {
                // Sorted -- redraw all images
                imageNode.addEventListener('load', check.bind(null, null, false, null));
              } else {
                // Not sorted -- just draw one image
                imageNode.addEventListener('load', check.bind(null, target, true, image));
              }
            } else {
              log('Drawing: ' + image.src);
              drawImage(image);
            }
          }
        }
  
        if (!imageLoaded && !loading) {
          processTargets(target);
        } else if (imageLoaded) {
          processTargets(imageLoaded);
        }
      }
    }
  
  
    /*
     * Throttle events
     */
    function throttle(callback) {
  
      if (get('windowEvents') === true) {
  
        if (throttleDelay) {
          clearTimeout(throttleDelay);
        }
  
        throttleDelay = setTimeout(callback, 200);
      }
    }
  
  
    /*
     * Setter
     */
    function set(property, newValue) {
  
      if (attrs[property] === undefined) {
        throw 'Unknown property - ' + property;
      } else if (newValue === undefined) {
        throw 'Missing value for ' + property;
      }
  
      if (property === 'targets' || property === 'images') {
  
        try {
          newValue = getElements(property === 'images' && !newValue ? 'img' : newValue, property === 'images' ? true : false);
        } catch (err) {
          newValue = [];
          throw err;
        }
      } else {
        checkType(newValue, typeof attrs[property]);
      }
  
      removeClasses();
      attrs[property] = newValue;
      check();
  
      if (property === 'debugOverlay') {
        showDebugOverlay();
      }
    }
  
  
    /*
     * Getter
     */
    function get(property) {
  
      if (attrs[property] === undefined) {
        throw 'Unknown property - ' + property;
      }
  
      return attrs[property];
    }
  
  
    /*
     * Get position and size of all images.
     * Used for testing purposes
     */
    function getImageData() {
      var images = get('images');
      var area;
      var data = [];
  
      for (var i = 0; i < images.length; i++) {
        area = getArea(images[i]);
        data.push(area);
      }
  
      return data;
    }
  
  
    return {
      /*
       * Init and destroy
       */
      init: init,
      destroy: destroy,
  
      /*
       * Expose main function
       */
      refresh: check,
  
      /*
       * Setters and getters
       */
      set: set,
      get: get,
  
      /*
       * Return image data
       */
      getImageData: getImageData
    };
  
  }));