var Frame = function (id)
{
  if (!id)
  {
    throw new Error('You have to pass id');
  }

  this.width    = 0;
  this.height   = 0;
  this.originX  = 0; // all offset is calculated from the (0, 0) in VIEW.
  this.originY  = 0;
  this.centerX  = 0;
  this.centerY  = 0;
  this.rotation = 0;
  this.scaleX   = 1;
  this.scaleY   = 1;
  this.alpha    = 1;

  // canvas
  this.actors             = [];
  this.buffers            = [];
  this.bullpen            = null;
  this.container          = null;
  this.currentFrameIndex  = 0;
  this.displayRect        = null;
  this.fnCache            = {};
  this.fnStuck            = [];
  this.geometry           = {};
  this.inMotion           = false;
  this.onShow             = false;
  this.ptTimeout          = null;
  // this.vignet             = null;
  this.pastDimension      = null;

  // init
  this.bullpen   = Frame.buildBullpen();
  this.container = Frame.buildContainer(id);
  this.onResize();
  this.fnCache.onFinishAnimation = EventRunner.scope(this, this.onFinishAnimation);
  this.fnCache.onResize = EventRunner.scope(this, this.onResize);
  this.fnCache.onUpdate = Frame.buildEventOnUpdate(this);

  if (Controller.IS_IE)
  {
    // for IE (with excanvas), DOM rebuilding is lighter than clearRect
    this.update = this.update4IE;
  }
  else
  {
    // for other modern browsers, clearRect is, of course, ligher than
    // DOM Manipulation
    this.update = this.update4Modern;
  }

  EventRunner.register(window, 'resize', this.onResize, this);
  EventRunner.register(this.container, 'click', this.onClick, this);
  EventRunner.register(this.container, 'mousemove', this.onMouseMove, this);
}


// STATICs
// _______________________________________________________________________


Frame.INT_DEFAULT_FRAME_LENGTH  = 2;
Frame.MILSEC_DEFAULT_DELAY      = 9;
Frame.PIX_PAGE_HEADER_WHITE     = 3;
Frame.SEC_ENTER_ANIME_DURATION  = 1.3;
Frame.SEC_LEAVE_ANIME_DURATION  = .4;


Frame.buildBullpen = function ()
{
  var d = document.createElement('div');
  d.style.visibility  = 'hidden';
  d.style.position    = 'absolute';
  d.style.width       = '1px';
  d.style.height      = '1px';
  d.style.top         = 0;
  d.style.left        = 0;
  d.className         = 'ieCanvasBullpen';
  document.body.appendChild(d);
  return d;
}


Frame.buildCanvas = function (bullpen, w, h)
{
  var c = document.createElement('CANVAS');
  if (Controller.IS_IE)
  {
    // this process is needed only by Internet Explorer
    // with using excanvas.js
    bullpen.appendChild(c);
    c = G_vmlCanvasManager.initElement(c);
    bullpen.removeChild(c);
  }
  c.width             = w;
  c.height            = h;
  c.style.position    = 'absolute';
  c.style.top         = 0;
  c.style.left        = 0;
  c.style.width       = w + 'px';
  c.style.height      = h + 'px';
  c.style.overflow    = 'hidden';
  c.style.visibility  = 'hidden';
  return c;
}


Frame.buildContainer = function (id)
{
  var d = document.createElement('div');
  d.id                = id;
  d.style.position    = 'absolute';
  d.style.top         = '0px';
  d.style.left        = '0px';
  d.style.overflow    = 'hidden';
  document.body.appendChild(d);
  return d;
}


Frame.buildEventOnUpdate = function (self)
{
  return function ()
  {
    self.draw();
    self.update();
    if (self.ptTimeout !== null)
    {
      self.ptTimeout = null;
    }
  }
}


Frame.clearCanvas = function (canvas, x, y, w, h)
{
  canvas.getContext('2d').clearRect(x, y, w, h);
}


// INIT Methods
// _______________________________________________________________________


