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) {
          var
            x = 0,
            y = 0;
          while (elem && !isNaN(elem.offsetLeft) && !isNaN(elem.offsetTop)) {
            x += elem.offsetLeft;
            y += elem.offsetTop;
            elem = elem.offsetParent;
          }
          return {
            top: y,
            left: x
          };
        },
        // 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};
        },
  
        HueToRGB: function (p, q, h) {
          if (h < 0)
            h += 1;
          else if (h > 1)
            h -= 1;
  
          if ((h * 6) < 1)
            return p + (q - p) * h * 6;
          else if ((h * 2) < 1)
            return q;
          else if ((h * 3) < 2)
            return p + (q - p) * ((2 / 3) - h) * 6;
          else
            return p;
        },
  
        //parse a string to HSB
        setColor: function (val) {
          val = val.toLowerCase();
          for (var key in Helper.stringParsers) {
            var parser = Helper.stringParsers[key];
            var match = parser.re.exec(val),
              values = match && parser.parse(match),
              space = parser.space || 'rgba';
            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);
          slider.knob = target.children[0].style;
          slider.left = event.pageX - Helper.getOffset(target).left;
          slider.top = event.pageY - Helper.getOffset(target).top;
  
          if (fixedPosition) {
            slider.left -= window.pageXOffset;
            slider.top -= window.pageYOffset;
          }
          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', function ($document, $compile, Color, Slider, Helper) {
      return {
        require: '?ngModel',
        restrict: 'A',
        link: function ($scope, elem, attrs, ngModel) {
          var
            template =
                '<div class="colorpicker f-dropdown">' +
                  '<colorpicker-saturation><i></i></colorpicker-saturation>' +
                  '<colorpicker-hue><i></i></colorpicker-hue>' +
                  '<colorpicker-alpha><i></i></colorpicker-alpha>' +
                  '<colorpicker-preview></colorpicker-preview>' +
                  '<button class="tiny secondary button right">&times;</button>' +
                '</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'),
            thisFormat = attrs.colorpicker ? attrs.colorpicker : 'hex',
            fixedPosition = angular.isDefined(attrs.colorpickerFixedPosition) ? attrs.colorpickerFixedPosition : false,
            target = angular.isDefined(attrs.colorpickerParent) ? elem.parent() : angular.element(document.body);
  
          $compile(colorpickerTemplate)($scope);
  
          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');
          }
  
          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));
            }
            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]);
  
              positionValue = {
                'top': positionOffset.top + elem[0].offsetHeight,
                'left': positionOffset.left
              };
  
            return {
              'top': positionValue.top + 'px',
              'left': positionValue.left + 'px'
            };
          };
  
          elem.on('click', function () {
            update();
            colorpickerTemplate
              .addClass('colorpicker-visible')
              .css(getColorpickerTemplatePosition());
          });
  
          colorpickerTemplate.on('mousedown', function (event) {
            event.stopPropagation();
            event.preventDefault();
          });
  
          var hideColorpickerTemplate = function() {
            if (colorpickerTemplate.hasClass('colorpicker-visible')) {
              colorpickerTemplate.removeClass('colorpicker-visible');
            }
          };
  
          colorpickerTemplate.find('button').on('click', function () {
            hideColorpickerTemplate();
          });
  
          $document.on('mousedown', function () {
            hideColorpickerTemplate();
          });
        }
      };
    }]);