text: 'use strict';
  
  angular.module('colorpicker.module', [])
      .factory('Helper', function () {
        return {
          closestSlider: function (elem) {
            var matchesSelector = elem.matches || elem.webkitMatchesSelector || elem.mozMatchesSelector || elem.msMatchesSelector;
            if (matchesSelector.bind(elem)('I')) {
              return elem.parentNode;
            }
            return elem;
          },
          getOffset: function (elem, fixedPosition) {
            var
                x = 0,
                y = 0,
                scrollX = 0,
                scrollY = 0;
            while (elem && !isNaN(elem.offsetLeft) && !isNaN(elem.offsetTop)) {
              x += elem.offsetLeft;
              y += elem.offsetTop;
              if (!fixedPosition && elem.tagName === 'BODY') {
                scrollX += document.documentElement.scrollLeft || elem.scrollLeft;
                scrollY += document.documentElement.scrollTop || elem.scrollTop;
              } else {
                scrollX += elem.scrollLeft;
                scrollY += elem.scrollTop;
              }
              elem = elem.offsetParent;
            }
            return {
              top: y,
              left: x,
              scrollX: scrollX,
              scrollY: scrollY
            };
          },
          // a set of RE's that can match strings and generate color tuples. https://github.com/jquery/jquery-color/
          stringParsers: [
            {
              re: /rgba?\(\s*(\d{1,3})\s*,\s*(\d{1,3})\s*,\s*(\d{1,3})\s*(?:,\s*(\d+(?:\.\d+)?)\s*)?\)/,
              parse: function (execResult) {
                return [
                  execResult[1],
                  execResult[2],
                  execResult[3],
                  execResult[4]
                ];
              }
            },
            {
              re: /rgba?\(\s*(\d+(?:\.\d+)?)\%\s*,\s*(\d+(?:\.\d+)?)\%\s*,\s*(\d+(?:\.\d+)?)\%\s*(?:,\s*(\d+(?:\.\d+)?)\s*)?\)/,
              parse: function (execResult) {
                return [
                  2.55 * execResult[1],
                  2.55 * execResult[2],
                  2.55 * execResult[3],
                  execResult[4]
                ];
              }
            },
            {
              re: /#([a-fA-F0-9]{2})([a-fA-F0-9]{2})([a-fA-F0-9]{2})/,
              parse: function (execResult) {
                return [
                  parseInt(execResult[1], 16),
                  parseInt(execResult[2], 16),
                  parseInt(execResult[3], 16)
                ];
              }
            },
            {
              re: /#([a-fA-F0-9])([a-fA-F0-9])([a-fA-F0-9])/,
              parse: function (execResult) {
                return [
                  parseInt(execResult[1] + execResult[1], 16),
                  parseInt(execResult[2] + execResult[2], 16),
                  parseInt(execResult[3] + execResult[3], 16)
                ];
              }
            }
          ]
        };
      })
      .factory('Color', ['Helper', function (Helper) {
        return {
          value: {
            h: 1,
            s: 1,
            b: 1,
            a: 1
          },
          // translate a format from Color object to a string
          'rgb': function () {
            var rgb = this.toRGB();
            return 'rgb(' + rgb.r + ',' + rgb.g + ',' + rgb.b + ')';
          },
          'rgba': function () {
            var rgb = this.toRGB();
            return 'rgba(' + rgb.r + ',' + rgb.g + ',' + rgb.b + ',' + rgb.a + ')';
          },
          'hex': function () {
            return  this.toHex();
          },
  
          // HSBtoRGB from RaphaelJS
          RGBtoHSB: function (r, g, b, a) {
            r /= 255;
            g /= 255;
            b /= 255;
  
            var H, S, V, C;
            V = Math.max(r, g, b);
            C = V - Math.min(r, g, b);
            H = (C === 0 ? null :
                V === r ? (g - b) / C :
                    V === g ? (b - r) / C + 2 :
                        (r - g) / C + 4
                );
            H = ((H + 360) % 6) * 60 / 360;
            S = C === 0 ? 0 : C / V;
            return {h: H || 1, s: S, b: V, a: a || 1};
          },
  
          //parse a string to HSB
          setColor: function (val) {
            val = val.toLowerCase();
            for (var key in Helper.stringParsers) {
              if (Helper.stringParsers.hasOwnProperty(key)) {
                var parser = Helper.stringParsers[key];
                var match = parser.re.exec(val),
                    values = match && parser.parse(match);
                if (values) {
                  this.value = this.RGBtoHSB.apply(null, values);
                  return false;
                }
              }
            }
          },
  
          setHue: function (h) {
            this.value.h = 1 - h;
          },
  
          setSaturation: function (s) {
            this.value.s = s;
          },
  
          setLightness: function (b) {
            this.value.b = 1 - b;
          },
  
          setAlpha: function (a) {
            this.value.a = parseInt((1 - a) * 100, 10) / 100;
          },
  
          // HSBtoRGB from RaphaelJS
          // https://github.com/DmitryBaranovskiy/raphael/
          toRGB: function (h, s, b, a) {
            if (!h) {
              h = this.value.h;
              s = this.value.s;
              b = this.value.b;
            }
            h *= 360;
            var R, G, B, X, C;
            h = (h % 360) / 60;
            C = b * s;
            X = C * (1 - Math.abs(h % 2 - 1));
            R = G = B = b - C;
  
            h = ~~h;
            R += [C, X, 0, 0, X, C][h];
            G += [X, C, C, X, 0, 0][h];
            B += [0, 0, X, C, C, X][h];
            return {
              r: Math.round(R * 255),
              g: Math.round(G * 255),
              b: Math.round(B * 255),
              a: a || this.value.a
            };
          },
  
          toHex: function (h, s, b, a) {
            var rgb = this.toRGB(h, s, b, a);
            return '#' + ((1 << 24) | (parseInt(rgb.r, 10) << 16) | (parseInt(rgb.g, 10) << 8) | parseInt(rgb.b, 10)).toString(16).substr(1);
          }
        };
      }])
      .factory('Slider', ['Helper', function (Helper) {
        var
            slider = {
              maxLeft: 0,
              maxTop: 0,
              callLeft: null,
              callTop: null,
              knob: {
                top: 0,
                left: 0
              }
            },
            pointer = {};
  
        return {
          getSlider: function() {
            return slider;
          },
          getLeftPosition: function(event) {
            return Math.max(0, Math.min(slider.maxLeft, slider.left + ((event.pageX || pointer.left) - pointer.left)));
          },
          getTopPosition: function(event) {
            return Math.max(0, Math.min(slider.maxTop, slider.top + ((event.pageY || pointer.top) - pointer.top)));
          },
          setSlider: function (event, fixedPosition) {
            var
              target = Helper.closestSlider(event.target),
              targetOffset = Helper.getOffset(target, fixedPosition);
            slider.knob = target.children[0].style;
            slider.left = event.pageX - targetOffset.left - window.pageXOffset + targetOffset.scrollX;
            slider.top = event.pageY - targetOffset.top - window.pageYOffset + targetOffset.scrollY;
  
            pointer = {
              left: event.pageX,
              top: event.pageY
            };
          },
          setSaturation: function(event, fixedPosition) {
            slider = {
              maxLeft: 100,
              maxTop: 100,
              callLeft: 'setSaturation',
              callTop: 'setLightness'
            };
            this.setSlider(event, fixedPosition);
          },
          setHue: function(event, fixedPosition) {
            slider = {
              maxLeft: 0,
              maxTop: 100,
              callLeft: false,
              callTop: 'setHue'
            };
            this.setSlider(event, fixedPosition);
          },
          setAlpha: function(event, fixedPosition) {
            slider = {
              maxLeft: 0,
              maxTop: 100,
              callLeft: false,
              callTop: 'setAlpha'
            };
            this.setSlider(event, fixedPosition);
          },
          setKnob: function(top, left) {
            slider.knob.top = top + 'px';
            slider.knob.left = left + 'px';
          }
        };
      }])
      .directive('colorpicker', ['$document', '$compile', 'Color', 'Slider', 'Helper', '$animate', function ($document, $compile, Color, Slider, Helper, $animate) {
        return {
          require: '?ngModel',
          restrict: 'A',
          link: function ($scope, elem, attrs, ngModel) {
            var
                thisFormat = attrs.colorpicker ? attrs.colorpicker : 'hex',
                position = angular.isDefined(attrs.colorpickerPosition) ? attrs.colorpickerPosition : 'bottom',
                fixedPosition = angular.isDefined(attrs.colorpickerFixedPosition) ? attrs.colorpickerFixedPosition : false,
                target = angular.isDefined(attrs.colorpickerParent) ? elem.parent() : angular.element(document.body),
                withInput = angular.isDefined(attrs.colorpickerWithInput) ? attrs.colorpickerWithInput : false,
                inputTemplate = withInput ? '<input type="text" name="colorpicker-input">' : '',
                template =
                    '<div class="colorpicker dropdown">' +
                        '<div class="dropdown-menu">' +
                        '<colorpicker-saturation><i></i></colorpicker-saturation>' +
                        '<colorpicker-hue><i></i></colorpicker-hue>' +
                        '<colorpicker-alpha><i></i></colorpicker-alpha>' +
                        '<colorpicker-preview></colorpicker-preview>' +
                        inputTemplate +
                        '<button class="close close-colorpicker">&times;</button>' +
                        '</div>' +
                        '</div>',
                colorpickerTemplate = angular.element(template),
                pickerColor = Color,
                sliderAlpha,
                sliderHue = colorpickerTemplate.find('colorpicker-hue'),
                sliderSaturation = colorpickerTemplate.find('colorpicker-saturation'),
                colorpickerPreview = colorpickerTemplate.find('colorpicker-preview'),
                pickerColorPointers = colorpickerTemplate.find('i');
  
            $compile(colorpickerTemplate)($scope);
  
            if (withInput) {
              var pickerColorInput = colorpickerTemplate.find('input');
              pickerColorInput
                  .on('mousedown', function(event) {
                    event.stopPropagation();
                  })
                  .on('keyup', function(event) {
                    var newColor = this.value;
                    elem.val(newColor);
                    if(ngModel) {
                      $scope.$apply(ngModel.$setViewValue(newColor));
                    }
                    event.stopPropagation();
                    event.preventDefault();
                  });
              elem.on('keyup', function() {
                pickerColorInput.val(elem.val());
              });
            }
  
            var bindMouseEvents = function() {
              $document.on('mousemove', mousemove);
              $document.on('mouseup', mouseup);
            };
  
            if (thisFormat === 'rgba') {
              colorpickerTemplate.addClass('alpha');
              sliderAlpha = colorpickerTemplate.find('colorpicker-alpha');
              sliderAlpha
                  .on('click', function(event) {
                    Slider.setAlpha(event, fixedPosition);
                    mousemove(event);
                  })
                  .on('mousedown', function(event) {
                    Slider.setAlpha(event, fixedPosition);
                    bindMouseEvents();
                  });
            }
  
            sliderHue
                .on('click', function(event) {
                  Slider.setHue(event, fixedPosition);
                  mousemove(event);
                })
                .on('mousedown', function(event) {
                  Slider.setHue(event, fixedPosition);
                  bindMouseEvents();
                });
  
            sliderSaturation
                .on('click', function(event) {
                  Slider.setSaturation(event, fixedPosition);
                  mousemove(event);
                })
                .on('mousedown', function(event) {
                  Slider.setSaturation(event, fixedPosition);
                  bindMouseEvents();
                });
  
            if (fixedPosition) {
              colorpickerTemplate.addClass('colorpicker-fixed-position');
            }
  
            colorpickerTemplate.addClass('colorpicker-position-' + position);
  
            target.append(colorpickerTemplate);
  
            if(ngModel) {
              ngModel.$render = function () {
                elem.val(ngModel.$viewValue);
              };
              $scope.$watch(attrs.ngModel, function() {
                update();
              });
            }
  
            elem.on('$destroy', function() {
              colorpickerTemplate.remove();
            });
  
            var previewColor = function () {
              try {
                colorpickerPreview.css('backgroundColor', pickerColor[thisFormat]());
              } catch (e) {
                colorpickerPreview.css('backgroundColor', pickerColor.toHex());
              }
              sliderSaturation.css('backgroundColor', pickerColor.toHex(pickerColor.value.h, 1, 1, 1));
              if (thisFormat === 'rgba') {
                sliderAlpha.css.backgroundColor = pickerColor.toHex();
              }
            };
  
            var mousemove = function (event) {
              var
                  left = Slider.getLeftPosition(event),
                  top = Slider.getTopPosition(event),
                  slider = Slider.getSlider();
  
              Slider.setKnob(top, left);
  
              if (slider.callLeft) {
                pickerColor[slider.callLeft].call(pickerColor, left / 100);
              }
              if (slider.callTop) {
                pickerColor[slider.callTop].call(pickerColor, top / 100);
              }
              previewColor();
              var newColor = pickerColor[thisFormat]();
              elem.val(newColor);
              if(ngModel) {
                $scope.$apply(ngModel.$setViewValue(newColor));
              }
              if (withInput) {
                pickerColorInput.val(newColor);
              }
              return false;
            };
  
            var mouseup = function () {
              $document.off('mousemove', mousemove);
              $document.off('mouseup', mouseup);
            };
  
            var update = function () {
              pickerColor.setColor(elem.val());
              pickerColorPointers.eq(0).css({
                left: pickerColor.value.s * 100 + 'px',
                top: 100 - pickerColor.value.b * 100 + 'px'
              });
              pickerColorPointers.eq(1).css('top', 100 * (1 - pickerColor.value.h) + 'px');
              pickerColorPointers.eq(2).css('top', 100 * (1 - pickerColor.value.a) + 'px');
              previewColor();
            };
  
            var getColorpickerTemplatePosition = function() {
              var
                  positionValue,
                  positionOffset = Helper.getOffset(elem[0]);
  
              if(angular.isDefined(attrs.colorpickerParent)) {
                positionOffset.left = 0;
                positionOffset.top = 0;
              }
  
              if (position === 'top') {
                positionValue =  {
                  'top': positionOffset.top - 147,
                  'left': positionOffset.left
                };
              } else if (position === 'right') {
                positionValue = {
                  'top': positionOffset.top,
                  'left': positionOffset.left + 126
                };
              } else if (position === 'bottom') {
                positionValue = {
                  'top': positionOffset.top + elem[0].offsetHeight + 2,
                  'left': positionOffset.left
                };
              } else if (position === 'left') {
                positionValue = {
                  'top': positionOffset.top,
                  'left': positionOffset.left - 150
                };
              }
              return {
                'top': positionValue.top + 'px',
                'left': positionValue.left + 'px'
              };
            };
  
            var documentMousedownHandler = function() {
              hideColorpickerTemplate();
            };
  
            elem.on('click', function () {
              update();
              colorpickerTemplate
                  .css(getColorpickerTemplatePosition());
              $animate.addClass(colorpickerTemplate, 'colorpicker-visible');
  
              // register global mousedown event to hide the colorpicker
              $document.on('mousedown mousewheel', documentMousedownHandler);
            });
  
            colorpickerTemplate.on('mousedown', function (event) {
              event.stopPropagation();
              event.preventDefault();
            });
  
            var emitEvent = function(name) {
              if(ngModel) {
                $scope.$emit(name, {
                  name: attrs.ngModel,
                  value: ngModel.$modelValue
                });
              }
            };
  
            var hideColorpickerTemplate = function() {
              if (colorpickerTemplate.hasClass('colorpicker-visible')) {
                $animate.removeClass(colorpickerTemplate, 'colorpicker-visible');
                emitEvent('colorpicker-closed');
                // unregister the global mousedown event
                $document.off('mousedown mousewheel', documentMousedownHandler);
              }
            };
  
            colorpickerTemplate.find('button').on('click', function () {
              hideColorpickerTemplate();
            });
          }
        };
      }]);