(function(){
  function InputManager()
{
  // state
  this.lastMouseDownTarget = null;
  this.lastMouseDownTargetDraggable = false;
  this.lastMouseDownPosition = null;
  this.dragInProcess = false;

  // queued events
  this.newClickQueue = new Array();
  this.clickQueue = new Array();

  this.newDragQueue = new Array();
  this.dragQueue = new Array();

  this.newKeyQueue = new Array();
  this.keyQueue = new Array();

  // notify events
  this.resize = false;
  this.newResize = false;

  this.scroll = false;
  this.newScroll = false;

  this.mouseMove = false;
  this.newMouseMove = false;
  
  // time
  this.timePassed = 0;
  this.now = new Date();
  
  // information
  this.window = this.newWindow = {  // fixme: should be making a copy
    top:0,
    left:0,
    width:0,
    height:0,
    bottom:0,
    right:0,
    mousePosition:{x:0,y:0}
  };
  this.keys = {};
  
  // default behaviour suppression
  this.suppressDrag = false;
  this.suppressDragElements = [];
  this.suppressKeys = {};
  this.suppressKeysElements = {};
  
  // recording / playback
  this.recording = {states:[], randomNumbers:[]};
  this.playPos = {statesPos:0, randomNumbersPos:0};
}

InputManager.statics =
{
  im:null,
  scrollX:0,
  scrollY:0,

  getManager:function()
  {
    InputManager.statics.im = new InputManager();
    InputManager.statics.getManager = InputManager.statics.getManagerSetup;
    return InputManager.statics.im;
  },
  
  getManagerSetup:function()
  {
    return InputManager.statics.im;
  },
  
  /**
   * suppressDrag - lets an element suppress request that events that cause 
   * dragging have their default behavior suppressed.
   * Doing this is mean and should only be done when it is really needed
   *
   * @parameter element 	- the element that is making the request
   */
  suppressDrag:function(element)
  {
    var im = InputManager.statics.getManager();
    im.suppressDrag = true;
    im.suppressDragElements.push(element.id)
  },
  
  /**
   * unsuppressDrag - lets an element remove its request to suppress
   * Please remeber to call this if you are going to suppress
   *
   * @parameter element 	- the element that is making the request
   */
  unsuppressDrag:function(element)
  {
    var im = InputManager.statics.getManager();
    
    var foundIndex = -1;
    for(var i=0; i<im.suppressDragElements.length && 0>foundIndex; i++)
      if(im.suppressDragElements==element.id)
        foundIndex = i;
    
    if(0<=foundIndex)
      im.suppressDragElements.splice(foundIndex, 1);
    
    if(0>=im.suppressDragElements.length)
      im.suppressDrag = false;
  },
  
  /**
   * suppressKey - lets an element suppress request that the default events 
   * from a keypress have their default behavior suppressed.
   * Doing this is mean and should only be done when it is really needed
   *
   * @parameter element 	- the element that is making the request
   * @parameter key				- the key that they wish to supress
   */
  suppressKey:function(element, key)
  {
    if(!key)
      return;

    var im = InputManager.statics.getManager();
    im.suppressKeys[key] = true;
    if(!im.suppressKeysElements[key])
      im.suppressKeysElements[key] = [];
    im.suppressKeysElements[key].push(element.id)
  },
  
  /**
   * unsuppressKey - lets an element remove its request to suppress
   * Please remeber to call this if you are going to suppress
   *
   * @parameter element 	- the element that is making the request
   * @parameter key				- the key that they wish to supress
   */
  unsuppressKey:function(element, key)
  {
    if(!key)
      return;

    var im = InputManager.statics.getManager();
    
    if(!im.suppressKeysElements[key])
      im.suppressKeysElements[key] = [];
    
    var foundIndex = -1;
    for(var i=0; i<im.suppressKeysElements[key].length && 0>foundIndex; i++)
      if(im.suppressKeysElements[key][i]==element.id)
        foundIndex = i;
    
    if(0<=foundIndex)
      im.suppressKeysElements[key].splice(foundIndex, 1);
    
    if(0>=im.suppressKeysElements[key].length)
      im.suppressKeys[key] = false;
  },

  /**
   * get3DPosition - finds the 3d position of a 2d position using the controler's perspective matrix
   *
   * @parameter pos 	- the 2d coordinate
   * @returns					-	the 3d coordinate
   */
  get3DPosition:function(pos)
  {
    // fixme: the y==z thing is wrong
    var c = Controller.statics.getController();
    return Matrix4.statics.untrasformPoint(c.perspectiveMatrix, {x:pos.x,y:pos.y,z:pos.y});
  },

  normalizeEvent:function(e)
  {
    if(!e)
      e = window.event;
    if(e)
    {
      e.keyNumber = e.keyCode;
      if(!e.keyNumber)
        e.keyNumber = e.which;
      if(!e.target)
        e.target = e.srcElement;
      if(e.target)
      {
        if (e.target.nodeType == 3) // Safari bug
          e.target = e.target.parentNode;
        return e;
      }
    }
    return null;
  },

  /**
   * findPos - finds the coordinate of the top left of an dom node with respect 
   * to the window or an absolutely positioned parent.
   *
   * @parameter obj 	- the object that you want to know coordinate of
   * @returns					-	the coordinate
   */
  findPos:function(obj)
  {
    var position = {x:0, y:0};
    var depth = 0;
    var objStyle = obj.currentStyle;
    if (!objStyle)
      objStyle = document.defaultView.getComputedStyle(obj,'');
    if (obj.offsetParent)
    {
      while (obj.offsetParent && (0==depth || 'absolute'!=objStyle.position))
      {
        position.x += obj.offsetLeft;
        position.y += obj.offsetTop;
        obj = obj.offsetParent;
        objStyle = obj.currentStyle;
        if (!objStyle)
          objStyle = document.defaultView.getComputedStyle(obj,'');
        depth += 1;
      }
    }
    else 
    {
      if (obj.x)
        position.x += obj.x;
      if (obj.y)
        position.y += obj.y;
    }
    return position;
  },
  
  /**
   * findAbsolutePos - finds absolute the coordinate of the top left of an dom node with respect to the viewport
   *
   * @parameter obj 	- the object that you want to know coordinate of
   * @returns					-	the coordinate
   */
  findAbsolutePos:function(obj)
  {
    var position = {x:0, y:0};
    var offsetParent = null;
    var depth = 0;
    var objStyle = obj.currentStyle;
    if (!objStyle)
      objStyle = document.defaultView.getComputedStyle(obj,'');
    if (obj.offsetParent)
    {
      offsetParent = obj;
      while (obj.parentNode)
      {
        if('body'==obj.nodeName.toLowerCase())
          break;
  
        if(obj == offsetParent)
        {
            position.x += obj.offsetLeft;
            position.y += obj.offsetTop;
            offsetParent = obj.offsetParent;
        }
        position.x -= parseInt(obj.scrollLeft);
        position.y -= parseInt(obj.scrollTop);
        if(objStyle && 'absolute'==objStyle.position)
        {
          // fixme: handle nested absolutely positioned elements
          break;
        }
        obj = obj.parentNode;
        try{
        objStyle = obj.currentStyle;
        if (!objStyle)
          objStyle = document.defaultView.getComputedStyle(obj,'');
        } catch (e) {}
        depth += 1;
      }
    }
    else 
    {
      if (obj.x)
        position.x += obj.x;
      if (obj.y)
        position.y += obj.y;
    }
    return position;
  },
  
  findEventPosition:function(e)
  {
    if(e.target)
      return {x:e.clientX+InputManager.statics.scrollX,y:e.clientY+InputManager.statics.scrollY};
    return null;
  },
  
  handleMouseDown:function(e)
  {
    e = InputManager.statics.normalizeEvent(e);
    if(e && e.target)
    {
      var pos = InputManager.statics.findEventPosition(e);
      var id = e.target.id;
      var draggable = false;
      if(e.target.getAttribute)
      {
        if('child'==e.target.getAttribute('eventHandling'))
        {
          id = null;
          var node = e.target.parentNode;
          while(node)
          {
            id = node.id;
            if(id)
            {
              if('1'==node.getAttribute('draggable'))
                draggable = true;
              break;
            }
            node = node.parentNode;
          }
        }
        else if('1'==e.target.getAttribute('draggable'))
            draggable = true;
      }
      var suppress = InputManager.statics.getManager().handleMouseDown(id, pos, draggable);
      if(id || suppress) // fixme: make smarter decision based on draggable?
      {
        if(suppress)
          window.focus();
        if (e.preventDefault)
          e.preventDefault();
        return false;
      }
    }
  },

  handleMouseUp:function(e)
  {
    e = InputManager.statics.normalizeEvent(e);
    var pos = InputManager.statics.findEventPosition(e);
    if(e && e.target)
      InputManager.statics.getManager().handleMouseUp(e.target.id, pos);
  },

  handleMouseMove:function(e)
  {
    e = InputManager.statics.normalizeEvent(e);
    var pos = InputManager.statics.findEventPosition(e);
    var im = InputManager.statics.getManager();
    var suppress = im.handleMouseMove(pos);
    if(im.lastMouseDownTarget || suppress)// && im.lastMouseDownTargetDraggable)
      return false;
  },

  handleKeyDown:function(e)
  {
    e = InputManager.statics.normalizeEvent(e);
    if(e && e.keyNumber)
    {
      var suppress = InputManager.statics.getManager().handleKeyPress(e.keyNumber, e.ctrlKey, e.altKey, e.shiftKey, true);
      if(suppress)
      {
        if (e.preventDefault)
          e.preventDefault();
        return false;
      }
    }
  },
  
  handleKeyUp:function(e)
  {
    e = InputManager.statics.normalizeEvent(e);
    if(e && e.keyNumber)
    {
      InputManager.statics.getManager().handleKeyPress(e.keyNumber, e.ctrlKey, e.altKey, e.shiftKey, false);
    }
  },
  
  handleResize:function()
  {
    var width = 0;
    var height = 0;
    if( typeof( window.innerWidth ) == 'number' )
    {
      width = window.innerWidth;
      height = window.innerHeight;
    }
    else if( document.documentElement && ( document.documentElement.clientWidth || document.documentElement.clientHeight ) )
    {
      width = document.documentElement.clientWidth;
      height = document.documentElement.clientHeight;
    }
    else if( document.body && ( document.body.clientWidth || document.body.clientHeight ) )
    {
      width = document.body.clientWidth;
      height = document.body.clientHeight;
    }
    
    InputManager.statics.getManager().handleResize(width, height);
  },
  
  handleScroll:function()
  {
    if (window.scrollX || 0===window.scrollX)
      InputManager.statics.scrollX = window.scrollX;
    else
      InputManager.statics.scrollX = document.documentElement.scrollLeft + document.body.scrollLeft - 2;

    if (window.scrollY || 0===window.scrollY)
      InputManager.statics.scrollY = window.scrollY;
    else
      InputManager.statics.scrollY = document.documentElement.scrollTop + document.body.scrollTop - 2;
    
    InputManager.statics.getManager().handleScroll(InputManager.statics.scrollX, InputManager.statics.scrollY);
  },
  
  init:function()
  {
    InputManager.statics.handleScroll();
    InputManager.statics.handleResize();
  },

  registerEvents:function()
  {
    InputManager.statics.init();
    if(window.addEventListener)
    {
      window.addEventListener('mousedown', InputManager.statics.handleMouseDown, false);
      window.addEventListener('mouseup', InputManager.statics.handleMouseUp, false);
      window.addEventListener('mousemove', InputManager.statics.handleMouseMove, false);
      window.addEventListener('keydown', InputManager.statics.handleKeyDown, false);
      window.addEventListener('keyup', InputManager.statics.handleKeyUp, false);
      window.addEventListener('resize', InputManager.statics.handleResize, false);
      window.addEventListener('scroll', InputManager.statics.handleScroll, false);
    }
    else if(document.attachEvent)
    {
      document.attachEvent('onmousedown', InputManager.statics.handleMouseDown);
      document.attachEvent('onmouseup', InputManager.statics.handleMouseUp);
      document.attachEvent('onmousemove', InputManager.statics.handleMouseMove);
      document.attachEvent('onkeydown', InputManager.statics.handleKeyDown);
      document.attachEvent('onkeyup', InputManager.statics.handleKeyUp);
      window.attachEvent('onresize', InputManager.statics.handleResize);
      window.attachEvent('onscroll', InputManager.statics.handleScroll);
    }
  },

  removeEvents:function()
  {
    if(window.removeEventListener)
    {
      window.removeEventListener('mousedown', InputManager.statics.handleMouseDown, false);
      window.removeEventListener('mouseup', InputManager.statics.handleMouseUp, false);
      window.removeEventListener('mousemove', InputManager.statics.handleMouseMove, false);
      window.removeEventListener('keydown', InputManager.statics.handleKeyDown, false);
      window.removeEventListener('keyup', InputManager.statics.handleKeyUp, false);
      window.removeEventListener('resize', InputManager.statics.handleResize, false);
      window.removeEventListener('scroll', InputManager.statics.handleScroll, false);
    }
    else if(document.detachEvent)
    {
      document.detachEvent('onmousedown', InputManager.statics.handleMouseDown);
      document.detachEvent('onmouseup', InputManager.statics.handleMouseUp);
      document.detachEvent('onmousemove', InputManager.statics.handleMouseMove);
      document.detachEvent('onkeydown', InputManager.statics.handleKeyDown);
      document.detachEvent('onkeyup', InputManager.statics.handleKeyUp);
      window.detachEvent('onresize', InputManager.statics.handleResize);
      window.detachEvent('onscroll', InputManager.statics.handleScroll);
    }
  },
  
  setRecord:function(record)
  {
    var im = InputManager.statics.getManager();
    if(record)
    {
      im.next = InputManager.prototype.nextRecord;
      im.getRandomNumber = InputManager.prototype.getRandomNumberRecord;
    }
    else
    {
      im.next = InputManager.prototype.next;
      im.getRandomNumber = InputManager.prototype.getRandomNumber;
    }
  },
  
  setPlay:function(recording)
  {
    var im = InputManager.statics.getManager();
    im.next = InputManager.prototype.nextPlay;
    im.getRandomNumber = InputManager.prototype.getRandomNumberPlay;
    im.recording = recording;
    im.playPos = {statesPos:0, randomNumbersPos:0};
  },
  
  getRecording:function()
  {
    var im = InputManager.statics.getManager();
    if(0<im.recording.states.length || 0<im.recording.randomNumbers.length)
      return im.recording;
    return null;
  }
}

InputManager.prototype =
{
  next:function()
  {
    // queue a piece of the drag if it is happening
    if(this.lastMouseDownTarget && this.lastMouseDownPosition)
    {
      var distance = Math.pow(this.lastMouseDownPosition.x-this.newWindow.mousePosition.x,2) + Math.pow(this.lastMouseDownPosition.y-this.newWindow.mousePosition.y,2);
      if(this.lastMouseDownTargetDraggable && (this.dragInProcess || 100<distance))
      {
        this.queueDrag(this.lastMouseDownTarget, this.lastMouseDownPosition.x, this.lastMouseDownPosition.y, this.newWindow.mousePosition.x, this.newWindow.mousePosition.y, this.dragInProcess?'middle':'start');
        this.dragInProcess = true;
        this.lastMouseDownPosition = this.newWindow.mousePosition;
      }
    }
    
    // fill the queues
    this.clickQueue = this.newClickQueue;
    this.newClickQueue = new Array();
    
    this.dragQueue = this.newDragQueue;
    this.newDragQueue = new Array();

    this.keyQueue = this.newKeyQueue;
    this.newKeyQueue = new Array();
    
    this.resize = this.newResize;
    this.newResize = false;
  
    this.scroll = this.newScroll;
    this.newScroll = false;
  
    this.mouseMove = this.newMouseMove;
    this.newMouseMove = false;
    
    this.window = this.newWindow; // fixme
    
    var now = new Date();
    this.timePassed = now - this.now;
    this.now = now;
  },
  
  nextRecord:function()
  {
    var keys = {};
    for(var i in this.keys)
      if(this.keys[i])
        keys[i] = true;
    var w = {}
    for(var i in this.window)
      if('mousePosition'==i)
        w[i] = {x:this.window[i].x, y:this.window[i].y};
      else
        w[i] = this.window[i];
    
    InputManager.prototype.next.call(this);
    this.recording.states[this.recording.states.length] = {
      clickQueue:this.clickQueue,
      dragQueue:this.dragQueue,
      keyQueue:this.keyQueue,
      resize:this.resize,
      scroll:this.scroll,
      mouseMove:this.mouseMove,
      window:w,
      timePassed:this.timePassed,
      now:this.now,
      keys:keys
      };
  },
  
  nextPlay:function()
  {
    var state = this.recording.states[this.playPos.statesPos++];
    for(var i in state)
      this[i] = state[i];
  },

  queueClick:function(id, x, y)
  {
    this.newClickQueue.push({id:id,x:x,y:y});
  },

  queueDrag:function(id, x0, y0, x1, y1, type)
  {
    this.newDragQueue.push({id:id,x0:x0,y0:y0,x1:x1,y1:y1,type:type});
  },

  queueKey:function(key, namedKey, ctrl, alt, shift)
  {
    this.newKeyQueue.push({key:key, namedKey:namedKey, ctrl:ctrl, alt:alt, shift:shift});
  },

  handleMouseDown:function(id, pos, draggable)
  {
    this.lastMouseDownTarget = id;
    this.lastMouseDownPosition = pos;
    this.lastMouseDownTargetDraggable = draggable;
    return this.suppressDrag;
  },

  handleMouseUp:function(id, pos)
  {
    if(this.lastMouseDownTarget)
    {
      // determine if we are calling this a drag or a click
      var distance = Math.pow(this.lastMouseDownPosition.x-pos.x,2) + Math.pow(this.lastMouseDownPosition.y-pos.y,2);
      if(this.lastMouseDownTargetDraggable && (this.dragInProcess || 100<distance))
        this.queueDrag(this.lastMouseDownTarget, this.lastMouseDownPosition.x, this.lastMouseDownPosition.y, pos.x, pos.y, this.dragInProcess?'end':'complete');
      else
        this.queueClick(this.lastMouseDownTarget, this.lastMouseDownPosition.x, this.lastMouseDownPosition.y);
    }
    else if(this.lastMouseDownPosition)
      this.queueClick('', this.lastMouseDownPosition.x, this.lastMouseDownPosition.y);
    this.lastMouseDownTarget = null;
    this.lastMouseDownPosition = null;
    this.dragInProcess = false;
  },

  handleMouseMove:function(pos)
  {
    this.newMouseMove = true;
    this.newWindow.mousePosition = pos;
    return this.suppressDrag;
  },
  
  handleKeyPress:function(key, ctrl, alt, shift, keyDown)
  {
    var namedKey = null;
    switch(key)
    {
      case 37: namedKey='left'; break;
      case 63234: namedKey='left'; break;
      case 38: namedKey='up'; break;
      case 63232: namedKey='up'; break;
      case 39: namedKey='right'; break;
      case 63235: namedKey='right'; break;
      case 40: namedKey='down'; break;
      case 63233: namedKey='down'; break;
      case 16: namedKey='shift'; break;
      case 17: namedKey='ctrl'; break;
      case 18: namedKey='alt'; break;
    }
    if(namedKey)
      this.keys[namedKey] = keyDown;
    else
      this.keys[key] = keyDown;
    if(keyDown)
      this.queueKey(key, namedKey, ctrl, alt, shift);
    //Controller.statics.log(key+"|"+namedKey);
    
    if(keyDown && (this.suppressKeys[namedKey] || this.suppressKeys[key]))
      return true;
    return false;
  },
  
  handleResize:function(width, height)
  {
    this.newResize = true;

    this.newWindow.width = width;
    this.newWindow.height = height;
    this.newWindow.bottom = this.newWindow.top + this.newWindow.height;
    this.newWindow.right = this.newWindow.left + this.newWindow.width;
  },

  handleScroll:function(scrollX, scrollY)
  {
    this.newScroll = true;

    this.newWindow.top = scrollY;
    this.newWindow.left = scrollX;
    this.newWindow.bottom = this.newWindow.top + this.newWindow.height;
    this.newWindow.right = this.newWindow.left + this.newWindow.width;
  },

  findSubElementClick:function(idBase)
  {
    var idBaseLength = idBase.length;
    for(var i=0; i<this.clickQueue.length; i++)
    {
      if(idBase==this.clickQueue[i].id.substr(0,idBaseLength))
        return this.clickQueue[i];
    }
    return null;
  },
  
  findSubElementDrag:function(idBase)
  {
    var idBaseLength = idBase.length;
    for(var i=0; i<this.dragQueue.length; i++)
    {
      if(idBase==this.dragQueue[i].id.substr(0,idBaseLength))
        return this.dragQueue[i];
    }
    return null;
  },
  
  getRandomNumber:function(limit)
  {
    if(limit)
      return Math.floor(Math.random()*limit);
    return Math.random();
  },
  
  getRandomNumberRecord:function(limit)
  {
    var n = InputManager.prototype.getRandomNumber.call(this, limit);
    this.recording.randomNumbers[this.recording.randomNumbers.length] = n;
    return n;
  },
  
  getRandomNumberPlay:function(limit)
  {
    return this.recording.randomNumbers[this.playPos.randomNumbersPos++];
  },
  
  donePlay:function()
  {
    if(this.recording.states.length<this.playPos.statesPos)
      return true;
    return false;
  }
}
function PaintManager()
{
  this.paints = new Object();
  
  this.newPaints = new Object();
  this.styleDirtyPaints = new Object();
  this.dirtyPaints = new Object();
  this.removedPaints = new Object();
}

PaintManager.statics =
{
  pm:null,
  
  getManager:function()
  {
    PaintManager.statics.pm = new PaintManager();
    PaintManager.statics.getManager = PaintManager.statics.getManagerSetup;
    return PaintManager.statics.pm;
  },
  
  getManagerSetup:function()
  {
    return PaintManager.statics.pm;
  }
}

PaintManager.prototype =
{
  getPaint:function(id, type, defaultParams)
  {
    //fixme: what to do if requesting something that exists but has the wrong type
    if(!this.paints[id] && type)
    {
      if(this.removedPaints[id])
      {
        this.paints[id].detach();
        delete this.removedPaints[id];
      }
      this.paints[id] = new type(id, this);
      this.newPaints[id] = this.paints[id];
      if(defaultParams)
      {
        for(var i in defaultParams)
          this.paints[id][i].apply(this.paints[id],defaultParams[i]);
      }
    }
    return this.paints[id];
  },
  
  removePaint:function(id)
  {
    if(this.paints[id])
      this.removedPaints[id] = true;
  },
  
  setStyleDirty:function(id)
  {
    if(this.newPaints[id])
      return;
    this.styleDirtyPaints[id] = true;
  },
  
  setDirty:function(id)
  {
    if(this.newPaints[id])
      return;
    this.dirtyPaints[id] = true;
  },
  
  paint:function(t)
  {
    if(this.painting)
      return false;
    this.painting = true;

    for (var id in this.newPaints)
      this.paints[id].attach();
    this.newPaints = new Object();

    for (var id in this.styleDirtyPaints)
      this.paints[id].updateStyle();
    this.styleDirtyPaints = new Object();

    for (var id in this.dirtyPaints)
      this.paints[id].update();
    this.dirtyPaints = new Object();

    for (var id in this.removedPaints)
    {
      this.paints[id].detach();
      delete this.paints[id];
    }
    this.removedPaints = new Object();

    this.painting = false;
  }
}
function CacheManager()
{
  this.images = {};
  this.outstanding = 0;
}

CacheManager.statics =
{
  c:null,
  
  getManager:function()
  {
    CacheManager.statics.c = new CacheManager();
    CacheManager.statics.getManager = CacheManager.statics.getManagerSetup;
    return CacheManager.statics.c;
  },
  
  getManagerSetup:function()
  {
    return CacheManager.statics.c;
  },
  
  doCache:function()
  {
    var cm = CacheManager.statics.getManager();
    
    //fixme: dislay progress or something
    
    if(0<cm.outstanding)
      return false;
    return true;
  },
  
  imageLoaded:function()
  {
    var cm = CacheManager.statics.getManager();
    var url = this.getAttribute('imageURL');
    --cm.outstanding;
  }
}

CacheManager.prototype =
{
  addImage:function(url)
  {
    if(!this.images[url])
    {
      this.images[url] = true;
      this.cacheImage(url);
    }
    return this;
  },
  
  addImages:function(urls)
  {
    for(var i=0; i<urls.length; i++)
      this.addImage(urls[i]);
    return this;
  },
  
  addScaleSpriteImages:function(frames)
  {
    for(var i=0; i<frames.length; i++)
      this.addImages(frames[i]);
    return this;
  },
  
  cacheImage:function(url)
  {
    ++this.outstanding;
    var img = new Image();
    img.setAttribute('imageURL', url);
    if(img.addEventListener)
      img.addEventListener('load', CacheManager.statics.imageLoaded, false);
    else
    {
      // fixme: make this work in ie
      --this.outstanding;
      //if(document.attachEvent)
        //img.attachEvent('onload', CacheManager.statics.imageLoaded);
    }
    img.src = Controller.statics.path + url;
  }
}
function Ball(id, manager)
{
  this.id = id;
  this.manager = manager;
  this.draggable = false;
  this.style = ['left:',0,'px;top:',0,'px;z-index:',1000,';width:',10,'px;height:',10,'px;background-color:','#336699',';'];
}
Ball.prototype =
{
  attach:function()
  {
    var b = document.createElement('div');
    b.id = this.id;
    b.className = 'ball';
    document.body.appendChild(b);
    
    this.update();
    this.updateStyle();
  },

  detach:function()
  {
    var b = document.getElementById(this.id);
    b.parentNode.removeChild(b);
  },

  updateStyle:function()
  {
    var b = document.getElementById(this.id);
    b.style.cssText = this.style.join('');
  },

  update:function()
  {
    var b = document.getElementById(this.id);
    b.setAttribute('draggable',this.draggable?'1':'0');
  },

  setPosition:function(point)
  {
    var x = Math.round(point.x - this.style[7]/2);
    var y = Math.round(point.y - this.style[7]/2);
    var z = 1000 + Math.round(point.z);
    if(this.style[1] != x || this.style[3] != y || this.style[5] != z)
    {
      this.manager.setStyleDirty(this.id);
      this.style[1] = x
      this.style[3] = y
      this.style[5] = z
    }
    return this;
  },

  setSize:function(size)
  {
    if(this.style[7] != size)
    {
      this.manager.setStyleDirty(this.id);
      this.style[7] = size;
      this.style[9] = size;
    }
    return this;
  },

  setColor:function(color)
  {
    if(this.style[11] != color)
    {
      this.manager.setStyleDirty(this.id);
      this.style[11] = color;
    }
    return this;
  },

  setDraggable:function(d)
  {
    if(this.draggable != d)
    {
      this.manager.setDirty(this.id);
      this.draggable = d;
    }
    return this;
  }
}
function Controller()
{
  this.systemElements = new Object(); // fixme: switch to arrays to avoid for in?
  this.elements = new Object();
  this.timeoutId = null;
  
  var temp = new Matrix4();
  temp.setAngles({x:-Math.PI/4, y:0, z:0});
  this.perspectiveMatrix = temp.getMatrix();
  
  this.debugOutputId = null;
  this.fps = 24;
}

Controller.statics =
{
  path:'',
  c:null,
  
  getController:function()
  {
    Controller.statics.c = new Controller();
    Controller.statics.getController = Controller.statics.getControllerSetup;
    return Controller.statics.c;
  },
  
  getControllerSetup:function()
  {
    return Controller.statics.c;
  },
  
  process:function()
  {
    Controller.statics.getController().process();
  },
  
  processPlay:function()
  {
    Controller.statics.getController().processPlay();
  },
  
  log:function(message)
  {
    var target = document.getElementById('log');
    if(target)
    {
      var m = document.createElement('div');
      m.innerHTML = message
      target.appendChild(m);
    }
  },
  
  serialize:function(target)
  {
    if('number'==typeof target && isFinite(target))
      return target;
    if('string'==typeof target)
      return '\'' + target.split('\'').join('\\\'') + '\'';
    if('boolean'==typeof target)
      return target?'true':'false';
    if('object'==typeof target && !target)
      return 'null';
    if(target && 'object'==typeof target && Date==target.constructor)
      return 'new Date('+target.valueOf()+')'
    if(target && 'object'==typeof target && Array==target.constructor)
    {
      var o=[], op=0;
      for(var i=0; i<target.length; i++)
        o[op++] = Controller.statics.serialize(target[i]);
      return '['+o.join(',')+']';
    }
    if(target && 'object'==typeof target)
    {
      var o=[], op=0;
      for(var i in target)
        o[op++] = i + ':' + Controller.statics.serialize(target[i]);
      return '{'+o.join(',')+'}';
    }
    return 'fixme: ' + (typeof target);
  }
}
$p = Controller.statics.log;  // lazy, clean this up

Controller.prototype =
{
  addSystemElement:function(element)
  {
    this.systemElements[element.id] = element;
    if(element.setPerspectiveMatrix)
      element.setPerspectiveMatrix(this.perspectiveMatrix);
  },
  
  addElement:function(element)
  {
    this.elements[element.id] = element;
    if(element.setPerspectiveMatrix)
      element.setPerspectiveMatrix(this.perspectiveMatrix);
  },
  
  removeSystemElement:function(element)
  {
    if(this.systemElements[element.id])
    {
      this.systemElements[element.id].detach();
      delete this.systemElements[element.id];
    }
  },
  
  removeElement:function(element)
  {
    if(this.elements[element.id])
    {
      this.elements[element.id].detach();
      delete this.elements[element.id];
    }
  },
  
  start:function(record)
  {
    if(!this.timeoutId)
    {
      if(CacheManager.statics.doCache())
      {
        InputManager.statics.registerEvents();
        InputManager.statics.setRecord(record?true:false);
        this.frameTime = 1000/this.fps;
        this.timeoutId = window.setInterval(Controller.statics.process, this.frameTime);
        window.t0 = new Date();  // fixme: Clean this shit up. Why was this even doing this to begin with?
        window.fps = [];
      }
      else
        window.setTimeout(function(){Controller.statics.getController().start();},100);
    }
  },
  
  stop:function()
  {
    if(this.timeoutId)
    {
      InputManager.statics.removeEvents(window);
      window.clearTimeout(this.timeoutId);
      var r = InputManager.statics.getRecording();
      if(r)
      {
        // cheesy output of the recording
        var w = window.open('about:blank','RecordingOutput');
        window.setTimeout(function()
          {
            var o=[], op=0;
            o[op++] = 'var recording = {states:[\n';
            for(var i=0; i<r.states.length; i++)
            {
              var state = r.states[i];
              var s = [];
              for(var j in state)
                s[s.length] = j+':'+Controller.statics.serialize(state[j]);
              o[op++] = '{'; 
              o[op++] = s.join(',');
              o[op++] = '},\n';
            }
            o[op++] = '],randomNumbers:[';
            var n = [];
            for(var i=0; i<r.randomNumbers.length; i++)
            {
              n[n.length] = r.randomNumbers[i];
            }
            o[op++] = n.join(',');
            o[op++] = ']};';
            var pre = w.document.createElement('pre');
            pre.appendChild(w.document.createTextNode(o.join('')));
            w.document.body.innerHTML = '';
            w.document.body.appendChild(pre);
          },
          1000);
      }
    }
    this.timeoutId = null;
  },
  
  process:function()
  {
    var t0 = new Date();
    
    var im = InputManager.statics.getManager();
    im.next();
    
    for (var i in this.systemElements)
      this.systemElements[i].process(im);
    for (var i in this.elements)
      this.elements[i].process(im);
    
    var t1 = new Date();
    
    for (var i in this.systemElements)
      this.systemElements[i].paint();
    for (var i in this.elements)
      this.elements[i].paint();
    PaintManager.statics.getManager().paint();
    
    var t2 = new Date();
    
    var updateTime = (t1-t0)/1000;
    var paintTime = (t2-t1)/1000;
    var frameTime = (new Date()-window.t0)/1000;
    window.fps.push({f:1/frameTime,u:updateTime,p:paintTime});
    window.t0 = new Date();
    
    // adjust frameTime to compensate for some browsers
    if(5<window.fps.length)
    {
      var avgFps = (window.fps[0].f + window.fps[1].f + window.fps[2].f + window.fps[3].f + window.fps[4].f)/5;
      var avgU = (window.fps[0].u + window.fps[1].u + window.fps[2].u + window.fps[3].u + window.fps[4].u)/5;
      var avgP = (window.fps[0].p + window.fps[1].p + window.fps[2].p + window.fps[3].p + window.fps[4].p)/5;
      var avgT = avgU + avgP;
      var fd = this.fps-avgFps;
      if(2<fd)
      {
        if(5<this.frameTime && avgT<this.frameTime)
        {
          this.frameTime -= 5;
          window.clearTimeout(this.timeoutId);
          this.timeoutId = window.setInterval(Controller.statics.process, this.frameTime);
        }
      }
      else if(-3>fd)
      {
        this.frameTime += 5;
        window.clearTimeout(this.timeoutId);
        this.timeoutId = window.setInterval(Controller.statics.process, this.frameTime);
      }
      this.setDebugMessage(avgU.toFixed(3) + ' + ' + avgP.toFixed(3) + ' = ' + avgT.toFixed(3) + '<br />' + avgFps.toFixed(2) + ' (' + this.frameTime +')');
      window.fps = [];
    }
  },
  
  startPlay:function(recording)
  {
    if(!this.timeoutId)
    {
      if(CacheManager.statics.doCache())
      {
        InputManager.statics.setPlay(recording);
        this.timeoutId = window.setInterval(Controller.statics.processPlay, 1);
        window.t0 = new Date();
      }
      else
        window.setTimeout(function(){Controller.statics.getController().start();},100);
    }
  },
  
  processPlay:function()
  {
    var im = InputManager.statics.getManager();
    im.next();
    
    for (var i in this.systemElements)
      this.systemElements[i].process(im);
    for (var i in this.elements)
      this.elements[i].process(im);
    
    for (var i in this.systemElements)
      this.systemElements[i].paint();
    for (var i in this.elements)
      this.elements[i].paint();
    PaintManager.statics.getManager().paint();
    
    if(im.donePlay())
      this.stopPlay();
  },
  
  stopPlay:function()
  {
    if(this.timeoutId)
    {
      window.clearTimeout(this.timeoutId);
      window.t1 = new Date();
      alert('Completed in ' + (window.t1 - window.t0)/1000 + ' seconds.');
    }
  },
  
  setPerspectiveMatrix:function(p)
  {
    this.perspectiveMatrix = p;
    for (var i in this.systemElements)
      if(this.systemElements[i].setPerspectiveMatrix)
        this.systemElements[i].setPerspectiveMatrix(this.perspectiveMatrix);
    for (var i in this.elements)
      if(this.elements[i].setPerspectiveMatrix)
        this.elements[i].setPerspectiveMatrix(this.perspectiveMatrix);
  },
  
  setDebugOutputId:function(id)
  {
    this.debugOutputId = id;
  },
  
  setDebugMessage:function(message)
  {
    if(this.debugOutputId)
    {
      var target = document.getElementById(this.debugOutputId);
      if(target)
        target.innerHTML = message;
    }
  }
}
function Matrix4()
{
  this.x = 0;
  this.y = 0;
  this.z = 0;
  
  this.xAngle = 0;
  this.yAngle = 0;
  this.zAngle = 0;
  
  this.dirty = false;
  
  this.data = [[1, 0, 0, 0],[0, 1, 0, 0],[0, 0, 1, 0],[0, 0, 0, 1]];
}
Matrix4.statics = 
{
  trasformPoint:function(m, point)
  {
    var tx = m[0][0]*point.x + m[0][1]*point.y + m[0][2]*point.z + m[0][3];
    var ty = m[1][0]*point.x + m[1][1]*point.y + m[1][2]*point.z + m[1][3];
    var tz = m[2][0]*point.x + m[2][1]*point.y + m[2][2]*point.z + m[2][3];
    return {x:tx, y:ty, z:tz};
  },
  
  untrasformPoint:function(m, point)
  {
    var det =
      (m[0][3] * m[1][2] * m[2][1] * m[3][0])-(m[0][2] * m[1][3] * m[2][1] * m[3][0])-(m[0][3] * m[1][1] * m[2][2] * m[3][0])+(m[0][1] * m[1][3] * m[2][2] * m[3][0])+
      (m[0][2] * m[1][1] * m[2][3] * m[3][0])-(m[0][1] * m[1][2] * m[2][3] * m[3][0])-(m[0][3] * m[1][2] * m[2][0] * m[3][1])+(m[0][2] * m[1][3] * m[2][0] * m[3][1])+
      (m[0][3] * m[1][0] * m[2][2] * m[3][1])-(m[0][0] * m[1][3] * m[2][2] * m[3][1])-(m[0][2] * m[1][0] * m[2][3] * m[3][1])+(m[0][0] * m[1][2] * m[2][3] * m[3][1])+
      (m[0][3] * m[1][1] * m[2][0] * m[3][2])-(m[0][1] * m[1][3] * m[2][0] * m[3][2])-(m[0][3] * m[1][0] * m[2][1] * m[3][2])+(m[0][0] * m[1][3] * m[2][1] * m[3][2])+
      (m[0][1] * m[1][0] * m[2][3] * m[3][2])-(m[0][0] * m[1][1] * m[2][3] * m[3][2])-(m[0][2] * m[1][1] * m[2][0] * m[3][3])+(m[0][1] * m[1][2] * m[2][0] * m[3][3])+
      (m[0][2] * m[1][0] * m[2][1] * m[3][3])-(m[0][0] * m[1][2] * m[2][1] * m[3][3])-(m[0][1] * m[1][0] * m[2][2] * m[3][3])+(m[0][0] * m[1][1] * m[2][2] * m[3][3]);
   /*
    var o = [[1, 0, 0, 0],[0, 1, 0, 0],[0, 0, 1, 0],[0, 0, 0, 1]];
     o[0][0] = (m[1][2]*m[2][3]*m[3][1] - m[1][3]*m[2][2]*m[3][1] + m[1][3]*m[2][1]*m[3][2] - m[1][1]*m[2][3]*m[3][2] - m[1][2]*m[2][1]*m[3][3] + m[1][1]*m[2][2]*m[3][3])/det;
     o[0][1] = (m[0][3]*m[2][2]*m[3][1] - m[0][2]*m[2][3]*m[3][1] - m[0][3]*m[2][1]*m[3][2] + m[0][1]*m[2][3]*m[3][2] + m[0][2]*m[2][1]*m[3][3] - m[0][1]*m[2][2]*m[3][3])/det;
     o[0][2] = (m[0][2]*m[1][3]*m[3][1] - m[0][3]*m[1][2]*m[3][1] + m[0][3]*m[1][1]*m[3][2] - m[0][1]*m[1][3]*m[3][2] - m[0][2]*m[1][1]*m[3][3] + m[0][1]*m[1][2]*m[3][3])/det;
     o[0][3] = (m[0][3]*m[1][2]*m[2][1] - m[0][2]*m[1][3]*m[2][1] - m[0][3]*m[1][1]*m[2][2] + m[0][1]*m[1][3]*m[2][2] + m[0][2]*m[1][1]*m[2][3] - m[0][1]*m[1][2]*m[2][3])/det;
     o[1][0] = (m[1][3]*m[2][2]*m[3][0] - m[1][2]*m[2][3]*m[3][0] - m[1][3]*m[2][0]*m[3][2] + m[1][0]*m[2][3]*m[3][2] + m[1][2]*m[2][0]*m[3][3] - m[1][0]*m[2][2]*m[3][3])/det;
     o[1][1] = (m[0][2]*m[2][3]*m[3][0] - m[0][3]*m[2][2]*m[3][0] + m[0][3]*m[2][0]*m[3][2] - m[0][0]*m[2][3]*m[3][2] - m[0][2]*m[2][0]*m[3][3] + m[0][0]*m[2][2]*m[3][3])/det;
     o[1][2] = (m[0][3]*m[1][2]*m[3][0] - m[0][2]*m[1][3]*m[3][0] - m[0][3]*m[1][0]*m[3][2] + m[0][0]*m[1][3]*m[3][2] + m[0][2]*m[1][0]*m[3][3] - m[0][0]*m[1][2]*m[3][3])/det;
     o[1][3] = (m[0][2]*m[1][3]*m[2][0] - m[0][3]*m[1][2]*m[2][0] + m[0][3]*m[1][0]*m[2][2] - m[0][0]*m[1][3]*m[2][2] - m[0][2]*m[1][0]*m[2][3] + m[0][0]*m[1][2]*m[2][3])/det;
     o[2][0] = (m[1][1]*m[2][3]*m[3][0] - m[1][3]*m[2][1]*m[3][0] + m[1][3]*m[2][0]*m[3][1] - m[1][0]*m[2][3]*m[3][1] - m[1][1]*m[2][0]*m[3][3] + m[1][0]*m[2][1]*m[3][3])/det;
     o[2][1] = (m[0][3]*m[2][1]*m[3][0] - m[0][1]*m[2][3]*m[3][0] - m[0][3]*m[2][0]*m[3][1] + m[0][0]*m[2][3]*m[3][1] + m[0][1]*m[2][0]*m[3][3] - m[0][0]*m[2][1]*m[3][3])/det;
     o[2][2] = (m[0][1]*m[1][3]*m[3][0] - m[0][3]*m[1][1]*m[3][0] + m[0][3]*m[1][0]*m[3][1] - m[0][0]*m[1][3]*m[3][1] - m[0][1]*m[1][0]*m[3][3] + m[0][0]*m[1][1]*m[3][3])/det;
     o[2][3] = (m[0][3]*m[1][1]*m[2][0] - m[0][1]*m[1][3]*m[2][0] - m[0][3]*m[1][0]*m[2][1] + m[0][0]*m[1][3]*m[2][1] + m[0][1]*m[1][0]*m[2][3] - m[0][0]*m[1][1]*m[2][3])/det;
     o[3][0] = (m[1][2]*m[2][1]*m[3][0] - m[1][1]*m[2][2]*m[3][0] - m[1][2]*m[2][0]*m[3][1] + m[1][0]*m[2][2]*m[3][1] + m[1][1]*m[2][0]*m[3][2] - m[1][0]*m[2][1]*m[3][2])/det;
     o[3][1] = (m[0][1]*m[2][2]*m[3][0] - m[0][2]*m[2][1]*m[3][0] + m[0][2]*m[2][0]*m[3][1] - m[0][0]*m[2][2]*m[3][1] - m[0][1]*m[2][0]*m[3][2] + m[0][0]*m[2][1]*m[3][2])/det;
     o[3][2] = (m[0][2]*m[1][1]*m[3][0] - m[0][1]*m[1][2]*m[3][0] - m[0][2]*m[1][0]*m[3][1] + m[0][0]*m[1][2]*m[3][1] + m[0][1]*m[1][0]*m[3][2] - m[0][0]*m[1][1]*m[3][2])/det;
     o[3][3] = (m[0][1]*m[1][2]*m[2][0] - m[0][2]*m[1][1]*m[2][0] + m[0][2]*m[1][0]*m[2][1] - m[0][0]*m[1][2]*m[2][1] - m[0][1]*m[1][0]*m[2][2] + m[0][0]*m[1][1]*m[2][2])/det;

     return Matrix4.statics.trasformPoint(o, point);
/**/
    var tx = 
      (
        point.x*(m[1][2]*m[2][3]*m[3][1] - m[1][3]*m[2][2]*m[3][1] + m[1][3]*m[2][1]*m[3][2] - m[1][1]*m[2][3]*m[3][2] - m[1][2]*m[2][1]*m[3][3] + m[1][1]*m[2][2]*m[3][3])
        + point.y*(m[0][3]*m[2][2]*m[3][1] - m[0][2]*m[2][3]*m[3][1] - m[0][3]*m[2][1]*m[3][2] + m[0][1]*m[2][3]*m[3][2] + m[0][2]*m[2][1]*m[3][3] - m[0][1]*m[2][2]*m[3][3])
        + point.z*(m[0][2]*m[1][3]*m[3][1] - m[0][3]*m[1][2]*m[3][1] + m[0][3]*m[1][1]*m[3][2] - m[0][1]*m[1][3]*m[3][2] - m[0][2]*m[1][1]*m[3][3] + m[0][1]*m[1][2]*m[3][3])
        + (m[0][3]*m[1][2]*m[2][1] - m[0][2]*m[1][3]*m[2][1] - m[0][3]*m[1][1]*m[2][2] + m[0][1]*m[1][3]*m[2][2] + m[0][2]*m[1][1]*m[2][3] - m[0][1]*m[1][2]*m[2][3])
      )/det;
    var ty = 
      (
        point.x*(m[1][3]*m[2][2]*m[3][0] - m[1][2]*m[2][3]*m[3][0] - m[1][3]*m[2][0]*m[3][2] + m[1][0]*m[2][3]*m[3][2] + m[1][2]*m[2][0]*m[3][3] - m[1][0]*m[2][2]*m[3][3])
        + point.y*(m[0][2]*m[2][3]*m[3][0] - m[0][3]*m[2][2]*m[3][0] + m[0][3]*m[2][0]*m[3][2] - m[0][0]*m[2][3]*m[3][2] - m[0][2]*m[2][0]*m[3][3] + m[0][0]*m[2][2]*m[3][3])
        + point.z*(m[0][3]*m[1][2]*m[3][0] - m[0][2]*m[1][3]*m[3][0] - m[0][3]*m[1][0]*m[3][2] + m[0][0]*m[1][3]*m[3][2] + m[0][2]*m[1][0]*m[3][3] - m[0][0]*m[1][2]*m[3][3])
        + (m[0][2]*m[1][3]*m[2][0] - m[0][3]*m[1][2]*m[2][0] + m[0][3]*m[1][0]*m[2][2] - m[0][0]*m[1][3]*m[2][2] - m[0][2]*m[1][0]*m[2][3] + m[0][0]*m[1][2]*m[2][3])
      )/det;
    var tz = 
      (
        point.x*(m[1][1]*m[2][3]*m[3][0] - m[1][3]*m[2][1]*m[3][0] + m[1][3]*m[2][0]*m[3][1] - m[1][0]*m[2][3]*m[3][1] - m[1][1]*m[2][0]*m[3][3] + m[1][0]*m[2][1]*m[3][3])
        + point.y*(m[0][3]*m[2][1]*m[3][0] - m[0][1]*m[2][3]*m[3][0] - m[0][3]*m[2][0]*m[3][1] + m[0][0]*m[2][3]*m[3][1] + m[0][1]*m[2][0]*m[3][3] - m[0][0]*m[2][1]*m[3][3])
        + point.z*(m[0][1]*m[1][3]*m[3][0] - m[0][3]*m[1][1]*m[3][0] + m[0][3]*m[1][0]*m[3][1] - m[0][0]*m[1][3]*m[3][1] - m[0][1]*m[1][0]*m[3][3] + m[0][0]*m[1][1]*m[3][3])
        + (m[0][3]*m[1][1]*m[2][0] - m[0][1]*m[1][3]*m[2][0] - m[0][3]*m[1][0]*m[2][1] + m[0][0]*m[1][3]*m[2][1] + m[0][1]*m[1][0]*m[2][3] - m[0][0]*m[1][1]*m[2][3])
      )/det;
    return {x:tx, y:ty, z:tz};
  }
}
Matrix4.prototype = 
{
  setOffset:function(point)
  {
    this.x = point.x;
    this.y = point.y;
    this.z = point.z;
    
    this.dirty = true;
    return this;
  },
  
  setOffsetX:function(x)
  {
    this.x = x;
    this.dirty = true;
    return this;
  },
  
  setOffsetY:function(y)
  {
    this.y = y;
    this.dirty = true;
    return this;
  },
  
  setOffsetZ:function(z)
  {
    this.z = z;
    this.dirty = true;
    return this;
  },
  
  getOffset:function()
  {
    return {x:this.x, y:this.y, z:this.z};
  },
  
  setAngles:function(angles)
  {
    this.xAngle = angles.x;
    this.yAngle = angles.y;
    this.zAngle = angles.z;
    
    this.dirty = true;
    return this;
  },
  
  setAngleX:function(x)
  {
    this.xAngle = x;
    this.dirty = true;
    return this;
  },
  
  setAngleY:function(y)
  {
    this.yAngle = y;
    this.dirty = true;
    return this;
  },
  
  setAngleZ:function(z)
  {
    this.zAngle = z;
    this.dirty = true;
    return this;
  },
  
  getAngles:function()
  {
    return {x:this.xAngle, y:this.yAngle, z:this.zAngle};
  },
  
  calculateMatrix:function()
  {
    if(this.dirty)
    {
      var cx = Math.cos(-this.xAngle);
      var sx = Math.sin(-this.xAngle);
      var cy = Math.cos(-this.yAngle);
      var sy = Math.sin(-this.yAngle);
      var cz = Math.cos(-this.zAngle);
      var sz = Math.sin(-this.zAngle);
      
      this.data[0][0] = cz*cy - sz*sx*sy;
      this.data[0][1] = -sz*cx;
      this.data[0][2] = cz*sy + sz*sx*cy;
      this.data[0][3] = this.x;
      
      this.data[1][0] = sz*cy + cz*sx*sy;
      this.data[1][1] = cz*cx;
      this.data[1][2] = sz*sy - cz*sx*cy;
      this.data[1][3] = this.y;
      
      this.data[2][0] = -cx*sy;
      this.data[2][1] = sx;
      this.data[2][2] = cx*cy;
      this.data[2][3] = this.z;
      
      this.data[3][0] = 0;
      this.data[3][1] = 0;
      this.data[3][2] = 0;
      this.data[3][3] = 1;
      
      this.dirty = false;
    }
  },
  
  multiplyLeft:function(m)
  {
    this.calculateMatrix();
    
    var o = [[1, 0, 0, 0],[0, 1, 0, 0],[0, 0, 1, 0],[0, 0, 0, 1]];
    
    o[0][0] = m[0][0]*this.data[0][0] + m[0][1]*this.data[1][0] + m[0][2]*this.data[2][0] + m[0][3]*this.data[3][0];
    o[0][1] = m[0][0]*this.data[0][1] + m[0][1]*this.data[1][1] + m[0][2]*this.data[2][1] + m[0][3]*this.data[3][1];
    o[0][2] = m[0][0]*this.data[0][2] + m[0][1]*this.data[1][2] + m[0][2]*this.data[2][2] + m[0][3]*this.data[3][2];
    o[0][3] = m[0][0]*this.data[0][3] + m[0][1]*this.data[1][3] + m[0][2]*this.data[2][3] + m[0][3]*this.data[3][3];
    
    o[1][0] = m[1][0]*this.data[0][0] + m[1][1]*this.data[1][0] + m[1][2]*this.data[2][0] + m[1][3]*this.data[3][0];
    o[1][1] = m[1][0]*this.data[0][1] + m[1][1]*this.data[1][1] + m[1][2]*this.data[2][1] + m[1][3]*this.data[3][1];
    o[1][2] = m[1][0]*this.data[0][2] + m[1][1]*this.data[1][2] + m[1][2]*this.data[2][2] + m[1][3]*this.data[3][2];
    o[1][3] = m[1][0]*this.data[0][3] + m[1][1]*this.data[1][3] + m[1][2]*this.data[2][3] + m[1][3]*this.data[3][3];
    
    o[2][0] = m[2][0]*this.data[0][0] + m[2][1]*this.data[1][0] + m[2][2]*this.data[2][0] + m[2][3]*this.data[3][0];
    o[2][1] = m[2][0]*this.data[0][1] + m[2][1]*this.data[1][1] + m[2][2]*this.data[2][1] + m[2][3]*this.data[3][1];
    o[2][2] = m[2][0]*this.data[0][2] + m[2][1]*this.data[1][2] + m[2][2]*this.data[2][2] + m[2][3]*this.data[3][2];
    o[2][3] = m[2][0]*this.data[0][3] + m[2][1]*this.data[1][3] + m[2][2]*this.data[2][3] + m[2][3]*this.data[3][3];
    
    o[3][0] = m[3][0]*this.data[0][0] + m[3][1]*this.data[1][0] + m[3][2]*this.data[2][0] + m[3][3]*this.data[3][0];
    o[3][1] = m[3][0]*this.data[0][1] + m[3][1]*this.data[1][1] + m[3][2]*this.data[2][1] + m[3][3]*this.data[3][1];
    o[3][2] = m[3][0]*this.data[0][2] + m[3][1]*this.data[1][2] + m[3][2]*this.data[2][2] + m[3][3]*this.data[3][2];
    o[3][3] = m[3][0]*this.data[0][3] + m[3][1]*this.data[1][3] + m[3][2]*this.data[2][3] + m[3][3]*this.data[3][3];

    return o;
  },
  
  getMatrix:function()
  {
    this.calculateMatrix();
    return this.data;
  },
  
  isDirty:function()
  {
    return this.dirty;
  },
  
  print:function()
  {
    this.calculateMatrix();
    
    alert('['+this.data[0][0]+']['+this.data[0][1]+']['+this.data[0][2]+']['+this.data[0][3]+']\n['+
    this.data[1][0]+']['+this.data[1][1]+']['+this.data[1][2]+']['+this.data[1][3]+']\n['+
    this.data[2][0]+']['+this.data[2][1]+']['+this.data[2][2]+']['+this.data[2][3]+']\n['+
    this.data[3][0]+']['+this.data[3][1]+']['+this.data[3][2]+']['+this.data[3][3]+']');
  }
}
function Model(id, data)
{
  this.id = id;

  this.transformation = new Matrix4();
  this.parentTransformation = null;
  
  this.nameBallMap = new Object();

  this.dirty = true;

  this.data = data;
  this.transformedData = new Array();

  this.joints = new Object();
}
Model.prototype = 
{
  attach:function(node)
  {
    var pm = PaintManager.statics.getManager();
    for(var i=0; i<this.data.length; i++)
    {
      var b = pm.getPaint(this.id+'_ball'+i, Ball);
      b.setColor(this.data[i].color);
      b.setSize(this.data[i].size);
      if(this.data[i].name)
         this.nameBallMap[this.data[i].name] = i;
    }
  },
  
  detach:function()
  {
    var pm = PaintManager.statics.getManager();
    for(var i=0; i<this.data.length; i++)
      pm.removePaint(this.id+'_ball'+i);
    this.nameBallMap = new Object();
  },

  process:function(im)
  {
    if(this.dirty)
    {
      var t = null;
      if(this.parentTransformation)
        t = this.transformation.multiplyLeft(this.parentTransformation);
      else
        t = this.transformation.getMatrix();
      
      for(var i=0; i<this.data.length; i++)
      {
        this.transformedData[i] = Matrix4.statics.trasformPoint(t, this.data[i]);
      }
    }
  },

  paint:function()
  {
    if(this.dirty)
    {
      var pm = PaintManager.statics.getManager();
      for(var i=0; i<this.data.length; i++)
      {
        var b = pm.getPaint(this.id+'_ball'+i);
        b.setPosition(this.transformedData[i]);
      }
      this.dirty = false;
    }
  },

  setPosition:function(point)
  {
    this.transformation.setOffset(point);
    this.dirty = true;
    return this;
  },

  setPostionX:function(x)
  {
    this.transformation.setOffsetX(x);
    this.dirty = true;
    return this;
  },

  setPostionY:function(y)
  {
    this.transformation.setOffsetY(y);
    this.dirty = true;
    return this;
  },

  setPostionZ:function(z)
  {
    this.transformation.setOffsetZ(z);
    this.dirty = true;
    return this;
  },

  getPosition:function()
  {
    return this.transformation.getOffset();
  },

  setAngles:function(angles)
  {
    this.transformation.setAngles(angles);
    this.dirty = true;
    return this;
  },

  setAngleX:function(x)
  {
    this.transformation.setAngleX(x);
    this.dirty = true;
    return this;
  },

  setAngleY:function(y)
  {
    this.transformation.setAngleY(y);
    this.dirty = true;
    return this;
  },

  setAngleZ:function(z)
  {
    this.transformation.setAngleZ(z);
    this.dirty = true;
    return this;
  },

  getAngles:function()
  {
    return this.transformation.getAngles();
  },

  setParentTransformation:function(m)
  {
    this.parentTransformation = m;
    this.dirty = true;
    return this;
  },

  addJoint:function(name, point)
  {
    this.joints[name] = point;
  },

  getJointPosition:function(name)
  {
    var t = this.transformation.getMatrix();
    return Matrix4.statics.trasformPoint(t, this.joints[name]);
  },
  
  getNamedBall:function(name)
  {
    var pm = PaintManager.statics.getManager();
    var name = this.id+'_ball'+this.nameBallMap[name]
    return pm.getPaint(name);
  },

  setDraggable:function(d)
  {
    var pm = PaintManager.statics.getManager();
    for(var i=0; i<this.data.length; i++)
      pm.getPaint(this.id+'_ball'+i, Ball).setDraggable(d);
    return this;
  }
}
function Butterfly(id)
{
  this.id = id;

  this.transformation = new Matrix4();
  this.perspectiveMatrix = new Matrix4();

  this.dirty = true;

  this.activity = Butterfly.statics.activities.rest;
  this.destination = {x:0,y:0,z:0};

  this.body = new Model(id+'_body', Butterfly.statics.body);
  this.leftWing = new Model(id+'_leftWing', Butterfly.statics.wing);
  this.rightWing = new Model(id+'_rightWing', Butterfly.statics.wing);
  
  this.flapOffset = 0;
}
Butterfly.statics =
{
  activities:{resting:0, flying:1, dragged:2},
  
  body:[
    {x:15, y:0, z:0, color:'#512805', size:14},
    {x:3, y:0, z:0, color:'#512805', size:14},
    {x:-5, y:0, z:0, color:'#512805', size:18},
    {x:-13, y:0, z:0, color:'#512805', size:18},
    {x:-21, y:0, z:0, color:'#512805', size:14},
    {x:-27, y:0, z:0, color:'#512805', size:8}
  ],

  wing:[
    {x:0, y:8, z:0, color:'#381500', size:10},
    {x:1, y:13, z:0, color:'#381500', size:10},
    {x:2, y:18, z:0, color:'#381500', size:10},
    {x:3, y:23, z:0, color:'#381500', size:10},
    {x:4, y:28, z:0, color:'#381500', size:10},
    {x:5, y:33, z:0, color:'#381500', size:10},
    {x:5, y:38, z:0, color:'#381500', size:10},
    {x:5, y:43, z:0, color:'#381500', size:10},
    {x:4, y:48, z:0, color:'#381500', size:10},
    {x:2, y:53, z:0, color:'#381500', size:10},
    {x:-2, y:57, z:0, color:'#381500', size:10},
    {x:-7, y:54, z:0, color:'#381500', size:10},
    {x:-11, y:50, z:0, color:'#381500', size:10},
    {x:-14, y:46, z:0, color:'#381500', size:10},
    {x:-18, y:41, z:0, color:'#381500', size:10},
    {x:-23, y:36, z:0, color:'#381500', size:10},
    {x:-28, y:32, z:0, color:'#381500', size:10},
    {x:-33, y:29, z:0, color:'#381500', size:10},
    {x:-38, y:26, z:0, color:'#381500', size:10},
    {x:-43, y:24, z:0, color:'#381500', size:10},
    {x:-49, y:20, z:0, color:'#381500', size:10},
    {x:-45, y:16, z:0, color:'#381500', size:10},
    {x:-39, y:15, z:0, color:'#381500', size:10},
    {x:-33, y:14, z:0, color:'#381500', size:10},
    {x:-27, y:13, z:0, color:'#381500', size:10},
    {x:-21, y:12, z:0, color:'#381500', size:10},
    {x:-16, y:10, z:0, color:'#381500', size:10},

    {x:-8, y:9, z:0, color:'#F55803', size:10},
    {x:-7, y:16, z:0, color:'#F55803', size:10},
    {x:-5, y:23, z:0, color:'#F55803', size:10},
    {x:-3, y:30, z:0, color:'#F55803', size:10},
    {x:-3, y:37, z:0, color:'#F55803', size:10},
    {x:-3, y:43, z:0, color:'#F55803', size:10},
    {x:-4, y:47, z:0, color:'#F55803', size:10},
    {x:-9, y:40, z:0, color:'#F55803', size:10},
    {x:-14, y:35, z:0, color:'#F55803', size:10},
    {x:-20, y:30, z:0, color:'#F55803', size:10},
    {x:-26, y:25, z:0, color:'#F55803', size:10},
    {x:-32, y:22, z:0, color:'#F55803', size:10},
    {x:-38, y:20, z:0, color:'#F55803', size:8},
    {x:-26, y:19, z:0, color:'#F55803', size:10},
    {x:-18, y:18, z:0, color:'#F55803', size:10},
    {x:-12, y:16, z:0, color:'#F55803', size:10},
    {x:-11, y:23, z:0, color:'#F55803', size:8},
    {x:-10, y:31, z:0, color:'#381500', size:10},
    {x:-14, y:26, z:0, color:'#381500', size:10},
    {x:-19, y:23, z:0, color:'#381500', size:8}
  ]
}

Butterfly.prototype =
{
  attach:function(node)
  {
    this.body.attach(node);
    this.leftWing.attach(node);
    this.rightWing.attach(node);
    this.leftWing.setAngleX(Math.PI*9/8);
    this.rightWing.setAngleX(-Math.PI/8);
  },
  
  detach:function()
  {
    this.body.detach();
    this.leftWing.detach();
    this.rightWing.detach();
  },

  process:function(im)
  {
    this.chooseActivity(im);

    switch(this.activity)
    {
      case Butterfly.statics.activities.flying: this.doFly(im); break;
    }

    if(this.dirty)
    {
      var m = this.transformation.multiplyLeft(this.perspectiveMatrix);
      this.body.setParentTransformation(m);
      this.leftWing.setParentTransformation(m);
      this.rightWing.setParentTransformation(m);
      this.dirty = false;
    }

    this.body.process(im);
    this.leftWing.process(im);
    this.rightWing.process(im);
  },

  paint:function()
  {
    this.body.paint();
    this.leftWing.paint();
    this.rightWing.paint();
  },
  
  chooseActivity:function(im)
  {
    if(this.activity==Butterfly.statics.activities.flying)
    {
      var pos = this.transformation.getOffset();
      var h = Math.pow(this.destination.x-pos.x,2) + Math.pow(this.destination.y-pos.y,2);
      if(100>h)
        this.activity = Butterfly.statics.activities.resting;
    }
    else if(this.activity==Butterfly.statics.activities.dragged)
    {
    }
    else
    {
      var screenPosition = Matrix4.statics.trasformPoint(this.perspectiveMatrix, this.transformation);
      if(4096>(Math.pow(im.window.mousePosition.x-screenPosition.x,2) + Math.pow(im.window.mousePosition.y-screenPosition.y,2)))
      {
        var screenPos = {x:im.window.left + im.getRandomNumber(im.window.width), y:im.window.top + im.getRandomNumber(im.window.height)};
        this.destination = InputManager.statics.get3DPosition(screenPos);
        this.activity = Butterfly.statics.activities.flying;
      }
    }
  },
  
  turnTowards:function(im, desiredAngle)
  {
    var currentDir = this.transformation.getAngles().z;
    var step = (Math.PI * im.timePassed)/700;
    var dif = desiredAngle - currentDir;
    if(0>dif)
      dif += 2*Math.PI;
    if(dif>Math.PI)
    {
      currentDir -= step;
      if(0>currentDir)
        currentDir += 2*Math.PI;
    }
    else if(dif<=Math.PI)
    {
      currentDir += step;
      if(2*Math.PI<currentDir)
        currentDir -= 2*Math.PI;
    }
    var difAfter = desiredAngle - currentDir;
    if(0>difAfter)
      difAfter += 2*Math.PI;
    if((dif>Math.PI && difAfter<=Math.PI) || (difAfter>Math.PI && dif<=Math.PI))
      currentDir = desiredAngle;
    this.transformation.setAngleZ(currentDir);
    return currentDir;
  },
  
  doFly:function(im)
  {
    var pos = this.transformation.getOffset();
    var screenPosition = Matrix4.statics.trasformPoint(this.perspectiveMatrix, pos);
    var mouseDistance = Math.pow(im.window.mousePosition.x-screenPosition.x,2) + Math.pow(im.window.mousePosition.y-screenPosition.y,2);

    var speed = 128/Math.sqrt(mouseDistance);
    if(8<speed) speed = 8;
    if(1>speed) speed = 1;
    
    this.animationFlap(im, speed);

    // turn towards pointing the right way
    var desiredAngle = -Math.atan2(pos.y-this.destination.y,pos.x-this.destination.x)+Math.PI;
    var currentDir = this.turnTowards(im, desiredAngle);

    // move closer
    var step = im.timePassed*speed/9;
    var dist = Math.sqrt(Math.pow(this.destination.x-pos.x,2) + Math.pow(this.destination.y-pos.y,2));
    if(step>dist)
      step = dist;
    var nextPos = Matrix4.statics.trasformPoint(this.transformation.getMatrix(),{x:step, y:0, z:0});
    this.transformation.setOffsetX(nextPos.x);
    this.transformation.setOffsetY(nextPos.y);
    
    this.dirty = true;
  },
  
  setPerspectiveMatrix:function(m)
  {
    this.perspectiveMatrix = m;
  },

  setPosition:function(point)
  {
    this.transformation.setOffset(point);
    this.dirty = true;
  },

  setAngles:function(angles)
  {
    this.transformation.setAngles(angles);
    this.dirty = true;
  },
  
  animationFlap:function(im, speed)
  {
    this.flapOffset += Math.PI/(im.timePassed/(6*speed));
    if((2 * Math.PI) <= this.flapOffset)
      this.flapOffset = 0;
    var flapAngle = Math.sin(this.flapOffset) * Math.PI/8;
    this.leftWing.setAngleX(Math.PI*9/8 + flapAngle);
    this.rightWing.setAngleX(-Math.PI/8 - flapAngle);
  }
}


  function init()
  {
    Controller.statics.path = 'http://jsballs.com/';
    var controller = Controller.statics.getController();
    
    var bf = new Butterfly('bf');
    bf.setPosition({x:670, y:120, z:2});
    bf.attach(document.body);
    controller.addElement(bf);
    
    controller.start();
  }
  if(window.addEventListener)
    window.addEventListener('load', init, false);
  else if(window.attachEvent)
    window.attachEvent('onload', init);
})();
