// circles
// copyright Artan Sinani
// https://github.com/lugolabs/circles

/*
  Lightwheight JavaScript library that generates circular graphs in SVG.

  Call Circles.create(options) with the following options:

    id         - the DOM element that will hold the graph
    radius     - the radius of the circles
    width      - the width of the ring (optional, has value 10, if not specified)
    value      - init value of the circle (optional, defaults to 0)
    maxValue   - maximum value of the circle (optional, defaults to 100)
    text       - the text to display at the centre of the graph (optional, the current "htmlified" value will be shown if not specified)
                 if `null` or an empty string, no text will be displayed
                 can also be a function: the returned value will be the displayed text
                     ex1. function(currentValue) {
                              return '$'+currentValue;
                          }
                     ex2.  function() {
                               return this.getPercent() + '%';
                           }
    colors     - an array of colors, with the first item coloring the full circle
                 (optional, it will be `['#EEE', '#F00']` if not specified)
    duration   - value in ms of animation duration; (optional, defaults to 500);
                 if 0 or `null` is passed, the animation will not run
    wrpClass     - class name to apply on the generated element wrapping the whole circle.
    textClass:   - class name to apply on the generated element wrapping the text content.

    API:
      updateRadius(radius) - regenerates the circle with the given radius (see spec/responsive.html for an example hot to create a responsive circle)
      updateWidth(width) - regenerates the circle with the given stroke width
      updateColors(colors) - change colors used to draw the circle
      update(value, duration) - update value of circle. If value is set to true, force the update of displaying
      getPercent() - returns the percentage value of the circle, based on its current value and its max value
      getValue() - returns the value of the circle
      getMaxValue() - returns the max value of the circle
       getValueFromPercent(percentage) - returns the corresponding value of the circle based on its max value and given percentage
       htmlifyNumber(number, integerPartClass, decimalPartClass) - returned HTML representation of given number with given classes names applied on tags

*/

