// Copyright (c) 2006 Sébastien Gruhier (http://xilinus.com, http://itseb.com)
// 
// Permission is hereby granted, free of charge, to any person obtaining
// a copy of this software and associated documentation files (the
// "Software"), to deal in the Software without restriction, including
// without limitation the rights to use, copy, modify, merge, publish,
// distribute, sublicense, and/or sell copies of the Software, and to
// permit persons to whom the Software is furnished to do so, subject to
// the following conditions:
// 
// The above copyright notice and this permission notice shall be
// included in all copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
// EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
// NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
// LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
// OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
// WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
//
// VERSION 1.1-trunk

if(typeof Draggable == 'undefined')
  throw("portal.js requires including script.aculo.us' dragdrop.js library");

// DahXilinus namespace
if(typeof Xilinus == 'undefined')
  Xilinus = {}

Xilinus.Widget = Class.create({
    lastId: 0,
    initialize: function(widget) {
        this._id   = widget.id || ("widget_" + Xilinus.Widget.lastId++);
        // we cannot use the title h2 because IE doesn't pass the event to us
        this._handle = $(widget).down('div.tl');
        this._title = this._handle.down('h2').innerHTML;
        this._div = $(widget);
        $(widget).addClassName('widget');
        $(widget).widget = this;
        return this;
    },
    remove: function(element, options) {
        if (options && options.afterFinish)
            options.afterFinish.call();
    },
  destroy: function() {
    this._div.remove();
  },
  
  getElement: function() {
    return this._div;
  },
  
  getHandle: function() {
    return this._handle
  },

  getTitle: function() {
    return this._title
  },

  // PRIVATE FUNCTIONS
  _getId: function(prefix) { 
      return (prefix ? prefix + "_" : "") + this._id;
  }
});


