/*



  SoundManager 2 Demo: 360-degree / "donut player"

  ------------------------------------------------

  http://schillmania.com/projects/soundmanager2/



  An inline player with a circular UI.

  Based on the original SM2 inline player.

  Inspired by Apple's preview feature in the

  iTunes music store (iPhone), among others.



  Requires SoundManager 2 Javascript API.

  Also uses Bernie's Better Animation Class (BSD):

  http://www.berniecode.com/writing/animator.html



*/



/*jslint white: false, onevar: true, undef: true, nomen: false, eqeqeq: true, plusplus: false, bitwise: true, regexp: false, newcap: true, immed: true */

/*global document, window, soundManager, navigator */



var threeSixtyPlayer, // instance

    ThreeSixtyPlayer; // constructor



(function(window) {



function ThreeSixtyPlayer() {



  var self = this,

      pl = this,

      sm = soundManager, // soundManager instance

      uA = navigator.userAgent,

      isIE = (uA.match(/msie/i)),

      isOpera = (uA.match(/opera/i)),

      isSafari = (uA.match(/safari/i)),

      isChrome = (uA.match(/chrome/i)),

      isFirefox = (uA.match(/firefox/i)),

      isTouchDevice = (uA.match(/ipad|iphone/i)),

      hasRealCanvas = (typeof window.G_vmlCanvasManager === 'undefined' && typeof document.createElement('canvas').getContext('2d') !== 'undefined'),

      fullCircle = (isOpera||isChrome?359.9:360); // I dunno what Opera doesn't like about this. I'm probably doing it wrong.



  this.excludeClass = 'threesixty-exclude'; // CSS class for ignoring MP3 links

  this.links = [];

  this.sounds = [];

  this.soundsByURL = [];

  this.indexByURL = [];

  this.lastSound = null;

  this.lastTouchedSound = null;

  this.soundCount = 0;

  this.oUITemplate = null;

  this.oUIImageMap = null;

  this.vuMeter = null;

  this.callbackCount = 0;

  this.peakDataHistory = [];



  this.config = {

  playNext: false,
  autoPlay: false,
  allowMultiple: false,
  loadRingColor: '#EEB6FF',
  playRingColor: '#0D0210',
  backgroundRingColor: '#401C69',
  circleDiameter: 237,
  circleRadius: 118.5,
  animDuration: 500,
  animTransition: Animator.tx.bouncy,
  showHMSTime: true,

  useWaveformData: true,
  waveformDataColor: '#86188E',
  waveformDataDownsample: 3,
  waveformDataOutside: false,
  waveformDataConstrain: false,
  waveformDataLineRatio: 0.44,

  useEQData: true,
  eqDataColor: '#73189A',
  eqDataDownsample: 4,
  eqDataOutside: false,
  eqDataLineRatio: 0.48,

  usePeakData: true,
  peakDataColor: '#ff33ff',
  peakDataOutside: true,
  peakDataLineRatio: 0.5,

  useAmplifier: true

};



  this.css = {



    // CSS class names appended to link during various states

    sDefault: 'sm2_link', // default state

    sBuffering: 'sm2_buffering',

    sPlaying: 'sm2_playing',

    sPaused: 'sm2_paused'



  };



  this.addEventHandler = function(o,evtName,evtHandler) {

    return typeof(window.attachEvent)==='undefined'?o.addEventListener(evtName,evtHandler,false):o.attachEvent('on'+evtName,evtHandler);

  };



  this.removeEventHandler = function(o,evtName,evtHandler) {

    return typeof(window.attachEvent)==='undefined'?o.removeEventListener(evtName,evtHandler,false):o.detachEvent('on'+evtName,evtHandler);

  };



  this.hasClass = function(o,cStr) {

    return typeof(o.className)!=='undefined'?o.className.match(new RegExp('(\\s|^)'+cStr+'(\\s|$)')):false;

  };



  this.addClass = function(o,cStr) {



    if (!o || !cStr || self.hasClass(o,cStr)) {

      return false;

    }

    o.className = (o.className?o.className+' ':'')+cStr;



  };



  this.removeClass = function(o,cStr) {



    if (!o || !cStr || !self.hasClass(o,cStr)) {

      return false;

    }

    o.className = o.className.replace(new RegExp('( '+cStr+')|('+cStr+')','g'),'');



  };



  this.getElementsByClassName = function(className,tagNames,oParent) {



    var doc = (oParent||document),

        matches = [], i,j, nodes = [];

    if (typeof tagNames !== 'undefined' && typeof tagNames !== 'string') {

      for (i=tagNames.length; i--;) {

        if (!nodes || !nodes[tagNames[i]]) {

          nodes[tagNames[i]] = doc.getElementsByTagName(tagNames[i]);

        }

      }

    } else if (tagNames) {

      nodes = doc.getElementsByTagName(tagNames);

    } else {

      nodes = doc.all||doc.getElementsByTagName('*');

    }

    if (typeof(tagNames)!=='string') {

      for (i=tagNames.length; i--;) {

        for (j=nodes[tagNames[i]].length; j--;) {

          if (self.hasClass(nodes[tagNames[i]][j],className)) {

            matches.push(nodes[tagNames[i]][j]);

          }

        }

      }

    } else {

      for (i=0; i<nodes.length; i++) {

        if (self.hasClass(nodes[i],className)) {

          matches.push(nodes[i]);

        }

      }

    }

    return matches;



  };



  this.getParentByNodeName = function(oChild,sParentNodeName) {



    if (!oChild || !sParentNodeName) {

      return false;

    }

    sParentNodeName = sParentNodeName.toLowerCase();

    while (oChild.parentNode && sParentNodeName !== oChild.parentNode.nodeName.toLowerCase()) {

      oChild = oChild.parentNode;

    }

    return (oChild.parentNode && sParentNodeName === oChild.parentNode.nodeName.toLowerCase()?oChild.parentNode:null);



  };



  this.getParentByClassName = function(oChild,sParentClassName) {



    if (!oChild || !sParentClassName) {

      return false;

    }

    while (oChild.parentNode && !self.hasClass(oChild.parentNode,sParentClassName)) {

      oChild = oChild.parentNode;

    }

    return (oChild.parentNode && self.hasClass(oChild.parentNode,sParentClassName)?oChild.parentNode:null);



  };



  this.getSoundByURL = function(sURL) {

    return (typeof self.soundsByURL[sURL] !== 'undefined'?self.soundsByURL[sURL]:null);

  };



  this.isChildOfNode = function(o,sNodeName) {



    if (!o || !o.parentNode) {

      return false;

    }

    sNodeName = sNodeName.toLowerCase();

    do {

      o = o.parentNode;

    } while (o && o.parentNode && o.nodeName.toLowerCase() !== sNodeName);

    return (o && o.nodeName.toLowerCase() === sNodeName?o:null);



  };



  this.isChildOfClass = function(oChild,oClass) {



    if (!oChild || !oClass) {

      return false;

    }

    while (oChild.parentNode && !self.hasClass(oChild,oClass)) {

      oChild = self.findParent(oChild);

    }

    return (self.hasClass(oChild,oClass));



  };



  this.findParent = function(o) {



    if (!o || !o.parentNode) {

      return false;

    }

    o = o.parentNode;

    if (o.nodeType === 2) {

      while (o && o.parentNode && o.parentNode.nodeType === 2) {

        o = o.parentNode;

      }

    }

    return o;



  };



  this.getStyle = function(o,sProp) {



    // http://www.quirksmode.org/dom/getstyles.html

    try {

      if (o.currentStyle) {

        return o.currentStyle[sProp];

      } else if (window.getComputedStyle) {

        return document.defaultView.getComputedStyle(o,null).getPropertyValue(sProp);

      }

    } catch(e) {

      // oh well

    }

    return null;



  };



  this.findXY = function(obj) {



    var curleft = 0, curtop = 0;

    do {

      curleft += obj.offsetLeft;

      curtop += obj.offsetTop;

    } while (!!(obj = obj.offsetParent));

    return [curleft,curtop];



  };



  this.getMouseXY = function(e) {



    // http://www.quirksmode.org/js/events_properties.html

    e = e?e:window.event;

    if (isTouchDevice && e.touches) {

      e = e.touches[0];

    }

    if (e.pageX || e.pageY) {

      return [e.pageX,e.pageY];

    } else if (e.clientX || e.clientY) {

      return [e.clientX+self.getScrollLeft(),e.clientY+self.getScrollTop()];

    }



  };



  this.getScrollLeft = function() {

    return (document.body.scrollLeft+document.documentElement.scrollLeft);

  };



  this.getScrollTop = function() {

    return (document.body.scrollTop+document.documentElement.scrollTop);

  };



  this.events = {



    // handlers for sound events as they're started/stopped/played



    play: function() {

      pl.removeClass(this._360data.oUIBox,this._360data.className);

      this._360data.className = pl.css.sPlaying;

      pl.addClass(this._360data.oUIBox,this._360data.className);

      self.fanOut(this);

    },



    stop: function() {

      pl.removeClass(this._360data.oUIBox,this._360data.className);

      this._360data.className = '';

      self.fanIn(this);

    },



    pause: function() {

      pl.removeClass(this._360data.oUIBox,this._360data.className);

      this._360data.className = pl.css.sPaused;

      pl.addClass(this._360data.oUIBox,this._360data.className);

    },



    resume: function() {

      pl.removeClass(this._360data.oUIBox,this._360data.className);

      this._360data.className = pl.css.sPlaying;

      pl.addClass(this._360data.oUIBox,this._360data.className);      

    },



    finish: function() {

      var nextLink;

      pl.removeClass(this._360data.oUIBox,this._360data.className);

      this._360data.className = '';

      // self.clearCanvas(this._360data.oCanvas);

      this._360data.didFinish = true; // so fan draws full circle

      self.fanIn(this);

      if (pl.config.playNext) {

        nextLink = (pl.indexByURL[this._360data.oLink.href]+1);

        if (nextLink<pl.links.length) {

          pl.handleClick({'target':pl.links[nextLink]});

        }

      }

    },



    whileloading: function() {

      if (this.paused) {

        self.updatePlaying.apply(this);

      }

    },



    whileplaying: function() {

      self.updatePlaying.apply(this);

      this._360data.fps++;

    },



    bufferchange: function() {

      if (this.isBuffering) {

        pl.addClass(this._360data.oUIBox,pl.css.sBuffering);

      } else {

        pl.removeClass(this._360data.oUIBox,pl.css.sBuffering);

      }

    }



  };



  this.stopEvent = function(e) {



   if (typeof e !== 'undefined' && typeof e.preventDefault !== 'undefined') {

      e.preventDefault();

    } else if (typeof window.event !== 'undefined' && typeof window.event.returnValue !== 'undefined') {

      window.event.returnValue = false;

    }

    return false;



  };



  this.getTheDamnLink = (isIE)?function(e) {

    // I really didn't want to have to do this.

    return (e && e.target?e.target:window.event.srcElement);

  }:function(e) {

    return e.target;

  };



  this.handleClick = function(e) {



    // a sound link was clicked

    if (e.button > 1) {

      // only catch left-clicks

      return true;

    }



    var o = self.getTheDamnLink(e),

        sURL, soundURL, thisSound, oContainer, has_vis, diameter;



    if (o.nodeName.toLowerCase() !== 'a') {

      o = self.isChildOfNode(o,'a');

      if (!o) {

        return true;

      }

    }



    if (!self.isChildOfClass(o,'ui360')) {

      // not a link we're interested in

      return true;

    }



    sURL = o.getAttribute('href');



    if (!o.href || !sm.canPlayLink(o) || self.hasClass(o,self.excludeClass)) {

      return true; // pass-thru for non-MP3/non-links

    }



    sm._writeDebug('handleClick()');

    soundURL = (o.href);

    thisSound = self.getSoundByURL(soundURL);



    if (thisSound) {

      // already exists

      if (thisSound === self.lastSound) {

        // and was playing (or paused)

        thisSound.togglePause();

      } else {

        // different sound

        thisSound.togglePause(); // start playing current

        sm._writeDebug('sound different than last sound: '+self.lastSound.sID);

        if (!self.config.allowMultiple && self.lastSound) {

          self.stopSound(self.lastSound);

        }

      }

    } else {



      // append some dom shiz, make noise



      oContainer = o.parentNode;

      has_vis = (self.getElementsByClassName('ui360-vis','div',oContainer.parentNode).length);



      // create sound

      thisSound = sm.createSound({

       id:'ui360Sound'+(self.soundCount++),

       url:soundURL,

       onplay:self.events.play,

       onstop:self.events.stop,

       onpause:self.events.pause,

       onresume:self.events.resume,

       onfinish:self.events.finish,

       onbufferchange:self.events.bufferchange,

       whileloading:self.events.whileloading,

       whileplaying:self.events.whileplaying,

       useWaveformData:(has_vis && self.config.useWaveformData),

       useEQData:(has_vis && self.config.useEQData),

       usePeakData:(has_vis && self.config.usePeakData)

      });



      // tack on some custom data



      diameter = parseInt(self.getElementsByClassName('sm2-360ui','div',oContainer)[0].offsetWidth, 10);



      thisSound._360data = {

        oUI360: self.getParentByClassName(o,'ui360'), // the (whole) entire container

        oLink: o, // DOM node for reference within SM2 object event handlers

        className: self.css.sPlaying,

        oUIBox: self.getElementsByClassName('sm2-360ui','div',oContainer)[0],

        oCanvas: self.getElementsByClassName('sm2-canvas','canvas',oContainer)[0],

        oButton: self.getElementsByClassName('sm2-360btn','span',oContainer)[0],

        oTiming: self.getElementsByClassName('sm2-timing','div',oContainer)[0],

        oCover: self.getElementsByClassName('sm2-cover','div',oContainer)[0],

        circleDiameter: diameter,

        circleRadius: diameter/2,

        lastTime: null,

        didFinish: null,

        pauseCount:0,

        radius:0,

        fontSize: 1,

        fontSizeMax: self.config.fontSizeMax,

        scaleFont: (has_vis && self.config.scaleFont),

        showHMSTime: has_vis,

        amplifier: (has_vis && self.config.usePeakData?0.9:1), // TODO: x1 if not being used, else use dynamic "how much to amplify by" value

        radiusMax: diameter*0.175, // circle radius

        width:0,

        widthMax: diameter*0.4, // width of the outer ring

        lastValues: {

          bytesLoaded: 0,

          bytesTotal: 0,

          position: 0,

          durationEstimate: 0

        }, // used to track "last good known" values before sound finish/reset for anim

        animating: false,

        oAnim: new window.Animator({

          duration: self.config.animDuration,

          transition:self.config.animTransition,

          onComplete: function() {

            // var thisSound = this;

            // thisSound._360data.didFinish = false; // reset full circle

          }

        }),

        oAnimProgress: function(nProgress) {

          var thisSound = this;

          thisSound._360data.radius = parseInt(thisSound._360data.radiusMax*thisSound._360data.amplifier*nProgress, 10);

          thisSound._360data.width = parseInt(thisSound._360data.widthMax*thisSound._360data.amplifier*nProgress, 10);

          if (thisSound._360data.scaleFont && thisSound._360data.fontSizeMax !== null) {

            thisSound._360data.oTiming.style.fontSize = parseInt(Math.max(1,thisSound._360data.fontSizeMax*nProgress), 10)+'px';

            thisSound._360data.oTiming.style.opacity = nProgress;

          }

          if (thisSound.paused || thisSound.playState === 0 || thisSound._360data.lastValues.bytesLoaded === 0 || thisSound._360data.lastValues.position === 0) {

            self.updatePlaying.apply(thisSound);

          }

        },

        fps: 0

      };



      // "Metadata" (annotations)

      if (typeof self.Metadata !== 'undefined' && self.getElementsByClassName('metadata','div',thisSound._360data.oUI360).length) {

        thisSound._360data.metadata = new self.Metadata(thisSound,self);

      }



      // minimize ze font

      if (thisSound._360data.scaleFont && thisSound._360data.fontSizeMax !== null) {

        thisSound._360data.oTiming.style.fontSize = '1px';

      }



      // set up ze animation

      thisSound._360data.oAnim.addSubject(thisSound._360data.oAnimProgress,thisSound);



      // animate the radius out nice

      self.refreshCoords(thisSound);



      self.updatePlaying.apply(thisSound);



      self.soundsByURL[soundURL] = thisSound;

      self.sounds.push(thisSound);

      if (!self.config.allowMultiple && self.lastSound) {

        self.stopSound(self.lastSound);

      }

      thisSound.play();



    }



    self.lastSound = thisSound; // reference for next call



    if (typeof e !== 'undefined' && typeof e.preventDefault !== 'undefined') {

      e.preventDefault();

    } else if (typeof window.event !== 'undefined') {

      window.event.returnValue = false;

    }

    return false;



  };



  this.fanOut = function(oSound) {



     var thisSound = oSound;

     if (thisSound._360data.animating === 1) {

       return false;

     }

     thisSound._360data.animating = 0;

     soundManager._writeDebug('fanOut: '+thisSound.sID+': '+thisSound._360data.oLink.href);

     thisSound._360data.oAnim.seekTo(1); // play to end

     window.setTimeout(function() {

       // oncomplete hack

       thisSound._360data.animating = 0;

     },self.config.animDuration+20);



  };



  this.fanIn = function(oSound) {



     var thisSound = oSound;

     if (thisSound._360data.animating === -1) {

       return false;

     }

     thisSound._360data.animating = -1;

     soundManager._writeDebug('fanIn: '+thisSound.sID+': '+thisSound._360data.oLink.href);

     // massive hack

     thisSound._360data.oAnim.seekTo(0); // play to end

     window.setTimeout(function() {

       // reset full 360 fill after animation has completed (oncomplete hack)

       thisSound._360data.didFinish = false;

       thisSound._360data.animating = 0;

       self.resetLastValues(thisSound);

     }, self.config.animDuration+20);



  };



  this.resetLastValues = function(oSound) {

    oSound._360data.lastValues.position = 0;

  };



  this.refreshCoords = function(thisSound) {



    thisSound._360data.canvasXY = self.findXY(thisSound._360data.oCanvas);

    thisSound._360data.canvasMid = [thisSound._360data.circleRadius,thisSound._360data.circleRadius];

    thisSound._360data.canvasMidXY = [thisSound._360data.canvasXY[0]+thisSound._360data.canvasMid[0], thisSound._360data.canvasXY[1]+thisSound._360data.canvasMid[1]];



  };



  this.stopSound = function(oSound) {



    soundManager._writeDebug('stopSound: '+oSound.sID);

    soundManager.stop(oSound.sID);

    soundManager.unload(oSound.sID);



  };



  this.buttonClick = function(e) {



    var o = e?(e.target?e.target:e.srcElement):window.event.srcElement;

    self.handleClick({target:self.getParentByClassName(o,'sm2-360ui').nextSibling}); // link next to the nodes we inserted

    return false;



  };



  this.buttonMouseDown = function(e) {



    // user might decide to drag from here

    // watch for mouse move

    if (!isTouchDevice) {

      document.onmousemove = function(e) {

        // should be boundary-checked, really (eg. move 3px first?)

        self.mouseDown(e);

      };

    } else {

      self.addEventHandler(document,'touchmove',self.mouseDown);

    }

    self.stopEvent(e);

    return false;



  };



  this.mouseDown = function(e) {



    if (!isTouchDevice && e.button > 1) {

      return true; // ignore non-left-click

    }



    if (!self.lastSound) {

      self.stopEvent(e);

      return false;

    }



    var evt = e?e:window.event,

        target, thisSound, oData;



    if (isTouchDevice && evt.touches) {

      evt = evt.touches[0];

    }

    target = (evt.target||evt.srcElement);



    thisSound = self.getSoundByURL(self.getElementsByClassName('sm2_link','a',self.getParentByClassName(target,'ui360'))[0].href); // self.lastSound; // TODO: In multiple sound case, figure out which sound is involved etc.

    // just in case, update coordinates (maybe the element moved since last time.)

    self.lastTouchedSound = thisSound;

    self.refreshCoords(thisSound);

    oData = thisSound._360data;

    self.addClass(oData.oUIBox,'sm2_dragging');

    oData.pauseCount = (self.lastTouchedSound.paused?1:0);

    // self.lastSound.pause();

    self.mmh(e?e:window.event);



    if (isTouchDevice) {

      self.removeEventHandler(document,'touchmove',self.mouseDown);

      self.addEventHandler(document,'touchmove',self.mmh);

      self.addEventHandler(document,'touchend',self.mouseUp);

    } else {

      // incredibly old-skool. TODO: Modernize.

      document.onmousemove = self.mmh;

      document.onmouseup = self.mouseUp;

    }



    self.stopEvent(e);

    return false;



  };



  this.mouseUp = function(e) {



    var oData = self.lastTouchedSound._360data;

    self.removeClass(oData.oUIBox,'sm2_dragging');

    if (oData.pauseCount === 0) {

      self.lastTouchedSound.resume();

    }

    if (!isTouchDevice) {

      document.onmousemove = null;

      document.onmouseup = null;

    } else {

      self.removeEventHandler(document,'touchmove',self.mmh);

      self.removeEventHandler(document,'touchend',self.mouseUP);

    }



  };



  this.mmh = function(e) {



    if (typeof e === 'undefined') {

      e = window.event;

    }

    var oSound = self.lastTouchedSound,

        coords = self.getMouseXY(e),

        x = coords[0],

        y = coords[1],

        deltaX = x-oSound._360data.canvasMidXY[0],

        deltaY = y-oSound._360data.canvasMidXY[1],

        angle = Math.floor(fullCircle-(self.rad2deg(Math.atan2(deltaX,deltaY))+180));



    oSound.setPosition(oSound.durationEstimate*(angle/fullCircle));

    self.stopEvent(e);

    return false;



  };



  // assignMouseDown();



  this.drawSolidArc = function(oCanvas, color, radius, width, radians, startAngle, noClear) {



    // thank you, http://www.snipersystems.co.nz/community/polarclock/tutorial.html



    var x = radius,

        y = radius,

        canvas = oCanvas,

        ctx, innerRadius, doesntLikeZero, endPoint;



    if (canvas.getContext){

      // use getContext to use the canvas for drawing

      ctx = canvas.getContext('2d');

    }



    // re-assign canvas as the actual context

    oCanvas = ctx;



    if (!noClear) {

      self.clearCanvas(canvas);

    }

    // ctx.restore();



    if (color) {

      ctx.fillStyle = color;

    }



    oCanvas.beginPath();



    if (isNaN(radians)) {

      radians = 0;

    }



    innerRadius = radius-width;

    doesntLikeZero = (isOpera || isSafari); // safari 4 doesn't actually seem to mind.



    if (!doesntLikeZero || (doesntLikeZero && radius > 0)) {

      oCanvas.arc(0, 0, radius, startAngle, radians, false);

      endPoint = self.getArcEndpointCoords(innerRadius, radians);

      oCanvas.lineTo(endPoint.x, endPoint.y);

      oCanvas.arc(0, 0, innerRadius, radians, startAngle, true);

      oCanvas.closePath();

      oCanvas.fill();

    }



  };



  this.getArcEndpointCoords = function(radius, radians) {



    return {

      x: radius * Math.cos(radians), 

      y: radius * Math.sin(radians)

    };



  };





  this.deg2rad = function(nDeg) {

    return (nDeg * Math.PI/180);

  };



  this.rad2deg = function(nRad) {

    return (nRad * 180/Math.PI);

  };



  this.getTime = function(nMSec,bAsString) {



    // convert milliseconds to mm:ss, return as object literal or string

    var nSec = Math.floor(nMSec/1000),

        min = Math.floor(nSec/60),

        sec = nSec-(min*60);

    // if (min === 0 && sec === 0) return null; // return 0:00 as null

    return (bAsString?(min+':'+(sec<10?'0'+sec:sec)):{'min':min,'sec':sec});



  };



  this.clearCanvas = function(oCanvas) {



    var canvas = oCanvas,

        ctx = null,

        width, height;

    if (canvas.getContext){

      // use getContext to use the canvas for drawing

      ctx = canvas.getContext('2d');

    }

    width = canvas.offsetWidth;

    height = canvas.offsetHeight;

    ctx.clearRect(-(width/2), -(height/2), width, height);



  };



  this.updatePlaying = function() {



    var timeNow = (this._360data.showHMSTime?self.getTime(this.position,true):parseInt(this.position/1000, 10));



    if (this.bytesLoaded) {

      this._360data.lastValues.bytesLoaded = this.bytesLoaded;

      this._360data.lastValues.bytesTotal = this.bytesTotal;

    }



    if (this.position) {

      this._360data.lastValues.position = this.position;

    }



    if (this.durationEstimate) {

      this._360data.lastValues.durationEstimate = this.durationEstimate;

    }



    self.drawSolidArc(this._360data.oCanvas,self.config.backgroundRingColor,this._360data.width,this._360data.radius,self.deg2rad(fullCircle),false);



    self.drawSolidArc(this._360data.oCanvas,(this._360data.metadata?self.config.loadRingColorMetadata:self.config.loadRingColor),this._360data.width,this._360data.radius,self.deg2rad(fullCircle*(this._360data.lastValues.bytesLoaded/this._360data.lastValues.bytesTotal)),0,true);



    // don't draw if 0 (full black circle in Opera)

    if (this._360data.lastValues.position !== 0) {

      self.drawSolidArc(this._360data.oCanvas,(this._360data.metadata?self.config.playRingColorMetadata:self.config.playRingColor),this._360data.width,this._360data.radius,self.deg2rad((this._360data.didFinish===1?fullCircle:fullCircle*(this._360data.lastValues.position/this._360data.lastValues.durationEstimate))),0,true);

    }



    // metadata goes here

    if (this._360data.metadata) {

      this._360data.metadata.events.whileplaying();

    }



    if (timeNow !== this._360data.lastTime) {

      this._360data.lastTime = timeNow;

      this._360data.oTiming.innerHTML = timeNow;

    }



    // draw spectrum, if applicable

    if (this.instanceOptions.useWaveformData && hasRealCanvas) { // IE <9 can render maybe 3 or 4 FPS when including the wave/EQ, so don't bother.

      self.updateWaveform(this);

    }



    if (self.config.useFavIcon && self.vuMeter) {

      self.vuMeter.updateVU(this);

    }



  };



  this.updateWaveform = function(oSound) {



    if ((!self.config.useWaveformData && !self.config.useEQData) || (!sm.features.waveformData && !sm.features.eqData)) {

      // feature not enabled..

      return false;

    }



    if (!oSound.waveformData.left.length && !oSound.eqData.length && !oSound.peakData.left) {

      // no data (or errored out/paused/unavailable?)

      return false;

    }



    /* use for testing the data */

    /*

     for (i=0; i<256; i++) {

       oSound.eqData[i] = 1-(i/256);

     }

    */



    var oCanvas = oSound._360data.oCanvas.getContext('2d'),

        offX = 0,

        offY = parseInt(oSound._360data.circleDiameter/2, 10),

        scale = offY/2, // Y axis (+/- this distance from 0)

        // lineWidth = Math.floor(oSound._360data.circleDiameter-(oSound._360data.circleDiameter*0.175)/(oSound._360data.circleDiameter/255)); // width for each line

        lineWidth = 1,

        lineHeight = 1,

        thisY = 0,

        offset = offY,

        i, j, direction, downSample, dataLength, sampleCount, startAngle, endAngle, waveData, innerRadius, perItemAngle, yDiff, eqSamples, playedAngle, iAvg, nPeak;



    if (self.config.useWaveformData) {

      // raw waveform

      downSample = self.config.waveformDataDownsample; // only sample X in 256 (greater number = less sample points)

      downSample = Math.max(1,downSample); // make sure it's at least 1

      dataLength = 256;

      sampleCount = (dataLength/downSample);

      startAngle = 0;

      endAngle = 0;

      waveData = null;

      innerRadius = (self.config.waveformDataOutside?1:(self.config.waveformDataConstrain?0.5:0.565));

      scale = (self.config.waveformDataOutside?0.7:0.75);

      perItemAngle = self.deg2rad((360/sampleCount)*self.config.waveformDataLineRatio); // 0.85 = clean pixel lines at 150? // self.deg2rad(360*(Math.max(1,downSample-1))/sampleCount);

      for (i=0; i<dataLength; i+=downSample) {

        startAngle = self.deg2rad(360*(i/(sampleCount)*1/downSample)); // +0.67 - counter for spacing

        endAngle = startAngle+perItemAngle;

        waveData = oSound.waveformData.left[i];

        if (waveData<0 && self.config.waveformDataConstrain) {

          waveData = Math.abs(waveData);

        }

        self.drawSolidArc(oSound._360data.oCanvas,self.config.waveformDataColor,oSound._360data.width*innerRadius,oSound._360data.radius*scale*1.25*waveData,endAngle,startAngle,true);

      }

    }



    if (self.config.useEQData) {

      // EQ spectrum

      downSample = self.config.eqDataDownsample; // only sample N in 256

      yDiff = 0;

      downSample = Math.max(1,downSample); // make sure it's at least 1

      eqSamples = 192; // drop the last 25% of the spectrum (>16500 Hz), most stuff won't actually use it.

      sampleCount = (eqSamples/downSample);

      innerRadius = (self.config.eqDataOutside?1:0.565);

      direction = (self.config.eqDataOutside?-1:1);

      scale = (self.config.eqDataOutside?0.5:0.75);

      startAngle = 0;

      endAngle = 0;

      perItemAngle = self.deg2rad((360/sampleCount)*self.config.eqDataLineRatio); // self.deg2rad(360/(sampleCount+1));

      playedAngle = self.deg2rad((oSound._360data.didFinish===1?360:360*(oSound._360data.lastValues.position/oSound._360data.lastValues.durationEstimate)));

      j=0;

      iAvg = 0;

      for (i=0; i<eqSamples; i+=downSample) {

        startAngle = self.deg2rad(360*(i/eqSamples));

        endAngle = startAngle+perItemAngle;

        self.drawSolidArc(oSound._360data.oCanvas,(endAngle>playedAngle?self.config.eqDataColor:self.config.playRingColor),oSound._360data.width*innerRadius,oSound._360data.radius*scale*(oSound.eqData.left[i]*direction),endAngle,startAngle,true);

      }

    }



    if (self.config.usePeakData) {

      if (!oSound._360data.animating) {

        nPeak = (oSound.peakData.left||oSound.peakData.right);

        // GIANT HACK: use EQ spectrum data for bass frequencies

        eqSamples = 3;

        for (i=0; i<eqSamples; i++) {

          nPeak = (nPeak||oSound.eqData[i]);

        }

        oSound._360data.amplifier = (self.config.useAmplifier?(0.9+(nPeak*0.1)):1);

        oSound._360data.radiusMax = oSound._360data.circleDiameter*0.175*oSound._360data.amplifier;

        oSound._360data.widthMax = oSound._360data.circleDiameter*0.4*oSound._360data.amplifier;

        oSound._360data.radius = parseInt(oSound._360data.radiusMax*oSound._360data.amplifier, 10);

        oSound._360data.width = parseInt(oSound._360data.widthMax*oSound._360data.amplifier, 10);

      }

    }



  };



  this.getUIHTML = function(diameter) {



    return [

     '<canvas class="sm2-canvas" width="'+diameter+'" height="'+diameter+'"></canvas>',

     ' <span class="sm2-360btn sm2-360btn-default"></span>', // note use of imageMap, edit or remove if you use a different-size image.

     ' <div class="sm2-timing'+(navigator.userAgent.match(/safari/i)?' alignTweak':'')+'"></div>', // + Ever-so-slight Safari horizontal alignment tweak

     ' <div class="sm2-cover"></div>'

    ];



  };



  this.uiTest = function(sClass) {



    // fake a 360 UI so we can get some numbers from CSS, etc.



    var oTemplate = document.createElement('div'),

        oFakeUI, oFakeUIBox, oTemp, fakeDiameter, uiHTML, circleDiameter, circleRadius, fontSizeMax, oTiming;



    oTemplate.className = 'sm2-360ui';



    oFakeUI = document.createElement('div');

    oFakeUI.className = 'ui360'+(sClass?' '+sClass:''); // ui360 ui360-vis



    oFakeUIBox = oFakeUI.appendChild(oTemplate.cloneNode(true));



    oFakeUI.style.position = 'absolute';

    oFakeUI.style.left = '-9999px';



    oTemp = document.body.appendChild(oFakeUI);



    fakeDiameter = oFakeUIBox.offsetWidth;



    uiHTML = self.getUIHTML(fakeDiameter);



    oFakeUIBox.innerHTML = uiHTML[1]+uiHTML[2]+uiHTML[3];



    circleDiameter = parseInt(oFakeUIBox.offsetWidth, 10);

    circleRadius = parseInt(circleDiameter/2, 10);



    oTiming = self.getElementsByClassName('sm2-timing','div',oTemp)[0];

    fontSizeMax = parseInt(self.getStyle(oTiming,'font-size'), 10);

    if (isNaN(fontSizeMax)) {

      // getStyle() etc. didn't work.

      fontSizeMax = null;

    }



    // soundManager._writeDebug('diameter, font size: '+circleDiameter+','+fontSizeMax);



    oFakeUI.parentNode.removeChild(oFakeUI);



    uiHTML = oFakeUI = oFakeUIBox = oTemp = null;



    return {

      circleDiameter: circleDiameter,

      circleRadius: circleRadius,

      fontSizeMax: fontSizeMax

    };



  };



  this.init = function() {



    sm._writeDebug('threeSixtyPlayer.init()');



    var oItems = self.getElementsByClassName('ui360','div'),

        i, j, oLinks = [], is_vis = false, foundItems = 0, oCanvas, oCanvasCTX, oCover, diameter, radius, uiData, uiDataVis, oUI, oBtn, o, o2, oID;



    for (i=0,j=oItems.length; i<j; i++) {

      oLinks.push(oItems[i].getElementsByTagName('a')[0]);

      // remove "fake" play button (unsupported case)

      oItems[i].style.backgroundImage = 'none';

    }

    // grab all links, look for .mp3



    self.oUITemplate = document.createElement('div');

    self.oUITemplate.className = 'sm2-360ui';



    self.oUITemplateVis = document.createElement('div');

    self.oUITemplateVis.className = 'sm2-360ui';



    uiData = self.uiTest();



    self.config.circleDiameter = uiData.circleDiameter;

    self.config.circleRadius = uiData.circleRadius;

    // self.config.fontSizeMax = uiData.fontSizeMax;



    uiDataVis = self.uiTest('ui360-vis');



    self.config.fontSizeMax = uiDataVis.fontSizeMax;



    // canvas needs inline width and height, doesn't quite work otherwise

    self.oUITemplate.innerHTML = self.getUIHTML(self.config.circleDiameter).join('');



    self.oUITemplateVis.innerHTML = self.getUIHTML(uiDataVis.circleDiameter).join('');



    for (i=0,j=oLinks.length; i<j; i++) {

      if (sm.canPlayLink(oLinks[i]) && !self.hasClass(oLinks[i],self.excludeClass)) {

        self.addClass(oLinks[i],self.css.sDefault); // add default CSS decoration

        self.links[foundItems] = (oLinks[i]);

        self.indexByURL[oLinks[i].href] = foundItems; // hack for indexing

        foundItems++;



        is_vis = self.hasClass(oLinks[i].parentNode, 'ui360-vis');



        diameter = (is_vis ? uiDataVis : uiData).circleDiameter;

        radius = (is_vis ? uiDataVis : uiData).circleRadius;



        // add canvas shiz

        oUI = oLinks[i].parentNode.insertBefore((is_vis?self.oUITemplateVis:self.oUITemplate).cloneNode(true),oLinks[i]);



        if (isIE && typeof window.G_vmlCanvasManager !== 'undefined') { // IE only

          o = oLinks[i].parentNode;

          o2 = document.createElement('canvas');

          o2.className = 'sm2-canvas';

          oID = 'sm2_canvas_'+parseInt(Math.random()*1048576, 10);

          o2.id = oID;

          o2.width = diameter;

          o2.height = diameter;

          oUI.appendChild(o2);

          window.G_vmlCanvasManager.initElement(o2); // Apply ExCanvas compatibility magic

          oCanvas = document.getElementById(oID);

        } else { 

          // add a handler for the button

          oCanvas = oLinks[i].parentNode.getElementsByTagName('canvas')[0];

        }

        oCover = self.getElementsByClassName('sm2-cover','div',oLinks[i].parentNode)[0];

        oBtn = oLinks[i].parentNode.getElementsByTagName('span')[0];

        self.addEventHandler(oBtn,'click',self.buttonClick);

        if (!isTouchDevice) {

          self.addEventHandler(oCover,'mousedown',self.mouseDown);

        } else {

          self.addEventHandler(oCover,'touchstart',self.mouseDown);

        }

        oCanvasCTX = oCanvas.getContext('2d');

        oCanvasCTX.translate(radius, radius);

        oCanvasCTX.rotate(self.deg2rad(-90)); // compensate for arc starting at EAST // http://stackoverflow.com/questions/319267/tutorial-for-html-canvass-arc-function

      }

    }

    if (foundItems>0) {

      self.addEventHandler(document,'click',self.handleClick);

      if (self.config.autoPlay) {

        self.handleClick({target:self.links[0],preventDefault:function(){}});

      }

    }

    sm._writeDebug('threeSixtyPlayer.init(): Found '+foundItems+' relevant items.');



    if (self.config.useFavIcon && typeof this.VUMeter !== 'undefined') {

      this.vuMeter = new this.VUMeter(this);

    }



  };



}



// Optional: VU Meter component



ThreeSixtyPlayer.prototype.VUMeter = function(oParent) {



  var self = oParent,

      me = this,

      _head = document.getElementsByTagName('head')[0],

      isOpera = (navigator.userAgent.match(/opera/i)),

      isFirefox = (navigator.userAgent.match(/firefox/i));



  this.vuMeterData = [];

  this.vuDataCanvas = null;



  this.setPageIcon = function(sDataURL) {



    if (!self.config.useFavIcon || !self.config.usePeakData || !sDataURL) {

      return false;

    }



    var link = document.getElementById('sm2-favicon');

    if (link) {

      _head.removeChild(link);

      link = null;

    }

    if (!link) {

      link = document.createElement('link');

      link.id = 'sm2-favicon';

      link.rel = 'shortcut icon';

      link.type = 'image/png';

      link.href = sDataURL;

      document.getElementsByTagName('head')[0].appendChild(link);

    }



  };



  this.resetPageIcon = function() {



    if (!self.config.useFavIcon) {

      return false;

    }

    var link = document.getElementById('favicon');

    if (link) {

      link.href = '/favicon.ico';

    }



  };



  this.updateVU = function(oSound) {



    if (soundManager.flashVersion >= 9 && self.config.useFavIcon && self.config.usePeakData) {

      me.setPageIcon(me.vuMeterData[parseInt(16*oSound.peakData.left, 10)][parseInt(16*oSound.peakData.right, 10)]);

    }



  };



  this.createVUData = function() {



    var i=0, j=0,

        canvas = me.vuDataCanvas.getContext('2d'),

        vuGrad = canvas.createLinearGradient(0, 16, 0, 0),

        bgGrad = canvas.createLinearGradient(0, 16, 0, 0),

        outline = 'rgba(0,0,0,0.2)';



    vuGrad.addColorStop(0,'rgb(0,192,0)');

    vuGrad.addColorStop(0.30,'rgb(0,255,0)');

    vuGrad.addColorStop(0.625,'rgb(255,255,0)');

    vuGrad.addColorStop(0.85,'rgb(255,0,0)');

    bgGrad.addColorStop(0,outline);

    bgGrad.addColorStop(1,'rgba(0,0,0,0.5)');

    for (i=0; i<16; i++) {

      me.vuMeterData[i] = [];

    }

    for (i=0; i<16; i++) {

      for (j=0; j<16; j++) {

        // reset/erase canvas

        me.vuDataCanvas.setAttribute('width',16);

        me.vuDataCanvas.setAttribute('height',16);

        // draw new stuffs

        canvas.fillStyle = bgGrad;

        canvas.fillRect(0,0,7,15);

        canvas.fillRect(8,0,7,15);

        /*

        // shadow

        canvas.fillStyle = 'rgba(0,0,0,0.1)';

        canvas.fillRect(1,15-i,7,17-(17-i));

        canvas.fillRect(9,15-j,7,17-(17-j));

        */

        canvas.fillStyle = vuGrad;

        canvas.fillRect(0,15-i,7,16-(16-i));

        canvas.fillRect(8,15-j,7,16-(16-j));

        // and now, clear out some bits.

        canvas.clearRect(0,3,16,1);

        canvas.clearRect(0,7,16,1);

        canvas.clearRect(0,11,16,1);

        me.vuMeterData[i][j] = me.vuDataCanvas.toDataURL('image/png');

        // for debugging VU images

        /*

        var o = document.createElement('img');

        o.style.marginRight = '5px'; 

        o.src = vuMeterData[i][j];

        document.documentElement.appendChild(o);

        */

      }

    }



  };



  this.testCanvas = function() {



    // canvas + toDataURL();

    var c = document.createElement('canvas'),

        ctx = null, ok;

    if (!c || typeof c.getContext === 'undefined') {

      return null;

    }

    ctx = c.getContext('2d');

    if (!ctx || typeof c.toDataURL !== 'function') {

      return null;

    }

    // just in case..

    try {

      ok = c.toDataURL('image/png');

    } catch(e) {

      // no canvas or no toDataURL()

      return null;

    }

    // assume we're all good.

    return c;



  };



  this.init = function() {



    if (self.config.useFavIcon) {

      me.vuDataCanvas = me.testCanvas();

      if (me.vuDataCanvas && (isFirefox || isOpera)) {

        // these browsers support dynamically-updating the favicon

        me.createVUData();

      } else {

        // browser doesn't support doing this

        self.config.useFavIcon = false;

      }

    }



  };



  this.init();



};



// completely optional: Metadata/annotations/segments code



ThreeSixtyPlayer.prototype.Metadata = function(oSound, oParent) {



  soundManager._wD('Metadata()');



  var me = this,

      oBox = oSound._360data.oUI360,

      o = oBox.getElementsByTagName('ul')[0],

      oItems = o.getElementsByTagName('li'),

      isFirefox = (navigator.userAgent.match(/firefox/i)),

      isAlt = false, i, oDuration;



  this.lastWPExec = 0;

  this.refreshInterval = 250;

  this.totalTime = 0;



  this.events = {



    whileplaying: function() {



      var width = oSound._360data.width,

          radius = oSound._360data.radius,

          fullDuration = (oSound.durationEstimate||(me.totalTime*1000)),

          isAlt = null, i, j, d;



      for (i=0,j=me.data.length; i<j; i++) {

        isAlt = (i%2===0);

        oParent.drawSolidArc(oSound._360data.oCanvas,(isAlt?oParent.config.segmentRingColorAlt:oParent.config.segmentRingColor),isAlt?width:width, isAlt?radius/2:radius/2, oParent.deg2rad(360*(me.data[i].endTimeMS/fullDuration)), oParent.deg2rad(360*((me.data[i].startTimeMS||1)/fullDuration)), true);

      }

      d = new Date();

      if (d-me.lastWPExec>me.refreshInterval) {

        me.refresh();

        me.lastWPExec = d;

      }



    }



  };



  this.refresh = function() {



    // Display info as appropriate

    var i, j, index = null,

        now = oSound.position,

        metadata = oSound._360data.metadata.data;



    for (i=0, j=metadata.length; i<j; i++) {

      if (now >= metadata[i].startTimeMS && now <= metadata[i].endTimeMS) {

        index = i;

        break;

      }

    }

    if (index !== metadata.currentItem && index < metadata.length) {

      // update

      oSound._360data.oLink.innerHTML = metadata.mainTitle+' <span class="metadata"><span class="sm2_divider"> | </span><span class="sm2_metadata">'+metadata[index].title+'</span></span>';

      // self.setPageTitle(metadata[index].title+' | '+metadata.mainTitle);

      metadata.currentItem = index;

    }



  };



  this.strToTime = function(sTime) {

    var segments = sTime.split(':'),

        seconds = 0, i;

    for (i=segments.length; i--;) {

      seconds += parseInt(segments[i], 10)*Math.pow(60,segments.length-1-i); // hours, minutes

    }

    return seconds;

  };



  this.data = [];

  this.data.givenDuration = null;

  this.data.currentItem = null;

  this.data.mainTitle = oSound._360data.oLink.innerHTML;



  for (i=0; i<oItems.length; i++) {

    this.data[i] = {

      o: null,

      title: oItems[i].getElementsByTagName('p')[0].innerHTML,

      startTime: oItems[i].getElementsByTagName('span')[0].innerHTML,

      startSeconds: me.strToTime(oItems[i].getElementsByTagName('span')[0].innerHTML.replace(/[()]/g,'')),

      duration: 0,

      durationMS: null,

      startTimeMS: null,

      endTimeMS: null,

      oNote: null

    };

  }

  oDuration = oParent.getElementsByClassName('duration','div',oBox);

  this.data.givenDuration = (oDuration.length?me.strToTime(oDuration[0].innerHTML)*1000:0);

  for (i=0; i<this.data.length; i++) {

    this.data[i].duration = parseInt(this.data[i+1]?this.data[i+1].startSeconds:(me.data.givenDuration?me.data.givenDuration:oSound.durationEstimate)/1000, 10)-this.data[i].startSeconds;

    this.data[i].startTimeMS = this.data[i].startSeconds*1000;

    this.data[i].durationMS = this.data[i].duration*1000;

    this.data[i].endTimeMS = this.data[i].startTimeMS+this.data[i].durationMS;

    this.totalTime += this.data[i].duration;

  }



};



if (navigator.userAgent.match(/webkit/i) && navigator.userAgent.match(/mobile/i)) {

  // iPad, iPhone etc.

  soundManager.useHTML5Audio = true;

}



soundManager.debugMode = (window.location.href.match(/debug=1/i)); // disable or enable debug output

soundManager.consoleOnly = true;

soundManager.flashVersion = 9;

soundManager.useHighPerformance = true;

soundManager.useFlashBlock = true;



// soundManager.useFastPolling = true; // for more aggressive, faster UI updates (higher CPU use)



// FPS data, testing/debug only

if (soundManager.debugMode) {

  window.setInterval(function() {

    var p = window.threeSixtyPlayer;

    if (p && p.lastSound && p.lastSound._360data.fps && typeof window.isHome === 'undefined') {

      soundManager._writeDebug('fps: ~'+p.lastSound._360data.fps);

      p.lastSound._360data.fps = 0;

    }

  },1000);

}



window.ThreeSixtyPlayer = ThreeSixtyPlayer; // constructor



}(window));



threeSixtyPlayer = new ThreeSixtyPlayer();



// hook into SM2 init

soundManager.onready(threeSixtyPlayer.init);
