(function() {
  var supportsCanvas = document.createElement('canvas');
  supportsCanvas = !!(supportsCanvas.getContext && supportsCanvas.getContext('2d'));

  // helper functions
  function is_touch_device() {
    return 'ontouchstart' in window || // works on most browsers
           'onmsgesturechange' in window; // works on ie10
  }

  function fill(value, target, container) {
    if (value + target < container)
      value = container - target;
    return value > 0 ? 0 : value;
  }

  function uri2blob(dataURI) {
      var uriComponents = dataURI.split(',');
      var byteString = atob(uriComponents[1]);
      var mimeString = uriComponents[0].split(':')[1].split(';')[0];
      var ab = new ArrayBuffer(byteString.length);
      var ia = new Uint8Array(ab);
      for (var i = 0; i < byteString.length; i++)
          ia[i] = byteString.charCodeAt(i);
      return new Blob([ab], { type: mimeString });
  }

  var pluginName = 'cropbox';

  function factory($) {
    function Crop($image, options, on_load) {
      this.width = null;
      this.height = null;
      this.img_width = null;
      this.img_height = null;
      this.img_left = 0;
      this.img_top = 0;
      this.minPercent = null;
      this.options = options;
      this.$image = $image;
      this.$image.hide().prop('draggable', false).addClass('cropImage').wrap('<div class="cropFrame" />'); // wrap image in frame;
      this.$frame = this.$image.parent();
	  this.on_load = on_load || function() {};
      this.init();
    }

    Crop.prototype = {
      init: function () {
        var self = this;

        var defaultControls = $('<div/>', { 'class' : 'cropControls' })
              .append($('<span>'+this.options.label+'</span>'))
              .append($('<button/>', { 'class' : 'cropZoomIn', 'type':'button' }).on('click', $.proxy(this.zoomIn, this)))
              .append($('<button/>', { 'class' : 'cropZoomOut', 'type':'button' }).on('click', $.proxy(this.zoomOut, this)));

        this.$frame.append(this.options.controls || defaultControls);
        this.updateOptions();

        if (typeof $.fn.hammer === 'function' || typeof Hammer !== 'undefined') {
          var hammerit, dragData;
          if (typeof $.fn.hammer === 'function')
            hammerit = this.$image.hammer().data('hammer'); // Get the hammer instance after it has been created.
          else
            hammerit = Hammer(this.$image.get(0));
		  // Enable panning in all directions without any threshold.
		  hammerit.get('pan').set({ direction: Hammer.DIRECTION_ALL, threshold: 0 });
		  // Enable pinching.
		  hammerit.get('pinch').set({ enable: true });
          hammerit.on('panleft panright panup pandown', function(e) {
            if (!dragData)
              dragData = {
                startX: self.img_left,
                startY: self.img_top	// Some IE versions complained about the extra comma.
              };
            dragData.dx = e.deltaX;
            dragData.dy = e.deltaY;
            e.preventDefault();
            self.drag.call(self, dragData, true);
          }).on('panend pancancel', function(e) {
            e.preventDefault();
            dragData = null;
            self.update.call(self);
          }).on('doubletap', function(e) {
            e.preventDefault();
            self.zoomIn.call(self);
          }).on('pinchin', function (e) {
            e.preventDefault();
            self.zoomOut.call(self);
          }).on('pinchout', function (e) {
            e.preventDefault();
            self.zoomIn.call(self);
          });
        } else {
          // prevent IE8's default drag functionality
          this.$image.on("dragstart", function () { return false; });
          this.$image.on('mousedown.' + pluginName, function(e1) {
            var dragData = {
              startX: self.img_left,
              startY: self.img_top
            };
            e1.preventDefault();
            $(document).on('mousemove.' + pluginName, function (e2) {
              dragData.dx = e2.pageX - e1.pageX;
              dragData.dy = e2.pageY - e1.pageY;
              self.drag.call(self, dragData, true);
            }).on('mouseup.' + pluginName, function() {
              self.update.call(self);
              $(document).off('mouseup.' + pluginName);
              $(document).off('mousemove.' + pluginName);
            });
          });
        }
        if ($.fn.mousewheel) {
          this.$image.on('mousewheel.' + pluginName, function (e) {
            e.preventDefault();
            if (e.deltaY < 0)
              self.zoomIn.call(self);
            else
              self.zoomOut.call(self);
          });
        }
      },

      updateOptions: function () {
        var self = this;
        self.img_top = 0;
        self.img_left = 0;
        self.$image.css({width: '', left: self.img_left, top: self.img_top});
        self.$frame.width(self.options.width).height(self.options.height);
        self.$frame.off('.' + pluginName);
        self.$frame.removeClass('hover');
        if (self.options.showControls === 'always' || self.options.showControls === 'auto' && is_touch_device())
          self.$frame.addClass('hover');
        else if (self.options.showControls !== 'never') {
          self.$frame.on('mouseenter.' + pluginName, function () { self.$frame.addClass('hover'); });
          self.$frame.on('mouseleave.' + pluginName, function () { self.$frame.removeClass('hover'); });
        }

        // Image hack to get width and height on IE
        var img = new Image();
        img.onload = function () {
          self.width = img.width;
          self.height = img.height;
          img.src = '';
          img.onload = null;
          self.percent = undefined;
          self.fit.call(self);
          if (self.options.result)
            self.setCrop.call(self, self.options.result);
          else
            self.zoom.call(self, self.minPercent);
          self.$image.fadeIn('fast');
		  self.on_load.call(self);
        };
        // onload has to be set before src for IE8
        // otherwise it never fires
        img.src = self.$image.attr('src');
      },

      remove: function () {
        var hammerit;
        if (typeof $.fn.hammer === 'function')
          hammerit = this.$image.data('hammer');	// Get hammer instance object.
        else if (typeof Hammer !== 'undefined')
          hammerit = Hammer(this.$image.get(0));
        if (hammerit)
          hammerit.off('panleft panright panup pandown panend pancancel doubletap pinchin pinchout');
        this.$frame.off('.' + pluginName);
        this.$image.off('.' + pluginName);
        this.$image.css({width: '', left: '', top: ''});
        this.$image.removeClass('cropImage');
        this.$image.removeData(pluginName);
        this.$image.insertAfter(this.$frame);
        this.$frame.removeClass('cropFrame');
        this.$frame.removeAttr('style');
        this.$frame.empty();
        this.$frame.remove();
      },

      fit: function () {
        var widthRatio = this.options.width / this.width,
          heightRatio = this.options.height / this.height;
        this.minPercent = (widthRatio >= heightRatio) ? widthRatio : heightRatio;
      },

      setCrop: function (result) {
        this.percent = Math.max(this.options.width/result.cropW, this.options.height/result.cropH);
        this.img_width = Math.ceil(this.width*this.percent);
        this.img_height = Math.ceil(this.height*this.percent);
        this.img_left = -Math.floor(result.cropX*this.percent);
        this.img_top = -Math.floor(result.cropY*this.percent);
        this.$image.css({ width: this.img_width, left: this.img_left, top: this.img_top });
        this.update();
      },

      zoom: function(percent) {
        var old_percent = this.percent;

        this.percent = Math.max(this.minPercent, Math.min(this.options.maxZoom, percent));
        this.img_width = Math.ceil(this.width * this.percent);
        this.img_height = Math.ceil(this.height * this.percent);

        if (old_percent) {
          var zoomFactor = this.percent / old_percent;
          this.img_left = fill((1 - zoomFactor) * this.options.width / 2 + zoomFactor * this.img_left, this.img_width, this.options.width);
          this.img_top = fill((1 - zoomFactor) * this.options.height / 2 + zoomFactor * this.img_top, this.img_height, this.options.height);
        } else {
          this.img_left = fill((this.options.width - this.img_width) / 2, this.img_width,  this.options.width);
          this.img_top = fill((this.options.height - this.img_height) / 2, this.img_height, this.options.height);
        }

        this.$image.css({ width: this.img_width, left: this.img_left, top: this.img_top });
        this.update();
      },
      zoomIn: function() {
        this.zoom(this.percent + (1 - this.minPercent) / (this.options.zoom - 1 || 1));
      },
      zoomOut: function() {
        this.zoom(this.percent - (1 - this.minPercent) / (this.options.zoom - 1 || 1));
      },
      drag: function(data, skipupdate) {
        this.img_left = fill(data.startX + data.dx, this.img_width, this.options.width);
        this.img_top = fill(data.startY + data.dy, this.img_height, this.options.height);
        this.$image.css({ left: this.img_left, top: this.img_top });
        if (!skipupdate)
          this.update();
      },
      update: function() {
        this.result = {
          cropX: -Math.ceil(this.img_left / this.percent),
          cropY: -Math.ceil(this.img_top / this.percent),
          cropW: Math.floor(this.options.width / this.percent),
          cropH: Math.floor(this.options.height / this.percent),
          stretch: this.minPercent > 1
        };

        this.$image.trigger(pluginName, [this.result, this]);
      },
      getDataURL: function () {
        if(!supportsCanvas) {
          // return an empty string for browsers that don't support canvas.
          // this allows it to fail gracefully.
          return false;
        }
        var canvas = document.createElement('canvas'), ctx = canvas.getContext('2d');
        canvas.width = this.options.width;
        canvas.height = this.options.height;
        ctx.drawImage(this.$image.get(0), this.result.cropX, this.result.cropY, this.result.cropW, this.result.cropH, 0, 0, this.options.width, this.options.height);
        return canvas.toDataURL();
      },
      getBlob: function () {
        return uri2blob(this.getDataURL());
      }
    };

    $.fn[pluginName] = function(options, on_load) {
      return this.each(function() {
        var $this = $(this), inst = $this.data(pluginName);
        if (!inst) {
          var opts = $.extend({}, $.fn[pluginName].defaultOptions, options);
          $this.data(pluginName, new Crop($this, opts, on_load));
        } else if (options) {
          $.extend(inst.options, options);
          inst.updateOptions();
        }
      });
    };

    $.fn[pluginName].defaultOptions = {
      width: 200,
      height: 200,
      zoom: 10,
      maxZoom: 1,
      controls: null,
      showControls: 'auto',
      label: 'Drag to crop'
    };
  }

  if (typeof require === "function" && typeof exports === "object" && typeof module === "object")
      factory(require("jquery"));
  else if (typeof define === "function" && define.amd)
      define(["jquery"], factory);
  else
      factory(window.jQuery || window.Zepto);

})();