Xilinus.Portal = Class.create({
  lastEvent: null,   
  widgets:   null,
  columns:   null, 
  
  initialize: function(columns, options) {   
    this.options = Object.extend({                  
                     url:          null,                 // Url called by Ajax.Request after a drop
                     page:         '',                   // page the portal is managing
                     onOverWidget: null,                 // Called when the mouse goes over a widget
                     onOutWidget:  null,                 // Called when the mouse goes out of a widget                                           
                     onChange:     null,                 // Called a widget has been move during drag and drop 
                     onUpdate:     null,                 // Called a widget has been move after drag and drop
                     removeEffect: Xilinus.Widget.remove // Remove effect (by default no effect), you can set it to Effect.SwitchOff for example
                   }, options)
    this._columns = (typeof columns == "string") ? $$(columns) : columns;
    this._widgets = new Array();          
    this._columns.each(function(element) {Droppables.add(element, {onHover: this.onHover.bind(this), 
                                                                   overlap: "vertical", 
                                                                   accept: this.options.accept})}.bind(this));  
    this._outTimer  = null;
    
    // Draggable calls makePositioned for IE fix (??), I had to remove it for all browsers fix :) to handle properly zIndex
    this._columns.invoke("undoPositioned");    
    
    this._currentOverWidget = null; 
    this._widgetMouseOver = this.widgetMouseOver.bindAsEventListener(this);
    this._widgetMouseOut  = this.widgetMouseOut.bindAsEventListener(this);
    
    Draggables.addObserver({ onEnd: this.endDrag.bind(this), onStart: this.startDrag.bind(this) }); 
  },
  
  add: function(widget, columnIndex, opts) {
    var widget_options = Object.extend({
        draggable: true,
        editable: false,
        edit_url: '',
        view_url: ''
    }, opts);
    // Add to widgets list
    this._widgets.push(widget);
    if (this.options.accept)
      widget.getElement().addClassName(this.options.accept);
    
    // Make header draggable   
    if (widget_options.draggable) {
      widget.draggable = new Draggable(widget.getElement(),{handle: widget.getHandle(), revert: false, scroll: window});
      widget.getHandle().addClassName("widget_draggable");   
    }
    // add the form if editable
    if (widget_options.editable) {
      widget.editable = new Dah.base.WidgetForm(widget.getElement(), {edit_url: widget_options.edit_url, view_url: widget_options.view_url});
    }

    // Add mouse observers  
    if (this.options.onOverWidget)
      widget.getElement().immediateDescendants().invoke("observe", "mouseover", this._widgetMouseOver);
    if (this.options.onOutWidget)
      widget.getElement().immediateDescendants().invoke("observe", "mouseout",  this._widgetMouseOut);
  },  
  
  onNewWidget: function(draggable, event) {
    var widget = draggable.element;
    var x = event.pageX, y = event.pageY;
    var w, overlap, column, row, element, parameters;
    // Find if it's overlapping a widget
    for (var i = 0, l = this._widgets.length; i < l; ++i) {
      w = this._widgets[i].getElement();
      if (!Position.within(w, x, y))
        continue;
      overlap = Position.overlap('vertical', w);     
      column = w.parentNode;
      element = column.down('.widget');
      break;
    }
    if (!column) {
      for(var i = 1, l = this._columns.length; i < l; ++i) {
        if (Position.within(this._columns[i].parentNode, x, y)) {
          var pos = this._columns[i].viewportOffset(), width = this._columns[i].getWidth();
          if (x >= pos.left && x <= pos.left + width) {
            column = this._columns[i];
            element = column.down('.widget');
            w = null;
            break;
          }
        }
      }
    }
    if (column) {
      // bottom of the column or bottom of a widget
      if (!overlap || overlap < 0.5)
        row = 0;
      else
        row = -1;
      do {
        row++;
        if (element != w)
          element = element.next('.widget');
        else
          element = null;
      } while (element);
      parameters = 'page=' + this.options.page + '&add=' + column.id + ',' + widget.id + ',' + row;
      new Ajax.Request(this.options.url, {
        parameters: parameters,
        onSuccess: function (response) {
          if (response.responseJSON && response.responseJSON.reload) {
            // setting a new location with anchor only causes the page to jump to the anchor
            // we need to reload the page with new html id's
            var index = window.location.href.indexOf('#');
            var loc = window.location.toString();
            if (index >= 0)
              loc = loc.substring(0, index) + '#settings';
            else
              loc = loc + '#settings';
            // replace the current url in the browser
            window.location.replace(loc);
            // reload the new url
            window.location.reload();
          }
        }
      });
    }
  },

  remove: function(widget) {
    // Remove from the list
    this._widgets.reject(function(w) { return w == widget});

    // Remove observers
    if (this.options.onOverWidget)
      widget.getElement().immediateDescendants().invoke("stopObserving", "mouseover", this._widgetMouseOver);
    if (this.options.onOutWidget)
      widget.getElement().immediateDescendants().invoke("stopObserving", "mouseout",  this._widgetMouseOut);

    // Remove draggable
    if (widget.draggable)
      widget.draggable.destroy();
    var parameters = 'page=' + this.options.page + '&remove=' + widget.getElement().id;
    new Ajax.Request(this.options.url, {
      parameters: parameters
    });
      
    // Remove from the dom
    this.options.removeEffect(widget.getElement(), {afterFinish: function() {widget.destroy();}});
  },
    
  serialize: function() {
    parameters = 'page=' + this.options.page + '&';
    this._columns.each(function(column) {   
      var p = column.immediateDescendants().collect(function(element) {
        if (element.id)
            return column.id + "=" + element.id
        return ''
      }).join("&") 
      parameters += p + "&"
    });               
    
    return parameters;                      
  },     
  
  addWidgetControls: function(element) {
    $(element).observe("mouseover", this._widgetMouseOver); 
    $(element).observe("mouseout", this._widgetMouseOut); 
  },
  
  // EVENTS CALLBACKS
  widgetMouseOver: function(event) {   
    this._clearTimer();
      
    var element =  Event.element(event).up(".widget");
    if (this._currentOverWidget == null || this._currentOverWidget != element) {
      if (this._currentOverWidget && this._currentOverWidget != element)
        this.options.onOutWidget(this, this._currentOverWidget.widget)    
        
      this._currentOverWidget = element;
      this.options.onOverWidget(this, element.widget)
    }
  },

  widgetMouseOut: function(event) {    
    this._clearTimer();
    var element =  Event.element(event).up(".widget"); 
    this._outTimer = setTimeout(this._doWidgetMouseOut.bind(this, element), 100);
  },
  
  _doWidgetMouseOut: function(element) {
    this._currentOverWidget = null;
    this.options.onOutWidget(this, element.widget)    
  },                                               
  
  // DRAGGABLE OBSERVER CALLBACKS
  startDrag: function(eventName, draggable) { 
    var widget = draggable.element;
    
    if (!this._widgets.find(function(w) {return w == widget.widget}))
      return;

    var column = widget.parentNode;
    
    // Create and insert ghost widget
    var ghost = new Element('div', {className: 'ghost'}); 
    $(ghost).setStyle({height: widget.getHeight()  + 'px'})

    column.insertBefore(ghost, widget);  

    // IE Does not absolutize properly the widget, needs to set width before
    widget.setStyle({width: widget.getWidth() + "px"});
    
    // Absolutize and move widget on body
    widget.absolutize();  
    document.body.appendChild(widget);   
    
    // Store ghost to drag widget for later use
    draggable.element.ghost = ghost; 
    
    // Store current position
    this._savePosition = this.serialize();    
  },   

  endDrag: function(eventName, draggable) {
    var widget = draggable.element;      
    if (!this._widgets.find(function(w) {return w == widget.widget}))
      return;
    
    var column = widget.ghost.parentNode;
    
    column.insertBefore(widget, widget.ghost); 
    widget.ghost.remove();   

    if (Prototype.Browser.Opera)     
      widget.setStyle({top: 0, left: 0, width: "100%", height: widget._originalHeight, zIndex: null, opacity: null, position: "relative"})
    else
      widget.setStyle({top: null, left: null, width: null, height: null, opacity: null, position: "relative"})

    widget.ghost = null;    
    
    // Fire events if changed
    if (this._savePosition != this.serialize()) {
      if (this.options.url) {
        new Ajax.Request(this.options.url, {
          parameters: this.serialize(),
          onSuccess: function (response) {
            if (response.responseJSON && response.responseJSON.reload)
              window.location.reload();
          }
        });
      }
      
      if (this.options.onUpdate)
        this.options.onUpdate(this);
    }
  },

  onHover: function(dragWidget, dropon, overlap) { 
    var offset = dropon.cumulativeOffset();
    var x = offset[0] + 10;
    var y = offset[1] + (1 - overlap) * dropon.getHeight();

    // Check over ghost widget
    if (!dragWidget.ghost || Position.within(dragWidget.ghost, x, y))
      return;
      
    // Find if it's overlapping a widget
    var found = false;
    var moved = false;
    for (var index = 0, len = this._widgets.length; index < len; ++index) {
      var w = this._widgets[index].getElement();
      if (w ==  dragWidget || w.parentNode != dropon)   
        continue;        

      if (Position.within(w, x, y)) {    
        var overlap = Position.overlap( 'vertical', w);     
        // Bottom of the widget
        if (overlap < 0.5) {         
          // Check if the ghost widget is not already below this widget
          if (w.next() != dragWidget.ghost) {
            w.parentNode.insertBefore(dragWidget.ghost, w.next());   
            moved = true;
          }
        } 
        // Top of the widget
        else {       
          // Check if the ghost widget is not already above this widget
          if (w.previous() != dragWidget.ghost) {      
            w.parentNode.insertBefore(dragWidget.ghost, w);   
            moved = true;
          }
        }
        found = true;
        break;
      }
    }
    // Not found a widget
    if (! found) {        
      // Check if dropon has ghost widget
      if (dragWidget.ghost.parentNode != dropon) {
        // Get last widget bottom value
        var last = dropon.immediateDescendants().last();
        var yLast = last ? last.cumulativeOffset()[1] + last.getHeight() : 0; 
        if (y > yLast && last != dragWidget.ghost) {
          dropon.appendChild(dragWidget.ghost);
          moved = true;
        }
      }
    }  
    if (moved && this.options.onChange) 
      this.options.onChange(this)            
  },                
  
 
  _clearTimer: function() {
    if (this._outTimer) {
      clearTimeout(this._outTimer);
      this._outTimer = null;
    }                        
  }
});

