(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
  };
  
  $.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"]);
          }
          
          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);
      }
      
      //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;
    }
    $.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",*/
          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();
    if(opts.ajaxUrl != "") {
      getAjax(e_jdimlist, navigation);
    } else {
      if(loadObjects(e_jdimlist)){
        loadNavigation(e_jdimlist,navigation);
      };
    }
    
    // Top level submit
    if(opts.objectSubmit) {
      opts.objectSubmitGenerator(e_jdimlist, opts);
    }
      
  }
  
  /**
   * 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;
    
    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).appendTo(e_objs);
    }
    
    for(var i=startPos; i<objs.length && i<startPos+opts.maxFirstLevel; i++) {
      
      var obj = objs[i];
      var e_obj = createObjectElement(obj, level).appendTo(e_objs);
      
      //Add a drop zone if list is reorganizable
      if(opts.reorganizable) {
        createDropZoneRow(level).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>');
          e_thisSpacer.html("&nbsp;").appendTo(e_obj_header);
          var thisWidth = parseInt(e_thisSpacer.css("width"),10);
          if(!isNaN(thisWidth)){
            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 = parseInt(e_thisLevelIcon.css("width"),10);
          if(!isNaN(thisWidth)){
            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 = parseInt(e_thisObjHeader.css("width"),10);
          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;
      
      // Object text
      if(opts.objectText) {
        $('<div class="jdimlist-obj-text"></div>').html(obj.text).appendTo(e_obj_body);
        bodyHasContent = true;
      }
      
      // Object submit
      if(opts.objectSubmit) {
        if(opts.objectSubmitGenerator(e_obj_body, opts)) {
          bodyHasContent = true;
        };
      }
      
      // Sub-objects
      var sub_objs = obj.subObjs;
      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) {
    
    //Style class for special cases
    var objSpecialClass = "";
    
    if(level == 0) {
      //Object is a top node
      objSpecialClass += " jdimlist-topnode-obj";
    }
    if(opts.subObjects && obj.subObjs != null && obj.subObjs.length != 0){
      //Object is a parent node - it has children
      objSpecialClass += " jdimlist-parentnode-obj";
    }
    if(opts.subObjects && (obj.subObjs == null || obj.subObjs.length == 0)) {
      //Object is a leaf node - it has no children
      objSpecialClass += " jdimlist-leafnode-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;
        
        //Place object where it was dropped. If dropped on a zone next to itself it should be handled differently.
        if(dropped_component.attr("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_component.attr("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
          moveDataObject(dropped_component.attr("id"), zoneDepth, 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   targetDepth   The depth level of the new position
   * @param   targetPosition    The position within a level
   */
  function moveDataObject(objID, targetDepth, targetPosition) {
    
    var objs = opts.data.objs;
    
    var objToMove = {};
    var objFound = false;
    var objMoved = false;
    
    var objOldPos = 0;
    var objNewPos = 0;
    var objOldParent = 0;
    var objNewParent = 0;
    
    function handleObject(obj, parentObj, pos, level) {
        
        var parentArray = [];
        
        if(parentObj == objs){
          //This is a first level object
          parentArray = objs;
        } else {
          parentArray = parentObj.subObjs;
        }
          
        //Determine whether this is the object to move by comparing ID
        if(obj.id == objID) {
          objToMove = obj;
          objOldPos = pos;
          objOldParent = parentObj.id;
          parentArray.splice(pos,1);
          objFound = true;
        }
        
        //TODO: objToMove might not yet have been found when inserted here
        //Determine if this object is in the target position
        if(level == targetDepth && pos == targetPosition) {
          objNewPos = pos;
          objNewParent = parentObj.id;
          parentArray.splice(pos+1,0,objToMove);
          objMoved = true;
        } else if(level == targetDepth && pos > targetPosition) {
          //Check if target position is beyond current array
          if(parentObj[pos+1] == null) {
            objNewPos = pos;
            objNewParent = parentObj.id;
            parentArray.splice(pos+1,0,objToMove);
            objMoved = true;
          }
        }
        
        //Continue recursively if not done
        if(!(objFound && objMoved)) {
          var subObjs = obj.subObjs;
          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], objs, i, 0);
    }
    
    opts.onObjectDrop(objOldParent, objOldPos, objNewParent, 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));
        } else {
          addObjectSubObjectsRoot(target_obj)
            .append(createDropZoneRow(targetDepth+1))
            .append(dropped_component.attr("style","position: relative;"))
            .append(createDropZoneRow(targetDepth+1));
        }
        
        //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) {
    var e_dropzone_row = $('<li class="jdimlist-dropzone-row"></li>');
    if(level != 0) {
      for(var j=level; j>1; j--) {
        $('<div class="jdimlist-dropzone-level-spacer"></div>').html("&nbsp;").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);
    
    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){return true;},   //A function that generates submit content
  subObjects:true,        //Enable sub-objects
  navigation:false,       //Enable navigation
  navigationLabelBack:"FORRIGE",
  navigationLabelNext:"NESTE",
  sortable:false,         //TODO
  reorganizable:false,    //Enable drag'n'drop
  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(oldParentID + " " + oldPos + " " + newParentID + " " + newPos);}
};