Frame.prototype.initFrames = function ()
{
  if (this.buffers.length === 0)
  {
    for (var i = 0, len = Frame.INT_DEFAULT_FRAME_LENGTH; i < len; ++i)
    {
      this.buffers[i] = this.container.appendChild(
        Frame.buildCanvas(this.bullpen, this.width, this.height)
      );
    }
  }
  else
  {
    // originX, originY are always negative value.
    var cleaning = this.buffers[this.getNextFrameIndex()];
    if (this.pastDimension === null)
    {
      Frame.clearCanvas(
        cleaning, 
        this.originX, this.originY,
        (this.width - this.originX), (this.height - this.originY)
      );
    }
    else
    {
      var p = this.pastDimension;
      Frame.clearCanvas(
        cleaning, 
        p.originX, p.originY,
        (p.width - p.originX), (p.height - p.originY)
      );
      cleaning.style.width  = this.width + 'px';
      cleaning.style.height = this.height + 'px';
      cleaning.width        = this.width;
      cleaning.height       = this.height;
    }
    this.pastDimension = null;

    this.ptTimeout = window.setTimeout(
      this.fnCache.onUpdate,
      Frame.MILSEC_DEFAULT_DELAY
    );

  }
}


// SCRATCH Pads
// _______________________________________________________________________






































// VIEW Methods
// _______________________________________________________________________


Frame.prototype.animateEnter = function ()
{
  // console.profile('animateEnter');
  this.inMotion = true;
  this.onShow   = true;
  JSTweener.addTween(
    this,
    {
      'delay'       : 0,
      'time'        : Frame.SEC_ENTER_ANIME_DURATION,
      'rotation'    : this.geometry.theata,
      'transition'  : 'easeOutBounce',
      'onUpdate'    : this.fnCache.onUpdate,
      'onComplete'  : this.fnCache.onFinishAnimation
    }
  );
}


Frame.prototype.animateLeave = function ()
{
  this.inMotion = true;
  this.onShow   = false;
  JSTweener.addTween(
    this,
    {
      'delay'       : 0,
      'time'        : Frame.SEC_LEAVE_ANIME_DURATION,
      'rotation'    : 0,
      'transition'  : 'easeInOutQuad',
      'onUpdate'    : this.fnCache.onUpdate,
      'onComplete'  : this.fnCache.onFinishAnimation
    }
  );
}


Frame.prototype.constructBackground = function (context)
{
  // cache value in local
  var rect       = this.geometry.rectangle;
  var rectWidth  = rect.width;
  var rectHeight = rect.height;
  context.save();
  // draw background
  context.fillStyle = "rgba(224,27,27,1)"; // red
  context.fillRect(
    rect.x,
    rect.y,
    rectWidth,
    rectHeight
  );
  // draw vignet   not anymore!! this make it look nice but too heavy.
  // if ((this.vignet !== null) && !Controller.IS_IE)
  // {
  //   // by Controller, it is guaranteed that the vignet is loaded already
  //   context.drawImage(this.vignet, 0, 0);
  // }
  // line the edge
  context.fillStyle = "rgba(255,255,255,1)";
  context.fillRect(
    0, 
    (rectHeight - Frame.PIX_PAGE_HEADER_WHITE),
    rectWidth,
    Frame.PIX_PAGE_HEADER_WHITE
  );
  // clear the edge
  // if (!Controller.IS_IE)
  // {
  //   context.clearRect(
  //     0, 
  //     rectHeight,
  //     rectWidth,
  //     rectHeight
  //   );
  // }
  context.restore();
}


Frame.prototype.draw = function ()
{
  var actors  = this.actors;
  var len     = actors.length;
  var ir      = this.geometry.innerRectangle;
  var sx      = this.scaleX;
  var sy      = this.scaleY;
  var cx      = this.centerX;
  var cy      = this.centerY;
  var context = this.getNextCanvas().getContext("2d");
  context.save();

  // scale
  if ((sx !== 1) || (sy !== 1))
  {
    context.scale(sx, sy);
  }
  // rotation
  context.translate(cx, cy);
  context.rotate(this.rotation);
  context.translate(-cx, -cy);
  // build background
  context.translate(this.originX, this.originY);
  this.constructBackground(context);
  // move to offset
  context.translate(ir.x, ir.y);
  for (var i = 0; i < len; ++i)
  {
    actors[i].draw(context);
  }

  context.restore();
}


