// FINGERBLAST.js
// --------------
// Adapted from phantom limb by Brian Cartensen
// Emulate touch event for desktop browsers

/* jshint bitwise: false */
/* global GLOBAL: true */

(function () {

  'use strict';

  function FingerBlast (element) {
    this.element = typeof element === 'string' ? document.querySelector(element) : element;

    if (this.element) {
      this.listen();
    }
  }

  FingerBlast.prototype = {
    x: NaN,
    y: NaN,

    startDistance: NaN,
    startAngle:    NaN,

    mouseIsDown: false,

    listen: function () {
      var activate = this.activate.bind(this);
      var deactivate = this.deactivate.bind(this);

      function contains (element, ancestor) {
        var descendants;
        var index;
        var descendant;

        if (!element) {
          return;
        }

        if ('compareDocumentPosition' in ancestor) {
          return !!(ancestor.compareDocumentPosition(element) & 16);
        } else if ('contains' in ancestor) {
          return ancestor !== element && ancestor.contains(element);
        } else {
          for ((descendants = ancestor.getElementsByTagName('*')), index = 0; (descendant = descendants[index++]);) {
            if (descendant === element) {
              return true;
            }
          }
          return false;
        }
      }

      this.element.addEventListener('mouseover', function (e) {
        var target = e.relatedTarget;
        if (target !== this && !contains(target, this)) {
          activate();
        }
      });

      this.element.addEventListener('mouseout', function (e) {
        var target = e.relatedTarget;
        if (target !== this && !contains(target, this)) {
          deactivate(e);
        }
      });
    },

    activate: function () {
      if (this.active) {
        return;
      }
      this.element.addEventListener('mousedown', (this.touchStart = this.touchStart.bind(this)), true);
      this.element.addEventListener('mousemove', (this.touchMove  = this.touchMove.bind(this)),  true);
      this.element.addEventListener('mouseup',   (this.touchEnd   = this.touchEnd.bind(this)),   true);
      this.element.addEventListener('click',     (this.click      = this.click.bind(this)),      true);
      this.active = true;
    },

    deactivate: function (e) {
      this.active = false;
      if (this.mouseIsDown) {
        this.touchEnd(e);
      }
      this.element.removeEventListener('mousedown', this.touchStart, true);
      this.element.removeEventListener('mousemove', this.touchMove,  true);
      this.element.removeEventListener('mouseup',   this.touchEnd,   true);
      this.element.removeEventListener('click',     this.click,      true);
    },

    click: function (e) {
      if (e.synthetic) {
        return;
      }
      e.preventDefault();
      e.stopPropagation();
    },

    touchStart: function (e) {
      if (e.synthetic || /input|textarea/.test(e.target.tagName.toLowerCase())) {
        return;
      }

      this.mouseIsDown = true;

      e.preventDefault();
      e.stopPropagation();

      this.fireTouchEvents('touchstart', e);
    },

    touchMove: function (e) {
      if (e.synthetic) {
        return;
      }

      e.preventDefault();
      e.stopPropagation();

      this.move(e.clientX, e.clientY);

      if (this.mouseIsDown) {
        this.fireTouchEvents('touchmove', e);
      }
    },

    touchEnd: function (e) {
      if (e.synthetic) {
        return;
      }

      this.mouseIsDown = false;

      e.preventDefault();
      e.stopPropagation();

      this.fireTouchEvents('touchend', e);

      if (!this.target) {
        return;
      }

      // Mobile Safari moves all the mouse events to fire after the touchend event.
      this.target.dispatchEvent(this.createMouseEvent('mouseover', e));
      this.target.dispatchEvent(this.createMouseEvent('mousemove', e));
      this.target.dispatchEvent(this.createMouseEvent('mousedown', e));
    },

    fireTouchEvents: function (eventName, originalEvent) {
      var events   = [];
      var gestures = [];

      if (!this.target) {
        return;
      }

      // Convert 'ontouch*' properties and attributes to listeners.
      var onEventName = 'on' + eventName;

      if (onEventName in this.target) {
        console.warn('Converting `' + onEventName + '` property to event listener.', this.target);
        this.target.addEventListener(eventName, this.target[onEventName], false);
        delete this.target[onEventName];
      }

      if (this.target.hasAttribute(onEventName)) {
        console.warn('Converting `' + onEventName + '` attribute to event listener.', this.target);
        var handler = new GLOBAL.Function('event', this.target.getAttribute(onEventName));
        this.target.addEventListener(eventName, handler, false);
        this.target.removeAttribute(onEventName);
      }

      // Set up a new event with the coordinates of the finger.
      var touch = this.createMouseEvent(eventName, originalEvent);

      events.push(touch);

      // Figure out scale and rotation.
      if (events.length > 1) {
        var x = events[0].pageX - events[1].pageX;
        var y = events[0].pageY - events[1].pageY;

        var distance = Math.sqrt(Math.pow(x, 2) + Math.pow(y, 2));
        var angle = Math.atan2(x, y) * (180 / Math.PI);

        var gestureName = 'gesturechange';

        if (eventName === 'touchstart') {
          gestureName = 'gesturestart';
          this.startDistance = distance;
          this.startAngle = angle;
        }

        if (eventName === 'touchend') {
          gestureName = 'gestureend';
        }

        events.forEach(function (event) {
          var gesture = this.createMouseEvent.call(event._finger, gestureName, event);
          gestures.push(gesture);
        }.bind(this));

        events.concat(gestures).forEach(function (event) {
          event.scale = distance / this.startDistance;
          event.rotation = this.startAngle - angle;
        });
      }

      // Loop through the events array and fill in each touch array.
      events.forEach(function (touch) {
        touch.touches = events.filter(function (e) {
          return ~e.type.indexOf('touch') && e.type !== 'touchend';
        });

        touch.changedTouches = events.filter(function (e) {
          return ~e.type.indexOf('touch') && e._finger.target === touch._finger.target;
        });

        touch.targetTouches = touch.changedTouches.filter(function (e) {
          return ~e.type.indexOf('touch') && e.type !== 'touchend';
        });
      });

      // Then fire the events.
      events.concat(gestures).forEach(function (event, i) {
        event.identifier = i;
        event._finger.target.dispatchEvent(event);
      });
    },

    createMouseEvent: function (eventName, originalEvent) {
      var e = new MouseEvent(eventName, {
        view       : window,
        detail     : originalEvent.detail,
        bubbles    : true,
        cancelable : true,
        target     : this.target || originalEvent.relatedTarget,
        clientX    : this.x || originalEvent.clientX,
        clientY    : this.y || originalEvent.clientY,
        screenX    : this.x || originalEvent.screenX,
        screenY    : this.y || originalEvent.screenY,
        ctrlKey    : originalEvent.ctrlKey,
        shiftKey   : originalEvent.shiftKey,
        altKey     : originalEvent.altKey,
        metaKey    : originalEvent.metaKey,
        button     : originalEvent.button
      });

      e.synthetic = true;
      e._finger   = this;

      return e;
    },

    move: function (x, y) {
      if (isNaN(x) || isNaN(y)) {
        this.target = null;
      } else {
        this.x = x;
        this.y = y;

        if (!this.mouseIsDown) {
          this.target = document.elementFromPoint(x, y);
        }
      }
    }
  };

  window.FingerBlast = FingerBlast;

}());
