/**
 * Cassette Tape UI Prototype (09/2012)
 * ALPHA build / experimental state, unsupported; use at own risk
 * Requires CSS3 border-radius + <canvas> support
 * --------------------------------------------------
 * http://www.schillmania.com/projects/soundmanager2/
 */

(function(window) {

/* global window, document, Image, soundManager */

var caughtError = false;
var utils;
var dragHandler;
var tapeUIs = [];
var ignoreNextClick = false;

var CanvasImage = function(canvas, image) {

  /**
   * https://github.com/ceramedia/examples/tree/gh-pages/canvas-blur/v5
   * original: http://www.flother.com/blog/2010/image-blur-html5-canvas/
   *
   * Light layer on top of a canvas element to represent an image displayed
   * within. Pass in a canvas element and an Image object and you'll see the
   * image within the canvas element. Use the provided methods (e.g. blur) to
   * manipulate it.
   *
   * @constructor
   * @param {HTMLElement} element HTML canvas element.
   * @param {Image} image Image object.
   */

  var width, height;

  this.image = image;

  width = this.image.width;
  height = this.image.height;

  this.element = canvas || document.createElement('canvas');
  // IE 10 complains if 'auto' is used for style.width|height.

  this.element.style.width = width + 'px';
  this.element.style.height = height + 'px';
  this.element.width = width;
  this.element.height = height;
  this.context = this.element.getContext('2d');
  this.context.drawImage(this.image, 0, 0);

};

CanvasImage.prototype = {
  /**
   * Runs a blur filter over the image.
   * @param {int} strength Strength of the blur.
   */
  blur: function (strength) {
    this.context.globalAlpha = 0.5; // Higher alpha made it more smooth
    // Add blur layers by strength to x and y
    // 2 made it a bit faster without noticeable quality loss
    for (var y = -strength; y <= strength; y += 2) {
      for (var x = -strength; x <= strength; x += 2) {
        // Apply layers
        this.context.drawImage(this.element, x, y);
        // Add an extra layer, prevents it from rendering lines
        // on top of the images (does makes it slower though)
        if (x >= 0 && y >= 0) {
          this.context.drawImage(this.element, -(x - 1), -(y - 1));
        }
      }
    }
    this.context.globalAlpha = 1.0;
  }
};

function imageMask(imageSrc, maskSrc, canvasWidth, canvasHeight, useRepeat, oncomplete) {

  /**
   * quick-and-dirty "load an image and apply a mask to it" function
   * parameters:
   * @imageSrc {URL} string Source image URL
   * @maskSrc {URL} string Mask image URL
   * @oncomplete {Function} function Callback with data: URI of masked image result
   */

  var images = [
    new Image(),
    new Image()
  ];

  // CORS option?
  // images[0].crossOrigin = 'anonymous';
  // images[1].crossOrigin = 'anonymous';

  var canvas = [
    document.createElement('canvas'),
    document.createElement('canvas')
  ];

  var loadCount = 0;

  var loadTarget = images.length;

  function applyMask() {

    // draw and mask image

    var srcWidth = canvasWidth;
    var srcHeight = canvasHeight;

    var targetWidth = canvasWidth;
    var targetHeight = canvasHeight;

    var ctx;

    var repeatPattern;

    canvas[0].width = srcWidth;
    canvas[0].height = srcHeight;

    canvas[1].width = targetWidth;
    canvas[1].height = targetHeight;

    ctx = [
      canvas[0].getContext('2d'),
      canvas[1].getContext('2d')
    ];

    if (!useRepeat) {

      // simple 1:1 case

      ctx[0].drawImage(images[0], 0, 0);

    } else {

      // tile the image across the canvas

      repeatPattern = ctx[0].createPattern(images[0], 'repeat');

      ctx[0].fillStyle = repeatPattern;

      ctx[0].fillRect(0, 0, targetWidth, targetHeight);

      ctx[0].fillStyle = '';

    }

    // draw mask on target canvas

    ctx[1].drawImage(images[1], 0, 0);

    // apply the mask

    ctx[0].globalCompositeOperation = 'destination-in';

    ctx[0].drawImage(canvas[1], 0, 0);

    ctx[0].globalCompositeOperation = null;

    // the resulting masked image

    return canvas[0];

  }

  function imageLoaded() {

    // load handler

    this.onload = this.onerror = null;

    loadCount++;

    if (loadCount >= loadTarget) {

      // apply the mask, providing the resulting data URI to the handler
      // TODO: Don't use data URL, just modify canvas directly; likely much faster.

      try {

        oncomplete(applyMask().toDataURL('image/png'));

      } catch(e) {

        // may fail with DOM exception 18 under Chrome when offline eg., file:// instead of over HTTP.

        if (typeof console !== 'undefined' && typeof console.warn !== 'undefined') {
          console.warn('Unable to apply image mask, likely a security exception from offline (file://) use.', e);
          if (!caughtError && typeof console.info !== 'undefined') {
            console.info('Using static skin image as a workaround.');
          }
        }

        // hack: try applying the body skin instead, without masking. HTML-based screw elements will overlay the skin, but, eh. this is an edge case anyway.

        if (!caughtError) {

          oncomplete('image/ma-r90-body-skin.png');
          caughtError = true;

        } else {

          oncomplete();

        }

      }

      // cleanup?
      canvas = null;
      images = null;

    }

  }

  function initImage() {

    // preload source / target images

    images[0].onload = images[0].onerror = imageLoaded;
    images[1].onload = images[1].onerror = imageLoaded;

    images[0].src = imageSrc;
    images[1].src = maskSrc;

    // TODO: check .complete (cached) case?

  }

  initImage();

}

function TapeUI(oOptions) {

  var css = {

    playing: 'playing',
    stopped: 'stopped',
    ready: 'ready',
    dropin: 'dropin'

  };

  var data = {

    progress: 0

  };

  var controlHandler;

  var sound;

  var dom = {};

  var spoolWidth = 91;
  var borderMaxWidth = 76;
  var tapeWidth = 480;
  var tapeHeight = parseInt(tapeWidth / 1.6, 10);

  var maskCanvas;
  var maskCanvasLoaded;
  // var maskContext;
  var maskImage;
  var maskVisible;

  function getBackgroundURL(node) {

    var url;
    var cssprop = 'backgroundImage';

    if (node.currentStyle) {

      // IE
      url = node.currentStyle[cssprop];

    } else if (document.defaultView && document.defaultView.getComputedStyle) {

      // Firefox
      url = document.defaultView.getComputedStyle(node, '')[cssprop];

    } else {

      // inline style?
      url = node.style[cssprop];

    }

    // HACK: strip url() and/or quotes from string

    url = url.replace('url(', '').replace(')', '');

    url = url.replace(/'/g, '').replace(/"/g, '');

    return url;

  }

  function maskedImageReady(node, uri) {

    // callback with data: URI of masked image

    if (node && uri) {
      node.style.backgroundImage = uri;
    }

  }

  function createMaskedImage(node) {

    // get background image (or if <img>, src?) and data-mask-image attributes

    var imageSrc = getBackgroundURL(node),
        elementWidth = node.offsetWidth,
        elementHeight = node.offsetHeight,
        useRepeat = !!node.getAttribute('data-image-repeat'),
        maskSrc = node.getAttribute('data-mask-url');

    return imageMask(imageSrc, maskSrc, elementWidth, elementHeight, useRepeat, function(maskURI) {
      var uri = (maskURI ? 'url(' + maskURI + ')' : null);
      maskedImageReady(node, uri);
    });

  }

  // canvas stuffs

  function scaleWidth(w) {
    return (w * tapeWidth);
  }

  function scaleHeight(h) {
    return (h * tapeHeight);
  }

  function initMask(callback) {

    if (!dom.node.className.match(/cutout/i)) {
      return;
    }

    // draw our tape cut-outs

    maskCanvas = document.createElement('canvas');

    maskImage = new Image();

    // Ensure same dimensions
    maskCanvas.width = tapeWidth;
    maskCanvas.height = tapeHeight;

/*
    maskContext = maskCanvas.getContext('2d');

    var spoolBoxRadius = 134;

    var spoolCoords = [
      // left
      tapeWidth * 0.29,
      tapeHeight * 0.45,
      // right
      tapeWidth * 0.71,
      tapeHeight * 0.45
    ];

    // filled shape color
    maskContext.fillStyle = "black";

    maskContext.arc(spoolCoords[0], spoolCoords[1], spoolBoxRadius, 0, 4 * Math.PI);

    maskContext.arc(spoolCoords[2], spoolCoords[3], spoolBoxRadius, 0, 4 * Math.PI);

    maskContext.fill();
*/

    maskImage.onload = function() {
      var ctx = maskCanvas.getContext('2d');
      ctx.drawImage(this, 0, 0);
      this.onload = null;
      maskCanvasLoaded = true;
      callback();
    };

    // load the mask to apply
    maskImage.src = 'image/ma-r90-mask.png';

  }

  function createBlurImage(callback) {

    var url = getBackgroundURL(document.getElementsByTagName('html')[0]);

    var image = new Image();

    // CORS option?
    // image.crossOrigin = 'anonymous';

    image.onload = image.onerror = function() {

      // in error state, w/h will be empty.

      if (this.width || this.height) {

        // make + blur

        var canvasImage = new CanvasImage(dom.blurNode, this);

        // var uri = canvasImage.element.toDataURL('image/png');

        // + assign to DOM

        dom.blurNode.style.backgroundImage = '';

        canvasImage.blur(2);

      }

      this.onload = this.onerror = null;

      if (callback) {
        callback();
      }

    };

    image.src = url;

  }

  function center() {

    // given screen x/y, position at center.

    // don't center unless draggable.
    if (!dragHandler.data.dragTarget.node) {
      return;
    }

    var screenX = window.innerWidth,
        screenX2 = parseInt(screenX / 2, 10),
        screenY = window.innerHeight,
        screenY2 = parseInt(screenY / 2, 10),
        x, y,
        node = dom.node,
        blurNode = dom.blurNode;

    if (node) {
      x = parseInt(screenX2 - (tapeWidth / 2), 10);
      y = parseInt(screenY2 - (tapeHeight / 2), 10);
      node.style.left = x + 'px';
      node.style.top = y + 'px';
      if (blurNode) {
        blurNode.style.marginLeft = -x + 'px';
        blurNode.style.marginTop = -y + 'px';
      }
    }

  }

  var readyCompleteTimer = null;

  function readyComplete(/*loader*/) {

    utils.css.add(dom.node, css.ready);
    utils.css.add(dom.node, css.dropin);

    if (readyCompleteTimer) {
      window.clearTimeout(readyCompleteTimer);
    }

    readyCompleteTimer = window.setTimeout(function() {
      utils.css.remove(dom.node, css.dropin);
      // demo hack: remove the loader, too
      /*
      if (loader) {
        document.body.removeChild(loader);
        loader = null;
      }
      */
    }, 1000);

  }

  function ready() {

    // demo hack: hide the loading element, if present
    var loader = document.getElementById('tape-loader'),
        wrapper = document.getElementById('demo-header-wrapper');

    if (loader) {
      utils.css.remove(loader, 'visible');
      utils.css.add(loader, 'hidden');
      if (wrapper) {
        utils.css.add(wrapper, 'visible');
      }
    }

    // trigger animations after slight yield (depending on loader element)
    window.setTimeout(function() {
      // reposition
      center();
      readyComplete(loader);
    }, loader ? 300 : 1);

  }

  function initTapeUI() {

    var i, j;

    sound = oOptions.sound;

    dom = {
      node: oOptions.node,
      canvas: oOptions.node.querySelectorAll('.connecting-tape'),
      reels: oOptions.node.querySelectorAll('div.reel'),
      spokes: oOptions.node.querySelectorAll('div.spokes'),
      label: oOptions.node.querySelectorAll('div.label'),
      maskImages: oOptions.node.querySelectorAll('.image-mask'),
      blurNode: oOptions.node.querySelectorAll('.blur-image')
    };

    if (dom.canvas && dom.canvas.length) {
      dom.canvas = dom.canvas[0];
      initMask(function() {
        // force redraw
        setProgress(0, true);
      });
    } else {
      dom.canvas = null;
    }

    // images needing masking?
    if (dom.maskImages) {
      for (i = 0, j = dom.maskImages.length; i < j; i++) {
        createMaskedImage(dom.maskImages[i]);
      }
    }

    // blur image?
    if (dom.blurNode && dom.blurNode.length) {
      dom.blurNode = dom.blurNode[0];
      createBlurImage(ready);
    } else {
      dom.blurNode = null;
      ready();
    }

    // controls
    controlHandler = new ControlHandler(dom, sound);

  }

  var reelStatus = [{
    borderWidth: null
  }, {
    borderWidth: null
  }];

  function setReelSize(reelCount, reel, size) {

    // return value: was an update applied?
    var newFrame = 0;

    // limit to between 0 and 1
    size = Math.min(1, Math.max(0, size));

    var borderWidth = Math.floor(borderMaxWidth * size);

    var margin;

    if (reelStatus[reelCount].borderWidth !== borderWidth) {

      reelStatus[reelCount].borderWidth = borderWidth;

      reel.style.borderWidth = borderWidth + 'px';

      margin = -(Math.floor(spoolWidth / 2) + borderWidth) + 'px';

      reel.style.marginLeft = margin;

      reel.style.marginTop = margin;

      newFrame = 1;

    }

    return newFrame;

  }

  function deg2rad(degrees) {

    return (Math.PI / 180) * degrees;

  }

  var tapeCache = {
    leftReel: {
      left: null
    },
    rightReel: {
      left: null
    }
  };

  function drawConnectingTape(canvas, progress, forceUpdate) {

    // draw a line of tape between the two reels, at angles relative to the amount of tape on each reel.

    var leftReelRadius = borderMaxWidth - (borderMaxWidth * progress * 0.995);

    var rightReelRadius = (borderMaxWidth * progress * 1.0425);

    var bottomTapeOffset = scaleHeight(0.998);

    var leftReel = {
      left: Math.floor(scaleWidth(0.29) - (spoolWidth / 2) - leftReelRadius) + 4,
      top: scaleHeight(0.42)
    };

    var guideRadius = 16;

    var rightReel = {
      left: Math.floor(scaleWidth(0.71) + (spoolWidth / 2) + rightReelRadius) - 3,
      top: scaleHeight(0.42)
    };

    var leftGuide = {
      left: scaleWidth(0.11) - guideRadius,
      top: bottomTapeOffset - (guideRadius * 2)
    };

    var rightGuide = {
      left: scaleWidth(0.89) - guideRadius,
      top: bottomTapeOffset
    };

    var bottomLeftPoint = {
      from: [leftGuide.left, leftGuide.top],
      to: [leftReel.left, leftReel.top]
    };

    var bottomRightPoint = {
      from: [rightGuide.left, rightGuide.top],
      to: [rightReel.left, rightReel.top]
    };

    var bottomMidPoint = {
      left: tapeWidth * 0.5,
      top: bottomTapeOffset
    };

    if (!forceUpdate && tapeCache.leftReel.left === leftReel.left && tapeCache.rightReel.left === rightReel.left) {
      // no change since last time.
      return;
    }

    // otherwise, update everything.
    tapeCache.leftReel.left = leftReel.left;
    tapeCache.rightReel.left = rightReel.left;

    var context = canvas.getContext('2d');

    canvas.width = tapeWidth;
    canvas.height = tapeHeight;

    // context.lineWidth = 1;

    context.beginPath();

    // move to bottom middle
    context.moveTo(bottomMidPoint.left, bottomMidPoint.top);

    // -> left guide
    context.lineTo(bottomLeftPoint.from[0] + guideRadius, bottomLeftPoint.from[1] + (guideRadius * 2));

    // arc
    context.arc(bottomLeftPoint.from[0] + guideRadius, bottomLeftPoint.from[1] + guideRadius, guideRadius, deg2rad(90), deg2rad(180), false);

    // -> left reel
    context.lineTo(leftReel.left, leftReel.top);

    context.lineWidth = 0.5;

    // move to bottom middle
    context.moveTo(bottomMidPoint.left, bottomMidPoint.top);

    // -> right guide
    context.lineTo(bottomRightPoint.from[0] + guideRadius, bottomRightPoint.from[1]);

    // right side
    context.arc(bottomRightPoint.from[0] + guideRadius, bottomRightPoint.from[1] - guideRadius, guideRadius, deg2rad(90), deg2rad(0), true); // -30 on last for curve effect

    // -> right reel
    context.lineTo(rightReel.left, rightReel.top);

    context.lineWidth = 1;

    context.stroke();

    // apply the mask (only the circular areas around the tape)

    if (maskCanvas) {

      // set composite operation

      context.globalCompositeOperation = 'destination-out';

      context.drawImage(maskCanvas, 0, 0);

      context.globalCompositeOperation = null;

      // visibility check

      if (maskCanvasLoaded && !maskVisible) {

        maskVisible = true;

        canvas.style.visibility = 'visible';

      }

    }

  }

  function setReelSpeed(/*reel, speed*/) {

    // base speed plus a given amount based on speed (multiplier?)

  }

  function applyProgress(progress) {

    var newFrames = 0;

    setReelSpeed(dom.reels[0], progress);
    newFrames += setReelSize(0, dom.reels[0], 1 - progress);
    newFrames += setReelSize(1, dom.reels[1], progress);

    return newFrames;

  }

  function setProgress(progress, forceUpdate) {

    var newFrames = 0;

    forceUpdate = forceUpdate || false;

    data.progress = progress;

    newFrames = applyProgress(progress);

    if ((newFrames || forceUpdate) && dom.canvas) {

      drawConnectingTape(dom.canvas, progress, forceUpdate);

    }

  }

  function start() {

    utils.css.remove(dom.node, css.stopped);
    utils.css.add(dom.node, css.playing);

  }

  function stop() {

    utils.css.remove(dom.node, css.playing);
    utils.css.add(dom.node, css.stopped);

  }

  return {

    refreshBlurImage: function(callback) {
      utils.css.remove(dom.node, css.dropin);
      utils.css.remove(dom.node, css.ready);
      return createBlurImage(function() {
        utils.css.add(dom.node, css.dropin);
        utils.css.add(dom.node, css.ready);
        readyComplete();
        if (callback) {
          callback(this);
        }
      });
    },
    dom: dom,
    init: initTapeUI,
    setProgress: setProgress,
    start: start,
    stop: stop

  };

}

function resetTapeUIs() {

  for (var i = tapeUIs.length; i--;) {
    tapeUIs[i].setProgress(0);
  }

}

dragHandler = (function() {

  var css,
      data,
      events;

  function findPos(obj) {
    var curleft = 0;
    var curtop = 0;
    if (obj.offsetParent) {
      do {
        curleft += obj.offsetLeft;
        curtop += obj.offsetTop;
      } while (obj = obj.offsetParent);
    }
    return [curleft, curtop];
  }

  css = {
    dragActive: 'dragging',
    dropActive: 'dropping'
  };

  data = {
    dragTarget: {
      node: null,
      blurNode: null,
      blurNodeContainer: null,
      x: null,
      y: null,
      lastX: null,
      lastY: null
    },
    mousedown: {
      x: null,
      y: null
    },
    drag: {
      xOffset: null,
      yOffset: null
    },
    background: {
      xOffset: -151,
      yOffset: -119
    }
  };

  events = {
    mousedown: function(e) {
      var target = data.dragTarget.node,
          clickTarget = e.target;
      if (!data.dragTarget.node) {
        return true;
      }
      // additional safety check
      if (clickTarget && clickTarget.tagName === 'INPUT') {
        ignoreNextClick = true;
        return true;
      }
      ignoreNextClick = false;
      var xy = findPos(target);
      data.dragTarget.x = xy[0];
      data.dragTarget.y = xy[1];
      data.mousedown.x = e.clientX;
      data.mousedown.y = e.clientY;
      data.drag.xOffset = data.mousedown.x - data.dragTarget.x;
      data.drag.yOffset = data.mousedown.y - data.dragTarget.y;
      utils.events.add(document, 'mousemove', events.mousemove);
      utils.events.add(document, 'mouseup', events.mouseup);
      e.preventDefault();
      return false;
    },
    mousemove: function(e) {
      var x, y,
          node = data.dragTarget.node,
          blurNode = data.dragTarget.blurNode;
      if (node) {
        x = (e.clientX - data.drag.xOffset);
        y = (e.clientY - data.drag.yOffset);
        node.style.left = x + 'px';
        node.style.top = y + 'px';
        if (blurNode) {
          blurNode.style.marginLeft = -x + 'px';
          blurNode.style.marginTop = -y + 'px';
        }
        if (!ignoreNextClick) {
          ignoreNextClick = true;
          utils.css.add(data.dragTarget.node, css.dragActive);
        }
      }
    },
    mouseup: function(e) {
      utils.css.remove(data.dragTarget.node, css.dragActive);
      if (ignoreNextClick) {
        // remove drag work
        utils.css.add(data.dragTarget.node, css.dropActive);
        window.setTimeout(function() {
          utils.css.remove(data.dragTarget.node, css.dropActive);
        }, 500);
      }
      utils.events.remove(document, 'mousemove', events.mousemove);
      utils.events.remove(document, 'mouseup', events.mouseup);
      e.preventDefault();
      return false;
    }

  };

  function addEvents() {
    if (data.dragTarget && data.dragTarget.node) {
      utils.events.add(data.dragTarget.node, 'mousedown', events.mousedown);
    }
  }

  function initDragHandler() {
    data.dragTarget.node = document.querySelector('.tape.draggable');
    data.dragTarget.blurNode = document.querySelector('.tape.draggable .blur-image');
    addEvents();
  }

  return {
    data: data,
    init: initDragHandler
  };

}());

function ControlHandler(tapeUIDOM, soundObject) {

  var dom, events, eventMap, soundOK;

  // percentage to jump with each click of the rewind / fast-forward buttons
  var rewind_ffwd_offset = 0.033;

  dom = {
    oControls: null,
    play: null,
    rew: null,
    ffwd: null,
    stop: null
  };

  events = {
    mousedown: function(e) {
      // need <a>
      var className = e.target.className;
      if (soundOK && typeof eventMap[className] !== 'undefined') {
        eventMap[className](e);
        events.stopEvent(e);
        return false;
      }
      return true;
    },
    stopEvent: function(e) {
      e.preventDefault();
      return false;
    },
    click: function(e) {
      // simple/dumb: just toggle the playstate of the tape.
      if (ignoreNextClick) {
        ignoreNextClick = false;
        events.stopEvent(e);
        return false;
      }
      var className = e.target.className;
      if (typeof eventMap[className] === 'undefined') {
        soundObject.togglePause();
      } else {
        events.mousedown(e);
        return false;
      }
      return true;
    }
  };

  eventMap = {
    play: function() {
      soundObject.play();
    },
    rew: function() {
      // rewind
      var newPosition = Math.max(0, soundObject.position - (soundObject.duration * rewind_ffwd_offset));
      if (soundObject.duration) {
        soundObject.setPosition(newPosition);
        // if not playing, force update?
        tapeUIs[0].setProgress(newPosition / soundObject.duration);
      }
    },
    ffwd: function() {
      // fast-forward
      var newPosition;
      if (soundObject.duration) {
        newPosition = Math.min(soundObject.duration, soundObject.position + (soundObject.duration * rewind_ffwd_offset));
        soundObject.setPosition(newPosition);
      }
    },
    stop: function() {
      // equivalent to digital pause
      soundObject.pause();
    }
  };

  function addEvents() {
    utils.events.add(dom.o, 'mousedown', events.mousedown);
    utils.events.add(tapeUIDOM.node, 'click', events.click);
  }

  function initControlHandler() {
    soundOK = soundManager.ok();
    dom.o = tapeUIDOM.node.querySelector('.controls');
    dom.play = dom.o.querySelector('.play');
    dom.rewind = dom.o.querySelector('.rew');
    dom.ffwd = dom.o.querySelector('.ffwd');
    dom.stop = dom.o.querySelector('.stop');
    addEvents();
  }

  initControlHandler();

}

function init() {

  var hasCanvas = (typeof document.createElement('canvas').getContext !== 'undefined');

  if (!hasCanvas) {
    // assume a crap browser (e.g., IE 8.)
    return;
  }

  var tapes,
      j, s;

  dragHandler.init();

  function genericStart() {
    for (var i = tapeUIs.length; i--;) {
      tapeUIs[i].start();
    }
  }

  function genericStop() {
    for (var i = tapeUIs.length; i--;) {
      tapeUIs[i].stop();
    }
  }

  if (soundManager.ok()) {

    s = soundManager.createSound({
      id: 'tapeSound',
      url: 'http://freshly-ground.com/data/audio/sm2/Chill With Schill (Summer 2012 Session Excerpt).mp3',
      multiShot: false,
      whileplaying: function() {
        for (var i = tapeUIs.length; i--;) {
          // console.log(this.position, this.durationEstimate);
          tapeUIs[i].setProgress(this.position / this.durationEstimate);
        }
      },
      onplay: genericStart,
      onfinish: genericStop,
      onpause: genericStop,
      onresume: genericStart
    });

  }

  tapes = document.querySelectorAll('div.tape');

  for (j = tapes.length; j--;) {
    tapeUIs.push(new TapeUI({
      node: tapes[j],
      sound: s
    }));
    tapeUIs[tapeUIs.length - 1].init();
  }

  resetTapeUIs();

}

function delayInit() {

  var loader = document.getElementById('tape-loader');

  if (loader) {
    loader.className = 'visible';
  }

  window.setTimeout(function() {
    init();
  }, 20);
}

utils = (function() {

  var addEventHandler = (typeof window.addEventListener !== 'undefined' ? function(o, evtName, evtHandler) {
    return o.addEventListener(evtName, evtHandler, false);
  } : function(o, evtName, evtHandler) {
    o.attachEvent('on' + evtName, evtHandler);
  });

  var removeEventHandler = (typeof window.removeEventListener !== 'undefined' ? function(o, evtName, evtHandler) {
    return o.removeEventListener(evtName, evtHandler, false);
  } : function(o, evtName, evtHandler) {
    return o.detachEvent('on' + evtName, evtHandler);
  });

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

  var addClass = function(o, cStr) {
    if (!o || !cStr || classContains(o, cStr)) {
      return;
    }
    o.className = (o.className ? o.className + ' ' : '') + cStr;
  };

  var removeClass = function(o, cStr) {
    if (!o || !cStr || classContains(o, cStr)) {
      return;
    }
    o.className = o.className.replace(new RegExp('( ' + cStr + ')|(' + cStr + ')', 'g'), '');
  };

  return {
    css: {
      has: classContains,
      add: addClass,
      remove: removeClass
    },
    events: {
      add: addEventHandler,
      remove: removeEventHandler
    }
  };

}());

soundManager.setup({
  url: '../../swf/',
  flashVersion: 9,
  useHighPerformance: true,
  preferFlash: false,
  html5PollingInterval: 50,
  onready: delayInit,
  ontimeout: delayInit
});

// interface
window.tapeUIs = tapeUIs;

}(window));