Frame.prototype.hide = function ()
{
  for (var i = 0, len = this.buffers.length; i < len; ++i)
  {
    this.buffers[i].style.visibility = 'hidden';
  }
  var canvas = this.getCurrentCanvas();
  canvas.style.visibility = 'visible';
  this.container.style.display = 'none';
}


Frame.prototype.pushDimenstion = function ()
{
  if (this.width === 0)
  {
    return;
  }
  this.pastDimension = {
    'width'   : this.width,
    'height'  : this.height,
    'originX' : this.originX,
    'originY' : this.originY
  };
}


Frame.prototype.show = function ()
{
  for (var i = 0, len = this.buffers.length; i < len; ++i)
  {
    this.buffers[i].style.visibility = 'hidden';
  }
  var canvas = this.getCurrentCanvas();
  canvas.style.visibility = 'visible';
  this.container.style.display = 'block';
}


Frame.prototype.update4IE = function ()
{
  // old
  var oldFrameIndex = this.currentFrameIndex;
  var oldCanvas     = this.getCurrentCanvas();
  oldCanvas.style.visibility = 'hidden';
  // new
  this.currentFrameIndex = this.getNextFrameIndex();
  this.getCurrentCanvas().style.visibility = 'visible';
  // swap
  var replace = Frame.buildCanvas(this.bullpen, this.width, this.height);
  this.container.replaceChild(replace, oldCanvas);
  this.buffers[oldFrameIndex] = replace;
  return this.currentFrameIndex;
}


Frame.prototype.update4Modern = function ()
{
  var oldCanvas     = this.getCurrentCanvas();
  oldCanvas.style.visibility = 'hidden';
  this.currentFrameIndex = this.getNextFrameIndex();
  this.getCurrentCanvas().style.visibility = 'visible';
  Frame.clearCanvas(
    oldCanvas, 
    this.originX, this.originY,
    (this.width - this.originX), (this.height - this.originY)
  );
  return this.currentFrameIndex;
}


// EVENT Methods
// _______________________________________________________________________

Frame.prototype.getAdjustedMouse = function (x, y)
{
  //console.log(ev);
  var rot     = this.rotation;
  var x0      = Math.abs(this.centerX - x);
  var y0      = Math.abs(this.centerY - y);
  var theata0 = Math.atan2(y0, x0);
  if (Math.abs(rot) < theata0) 
  { 
    return null;
  }
  // rotation matrix
  var cos     = Math.cos(rot);
  var sin     = Math.sin(rot);
  var x1      = cos * x0 - sin * y0;
  var y1      = sin * x0 + cos * y0;
  var rect    = this.geometry.rectangle;
  return {
    'x': (rect.width - x1),
    'y': (rect.height + y1)
  };
}


Frame.prototype.onMouseMove = function (ev)
{
  //console.log(ev);
  var mouse = this.getAdjustedMouse(ev.clientX, ev.clientY);
  if (mouse === null)
  {
    return;
  }
  ev.clientX = mouse.x;
  ev.clientY = mouse.y;

  var result = false;
  var origin = this.geometry.innerRectangle;
  for (var i = 0, len = this.actors.length; i < len; ++i)
  {
    result = result || (
      this.actors[i].onHover(
        ev, 
        origin.x, 
        origin.y
      )
    );
    if (result)
    {
      break;
    }
  }
  if (result)
  {
    // change to pointer icon
    document.body.style.cursor = 'pointer';
  }
  else
  {
    // change to auto
    document.body.style.cursor = 'auto';
  }
}


Frame.prototype.onClick = function (ev)
{
  //console.log(ev);
  var mouse = this.getAdjustedMouse(ev.clientX, ev.clientY);
  if (mouse === null)
  {
    return;
  }
  ev.clientX = mouse.x;
  ev.clientY = mouse.y;

  //console.log('mouseX: ' + mouseX + ', mouseY: ' + mouseY);
  // delegate event to actors
  var result = false;
  var origin = this.geometry.innerRectangle;
  for (var i = 0, len = this.actors.length; i < len; ++i)
  {
    result = result || (
      this.actors[i].onClick(
        ev, 
        origin.x, 
        origin.y
      )
    );
  }
  if (result)
  {
    this.animateLeave();
  }
}


