
function Container(htmlElement) {
  
  var bodyHtmlElement = document.getElementsByTagName("body")[0];
  
  var BoundingRectangle = function () {
    var vOffset,
        hOffset,
        boundingRectangle = htmlElement.getBoundingClientRect();

    with (bodyHtmlElement) {
      vOffset = scrollTop - clientTop;
      hOffset = scrollLeft - clientLeft;
    }

    with (boundingRectangle) {
      this.top = top + vOffset;
      this.left = left + hOffset;
      this.bottom = bottom + vOffset;
      this.right = right + hOffset;
    }

    boundingRectangle = null;
  }

  this.getHtmlElement = function () {
    return htmlElement;
  }

  this.getBoundingRectangle = function() {
    return new BoundingRectangle();
  }

  this.addEventListener = function (type, listener) {
    htmlElement.detachEvent("on" + type, listener);
    htmlElement.attachEvent("on" + type, listener);
  }

  this.removeEventListener = function (type, listener) {
    htmlElement.detachEvent("on" + type, listener);
  }

  this.contains = function (childHtmlElement) {
    return htmlElement.contains(childHtmlElement);
  }

}

function BusinessCard(cardHtmlElement, trayHtmlElement, trayFrameHtmlElement, buttonHtmlElement) {

  var context = this,
      cardContainer = new Container(cardHtmlElement),

      tray = new BusinessCardTray(trayHtmlElement, trayFrameHtmlElement),
      button = new BusinessCardButton(buttonHtmlElement);

  this.activate = function () {
    tray.activate(context);
    button.activate(context);

    cardContainer.addEventListener("mouseover", getMouseOverListener(cardContainer, context.open));
    cardContainer.addEventListener("mouseout", getMouseOutListener(cardContainer, context.close));
  }

  this.deactivate = function () {
    tray.deactivate();
    button.deactivate();

    cardContainer.removeEventListener("mouseover", getMouseOverListener(cardContainer, context.open));
    cardContainer.removeEventListener("mouseout", getMouseOutListener(cardContainer, context.close));
  }

  this.open = function () {
    button.setState("pressed");
    button.setPressAction(context.close);
    tray.open();
  }

  this.close = function () {
    button.setState("released");
    button.setPressAction(context.open);
    tray.close();
  }

  this.getContainer = function () {
    return cardContainer;
  }

}

function BusinessCardTray(trayHtmlElement, frameHtmlElement) {
  
  var FRAME_RATE = 85, // Hz (average)
      FRAME_DELAY = Math.round(1000/FRAME_RATE);
  
  var card,
      cardContainer,
      trayContainer = new Container(trayHtmlElement),
      frameContainer = new Container(frameHtmlElement),

      offsetStep,
      currOffset,

      timerId,
      state; //closed, intermediate, opened

  function moveTrayTo(offsetMul) { // x = -1...1
    var cardRectangle = cardContainer.getBoundingRectangle(),
        trayRectangle = trayContainer.getBoundingRectangle(),
        trayHeight = trayRectangle.bottom > cardRectangle.bottom ? trayRectangle.bottom - trayRectangle.top : cardRectangle.bottom - cardRectangle.top,
        trayWidth = trayRectangle.right - trayRectangle.left;

    with (frameHtmlElement.style) {
      left = cardRectangle.right + "px";
      top = cardRectangle.top + "px";
      width = trayWidth + "px";
      height = trayHeight + "px";
    }

    with (trayHtmlElement.style) {
      left = Math.round(trayWidth*offsetMul) + "px";
      height = trayHeight + "px";
    }
  }

  function startMove() {

    if (state == "closed") {
      currOffset = -1;
      moveTrayTo(currOffset);
      showTray();
      state = "intermediate";
    }

    if (state == "opened") {
      currOffset = 0;
      moveTrayTo(currOffset);
      state = "intermediate";
    }

    timerId = setInterval(function () {
      currOffset += offsetStep;

      if (currOffset <= -1) {
        currOffset = -1;
        state = "closed";
        hideTray();
        stopMove();
      }
      if (currOffset >= 0) {
        currOffset = 0;
        state = "opened";
        stopMove();
      }

      moveTrayTo(currOffset);

    }, FRAME_DELAY);
  }
  
  function stopMove() {
    clearInterval(timerId);
  }

  function showTray() {
    frameHtmlElement.style.display = "block";
  }

  function hideTray() {
    frameHtmlElement.style.display = "none";
  }

  this.activate = function (newCard) {
    card = newCard;
    cardContainer = card.getContainer();
    stopMove();
    hideTray();
    state = "closed";

    trayContainer.addEventListener("mouseover", getMouseOverListener(trayContainer, card.open));
    trayContainer.addEventListener("mouseout", getMouseOutListener(trayContainer, card.close));
  }

  this.deactivate = function () {
    stopMove();
    hideTray();

    trayContainer.removeEventListener("mouseover", getMouseOverListener(trayContainer, card.open));
    trayContainer.removeEventListener("mouseout", getMouseOutListener(trayContainer, card.close));
  }

  this.open = function () {
    stopMove();
    offsetStep = 0.05;
    startMove();
  }

  this.close = function () {
    stopMove();
    offsetStep = -0.05;
    startMove();
  }

}

function BusinessCardButton(htmlElement) {

  var context = this,
      container = new Container(htmlElement),
      pressAction = function () {},
      releaseAction = function () {},
      state;

  this.container = container;

  this.activate = function () {
    context.setState("released");
    container.addEventListener("mousedown", context.press);
    container.addEventListener("mouseup", context.release);
  }

  this.deactivate = function () {
    container.removeEventListener("mousedown", context.press);
    container.removeEventListener("mouseup", context.release);
  }

  this.press = function () {
    context.setState(state == "pressed" ? "released" : "pressed");
    pressAction();
  }

  this.release = function () {
    releaseAction();
  }

  this.setPressAction = function (action) {
    pressAction = action;
  }

  this.setReleaseAction = function (action) {
    releaseAction = action;
  }

  this.setState = function (newState) {
    state = newState;
    if (state == "pressed") {
      htmlElement.style.backgroundPosition = "right";
    }
    else {
      htmlElement.style.backgroundPosition = "left";
    }
  }
}

/* Additional functions */

function getMouseOverListener(container, listener) {
  var containerContains = container.contains;
  return function () {
    if (!containerContains(event.fromElement)) {
      listener();
    }
  }
}

function getMouseOutListener(container, listener) {
  var containerContains = container.contains;
  return function () {
    if (!containerContains(event.toElement)) {
      listener();
    }
  }
}

/* End of additional functions */