

//// View Manager Abstract Class
// View Managers are paired with a document Element to provide functionality
// akin to a GUI toolkit (containment, callbacks, etc.)
// These are all private methods.
var ViewManager = Class.create({
  doCallback: function(element, eventName, once) {
    if(once) {
      var propName = 'vpCallbackHasRun_' + eventName; // Reused
      if(element[propName]) {
        return;
      } else {
        element[propName] = true;
      }
    }
    propName = 'vpCallback_' + eventName;
    if(element[propName]) { element[propName](); }
  },

  doShow: function(element) {
    if(!element.visible()) { element.show(); }
    this.doCallback(element, 'initialize', true);
    this.doCallback(element, 'show', false);
    return element;
  }
})


//// View Controller Abstract Class
// Organizes methods for working with a specific view (e.g. a page)

var ViewController = Class.create({
  initialize: function(element) {
    this.view = $(element);
    this.view.vpController = this;
    // Fire a controller:loaded event on our element as soon as the 
    // implementing class' initializer is finished. This allows blocks of
    // JS in the ERB files to observe and run when the controller is ready.
    (function() { this.view.fire("controller:loaded"); }).bind(this).defer();
  }
});


//// Extend Element with our utilities

Element.addMethods({
  // Find your local view manager
  vm: function(element) {
    while(element.vpManager == undefined) {
      element = element.parentNode;
      if(element == null) { return null; }
    }
    return element.vpManager;
  },

  // Find your local controller
  controller: function(element) {
    while(element.vpController == undefined) {
      element = element.parentNode;
      if(element == null) { return null; }
    }
    return element.vpController;
  },

  // Register a callback on a view
  vmSetCallback: function(element, eventName, callback) {
    element['vpCallback_' + eventName] = callback;
  },

  // Get rid of a callback
  vmUnsetCallback: function(element, eventName) {
    element['vpCallback_' + eventName] = undefined;
  }
});

//// View Stack (allows "go back" within a div)
// Fires events: initialize, show, hide, destroy

var ViewStack = Class.create(ViewManager, {
  initialize: function(container) {
    this.container = $(container); // Extend it
    this.container.vpManager = this;
    this.stack = new Array;
  },

  push: function(element) {
    element = $(element);
    if(this.stack.length > 0) {
      var covered = this.stack.last();
      this.doCallback(covered, 'hide', false);
      covered.hide();
    }
    this.stack.push(element);
    this.container.appendChild(element);
    return this.doShow(element);
  },

  pushEmpty: function() {
    // Convenience method
    return this.push(new Element('div'));
  },

  pop: function(count) {
    if(count != undefined) {
      $R(0, count, true).each(function() { this.pop(); }, this)
    } else {
      var popped = this.stack.pop();
      this.doCallback(popped, 'destroy', false);
      popped.remove();
      if(this.stack.length > 0) { this.doShow(this.stack.last()); }
    }
  },

  depth: function() {
    return this.stack.length;
  },

  current: function() {
    return this.stack.last();
  }
});




//// View Deck Manager (commonly used to implement tabs)
// Fires events: initialize, show, hide

var ViewDeck = Class.create(ViewManager, {
  initialize: function(container) {
    this.container = $(container); // Extend it
    this.container.vpManager = this;

    this.views = new Hash;
    this.currentView = null; // Key

    // currentActivatorClass is a CSS class assigned to activator elements when
    // their associated view is shown.
    this.currentActivatorClass = 'active';
  },

  // activator is optional
  // append (defaults to true) determines whether the element is added to the
  //   container. If the element is already contained, pass false.
  add: function(key, element, activator, append) {
    element = $(element);
    this.views[key] = {element: element};
    if(append || append == null) this.container.appendChild(element);
    if(activator) { this.addActivator(key, activator); }
    return element.hide();
  },

  addEmpty: function(key) {
    // Convenience method
    var element = this.add(key, new Element('div'));
    element.id = this.container.identify() + "_" + key;
    return element;
  },

  show: function(key) {
    if(this.currentView) {
      this.doCallback(this.views[this.currentView].element, 'hide', false);
      this.views[this.currentView].element.hide();
      if(this.views[this.currentView].activator) {
        this.views[this.currentView].activator.removeClassName(
          this.currentActivatorClass);
      }
    }
    this.currentView = key;
    this.doShow(this.views[key].element);
    if(this.views[key].activator) {
      this.views[key].activator.addClassName(this.currentActivatorClass);
    }
  },

  // Sets up an element to act as a activator for activating a tab
  addActivator: function(key, element) {
    element = $(element);
    Event.observe(element, 'click',
      function() { this.show(key) }.bindAsEventListener(this));
    this.views[key].activator = element;
  }
})


//// View Overlay Manager
// Fires events: initialize, show, destroy

var ViewOverlay = Class.create(ViewManager, {
  initialize: function(contents) {
    this.contents = $(contents); // Extend it
    this.contents.vpManager = this;
  },

  // Create and display the overlay. overlayClass is optional.
  open: function(overlayClass) {
    this.container = new Element('div', {'class': 'overlay'});
    if(overlayClass != undefined) {
      this.container.addClassName(overlayClass)
    }
    this.container.appendChild(
      new Element('div', {'class': 'overlayCurtain'}));
    this.container.appendChild(
      new Element('div', {'class': 'overlayBorder'}));
    this.body = new Element('div', {'class': 'overlayBody'});
    this.body.appendChild(this.contents);
    this.container.appendChild(this.body);
    Util.hideInconcealableElements();
    document.body.appendChild(this.container);
    this.doShow(this.contents);
    Util.assureElementCenteredInIE(this.body);
    if(Prototype.Browser.IE) window.scrollTo(0, 0);
  },

  close: function() {
    this.doCallback(this.contents, 'destroy', false);
    this.container.remove();
    Util.showInconcealableElements();
  }
});