/*
 * Version 0.5b, 2009-07-14
 * 
 * @author  Are Nybakk, e-vita
 * 
 * See jdimlist-readme.txt for recent changes and known issues
 */

(function($){
  
  var e_lists = [];
  
  var opts = {};
    
  //TODO: Navigation should not be shared by all list elements
  var navigation = { //Shared object to keep track of navigation
    currentStep:0
  };
  
  //TODO: Should not be shared by all list elements
  //var sorting = {};
  
  $.fn.jdimlist = function(options) {
    
    opts = $.extend({}, $.fn.jdimlist.defaults, options);
    
    var data = opts.data;
    var objs = data.objs;
    var columns = opts.columns;
    var column_count = columns.length;
      
    return $(this).each(function(){
      
      e_lists = e_lists.concat($(this));
      
      var e_jdimlist = $(this).addClass("jdimlist");
      
      //Loading with ajax
      if(opts.ajaxUrl != "") {
        getAjax(e_jdimlist, navigation);
      }
      
      //Show headers if enabled
      if(opts.headers) {
        var e_headers = $('<div class="jdimlist-headers"></div>');
        $('<div class="jdimlist-header jdimlist-icon-header"></div>').appendTo(e_headers);
        for(var i=0; i<column_count; i++){
          
          var column = columns[i];
          var e_header = $('<div class="jdimlist-header"></div>').addClass(column["class"]);
          
          //Wrapper
          if(opts.headerWrapper != null) {
            e_header.append(opts.headerWrapper.clone().html(column["name"]));
          } else {
            e_header.html(column["name"]);
          }
          
          //Sorting
          if(column["sortable"]) {
            
            $('<div class="jdimlist-column-sort"></div>').appendTo(e_header);
            
            e_header.click(function(){
              
              var e_this = $(this);
              var columnName = "";
              if(opts.headerWrapper != null) {
                columnName = e_this.children().html();
              } else {
                columnName = e_this.html();
              }
              
              var isAsc = e_this.hasClass("jdimlist-header-ascending");
              var isDesc = e_this.hasClass("jdimlist-header-descending");
              var sortMode = "";
              if(isAsc) {
                e_this.removeClass("jdimlist-header-ascending");
                e_this.addClass("jdimlist-header-descending");
                sortMode = "desc";
              } else if(isDesc) {
                e_this.removeClass("jdimlist-header-descending");
                e_this.addClass("jdimlist-header-ascending");
                sortMode = "asc";
              } else if(!isAsc && !isDesc) { //Default
                e_this.addClass("jdimlist-header-descending");
                sortMode = "desc";
              }
              e_this.siblings()
                .removeClass("jdimlist-header-ascending")
                .removeClass("jdimlist-header-descending");
              
              var dataField = "";
              for(var i=0; i<opts.columns.length; i++) {
                if(opts.columns[i]["name"] == columnName) {
                  dataField = opts.columns[i]["dataField"];
                }
              }
              if(dataField != "") {
                sortObjects(opts.data, dataField, sortMode);
                //sorting[dataField] = sortMode;
                //opts.data.objs = $(opts.data.objs).sort(dataField,sortMode);
                //opts.data.objs = $(opts.data.objs).sort(sorting);
              }
              
              refreshObjects(e_jdimlist, navigation);
              
            });
          }
          
          e_header.appendTo(e_headers);
          
        }
        $('<div class="jdimlist-terminator"></div>').appendTo(e_headers);
        e_headers.appendTo(e_jdimlist);
      }
    
      // Top level submit
      if(opts.objectSubmit) {
        opts.objectSubmitGenerator(e_jdimlist, opts, -1);
      }
      
      //If not loading with ajax
      if(opts.ajaxUrl == "") {
        if(loadObjects(e_jdimlist)){
          loadNavigation(e_jdimlist,navigation);
        };
      }
      
    });
    
  };
  
  /**
   * Reloads objects
   * 
   * @param     id      object ID
   */
  $.fn.jdimlist.reloadObjects = function(e_jdimlist) {
    reloadObjects(e_jdimlist, navigation);
    return this;
  };
  
  /**
   * Select an object
   * 
   * @param     id      object ID
   */
  $.fn.jdimlist.setSelection = function(id) {
    var e_this = $("#"+id);
    e_this.parents(".jdimlist-obj").each(function(){
      toggleObjectBody($(this), opts.animation);
    });
    getObjectHeaders(e_this).click();
    return this;
  };
  
  /**
   * Select the first object
   */
  $.fn.jdimlist.setFirstSelection = function() {
    for(var i=0; i<e_lists.length; i++) {
      getObjectHeaders(e_lists[i].children(".jdimlist-objs").children(".jdimlist-obj").eq(0)).click();
    }
    return this;
  };
  
  /**
   * Retrieves objects using ajax
   * 
   * @param     e_jdimlist    the list element
   * @param     navigation    the navigation object used to keep track
   */
  function getAjax(e_jdimlist, navigation) {
    var ajaxParams = opts.ajaxParams;
    if(opts.ajaxProgressive) {
      ajaxParams.p_start = navigation.currentStep;
      ajaxParams.p_end = opts.maxFirstLevel-1;
    }
    // TODO: Handle incorrect JSON, callback is not called
    $.getJSON(
      opts.ajaxUrl, 
      ajaxParams, 
      function(recievedData, textStatus){      
        if(textStatus == "success") {
          opts.ajaxBeforeLoad(recievedData);
          opts.data = recievedData;
          if(loadObjects(e_jdimlist)){
            loadNavigation(e_jdimlist, navigation);
          };
          opts.ajaxAfterLoad(recievedData);
        }
      }
    );
  }
  
  /**
   * Load objects
   * 
   * @param     e_jdimlist    the list element
   */
  function loadObjects(e_jdimlist) {
    
    //Only if any objs exists
    var objs = opts.data.objs;
    if(objs != null && objs.length != 0) {
    
      //Handle this object and any sub-objects
      handleobjs(objs, e_jdimlist, 0, 0, false);
      
      //If list should be reorganizable, make objects draggable and initialize drop zones
      if(opts.reorganizable) {
        e_jdimlist.find(".jdimlist-obj").draggable({
          /*axis: "y",*/
          distance: opts.stickyness,
          revert: "invalid"
        });
        initializeDropZones(e_jdimlist, opts);
      }
      
      return true;
      
    } else {
      return false;
    }
    
  }
  
  /**
   * Reloads all objects
   * 
   * @param     e_jdimlist    the list element
   * @param     navigation    the navigation object used to keep track
   */
  function reloadObjects(e_jdimlist, navigation) {
    
    e_jdimlist.children(".jdimlist-objs, .jforum-reply").remove();
    
    // Top level submit
    if(opts.objectSubmit) {
      opts.objectSubmitGenerator(e_jdimlist, opts, -1);
    }
    
    if(opts.ajaxUrl != "") {
      getAjax(e_jdimlist, navigation);
    } else {
      if(loadObjects(e_jdimlist)){
        loadNavigation(e_jdimlist,navigation);
      };
    }
      
  }
  
  /**
   * Refreshes all objects
   * 
   * @param     e_jdimlist    the list element
   * @param     navigation    the navigation object used to keep track
   */
  function refreshObjects(e_jdimlist, navigation) {
    
    e_jdimlist.children(".jdimlist-objs, .jforum-reply").remove();
    
    // Top level submit
    if(opts.objectSubmit) {
      opts.objectSubmitGenerator(e_jdimlist, opts, -1);
    }
    
    if(loadObjects(e_jdimlist)){
      loadNavigation(e_jdimlist,navigation);
    }
      
  }
  
  /**
   * Sorts objects on all levels
   * 
   * @param     data          the data object
   * @param     dataField     which data field to sort by.
   * @param     sortType      type of sorting to perform, either "asc" or "desc".
   */
  function sortObjects(data, dataField, sortType) {
    
    function sortObject(obj) {
      var subObjs = obj.objs;
      if(subObjs != null && subObjs != undefined) {
        for(var i=0; i<subObjs.length; i++) {
          sortObject(subObjs[i]);
        }
        obj.objs = opts.doSort(subObjs, dataField, sortType);
      }
    }
    
    sortObject(data);
    
  }
  
  /**
   * Load navigation bar
   * 
   * @param     e_jdimlist    the list element
   * @param     navigation    the navigation object used to keep track
   */
  function loadNavigation(e_jdimlist, navigation) {
    //Show navigation bar if enabled and there are more objects than maximum first level
    if(opts.navigation && opts.data.objs.length > opts.maxFirstLevel) {
      var e_navigation = $('<div class="jdimlist-navigation"></div>');
      $('<div class="jdimlist-navigation-icon-offset"></div>').appendTo(e_navigation);
      $('<div class="jdimlist-navigation-action jdimlist-navigation-back"></div>').html(opts.navigationLabelBack).appendTo(e_navigation).click(createNavigationClickHandler(navigation, -1*opts.maxFirstLevel, e_jdimlist));
      $('<div class="jdimlist-navigation-action jdimlist-navigation-next"></div>').html(opts.navigationLabelNext).appendTo(e_navigation).click(createNavigationClickHandler(navigation, 1*opts.maxFirstLevel, e_jdimlist));
      $('<div class="jdimlist-terminator"></div>').appendTo(e_navigation);
      e_navigation.appendTo(e_jdimlist);
    }
  }
  
  /**
   * A method that recursively handles objs
   * 
   * @param   objs            an array of objs
   * @param   parentElement   an element to add the content to
   * @param   level           depth level
   * @param   startPos        the index from which to start
   * @param   isNavigating    whether a navigation event has occured or not
   */
  function handleobjs(objs, parentElement, level, startPos, isNavigating) {
    
    var columns = opts.columns;
    var actions = opts.actions;
    var animation = opts.animation;
    
    /* Helper for condition check in loop */
    function objConditionHelper(count) {
      if(level==0) {
        return count < startPos+opts.maxFirstLevel; //First level has a constraint
      } else {
        return true;
      }
    }
    
    var e_objs = null;
    if(isNavigating) {
      e_objs = parentElement.children(".jdimlist-objs"); //Retrieve current objs container
      e_objs.empty();
    } else {
      e_objs = $('<ul class="jdimlist-objs"></ul>').appendTo(parentElement); //Create new objs container
    }
    
    if(opts.reorganizable) {
      createDropZoneRow(level, true, false).appendTo(e_objs);
    }
    
    // For each object
    for(var i=startPos; i<objs.length && objConditionHelper(i); i++) {
      
      var isFirst = i == startPos;
      var isLast = i == objs.length-1 || i == startPos+opts.maxFirstLevel -1;
      
      var obj = objs[i];
      var e_obj = createObjectElement(obj, level, isFirst, isLast).appendTo(e_objs);
      
      //Add a drop zone if list is reorganizable
      if(opts.reorganizable) {
        createDropZoneRow(level, false, isLast).appendTo(e_objs);
      }
    
      // object header
      var e_obj_header = $('<div class="jdimlist-obj-headers"></div>');
      var firstColumnWidthLevelModifier = 0; // Depth requires modification of first column so that shifting does not occur
      if(level != 0) {
        for(var j=level; j>1; j--) {
          var e_thisSpacer = $('<div class="jdimlist-obj-header jdimlist-column-level-spacer"></div>');
          if(j==2){
            e_thisSpacer.addClass("jdimlist-column-level-spacer-last");
          }
          e_thisSpacer.html("&nbsp;").appendTo(e_obj_header);
          var thisWidth = opts.levelSpacerWidth;
          /*if(!isNaN(thisWidth)){
            thisWidth = parseInt(e_thisSpacer.css("width"),10); //Issue: widths not set directly to exact class is not found
          }*/
          firstColumnWidthLevelModifier += thisWidth;
        }
        var e_thisLevelIcon = $('<div class="jdimlist-obj-header jdimlist-obj-level-icon"></div>');
        e_thisLevelIcon.html("&nbsp;").appendTo(e_obj_header);
        var thisWidth = opts.levelIconWidth;
          /*if(!isNaN(thisWidth)){
            thisWidth = parseInt(e_thisLevelIcon.css("width"),10); //Issue: widths not set directly to exact class is not found
          }*/
          firstColumnWidthLevelModifier += thisWidth;
      }
      $('<div class="jdimlist-obj-header jdimlist-obj-icon"></div>').appendTo(e_obj_header);
      for(var j=0; j<columns.length; j++) {
        var column = columns[j];
        var e_thisObjHeader = $('<div class="jdimlist-obj-header"></div>');
        e_thisObjHeader.addClass(column["class"]).html(obj[column["dataField"]]).appendTo(e_obj_header);
        //If first column, modify width
        if(j == 0) {
          var thisWidth = column["width"]; //parseInt(e_thisObjHeader.css("width"),10); //Issue: widths not set directly to exact class is not found
          if(!isNaN(thisWidth)){
            e_thisObjHeader.css("width","" + thisWidth - firstColumnWidthLevelModifier);
          }
        }
      }
      for(var j=0; j<actions.length; j++) {
        var current_action = actions[j];
        var e_current_action = $('<div class="jdimlist-obj-action"></div>')
          .addClass(current_action["class"])
          .hover(
            createActionHoverOnHandler(current_action),
            createActionHoverOffHandler(current_action)
          )
          .click(createActionClickHandler(current_action.handler));
        if(opts.individualActions) {
          e_current_action.html(obj[current_action["dataField"]]);
        } else {
          e_current_action.html(current_action["content"]);
        }
        e_current_action.appendTo(e_obj_header);
      }
      $('<div class="jdimlist-terminator"></div>').appendTo(e_obj_header);
      e_obj_header.appendTo(e_obj);
        
      // Object body
      var e_obj_body = $('<div class="jdimlist-obj-body"></div>');
      var bodyHasContent = false;
      
      if(opts.objectText) {
        
        e_obj_body_content = $('<div class="jdimlist-obj-body-content"></div>').appendTo(e_obj_body);
        /*if(level != 0) {
          for(var j=level; j>1; j--) {
            var e_thisSpacer = $('<div class="jdimlist-obj-body-spacer"></div>').html("&nbsp;").appendTo(e_obj_body_content);
          }
          $('<div class="jdimlist-obj-body-level-icon"></div>').html("&nbsp;").appendTo(e_obj_body_content);
        }
        $('<div class="jdimlist-obj-body-icon"></div>').html("&nbsp;").appendTo(e_obj_body_content);*/
        
        //Fix:
        if((typeof opts.bodyLRPadding) !== "undefined" && (typeof opts.bodyLRPadding) !== null 
            && (typeof opts.bodyLRStepPadding) !== "undefined" && (typeof opts.bodyLRStepPadding) !== null) {
          var padding = level * opts.bodyLRStepPadding + opts.bodyLRPadding;
          e_obj_body_content.css("padding", "0 "+padding+"px");
        }
        
        var body_inner_content_width = -firstColumnWidthLevelModifier;
        if(opts.objectText || opts.objectSubmit) {
          for(var j=0; j<columns.length; j++) {
            var column = columns[j];
            body_inner_content_width += column["width"];
          }
        }
        
        // Object text
        $('<div class="jdimlist-obj-text"></div>').html(obj.text).css("width",""+body_inner_content_width+"px").appendTo(e_obj_body_content);
        bodyHasContent = true;
        
        $('<div class="jdimlist-terminator"></div>').appendTo(e_obj_body_content);/**/
      
      }
      
      // Object submit
      if(opts.objectSubmit) {
        if(opts.objectSubmitGenerator(e_obj_body, opts, level)) {
          bodyHasContent = true;
        };
      }
      
      // Sub-objects
      var sub_objs = obj.objs;
      if(opts.subObjects && sub_objs != null && sub_objs.length != 0) {
        handleobjs(sub_objs, e_obj_body, level+1, startPos, false);
        bodyHasContent = true;
      }
      
      // Append body element only if it has any content
      if(bodyHasContent) {
        e_obj_body.appendTo(e_obj).hide();
      }
      
      // Object events
      e_obj_header
        .hover(
          function(){
            //this.style.cursor = "pointer";
            $(this).addClass("jdimlist-obj-headers-hover");
          },
          function(){
            //this.style.cursor = "default"
            $(this).removeClass("jdimlist-obj-headers-hover");
          }
        )
        .click(createobjClickEventHandler(animation, opts.onObjectClick));
      
    }
  }
  
  /**
   * Creates an object element
   * 
   * @param   obj         data object
   * @param   level       object level
   * @param   opts        options object
   */
  function createObjectElement(obj, level, isFirst, isLast) {
    
    //Style class for special cases
    var objSpecialClass = "";
    
    if(level == 0) {
      //Object is a top node
      objSpecialClass += " jdimlist-topnode-obj";
    }
    if(opts.subObjects && obj.objs != null && obj.objs.length != 0){
      //Object is a parent node - it has children
      objSpecialClass += " jdimlist-parentnode-obj";
    }
    if(opts.subObjects && (obj.objs == null || obj.objs.length == 0)) {
      //Object is a leaf node - it has no children
      objSpecialClass += " jdimlist-leafnode-obj";
    }
    if(isFirst) {
      objSpecialClass += " jdimlist-firstnode-obj";
    }
    if(isLast){
      objSpecialClass += " jdimlist-lastnode-obj";
    }
    
    return $('<li class="jdimlist-obj'+objSpecialClass+' jdimlist-collapsed-obj"></li>').attr("id",obj.id);
    
  }
  
  /**
   * Edits an object element
   * 
   * @param   objElement  data object
   * @param   level       object level
   */
  function editObjectElement(objElement, level) {
    
    if(level == 0) {
      //Object is a top node
      objElement.addClass("jdimlist-topnode-obj");
    } else {
      objElement.removeClass("jdimlist-topnode-obj");
    }
    
    if(objElement.children(".jdimlist-obj-body").children(".jdimlist-objs").length != 0){
      //Object is a parent node - it has children
      objElement.addClass("jdimlist-parentnode-obj");
      objElement.removeClass("jdimlist-leafnode-obj");
    } else {
      //Object is a leaf node - it has no children
      objElement.addClass("jdimlist-leafnode-obj");
      objElement.removeClass("jdimlist-parentnode-obj");
    }
    
  }
  
  /**
   * Initializes all drop zones in a list
   * 
   * @param   jdimlistElement       a list element
   */
  function initializeDropZones(jdimlistElement, opts) {
    jdimlistElement.find(".jdimlist-dropzone").droppable(getDropzoneDroppableOptions(opts));
    jdimlistElement.find(".jdimlist-obj-headers").droppable(getObjectDroppableOptions(opts));
  }
  
  /**
   * Returns the options, including drop handler, used for drop zones
   */
  function getDropzoneDroppableOptions(opts) {
    return {
      accept: ".jdimlist-obj",
      greedy: true,
      activeClass: "jdimlist-possible-dropzone",
      hoverClass: "jdimlist-hovered-dropzone",
      tolerance: "pointer",
      drop: function(event, ui) {
        
        var drop_zone_row = $(this).parent();
        var dropped_component = ui.draggable;
        var dropped_id = dropped_component.attr("id");
        
        //Place object where it was dropped. If dropped on a zone next to itself it should be handled differently.
        if(dropped_id == drop_zone_row.prev().attr("id")) {
          //Zone directly below the dragged object
          
          //Add source element
          drop_zone_row.before(dropped_component.attr("style","position: relative;"));
          
        } else if(dropped_id == drop_zone_row.next().attr("id")) { 
          //Zone directly above the dragged object
      
          //Add source element
          drop_zone_row.after(dropped_component.attr("style","position: relative;"));
          
        } else { 
          //Zone somewhere else
          
          //Remove a zone from source to avoid duplicates
          dropped_component.prev().remove();
          
          //Determine depth of drop zone
          var zoneDepth = getDropzoneDepth(drop_zone_row);
          
          //Determine depth of source element
          var objDepth = getObjectDepth(dropped_component);
          
          //Set correct depth accordingly
          editObjDepth(dropped_component, zoneDepth - objDepth);
          
          //Set correct status on the element
          editObjectElement(dropped_component, zoneDepth);
          
          //TODO: The shared data object is not correctly changed
          alert("NEWPOS"+getZonePosition(drop_zone_row)+"TARGETDEPTH:"+zoneDepth);
          //moveDataObject(dropped_id, zoneDepth, getZonePosition(drop_zone_row));
          moveDataObject(dropped_id, drop_zone_row.parent().parent().parent(), getZonePosition(drop_zone_row));
              
          //Insert a new drop zone above element that is coming
          drop_zone_row.before(drop_zone_row.clone());
          
          //Add source element
          drop_zone_row.before(dropped_component.attr("style","position: relative;"));
          
        }
        
        //Reinitialize all zones
        initializeDropZones(drop_zone_row.parents(".jdimlist"), opts);
        
      }
    };
  }
  
  /**
   * Manipulates the data object by moving one of it's contained objects from 
   * one position to another.
   * 
   * @param   objID           ID of the object to be moved
   * @param   targetParentID  ID of the target parent object
   * @param   targetPosition  Position index within target parents sub-objects
   */
  function moveDataObject(objID, targetID, targetPosition) {

    var objToMove = null;
    
    function removeObj(obj, parentObj, pos) {
      var thisObjID = obj.id;
      if(thisObjID == null || thisObjID == undefined) { thisObjID = 0; }
      if(thisObjID == objID) {
        objToMove = obj;
        parentObj.objs.splice(pos,1); //Remove
      }
      var thisSubObjs = obj.objs;
      for(var i=0; i<thisSubObjs.length; i++) {
        removeObj(thisSubObjs[i],obj,i);
      }
    }
    
    //TODO: Insert does not work properly
    function insertObj(obj) {
      var thisObjID = obj.id;
      if(thisObjID == null || thisObjID == undefined) { thisObjID = 0; }
      if(thisObjID == targetID) {
        obj.splice(targetPosition,0,objToMove); //Insert
      }
      var thisSubObjs = obj.objs;
      for(var i=0; i<thisSubObjs.length; i++) {
        insertObj(thisSubObjs[i]);
      }
    }
    
    removeObj(opts.data, opts.data, 0);
    insertObj(opts.data);
    
    var j = 1;
    
    //opts.onObjectDrop(objOldParentID, objOldPos, objNewParentID, objNewPos);
    
    /*
    var objs = opts.data.objs;
    
    var objToMove = {};
    var objSiblings = null;
    var newSiblings = null;
    var objOldPos = 0;
    var objNewPos = 0;
    var objOldParentID = 0;
    var objNewParentID = 0;
    var newParentObj = {};
    
    var objFound = false;
    var targetFound = false;
    
    
    function handleObject(obj, parentObj, pos, level) {
        
        var siblings = parentObj.objs;
          
        //Determine whether this is the object to move by comparing ID
        if(obj.id == objID) {
          objToMove = obj;
          objOldPos = pos;
          objOldParentID = parentObj.id;
          objFound = true;
          objSiblings = siblings;
          objSiblings.splice(objOldPos,1); //Remove
        }
        
        //If this is the correct depth level
        if(level == targetDepth) {
          if(targetPosition > siblings.length-1) {
            newParentObj = parentObj;
            objNewPos = siblings.length;
            objNewParentID = parentObj.id;
            targetFound = true;
            newSiblings = siblings;
          } else if(pos == targetPosition) {
            newParentObj = parentObj;
            objNewPos = pos;
            objNewParentID = parentObj.id;
            targetFound = true;
            newSiblings = siblings;
          }
        }
      
        //Continue recursively if not done and depth hasn't been reached
        if((!objFound || !targetFound) && targetDepth > level) {
          var subObjs = obj.objs;
          if(subObjs != null) {
            for(var i=0; i<subObjs.length; i++) {
              handleObject(subObjs[i], obj, i, level+1);
            }
          }
        }
      
    }
    
    for(var i=0; i<objs.length; i++) {
      handleObject(objs[i], opts.data, i, 0);
    }
        
    if(targetFound) {
      newParentObj.objs.splice(objNewPos,0,objToMove); //Insert
    }
    
    if(objOldParentID == null || objOldParentID == undefined) { objOldParentID = 0; }
    if(objNewParentID == null || objNewParentID == undefined) { objNewParentID = 0; }
    opts.onObjectDrop(objOldParentID, objOldPos, objNewParentID, objNewPos);
    
    */
    
  }
  
  /**
   * Calculates the position of a drop zone (that is, the position of the previous object).
   * Used to determine new position for a dropped object
   * 
   * @param   drop_zone_row       the target drop zone row on which the object was dropped
   */
  function getZonePosition(drop_zone_row) {
    //Count number of objects before this zone
    return drop_zone_row.prevAll(".jdimlist-obj").length;
  }
  
  /**
   * Returns the options, including drop handler, used for droppable objects
   * 
   * @param     opts      the options object
   */
  function getObjectDroppableOptions(opts) {
    return {
      accept: ".jdimlist-obj",
      greedy: true,
      hoverClass: "jdimlist-obj-headers-drop-hover",
      tolerance: "pointer",
      drop: function(event, ui) {
        
        var target_obj = $(this).parent();
        var dropped_component = ui.draggable;
        
        //Remove a zone from source to avoid duplicates
        var drop_zone_row = dropped_component.prev().remove();
        
        //Save old parent
        var dropped_parent = getObjectParent(dropped_component);
        
        //Determine depth of drop target
        var targetDepth = getObjectDepth(target_obj);
        
        //Determine depth of source element
        var objDepth = getObjectDepth(dropped_component);
        
        //Set correct depth accordingly
        editObjDepth(dropped_component, targetDepth+1 - objDepth);
        
        //Set correct status on the element
        editObjectElement(dropped_component, targetDepth);
        
        var subObjectsRoot = getObjectSubObjectsRoot(target_obj);
        if(hasSubObjects(target_obj)) {
          //Add source element and a drop zone
          subObjectsRoot
            .append(dropped_component.attr("style","position: relative;"))
            .append(createDropZoneRow(targetDepth+1), false, false); //TODO: Handle isFirst/isLast correctly
        } else {
          addObjectSubObjectsRoot(target_obj)
            .append(createDropZoneRow(targetDepth+1), false, false) //TODO: Handle isFirst/isLast correctly
            .append(dropped_component.attr("style","position: relative;"))
            .append(createDropZoneRow(targetDepth+1), false, false); //TODO: Handle isFirst/isLast correctly
        }
        
        //If the old parent no longer has any sub-objects
        if(getObjectSubObjects(dropped_parent).length == 0) {
          //Remove body if text is diabled, otherwise only sub-objects root
          if(!opts.objectText) {
            getObjectBody(dropped_parent).remove();
          } else {
            getObjectSubObjectsRoot(dropped_parent).remove();
          }
        }
        
        //Reinitialize all zones
        initializeDropZones(target_obj.parents(".jdimlist"), opts);
        
      }
    };
  }
  
  function getObjectParent(obj) {
    return obj.parent().parent().parent();
  }
  
  function getObjectHeaders(obj) {
    return obj.children(".jdimlist-obj-headers");
  }
  
  function getObjectBody(obj) {
    return obj.children(".jdimlist-obj-body");
  }
  
  function getObjectSubObjectsRoot(obj) {
    return getObjectBody(obj).children(".jdimlist-objs");
  }
  
  function getObjectSubObjects(obj) {
    return getObjectSubObjectsRoot(obj).children(".jdimlist-obj");
  }
  
  function getObjectDropzones(obj) {
    return getObjectSubObjectsRoot(obj).children(".jdimlist-dropzone-row");
  }
  
  function hasSubObjects(obj) {
    return getObjectSubObjects(obj).length != 0;
  }
  
  function addObjectBody(obj) {
    return $('<div class="jdimlist-obj-body"></div>').appendTo(obj);
  }
  
  function addObjectSubObjectsRoot(obj) {
    return $('<div class="jdimlist-objs"></div>').appendTo(addObjectBody(obj));
  }
  
  function isExpanded(obj) {
    return obj.hasClass("jdimlist-expanded-obj");
  }
  
  function isSelected(obj) {
    return obj.hasClass("jdimlist-selected-obj");
  }
  
  function setSelected(obj, mode) {
    if(mode) {
      obj.parents(".jdimlist").find(".jdimlist-obj").removeClass("jdimlist-selected-obj");
      obj.addClass("jdimlist-selected-obj");
    } else {
      obj.removeClass("jdimlist-selected-obj");
    }
  }
  
  /**
   * Calculates the depth of a drop zone
   * 
   * @param   dropZoneRow   a drop zone row element
   */
  function getDropzoneDepth(dropzoneRow) {
    var depthCount = 0;
    if(dropzoneRow.children(".jdimlist-dropzone-level-icon").length != 0) {
      depthCount += 1 + dropzoneRow.children(".jdimlist-dropzone-level-spacer").length;
    }
    return depthCount;
  }
  
  /**
   * Calculates the depth of an object
   * 
   * @param   obj           an object element
   */
  function getObjectDepth(obj) {
    var depthCount = 0;
    if(obj.children(".jdimlist-obj-headers").children(".jdimlist-obj-level-icon").length != 0) {
      depthCount += 1 + obj.children(".jdimlist-obj-headers").children(".jdimlist-column-level-spacer").length;
    }
    return depthCount;
  }
  
  /**
   * Edits the depth of an object and any sub-objects
   * 
   * @param   obj           an object element
   * @param   offset        a relative whole number value
   */
  function editObjDepth(obj, offset) {
    
    if(offset != 0) {
      
      var e_headers = getObjectHeaders(obj);
      var e_levelIcon = e_headers.children(".jdimlist-obj-level-icon");
      var e_objectIcon = e_headers.children(".jdimlist-obj-icon");
      
      //Object should go deeper
      if(offset > 0) {
        
        var offsetCount = offset;
        
        //Begin with level icon if it does not already exist
        if(e_levelIcon.length == 0) {
          e_levelIcon = $('<div class="jdimlist-obj-header jdimlist-obj-level-icon">&nbsp;</div>');
          e_objectIcon.before(e_levelIcon);
          offsetCount--;
        }
        
        //Add spacers
        for(var i=0; i<offsetCount; i++) {
          e_levelIcon.before(
            $('<div class="jdimlist-obj-header jdimlist-column-level-spacer">&nbsp;</div>')
          );
        }
        
      } else {
        
        for(var i=-offset; i>0; i--) {
          
          var e_spacers = e_headers.children(".jdimlist-column-level-spacer");
          
          //Remove spacers as long as there are any, else remove level icon
          if(e_spacers.length != 0) {
            e_spacers.eq(0).remove();
          } else {
            e_levelIcon.remove();
          }
        
        }
        
        
      }
    
      //Adjust any drop zones
      var dropzones = getObjectDropzones(obj);
      for(var i=0; i<dropzones.length; i++) {
        editDropzoneDepth(dropzones.eq(i), offset);
      }
    
      //Do recursively for any sub-objects
      var subObjs = getObjectSubObjects(obj);
      for(var i=0; i<subObjs.length; i++) {
        editObjDepth(subObjs.eq(i), offset);
      }
      
      
    }
    
  }
  
  /**
   * Edit the depth of a drop zone
   * 
   * @param     obj     the object
   * @param     offset  an offset for adjustment
   */
  function editDropzoneDepth(obj, offset){
      
    var e_levelIcon = obj.children(".jdimlist-dropzone-level-icon");
    var e_objectIcon = obj.children(".jdimlist-dropzone-icon");
    
  
    //Zone should go deeper
    if(offset > 0) {
      
      var offsetCount = offset;
      
      //Begin with level icon if it does not already exist
      if(e_levelIcon.length == 0) {
        e_levelIcon = $('<div class="jdimlist-dropzone-level-icon">&nbsp;</div>');
        e_objectIcon.before(e_levelIcon);
        offsetCount--;
      }
      
      //Add spacers
      for(var i=0; i<offsetCount; i++) {
        e_levelIcon.before(
          $('<div class="jdimlist-dropzone-level-spacer">&nbsp;</div>')
        );
      }
      
    } else {
      
      for(var i=-offset; i>0; i--) {
        
        var e_spacers = obj.children(".jdimlist-dropzone-level-spacer");
        
        //Remove spacers as long as there are any, else remove level icon
        if(e_spacers.length != 0) {
          e_spacers.eq(0).remove();
        } else {
          e_levelIcon.remove();
        }
      
      }
      
    }
      
  }
  
  /**
   * Creates a drop zone row elemenent 
   * 
   * @param   level       at which depth level
   */
  function createDropZoneRow(level, isFirst, isLast) {
    var e_dropzone_row = $('<li class="jdimlist-dropzone-row"></li>');
    if(level != 0) {
      for(var j=level; j>1; j--) {
        var e_spacer = $('<div class="jdimlist-dropzone-level-spacer"></div>').html("&nbsp;");
        if(j == 2) {
          e_spacer.addClass("jdimlist-dropzone-level-spacer-last");
        }
        e_spacer.appendTo(e_dropzone_row);
      }
      $('<div class="jdimlist-dropzone-level-icon"></div>').html("&nbsp;").appendTo(e_dropzone_row);
    }
    $('<div class="jdimlist-dropzone-icon"></div>').appendTo(e_dropzone_row);
    $('<div class="jdimlist-dropzone"></div>').appendTo(e_dropzone_row);
    $('<div class="jdimlist-terminator"></div>').appendTo(e_dropzone_row);
    if(isFirst) {
      e_dropzone_row.addClass("jdimlist-dropzone-first-row");
    }
    if(isLast) {
      e_dropzone_row.addClass("jdimlist-dropzone-last-row");
    }
    
    return e_dropzone_row;
  }
  
  /**
   * Animeate an element
   * 
   * @param     targetElement   the element
   * @param     mode            animation mode (like "toggle")
   * @param     animationPrefs  animation preferences
   * @param     callback        a callback function
   */
  function jdimlistAnimate(targetElement, mode, animationPrefs, callback){
    targetElement.animate(
      {
        height : mode, 
        opacity : mode
      }, 
      animationPrefs.duration, 
      animationPrefs.easing, 
      callback
    );
  }
  
  /**
   * Creates an event handler for obj clicks
   * 
   * @param   animation   animation hash object
   * @param   callback    callback function
   */
  function createobjClickEventHandler(animationPrefs, callback){
    return function(e){
      
      var e_thisObj = $(this).parent();
      
      if(isSelected(e_thisObj)){
        setSelected(e_thisObj, false);
        //Will collapse
      } else {
        if(isExpanded(e_thisObj)) {
          //Will collapse
        } else {
          setSelected(e_thisObj, true);
          //Will expand
        }
      }
      
      //Collapse any expanded children
      if(opts.cascadeCollapse) {
        e_thisObj.find(".jdimlist-expanded-obj").each(function(i){
          toggleObjectBody($(this), animationPrefs);
        });
      }
      
      //Toggle this object's body
      toggleObjectBody(e_thisObj, animationPrefs);

      callback();
      
    }
  }
  
  /**
   * Toggles the body of an object
   * 
   * @param     objs              the object(s)
   * @param     animationPrefs    animation preferences
   */
  function toggleObjectBody(obj, animationPrefs) {
    jdimlistAnimate(
      getObjectBody(obj), 
      "toggle", 
      animationPrefs,
      function(e){
        // Toggle expand/collapse state
        if(obj.hasClass("jdimlist-collapsed-obj")) {
          obj.removeClass("jdimlist-collapsed-obj").addClass("jdimlist-expanded-obj");
        } else {
          obj.removeClass("jdimlist-expanded-obj").addClass("jdimlist-collapsed-obj");
        }
      }
    );
  }
  
 /**
  * Creates an event handler for action clicks. Necessary to avoid event propagation.
  *
  * @param   handler     handler function
  */
  function createActionClickHandler(handler) {
    return function(e){
      e.stopPropagation();
      var obj = $(this).parent().parent();
      handler(e,obj);
    }
  }
  
  /**
   * Creates an event handler for navigation clicks
   * 
   * @param     navigation    the navigation data object
   * @param     offset        how many steps to move
   * @param     parentElement parent element
   */
  function createNavigationClickHandler(navigation, offset, parentElement){
    return function(e) {
      var newStep = navigation.currentStep + offset;
      //Allow only if new step is not negative and not beyond data size
      if(newStep >= 0 && newStep < opts.data.objs.length) {
        navigation.currentStep = newStep;
        handleobjs(opts.data.objs, parentElement, 0, navigation.currentStep, true);
      }
    }
  }
  
  /**
   * Returns an event handler for hovering on action elements
   * 
   * @param     current_action    the action element
   */
  function createActionHoverOnHandler(current_action) {
    return function(e) {
      $(this).addClass(current_action.classHover);
    }
  }
  
  /**
   * Returns an event handler for end of hovering on action elements
   * 
   * @param     current_action    the action element
   */
  function createActionHoverOffHandler(current_action) {
    return function(e) {
      $(this).removeClass(current_action.classHover);
    }
  }
  
})(jQuery);