Frame.prototype.onFinishAnimation = function (context)
{
  // console.profileEnd();
  this.inMotion = false;
  for (var i = 0, len = this.fnStuck.length; i < len; ++i)
  {
    this.fnStuck[i]();
  }
  this.fnStuck = [];
}


Frame.prototype.onResize = function (ev)
{
  if (this.inMotion)
  {
    var len = this.fnStuck.length;
    if (len > 0)
    {
      if (this.fnStuck[len - 1] === this.fnCache.onResize)
      {
        this.fnStuck.pop();
      }
    }
    this.fnStuck.push(this.fnCache.onResize);
    return;
  }

  var w = $(document.body).width();
  var h = $(document.body).height();

  var gap         = Math.round(w * 0.1);
  var tri_w       = w - gap;
  var tri_h       = h - gap;
  var theata      = Math.atan2(tri_h, tri_w);
  var rect_w      = Math.sqrt(tri_w * tri_w + tri_h * tri_h);
  var rect_h      = tri_w * Math.sin(theata);
  var half_tri_w  = tri_w / 2;
  var half_tri_h  = tri_h / 2;
  var inrect_w    = Math.sqrt(half_tri_w * half_tri_w + half_tri_h * half_tri_h );
  var inrect_h    = rect_h / 2;
  var offset_x    = Math.cos(theata) * half_tri_w;

  rect_w    = Math.round(rect_w);
  rect_h    = Math.round(rect_h);
  inrect_w  = Math.round(inrect_w);
  inrect_h  = Math.round(inrect_h);
  offset_x  = Math.round(offset_x);

  this.pushDimenstion();

  this.container.style.width  = w + 'px';
  this.container.style.height = tri_h + 'px';
  this.width    = w;
  this.height   = tri_h;
  // all offset is calculated from the (0, 0) in VIEW.
  this.originX  = tri_w - rect_w;
  this.originY  = -rect_h;
  this.centerX  = tri_w;
  this.centerY  = 0;
  if (this.onShow)
  {
    this.rotation = -theata;
  }
  else
  {
    this.rotation = 0;
  }
  this.geometry.triangle = {
    'width'   : tri_w, 
    'height'  : tri_h };
  this.geometry.rectangle = {
    'width'   : rect_w, 
    'height'  : rect_h, 
    'x'       : 0, 
    'y'       : 0 };
  this.geometry.innerRectangle = {
    'width'   : inrect_w, 
    'height'  : inrect_h, 
    'x'       : rect_w - (offset_x + inrect_w), 
    'y'       : inrect_h };
  this.geometry.theata = -theata;

  if (this.displayRect !== null)
  {
    this.setScaleToActors();
  }
  this.initFrames();
}


Frame.prototype.onUpdate = function ()
{
  this.draw();
  this.update();
  if (this.ptTimeout !== null)
  {
    this.ptTimeout = null;
  }
}


// UTILITY Methods
// _______________________________________________________________________


Frame.prototype.setDisplayRect = function (obj)
{
  this.displayRect = obj;
  if (this.displayRect.height > this.geometry.innerRectangle.height)
  {
    this.setScaleToActors();
  }
}


Frame.prototype.setScaleToActors = function ()
{
  var scale = Controller.round(
    this.geometry.innerRectangle.height / this.displayRect.height,
    2
  );
  for (var i = 0, len = this.actors.length; i < len; ++i)
  {
    this.actors[i].scaleX = scale;
    this.actors[i].scaleY = scale;
  }
  return scale;
}


Frame.prototype.addActor = function (actor)
{
  var scale = 1;
  if (this.displayRect.height > this.geometry.innerRectangle.height)
  {
    scale = Controller.round(
      this.geometry.innerRectangle.height / this.displayRect.height,
      2
    );
  }
  this.actors[this.actors.length] = actor;
  actor.scaleX = scale;
  actor.scaleY = scale;
//   actor.setFrame(this);
}


Frame.prototype.getCurrentCanvas = function ()
{
  return this.buffers[this.currentFrameIndex];
}


Frame.prototype.getNextCanvas = function ()
{
  return this.buffers[this.getNextFrameIndex()];
}


Frame.prototype.getNextFrameIndex = function ()
{
  var i = this.currentFrameIndex + 1;
  if (i >= this.buffers.length)
  {
    i = 0;
  }
  return i;
}


