/*  Spinners 1.2.2
 *  (c) 2010-2011 Nick Stakenburg - http://www.nickstakenburg.com
 *
 *  Spinners is freely distributable under the terms of an MIT-style license.
 *
 *  Works with your framework of choice using BridgeJS:
 *  http://www.github.com/staaky/bridgejs
 *
 *  Requires ExplorerCanvas to work in Internet Explorer:
 *  http://code.google.com/p/explorercanvas
 *
 *  GitHub: https://github.com/staaky/spinners
 */

var Spinners = {
  Version: '1.2.2'
};

(function($B) {
$B.Object.extend(Spinners, {
  spinners: [],

  Required: {
    Bridge: '1.0.4'
  },

  support: {
    canvas: !!document.createElement('canvas').getContext
  },

  insertScript: function(source) {
    try {
      document.write("<script type='text/javascript' src='" + source + "'><\/script>");
    } catch(e) {
      var head = document.head || Bridge.$$('head').source[0];
      head.appendChild(new Bridge.Element('script', {
          src: source,
          type: 'text/javascript'
      }));
    }
  },

  require: function(library, name) {
    if ((typeof window[library] == 'undefined') ||
      (this.convertVersionString(window[library].Version) <
       this.convertVersionString(this.Required[library])))
      alert('Spinners requires ' + (name || library) + ' >= ' + this.Required[library]);
  },

  convertVersionString: function(versionString) {
    var v = versionString.replace(/_.*|\./g, '');
    v = parseInt(v + Bridge.String.times('0', 4 - v.length));
    return versionString.indexOf('_') > -1 ? v - 1 : v;
  },

  start: function() {
    this.require('Bridge');

    // require excanvas
    if (!this.support.canvas && !window.G_vmlCanvasManager)
      alert('Spinners requires ExplorerCanvas (excanvas.js)');
  },

  get: function(element) {
    element = $B.$(element).source;
    if (!element) return;
    var matched = null;
    $B._each(this.spinners, function(spinner) {
      if (spinner.element == element)
        matched = spinner;
    });
    return matched;
  },

  add: function(spinner) {
    this.spinners.push(spinner);
  },

  remove: function(element) {
    var spinner = this.get(element);
    if (spinner) {
      spinner.remove();
      this.spinners = $B.Array.without(this.spinners, spinner);
    }
  },

  // remove all spinners that are not attached to the DOM
  removeDetached: (function() {
    function isAttached(node) {
      var topAncestor = findTopAncestor(node);
      return !!(topAncestor && topAncestor.body);
    }

    function findTopAncestor(node) {
      var ancestor = node;
      while(ancestor && ancestor.parentNode) {
        ancestor = ancestor.parentNode;
      }
      return ancestor;
    }

    return function() {
      $B.each(this.spinners, function(spinner) {
        if (spinner.element && !isAttached(spinner.element)) {
          this.remove(spinner.element);
        }
      }, this);
    }
  })()
});


/*
 * Math
 */
function pyth(a,b) {
  return Math.sqrt(a*a + b*b);
}

function degrees(radian) {
  return (radian*180) / Math.PI;
}

function radian(degrees) {
  return (degrees*Math.PI) / 180;
}

function scrollArray(array, distance) {
  if (!distance) return array;
  var first = array.slice(0, distance),
      last  = array.slice(distance, array.length);
  return last.concat(first);
}
  
/*
 * Helpers
 */
var Canvas = {
  drawRoundedRectangle: function(ctx) {
    var options = $B.Object.extend({
      top:    0,
      left:   0,
      width:  0,
      height: 0,
      radius: 0
    }, arguments[1] || {});

    var o      = options,
        left   = o.left,
        top    = o.top,
        width  = o.width,
        height = o.height,
        radius = o.radius;

    ctx.beginPath();

    ctx.moveTo(left + radius, top);
    ctx.arc(left + width - radius, top + radius, radius, radian(-90), radian(0), false);
    ctx.arc(left + width - radius, top + height - radius, radius, radian(0), radian(90), false);
    ctx.arc(left + radius, top + height - radius, radius, radian(90), radian(180), false);
    ctx.arc(left + radius, top + radius, radius, radian(-180), radian(-90), false);

    ctx.closePath();

    ctx.fill();
  }
};


var Color = (function() {
  var hexNumber = '0123456789abcdef',
      hexRegExp = new RegExp('[' + hexNumber + ']', 'g');
  
  function returnRGB(rgb) {
    var result = rgb;
    result.red = rgb[0];
    result.green = rgb[1];
    result.blue = rgb[2];
    return result;
  }

  function h2d(h) { return parseInt(h,16); }

  function hex2rgb(hex) {
    var rgb = [];

    if (hex.indexOf('#') == 0) hex = hex.substring(1);
    hex = hex.toLowerCase();

    if (hex.replace(hexRegExp, '') != '')
    return null;

    if (hex.length == 3) {
      rgb[0] = hex.charAt(0) + hex.charAt(0);
      rgb[1] = hex.charAt(1) + hex.charAt(1);
      rgb[2] = hex.charAt(2) + hex.charAt(2);
    } else {
      rgb[0] = hex.substring(0, 2);
      rgb[1] = hex.substring(2, 4);
      rgb[2] = hex.substring(4);
    }
    for(var i = 0; i < rgb.length; i++)
      rgb[i] = h2d(rgb[i]);

    return returnRGB(rgb);
  }

  function hex2rgba(hex, opacity) {
    var rgba = hex2rgb(hex);
    rgba[3] = opacity;
    rgba.opacity = opacity;
    return rgba;
  }

  function hex2fill(hex, opacity) {
    if ($B.Object.isUndefined(opacity)) opacity = 1;
    return "rgba(" + hex2rgba(hex, opacity).join() + ")";
  }

  var rgb2hex = (function() {
    function toPaddedString(string, length, radix) {
      string = (string).toString(radix || 10);
      return $B.String.times('0', length - string.length) + string;
    }

    return function(red, green, blue) {
      return '#' + toPaddedString(red, 2, 16) +
                   toPaddedString(green, 2, 16) +
                   toPaddedString(blue, 2, 16);
    };
  })();

  return {
    hex2rgb:  hex2rgb,
    hex2fill: hex2fill,
    rgb2hex:  rgb2hex
  };
})();


/*
 * Spinner
 */
function Spinner(element) {
  element = $B.$(element);
  if (!element) return;

  this.element = element.source;

  Spinners.remove(element);
  Spinners.removeDetached();

  this.options = $B.Object.extend({
    radii: [5, 10],
    color: '#000',
    dashWidth: 1.8,
    dashes:  12,
    opacity: 1,
    padding: 3,
    speed:   .7,
    build:   true
  }, arguments[1]);

  this._position = 0;
  this._state = 'stopped';

  // IE VML needs the element build inside an element attached
  // to the DOM, this allows you to delay the build and do it manually
  if (this.options.build) this.build();

  Spinners.add(this);
}

$B.Object.extend(Spinner.prototype, (function() {
  function remove() {
    if (!this.canvas) return;

    this.stop();

    this.canvas.remove();

    this.canvas = null;
    this.ctx = null;
  }

  function build() {
    this.remove();

    var layout            = this.getLayout(),
        workspaceRadius   = layout.workspace.radius,
        workspaceDiameter = workspaceRadius * 2;

    $B.$(this.element).insert(
        this.canvas = new $B.Element('canvas', {
          height: workspaceDiameter,
          width:  workspaceDiameter
        }).setStyle({ zoom: 1 })
    );

    // init canvas
    if (window.G_vmlCanvasManager)
      G_vmlCanvasManager.initElement(this.canvas.source);

    this.ctx = this.canvas.source.getContext('2d');
    this.ctx.globalAlpha = this.options.opacity;

    this.ctx.translate(workspaceRadius, workspaceRadius);
    this.drawPosition(0);

    return this;
  }

  /*
   * Draw
   */
  function drawPosition(position) {
    var workspace          = this.getLayout().workspace,
        workspaceDiameter  = workspace.radius * 2,
        workspaceNegRadius = -1 * workspace.radius,
        dashes             = this.options.dashes;

    this.ctx.clearRect(workspaceNegRadius, workspaceNegRadius, workspaceDiameter, workspaceDiameter);

    var rotation  = radian(360 / dashes),
        opacities = scrollArray(workspace.opacities, position * -1);

    for (var i = 0, len = dashes; i < len; i++) {
      this.drawDash(opacities[i], this.options.color);
      this.ctx.rotate(rotation);
    }
  }

  function drawDash(opacity, color) {
    this.ctx.fillStyle = Color.hex2fill(color, opacity);

    var layout          = this.getLayout(),
        workspaceRadius = layout.workspace.radius,
        dashPosition    = layout.dash.position,
        dashDimensions  = layout.dash.dimensions;

    Canvas.drawRoundedRectangle(this.ctx, {
      top:    dashPosition.top - workspaceRadius,
      left:   dashPosition.left - workspaceRadius,
      width:  dashDimensions.width,
      height: dashDimensions.height,
      radius: Math.min(dashDimensions.height, dashDimensions.width) / 2
    });
  }

  /*
   * Position
   */
  function _nextPosition() {
    var ms = this.options.speed * 1000 / this.options.dashes;
    this.nextPosition();
    this._playTimer = window.setTimeout($B.Function.bind(_nextPosition, this), ms);
  }

  function nextPosition() {
    if (this._position == this.options.dashes - 1)
      this._position = -1;
    this._position++;
    this.drawPosition(this._position);
  }

  /*
   * Controls
   * play, pause, stop
   */
  function play() {
    if (this._state == 'playing') return;

    this._state = 'playing';

    var ms = this.options.speed * 1000 / this.options.dashes;
    this._playTimer = window.setTimeout($B.Function.bind(_nextPosition, this), ms);
    return this;
  }

  function pause() {
    if (this._state == 'paused') return;

    this._pause();

    this._state = 'paused';
    return this;
  }

  function _pause() {
    if (!this._playTimer) return;
    window.clearTimeout(this._playTimer);
    this._playTimer = null;
  }

  function stop() {
    if (this._state == 'stopped') return;

    this._pause();

    this._position = 0;
    this.drawPosition(0);

    this._state = 'stopped';
    return this;
  }

  function toggle() {
    this[this._state == 'playing' ? 'pause' : 'play']();
    return this;
  }

  /*
   * Layout
   */
  function getOpacityArray(dashes) {
    var step  = 1 / dashes, array = [];
    for (var i = 0;i<dashes;i++)
      array.push((i + 1) * step);
    return array;
  }

  function getLayout() {
    if (this._layout) return this._layout;

    var options   = this.options,
        dashes    = options.dashes,
        radii     = options.radii,
        dashWidth = options.dashWidth,
        minRadius = Math.min(radii[0], radii[1]),
        maxRadius = Math.max(radii[0], radii[1]),

        maxWorkspaceRadius = Math.max(dashWidth, maxRadius),
        workspaceRadius = Math.ceil(Math.max(
          maxWorkspaceRadius,
          // the hook created by dashWidth
          // could give a bigger radius
          pyth(maxRadius, dashWidth/2)
        ));

    workspaceRadius += options.padding;

    var layout = {
      workspace: {
        radius: workspaceRadius,
        opacities: getOpacityArray(dashes)
      },
      dash: {
        position: {
          top:  workspaceRadius - maxRadius,
          left: workspaceRadius - dashWidth / 2
        },
        dimensions: {
          width: dashWidth,
          height: maxRadius - minRadius
        }
      }
    };

    // cache
    this._layout = layout;

    return layout;
  } 

  return {
    remove:        remove,
    build:         build,
    getLayout:     getLayout,
    _nextPosition: _nextPosition,
    nextPosition:  nextPosition,
    drawPosition:  drawPosition,
    drawDash:      drawDash,
    play:          play,
    pause:         pause,
    _pause:        _pause,
    stop:          stop,
    toggle:        toggle
  };
})());

// expose
window.Spinner = Spinner;

Spinners.start();
})(Bridge);
