text: function ResourceManager(options) {
    var t = this;
    // exports
    t.fetchResources = fetchResources;
    t.setResources = setResources;
    t.mutateResourceEvent = mutateResourceEvent;
    // locals
    var resourceSources = [];
    var cache;
    // initialize the resources.
    setResources(options.resources);
    // add the resource sources
  
    function setResources(sources) {
      resourceSources = [];
      var resource;
      if ($.isFunction(sources)) {
        // is it a function?
        resource = {
          resources: sources
        };
        resourceSources.push(resource);
        cache = undefined;
      } else if (typeof sources == 'string') {
        // is it a URL string?
        resource = {
          url: sources
        };
        resourceSources.push(resource);
        cache = undefined;
      } else if (typeof sources == 'object' && sources != null) {
        // is it json object?
        for (var i = 0; i < sources.length; i++) {
          var s = sources[i];
          normalizeSource(s);
          resource = {
            resources: s
          };
          resourceSources.push(resource);
        }
        cache = undefined;
      }
    }
    /**
     * ----------------------------------------------------------------
     * Fetch resources from source array
     * ----------------------------------------------------------------
     */
  
    function fetchResources(useCache, currentView) {
      // if useCache is not defined, default to true
      useCache = (typeof useCache !== 'undefined' ? useCache : true);
      if (!useCache || cache === undefined) {
        // do a fetch resource from source, rebuild cache
        cache = [];
        var len = resourceSources.length;
        for (var i = 0; i < len; i++) {
          var resources = fetchResourceSource(resourceSources[i], currentView);
          cache = cache.concat(resources);
        }
      }
  
      if($.isFunction(options.resourceFilter)) {
        return $.grep(cache, options.resourceFilter);
      }
  
      return cache;
    }
  
    /**
     * ----------------------------------------------------------------
     * Fetch resources from each source.  If source is a function, call
     * the function and return the resource.  If source is a URL, get
     * the data via synchronized ajax call.  If the source is an
     * object, return it as is.
     * ----------------------------------------------------------------
     */
  
    function fetchResourceSource(source, currentView) {
      var resources = source.resources;
      if (resources) {
        if ($.isFunction(resources)) {
          return resources();
        }
      } else {
        var url = source.url;
        if (url) {
          var data = {};
          if (typeof currentView === 'object') {
            var startParam = options.startParam;
            var endParam = options.endParam;
            if (startParam) {
              data[startParam] = Math.round(+currentView.intervalStart / 1000);
            }
            if (endParam) {
              data[endParam] = Math.round(+currentView.intervalEnd / 1000);
            }
          }
          $.ajax($.extend({}, ajaxDefaults, source, {
            data: data,
            dataType: 'json',
            cache: false,
            success: function(res) {
              res = res || [];
              resources = res;
            },
            error: function() {
              // TODO - need to rewrite callbacks, etc.
              //alert("ajax error getting json from " + url);
            },
            async: false // too much work coordinating callbacks so dumb it down
          }));
        }
      }
      return resources;
    }
    /**
     * ----------------------------------------------------------------
     * normalize the source object
     * ----------------------------------------------------------------
     */
  
    function normalizeSource(source) {
      if (source.className) {
        if (typeof source.className == 'string') {
          source.className = source.className.split(/\s+/);
        }
      } else {
        source.className = [];
      }
      var normalizers = fc.sourceNormalizers;
      for (var i = 0; i < normalizers.length; i++) {
        normalizers[i](source);
      }
    }
  
    /* Event Modification Math
    -----------------------------------------------------------------------------------------*/
  
  
    // Modify the date(s) of an event and make this change propagate to all other events with
    // the same ID (related repeating events).
    //
    // If `newStart`/`newEnd` are not specified, the "new" dates are assumed to be `event.start` and `event.end`.
    // The "old" dates to be compare against are always `event._start` and `event._end` (set by EventManager).
    //
    // Returns an object with delta information and a function to undo all operations.
    //
    function mutateResourceEvent(event, newResources, newStart, newEnd) {
      var oldAllDay = event._allDay;
      var oldStart = event._start;
      var oldEnd = event._end;
      var clearEnd = false;
      var newAllDay;
      var dateDelta;
      var durationDelta;
      var undoFunc;
  
      // if no new dates were passed in, compare against the event's existing dates
      if (!newStart && !newEnd) {
        newStart = event.start;
        newEnd = event.end;
      }
  
      // NOTE: throughout this function, the initial values of `newStart` and `newEnd` are
      // preserved. These values may be undefined.
  
      // detect new allDay
      if (event.allDay != oldAllDay) { // if value has changed, use it
        newAllDay = event.allDay;
      }
      else { // otherwise, see if any of the new dates are allDay
        newAllDay = !(newStart || newEnd).hasTime();
      }
  
      // normalize the new dates based on allDay
      if (newAllDay) {
        if (newStart) {
          newStart = newStart.clone().stripTime();
        }
        if (newEnd) {
          newEnd = newEnd.clone().stripTime();
        }
      }
  
      // compute dateDelta
      if (newStart) {
        if (newAllDay) {
          dateDelta = dayishDiff(newStart, oldStart.clone().stripTime()); // treat oldStart as allDay
        }
        else {
          dateDelta = dayishDiff(newStart, oldStart);
        }
      }
  
      if (newAllDay != oldAllDay) {
        // if allDay has changed, always throw away the end
        clearEnd = true;
      }
      else if (newEnd) {
        durationDelta = dayishDiff(
          // new duration
          newEnd || t.getDefaultEventEnd(newAllDay, newStart || oldStart),
          newStart || oldStart
        ).subtract(dayishDiff(
          // subtract old duration
          oldEnd || t.getDefaultEventEnd(oldAllDay, oldStart),
          oldStart
        ));
      }
  
      undoFunc = mutateResourceEvents(
        t.clientEvents(event._id), // get events with this ID
        clearEnd,
        newAllDay,
        dateDelta,
        durationDelta,
        newResources
      );
  
      return {
        dateDelta: dateDelta,
        durationDelta: durationDelta,
        undo: undoFunc
      };
    }
  
  
    // Modifies an array of events in the following ways (operations are in order):
    // - clear the event's `end`
    // - convert the event to allDay
    // - add `dateDelta` to the start and end
    // - add `durationDelta` to the event's duration
    //
    // Returns a function that can be called to undo all the operations.
    //
    function mutateResourceEvents(events, clearEnd, forceAllDay, dateDelta, durationDelta, newResources) {
      var isAmbigTimezone = t.getIsAmbigTimezone();
      var undoFunctions = [];
  
      $.each(events, function(i, event) {
        var oldResources = event.resources;
        var oldAllDay = event._allDay;
        var oldStart = event._start;
        var oldEnd = event._end;
        var newAllDay = forceAllDay != null ? forceAllDay : oldAllDay;
        var newStart = oldStart.clone();
        var newEnd = (!clearEnd && oldEnd) ? oldEnd.clone() : null;
  
        // NOTE: this function is responsible for transforming `newStart` and `newEnd`,
        // which were initialized to the OLD values first. `newEnd` may be null.
  
        // normlize newStart/newEnd to be consistent with newAllDay
        if (newAllDay) {
          newStart.stripTime();
          if (newEnd) {
            newEnd.stripTime();
          }
        }
        else {
          if (!newStart.hasTime()) {
            newStart = t.rezoneDate(newStart);
          }
          if (newEnd && !newEnd.hasTime()) {
            newEnd = t.rezoneDate(newEnd);
          }
        }
  
        // ensure we have an end date if necessary
        if (!newEnd && (options.forceEventDuration || +durationDelta)) {
          newEnd = t.getDefaultEventEnd(newAllDay, newStart);
        }
  
        // translate the dates
        newStart.add(dateDelta);
        if (newEnd) {
          newEnd.add(dateDelta).add(durationDelta);
        }
  
        // if the dates have changed, and we know it is impossible to recompute the
        // timezone offsets, strip the zone.
        if (isAmbigTimezone) {
          if (+dateDelta || +durationDelta) {
            newStart.stripZone();
            if (newEnd) {
              newEnd.stripZone();
            }
          }
        }
  
        event.allDay = newAllDay;
        event.start = newStart;
        event.end = newEnd;
        event.resources = newResources;
        backupEventDates(event);
  
        undoFunctions.push(function() {
          event.allDay = oldAllDay;
          event.start = oldStart;
          event.end = oldEnd;
          event.resources = oldResources;
          backupEventDates(event);
        });
      });
  
      return function() {
        for (var i=0; i<undoFunctions.length; i++) {
          undoFunctions[i]();
        }
      };
    }
  }
  