$.fn.jdimlist.defaults = {
  data:{},                //Data JSON object (if ajax isn't enabled)
  columns:[],             //An array of columns to use
  animation:{             //Animation preferences
    duration:"slow", 
    easing:"swing"
  },
  actions:[],             //An array of actions for each object
  individualActions:true, //Whether actions are defined on an object level
  ajaxUrl:"",             //url to json feed to enable ajax
  ajaxParams:{},          //ajax parameters
  ajaxProgressive:true,   //Whether to load data progressively
  ajaxBeforeLoad:function(data){},  //a function to process ajax response before loading
  ajaxAfterLoad:function(data){},   //a function to process ajax response after loading
  maxFirstLevel:50,       //Maximum of first level objects
  headers:true,           //Show headers
  headerWrapper:null,     //a jQuery element to use as wrapper around header labels
  objectText:false,       //Show object's text content in body
  objectSubmit:false,     //Enable submit for each object and top level
  objectSubmitGenerator:function(e_obj_body, opts, level){return true;},   //A function that generates submit content
  subObjects:true,        //Enable sub-objects
  navigation:false,       //Enable navigation
  navigationLabelBack:"FORRIGE",
  navigationLabelNext:"NESTE",
  reorganizable:false,    //Enable drag'n'drop
  stickyness:10,          //Number of pixels an object must be dragged before dragging starts
  cascadeCollapse:true,   //Whether or not to cascade collapsing to child objects
  onObjectClick:function(obj){},  //Event handler for clicks on objects
  onObjectDrop:function(oldParentID, oldPos, newParentID, newPos){alert("[FROM ID:" + oldParentID + " POS:" + oldPos + "] [TO ID:" + newParentID + " POS:" + newPos + "]");},
  doSort:function(objs, dataField, sortType){return $(objs).sort(dataField,sortType);},
  levelSpacerWidth:0,
  levelIconWidth:0,
  bodyLRPadding:0,        //Fix
  bodyLRStepPadding:0     //Fix
};