(function() {
  "use strict";

  var requestAnimFrame = window.requestAnimationFrame       ||
                     window.webkitRequestAnimationFrame ||
                     window.mozRequestAnimationFrame    ||
                     window.oRequestAnimationFrame      ||
                     window.msRequestAnimationFrame     ||
                     function (callback) {
                       setTimeout(callback, 1000 / 60);
                     },

  Circles = window.Circles = function(options) {
    var elId = options.id;
    this._el = document.getElementById(elId);

    if (this._el === null) return;

    this._radius         = options.radius || 10;
    this._duration       = options.duration === undefined ? 500 : options.duration;

    this._value          = 0;
    this._maxValue       = options.maxValue || 100;

    this._text           = options.text === undefined ? function(value){return this.htmlifyNumber(value);} : options.text;
    this._strokeWidth    = options.width  || 10;
    this._colors         = options.colors || ['#EEE', '#F00'];
    this._svg            = null;
    this._movingPath     = null;
    this._wrapContainer  = null;
    this._textContainer  = null;

    this._wrpClass       = options.wrpClass || 'circles-wrp';
    this._textClass      = options.textClass || 'circles-text';

    this._valClass       = options.valueStrokeClass || 'circles-valueStroke';
    this._maxValClass    = options.maxValueStrokeClass || 'circles-maxValueStroke';

    this._styleWrapper   = options.styleWrapper === false ? false : true;
    this._styleText      = options.styleText === false ? false : true;

    var endAngleRad      = Math.PI / 180 * 270;
    this._start          = -Math.PI / 180 * 90;
    this._startPrecise   = this._precise(this._start);
    this._circ           = endAngleRad - this._start;

    this._generate().update(options.value || 0);
  };

  Circles.prototype = {
    VERSION: '0.0.6',

    _generate: function() {

      this._svgSize        = this._radius * 2;
      this._radiusAdjusted = this._radius - (this._strokeWidth / 2);

      this._generateSvg()._generateText()._generateWrapper();

      this._el.innerHTML = '';
      this._el.appendChild(this._wrapContainer);

      return this;
    },

    _setPercentage: function(percentage) {
      this._movingPath.setAttribute('d', this._calculatePath(percentage, true));
      this._textContainer.innerHTML	=	this._getText(this.getValueFromPercent(percentage));
    },

    _generateWrapper: function() {
      this._wrapContainer	=	document.createElement('div');
      this._wrapContainer.className = this._wrpClass;

      if (this._styleWrapper) {
        this._wrapContainer.style.position	=	'relative';
        this._wrapContainer.style.display	=	'inline-block';
      }

      this._wrapContainer.appendChild(this._svg);
      this._wrapContainer.appendChild(this._textContainer);

      return this;
    },

    _generateText: function() {

      this._textContainer = document.createElement('div');
      this._textContainer.className = this._textClass;

      if (this._styleText) {
        var style	=	{
          position:   'absolute',
          top:        0,
          left:       0,
          textAlign:  'center',
          width:      '100%',
          fontSize:   (this._radius * .7) + 'px',
          height:     this._svgSize + 'px',
          lineHeight: this._svgSize + 'px'
        };

        for(var prop in style) {
          this._textContainer.style[prop]	=	style[prop];
        }
      }

      this._textContainer.innerHTML	=	this._getText(0);
      return this;
    },

    _getText: function(value) {
      if (!this._text) return '';

      if (value === undefined) value = this._value;

      value = parseFloat(value.toFixed(2));

      return typeof this._text === 'function' ? this._text.call(this, value) : this._text;
    },

    _generateSvg: function() {

      this._svg = document.createElementNS('http://www.w3.org/2000/svg', 'svg');
      this._svg.setAttribute('xmlns', 'http://www.w3.org/2000/svg');
      this._svg.setAttribute('width', this._svgSize);
      this._svg.setAttribute('height', this._svgSize);

      this._generatePath(100, false, this._colors[0], this._maxValClass)._generatePath(1, true, this._colors[1], this._valClass);

      this._movingPath = this._svg.getElementsByTagName('path')[1];

      return this;
    },

    _generatePath: function(percentage, open, color, pathClass) {
      var path = document.createElementNS('http://www.w3.org/2000/svg', 'path');
      path.setAttribute('fill', 'transparent');
      path.setAttribute('stroke', color);
      path.setAttribute('stroke-width', this._strokeWidth);
      path.setAttribute('d',  this._calculatePath(percentage, open));
      path.setAttribute('class', pathClass);

      this._svg.appendChild(path);

      return this;
    },

    _calculatePath: function(percentage, open) {
      var end      = this._start + ((percentage / 100) * this._circ),
        endPrecise = this._precise(end);
      return this._arc(endPrecise, open);
    },

    _arc: function(end, open) {
      var endAdjusted = end - 0.001,
        longArc       = end - this._startPrecise < Math.PI ? 0 : 1;

      return [
        'M',
        this._radius + this._radiusAdjusted * Math.cos(this._startPrecise),
        this._radius + this._radiusAdjusted * Math.sin(this._startPrecise),
        'A', // arcTo
        this._radiusAdjusted, // x radius
        this._radiusAdjusted, // y radius
        0, // slanting
        longArc, // long or short arc
        1, // clockwise
        this._radius + this._radiusAdjusted * Math.cos(endAdjusted),
        this._radius + this._radiusAdjusted * Math.sin(endAdjusted),
        open ? '' : 'Z' // close
      ].join(' ');
    },

    _precise: function(value) {
      return Math.round(value * 1000) / 1000;
    },

    /*== Public methods ==*/

    htmlifyNumber: function(number, integerPartClass, decimalPartClass) {

      integerPartClass = integerPartClass || 'circles-integer';
      decimalPartClass = decimalPartClass || 'circles-decimals';

      var parts = (number + '').split('.'),
          html  = '<span class="' + integerPartClass + '">' + parts[0]+'</span>';

      if (parts.length > 1) {
              html += '.<span class="' + decimalPartClass + '">' + parts[1].substring(0, 2) + '</span>';
          }
      return html;
    },

    updateRadius: function(radius) {
      this._radius = radius;

      return this._generate().update(true);
    },

    updateWidth: function(width) {
      this._strokeWidth = width;

      return this._generate().update(true);
    },

    updateColors: function(colors) {
      this._colors = colors;

      var paths = this._svg.getElementsByTagName('path');

      paths[0].setAttribute('stroke', colors[0]);
      paths[1].setAttribute('stroke', colors[1]);

      return this;
    },

    getPercent: function() {
      return (this._value * 100) / this._maxValue;
    },

    getValueFromPercent: function(percentage) {
      return (this._maxValue * percentage) / 100;
    },

    getValue: function()
    {
      return this._value;
    },

    getMaxValue: function()
    {
      return this._maxValue;
    },

    update: function(value, duration) {
      if (value === true) {//Force update with current value
        this._setPercentage(this.getPercent());
        return this;
      }

      if (this._value == value || isNaN(value)) return this;
      if (duration === undefined) duration = this._duration;

      var self          = this,
          oldPercentage = self.getPercent(),
          delta         = 1,
          newPercentage, isGreater, steps, stepDuration;

      this._value = Math.min(this._maxValue, Math.max(0, value));

      if (!duration) {//No duration, we can't skip the animation
        this._setPercentage(this.getPercent());
        return this;
      }

      newPercentage   = self.getPercent();
      isGreater       = newPercentage > oldPercentage;

      delta           += newPercentage % 1; //If new percentage is not an integer, we add the decimal part to the delta
      steps           = Math.floor(Math.abs(newPercentage - oldPercentage) / delta);
      stepDuration    = duration / steps;


      (function animate(lastFrame) {
        if (isGreater)
         oldPercentage += delta;
        else
         oldPercentage -= delta;

        if ((isGreater && oldPercentage >= newPercentage) || (!isGreater && oldPercentage <= newPercentage))
        {
          requestAnimFrame(function(){ self._setPercentage(newPercentage); });
          return;
        }

        requestAnimFrame(function() { self._setPercentage(oldPercentage); });

        var now     = Date.now(),
          deltaTime = now - lastFrame;

        if (deltaTime >= stepDuration) {
          animate(now);
        } else {
          setTimeout(function() {
            animate(Date.now());
          }, stepDuration - deltaTime);
        }

      })(Date.now());

      return this;
    }
  };

  Circles.create = function(options) {
    return new Circles(options);
  };
})();
