/*
 * Ext Scheduler v1.5 beta
 * Copyright(c) 2009-2010 Mats Bryntse Consulting
 * mats@ext-scheduler.com
 * http://www.ext-scheduler.com/license.html
 *
 */
 
Ext.ns('Sch');

/**
 * @class Sch.SchedulerPanel
 * @extends Ext.grid.GridPanel
 * <p>This class represents the primary interface of a component based grid control to represent events 
 * in a timeline format of rows and columns. The SchedulerPanel is composed of the following:</p>
 * <div class="mdetail-params"><ul>
 * <li><b>{@link Ext.data.Store Store}</b> : The Model holding the resource records (rows)
 * <div class="sub-desc"></div></li>
 * <li><b>{@link Ext.data.Store EventStore}</b> : The Model holding the event records (events)
 * <div class="sub-desc"></div></li>
 * <li><b>{@link Ext.grid.ColumnModel Column model}</b> : Static columns defined be you, to which the time columns are later appended
 * <div class="sub-desc"></div></li>
 * <li><b>{@link Sch.SchedulerView View}</b> : Encapsulates the user interface 
 * <div class="sub-desc"></div></li>
 * <li><b>{@link Sch.EventSelectionModel Selection model}</b> : Selection behavior 
 * <div class="sub-desc"></div></li>
 * </ul></div>
 * <p>Example usage:</p>
 * <pre><code>
        // Store holding event categories
        var eventCategoryStore = new Ext.data.JsonStore({
            proxy: new Ext.data.ScriptTagProxy({
                callbackParam : 'callback',
                url: 'http://upcoming.yahooapis.com/services/rest/'
            })
            sortInfo:{field: 'Name', direction: "ASC"},
            root : 'yourRoot',
            idProperty : 'Id',
            fields : [
                {name: 'Id', mapping: 'id'},    // Mandatory field
                {name: 'Description', mapping: 'description'},
                {name: 'Name', mapping: 'name'}
            ]
        });
        
        // Store holding all the events
        var eventStore = new Ext.data.JsonStore({
             proxy: new Ext.data.ScriptTagProxy({
                callbackParam : 'callback',
                url: 'http://upcoming.yahooapis.com/services/rest/'
            }),
            sortInfo:{field: 'ResourceId', direction: "ASC"},
            idProperty : 'Id',
            root : 'yourRoot',
            fields : [
                {name: 'Id', type:'string', mapping: 'id'},     // Mandatory field
                {name: 'ResourceId', mapping: 'category_id'},   // Mandatory field
                {name: 'StartDate', type : 'date'},             // Mandatory field
                {name: 'EndDate', type : 'date'}                // Mandatory field
                
                {name: 'Title', mapping: 'name'},   
                {name: 'Description', mapping: 'description'}
            ]
        });
        
        var g = new Sch.SchedulerPanel({
            height:600,
            autoWidth:true,
            renderTo : 'grid-upcoming',
            startParamName : 'min_date',
            endParamName : 'max_date',
            trackMouseOver : false,
            region : 'center',
            stripeRows : true,
            
            // Setup static columns
            columns : [
               {header : 'Category', sortable:true, width:170, dataIndex : 'Name'},
               {header : 'Description', sortable:true, width:170, dataIndex : 'Description'}
            ],
            
            viewConfig : {
                forceFit:true
            },
            
            store : eventCategoryStore,     // Mandatory property
            eventStore : eventStore,        // Mandatory property
            
             // Setup your view configuration
            autoViews : [
                {
                    // Timespan <= 1 week show day columns
                    timeSpan : 7, 
                    columnType : 'day',
                    renderer : this.renderer
                }
            ]
        });
 * </code></pre>
 * <p><b><u>Notes:</u></b></p>
 * <div class="mdetail-params"><ul>
 * <li>A scheduler panel requires some config parameters to function properly:</li>
 * <li>If you're making use of the autoViews functionality, you need to supply names of your start/end parameters, 'startParamName' and 'endParamName'</li>
 * <li>Store and EventStore must be supplied, both stores need some mandatory fields, see code above</li>
 * </ul>
 *
 * The columnType property maps to constructor functions used to create the actual grid columns (defined in Sch.ColumnFactory). Possible values:
 *<ul>
 *   <li>quarterMinutes</li>
 *   <li>hour</li>
 *   <li>hourAndDay</li>     
 *   <li>dayAndHours</li>    
 *   <li>day</li>
 *   <li>dayAndWeeks</li>    
 *   <li>dayAndMonths</li>   
 *   <li>dayWeekAndMonths</li>  
 *   <li>week</li>   
 *   <li>weekAndMonths</li>  
 *   <li>weekAndDays</li>    
 *   <li>month</li>
 *   <li>monthAndQuarters</li>   
 *   <li>year</li>   
 *</ul>
 *
 *</div>
 * @constructor
 * @param {Object} config The config object
 * @xtype scheduler
 */

Sch.SchedulerPanel = Ext.extend(Ext.grid.GridPanel, {
    /**
     * @cfg {Ext.Template} tooltipTpl Template used to render a tooltip over a grid event. null by default (meaning no tooltip)
     */
    tooltipTpl : null,
    
    /**
     * @cfg {String} resizeHandles Defines which resize handles are to be used. Possible values: 'none', 'left', 'right', 'both'. Defaults to 'right'
     */
    resizeHandles : 'right',
    
    /**
     * @cfg {Boolean} enableEventDragDrop true to enable drag and drop of events, defaults to true
     */
    enableEventDragDrop : true,
    
    /**
     * @cfg {Boolean} enableDragCreation true to enable creating new events by click and drag, defaults to true
     */
    enableDragCreation : true,
    
    /**
     * @cfg {String} startParamName Name of the start date parameter used to load the event store
     */
    startParamName : 'start',
    
     /**
     * @cfg {Boolean} allowOverlap True if events are allowed to overlap
     */
    allowOverlap : true,
    
    /**
     * @cfg {String} endParamName Name of the end date parameter used to load the event store
     */
    endParamName : 'end',
    
    columnLines : true,

    /**
     * @cfg {Ext.data.Store} eventStore The {@link Ext.data.Store} holding the events to be rendered into the scheduler (required).
     */
   
    /**
     * @cfg {Ext.Template} eventTemplate The template used to renderer your events in the scheduler. See {@link Ext.Template} for more information.
     */
     
   /**
     * An empty function by default, but provided so that you can perform custom validation on 
     * the item being dragged. This function is called during a drag and drop process and also after the drop is made
     * @param {Array} dragRecords an array containing the records for the events being dragged
     * @param {Ext.data.Record} targetResourceRecord the target resource of the the event 
     * @param {Date} date The date corresponding to the current mouse position
     * @param {Int} duration The duration of the item being dragged
     * @param {Event} e The event object
     * @return {Boolean} true if the drop position is valid, else false to prevent a drop
     */
    dndValidatorFn : function(dragRecords, targetResourceRecord, date, duration, e) {
        return true;
    },
    
    /**
     * An empty function by default, but provided so that you can perform custom validation on 
     * an item being resized.
     * @param {Ext.data.Record} resourceRecord the resource of the row in which the event is located
     * @param {Ext.data.Record} eventRecord the event being resized
     * @param {Date} startDate
     * @param {Date} endDate
     * @param {Event} e The event object
     * @return {Boolean} isValid True if the creation event is valid, else false to cancel
     */
    resizeValidatorFn : function(resourceRecord, eventRecord, startDate, endDate, e) {
        return true;
    },
    
    /**
     * An empty function by default, but provided so that you can perform custom validation on the item being created
     * @param {Ext.data.Record} resourceRecord the resource of the row in which the event is being created
     * @param {Date} startDate
     * @param {Date} endDate
     * @param {Event} e The event object
     * @return {Boolean} isValid True if the creation event is valid, else false to cancel
     */
    createValidatorFn : function(resourceRecord, startDate, endDate, e) {
        return true;
    },
    
    /**
     * @cfg {Object} validatorFnScope
     * The scope used for the different validator functions called during drag drop, drag create and resize.
     */
    validatorFnScope : null,
    
    // The width of the left + right border of your event, needed to calculate the correct start/end positions
    eventBorderWidth : 2,
    
    /**
    * @cfg {Array} autoViews 
    * Using this property puts the grid in 'auto' mode. The grid will react on store load and reconfigure itself based on the start/end dates passed as parameters in the load call.
    * The grid will then look in this array to find the appropriate view to choose based on the duration of the timespan to load.
    * The array should contain object with the following 4 properties:
    * <pre><code>
    *
    *       {
    *           timeSpan : 7,                              // The maximum number of days this timespan is to used for
    *           columnType : 'day',                        // The column type, maps to the constructors found in the Sch.ColumnFactory
    *           viewBehaviour : Sch.ViewBehaviour.DayView, // An object where view specific behaviour is implemented, see the files named Sch.viewbehaviour.xxx for reference
    *           renderer : this.renderer                   // The renderer to use for this view
    *       }
    * </pre></code>
    */
    
    /**
    * @cfg {Array} viewModel
    * The intial view model of the scheduler. (Don't use this together with autoViews)
    * A scheduler view model is composed by 5 properties:
    * <pre><code>
    *
    *       {
    *           start: new Date(),
    *           end :  new Date.add(Date.DAY, 7),
    *           columnType : 'day',                        // The column type, maps to the constructors found in the Sch.ColumnFactory
    *           viewBehaviour : Sch.ViewBehaviour.DayView, // An object where view specific behaviour is implemented, see the files named sch.viewbehaviour.xxx for reference
    *           renderer :  function() {...}               // The renderer to use for this view
    *       }
    * </pre></code>
    */
    
    /**
     * @cfg {Object} timeColumnDefaults
     * The default properties to be assigned to the time columns. See {@link Ext.grid.ColumnModel#defaults} for more information.
     */
    timeColumnDefaults : {},
    
    overClass : 'sch-event-hover',
    
    // eventRenderer 
    eventRenderer : Ext.emptyFn,
    
    /**
     * An empty function by default, but provided so that you can manipulate the html cells that make up the grid.
     * This is called once for each cell, just like a normal GridPanel renderer though returning values from it has no effect.
     * @param {Array} events The events that are going to be rendered into this cell
     * @param {Object} meta The same meta object as seen in a standard GridPanel cell renderer. Use it to modify CSS/style of the cell.
     * @param {Int} row The row index
     * @param {Int} col The col index
     * @param {Date} date The start date of the cell
     * @param {Date} date The end date of the cell
     * @param {SchedulerPanel} the Scheduler itself
     */
    timeCellRenderer : Ext.emptyFn,
    
    // private
    resizeHandleHtml : '<div class="x-resizable-handle x-resizable-handle-{0}"></div>',
    
    /**
     * <p>Returns the event record for a DOM element </p>
     * @param (mixed) el The DOM node or Ext Element to lookup
     * @return {Record} The event record
     */
    getEventRecordFromElement : function(el) {
        var element = Ext.get(el);
        if (!element.is(this.eventSelector)) {
            element = element.up(this.eventSelector);
        }
        return this.getEventRecordFromDomId(element.id);
    },
    
    /**
     * <p>Returns the event record for a DOM id </p>
     * @param (string) id The id of the DOM node
     * @return {Record} The event record
     */
    getEventRecordFromDomId : function(id) {
        var trueId = this.getEventIdFromDomNodeId(id);
        return this.eventStore.getAt(this.eventStore.findBy(function(r){return trueId == r.id; }));
    },
    
    /**
     * <p>Returns the event id for a DOM id </p>
     * @param (string) id The id of the DOM node
     * @return {Record} The event record
     */
    getEventIdFromDomNodeId : function(id) {
        return id.substring(this.eventPrefix.length);
    },
    
    /**
     * <p>Returns the Ext Element representing an event record</p> 
     * @param (string) id The id of the event
     * @return {Ext.Element} The Ext Element representing the event record
     */
    getElementFromEventId : function(id) {
        return Ext.get(this.eventPrefix + id);
    },
    
    /**
     * <p>Returns the Ext Element representing an event record</p> 
     * @param (Ext.data.Record) record The record
     * @return {Ext.Element} The Ext Element representing the event record
     */
    getElementFromEventRecord : function(record) {
        return this.getElementFromEventId(record.id);
    },
    
    /*
     * Gets an x-coordinate representing the date parameter
     * @param (Date) date, the date to get x coordinate for
     * @return {Number} x coordinate for date parameter 
     */
    getXFromDate : function(date) {
       var retVal = -1,
           cm = this.getColumnModel(),
           count = cm.getColumnCount();
           
       for (var i = this.nbrStaticColumns; i < count; i++) {
            if (date <= this.getColumnEnd(i)) {
                var diff = date - this.getColumnStart(i),
                    timeInColumn = this.getColumnEnd(i) - this.getColumnStart(i);
                    
                return this.view.getAccColumnWidth(i) + (diff * cm.getColumnWidth(i) / timeInColumn);
            } 
       }
       
       return null;
    },
    
    /*
     *  Gets the time for a DOM event such as 'mousemove' or 'click', calculated using the X coordinate of the DOM event
     *  @param {EventObject} e, the EventObject instance
     *  @param {String} floorOrRound (optional), 'floor' to floor the value or 'round' to round the value to nearest increment
     *  @returns (Date) the Date corresponding to the EventObject x coordinate
    */
    getTimeFromDomEvent : function(e, floorOrRound, depth) {
        return this.getTimeFromX(e.getTarget('.sch-timetd', depth || this.view.cellSelectorDepth),
                                 e.getPageX(),
                                 floorOrRound);
    },
    
    /*
     *  Gets the time for an X coordinate
     *  @param {HTMLElement} timeCell, the timeCell inside which the coordinate is located
     *  @param {Int} x, the X coordinate
     *  @param {String} roundingMethod (Optional) floor or round to floor/round the value to nearest increment
     *  @returns {Date} the Date corresponding to the x coordinate
    */
    getTimeFromX : function(timeCell, x, roundingMethod) {
        var cellDomNode = typeof timeCell === 'number' ? this.view.getCell(0, timeCell + this.nbrStaticColumns) : timeCell;
        
        if (!cellDomNode) return null;
        
        var el = Ext.get(cellDomNode),
            diffX = x - el.getX(),
            colWidth = el.getWidth(),   
            colIndex = this.view.getCellIndex(cellDomNode),
            cellStart = this.getColumnStart(colIndex),
            availableTimeInCell = Date.getDurationInMinutes(cellStart, this.getColumnEnd(colIndex)),
            diffInMinutes = (availableTimeInCell * diffX / colWidth);
            
        if (roundingMethod) {
            var viewResolution = this.getViewResolution();
            diffInMinutes = Math[roundingMethod](diffInMinutes / viewResolution) * viewResolution;
        }
        
        return cellStart.add(Date.MINUTE, diffInMinutes);
    },
    
    getTimeFromX2 : function(x, roundingMethod) {
        var timecell = this.body.child('.sch-timetd'),
            colWidth = timecell.getWidth(),   
            x = x - timecell.getX();
        
        if (x < 0) return;
        
        var nbrTimeCols = Math.floor(x / colWidth),
            diffX = x - (nbrTimeCols * colWidth),
            colIndex = nbrTimeCols + this.nbrStaticColumns,
            cellStart = this.getColumnStart(colIndex),
            availableTimeInCell = Date.getDurationInMinutes(cellStart, this.getColumnEnd(colIndex)),
            diffInMinutes = (availableTimeInCell * diffX / colWidth);
            
        if (roundingMethod) {
            var viewResolution = this.getViewResolution();
            diffInMinutes = Math[roundingMethod](diffInMinutes / viewResolution) * viewResolution;
        }
        
        return cellStart.add(Date.MINUTE, diffInMinutes);
    },
    
    //private
    roundDate : function(date) {
        return date.round(this.viewBehaviour.timeResolution);
    },
    
    //private
    floorDate : function(date) {
        return date.floor(this.viewBehaviour.timeResolution);
    },
    
    /*
     *  Use this method to set a new event template
     *  @param {Ext.Template} template the template to use for rendering event items
    */
    setEventTemplate : function(template) {
        this.eventTemplate = template;
    },
    
    /**
     * Gets an array of the selected records
     * @return {Array} An array of {@link Ext.data.Record} objects (scheduler events)
     */
    getSelectedRecords : function(){
        var r = [], s = this.getSelectionModel().selected.elements;
        for(var i = 0, len = s.length; i < len; i++){
            r[r.length] = this.getEventRecordFromDomId(s[i].id);
        }
        return r;
    },
    
    /**
     * Method to get a formatted date, the grid delegates this to the current view behaviour in use.
     * This way you can format your dates differently in each view type
     * @param {Date} date The date
     * @return {String} The formatted date
     */
    getFormattedDate : function(date, roundingMethod) {
        var vb = this.viewBehaviour;
        return date[roundingMethod || 'floor'](vb.timeResolution).format(vb.dateFormat);
    },
    
    /**
     * Method to get a formatted end date, the grid uses the date format set in the current view behaviour.
     * This way you can format your dates differently in each view type
     * @param {Date} date The date
     * @return {String} The formatted date
     */
    getFormattedEndDate : function(date, roundingMethod) {  
        var vb = this.viewBehaviour;
        return (vb.timeResolution === 1440 && date.getHours() === 0 ? date.add(Date.DAY, -1) : date)[roundingMethod || 'floor'](vb.timeResolution).format(vb.dateFormat);
    },
    
    /**
         * Tells the scheduler panel to reconfigure itself according to the data in the parameters
         * @param {Date} startDate The start date of the new view
         * @param {Date} endDate The end date of the new view
         * @param {String} columnType (Optional) The column type of the new view
         * <p>The columnType property defines the column interval and also controls what is being rendered into the column headers. The string value maps to a constructor function used to create the actual grid columns (defined in Sch.ColumnFactory) and headers. Possible values:</p>
         *<ul>
         *   <li>quarterMinutes</li>
         *   <li>hour</li>
         *   <li>hourAndDay</li>     
         *   <li>dayAndHours</li>    
         *   <li>day</li>
         *   <li>dayAndWeeks</li>    
         *   <li>dayAndMonths</li>   
         *   <li>dayWeekAndMonths</li>  
         *   <li>week</li>   
         *   <li>weekAndMonths</li>  
         *   <li>weekAndDays</li>    
         *   <li>month</li>
         *   <li>monthAndQuarters</li>   
         *   <li>year</li>   
         *</ul>
         * @param {Function} viewBehaviour (Optional) The viewBehaviour constructor to use of the new view
         * @param {Function} renderer (Optional) The renderer to use in this view
         * 
         * The renderer function is called with the following arguments
         *<ul>
         *   <li>eventRecord (Record) - The event record about to be rendered</li>
         *   <li>resourceRecord (Record) - The resource record to which the event belongs</li>
         *   <li>row (Int)</li>     
         *   <li>col (Int)</li>    
         *   <li>resourceStore (Store)</li>
         *</ul>
         *
         * The renderer function shall return an object containing the properties which will be applied to your {@link #eventTemplate template} 
         */
    setView : function(startDate, endDate, columnType, viewBehaviour, renderer) {
        this.setViewInternal(startDate, endDate, columnType, viewBehaviour, renderer);
    },
    
    // private
    eventSelector : '.sch-event',
    
    // private
    // returns the current view time resolution defined in the view behaviour object (in minutes)
    getViewResolution : function() {
        return this.viewBehaviour.timeResolution;
    },
    
    //private helper
    getRowIndex : function(t) {
        return this.view.findRowIndex(t);
    },
    
    // private
    constructor : function(config) {
        this.addEvents(
            /**
             * @event eventclick
             * Fires when an event is clicked
             * @param {SchedulerPanel} grid The grid object
             * @param {Record} eventRecord The event record of the clicked event
             * @param {EventObject} e The event object
             */
            'eventclick', 
            
            /**
             * @event eventdblclick
             * Fires when an event is double clicked
             * @param {SchedulerPanel} grid The grid object
             * @param {Record} eventRecord The event record of the clicked event
             * @param {EventObject} e The event object
             */
            'eventdblclick', 
            
            /**
             * @event eventcontextmenu
             * Fires when contextmenu is activated on an event
             * @param {SchedulerPanel} grid The grid object
             * @param {Record} eventRecord The event record of the clicked event
             * @param {EventObject} e The event object
             */
            'eventcontextmenu', 
            
            /**
             * @event beforetooltipshow
             * Fires before the event tooltip is shown, return false to suppress it.
             * @param {Record} eventRecord The event record of the clicked record
             */
            'beforetooltipshow',
            
            /**
             * @event beforeviewchange
             * Fires before the timespan of the current view has changed
             * @param {SchedulerPanel} grid this
             */
            'beforeviewchange',
            
            /**
             * @event timeheaderdblclick
             * Fires after a doubleclick happens on a time header cell
             * @param {SchedulerPanel} grid this
             * @param {Date} startDate The start date of the header cell
             * @param {Date} endDate The start date of the header cell
             * @param {EventObject} e The event object
             */
            'timeheaderdblclick',
            
            /**
             * @event afterdragcreate
             * Always fires after a drag-create operation
             * @param {SchedulerPanel} scheduler The scheduler object
             */
             
            // Resizing events start --------------------------
            /**
             * @event beforeresize
             * Fires before a resize starts, return false to stop the execution
             * @param {SchedulerPanel} scheduler The scheduler object
             * @param {Record} el The record corresponding to  element corresponding about to be resized
             * @param {EventObject} e The event object
             */
            'beforeresize', 
            
            /**
             * @event resizestart
             * Fires when resize starts
             * @param {SchedulerPanel} scheduler The scheduler object
             * @param {Record} el The record corresponding to  element corresponding about to be resized
             */
            'resizestart', 
            
            /**
             * @event partialresize
             * Fires during a resize operation and provides information about the current start and end of the resized event
             * @param {SchedulerPanel} scheduler The scheduler object
             * @param {Record} record The event record corresponding to the element being resized
             * @param {Date} startDate The start date of the event
             * @param {Date} endDate The end date of the event
             * @param {Ext.Element} The element being resized
             * @param {EventObject} e The event object
             */
            'partialresize', 
            
            /**
             * @event afterresize
             * Fires after a succesful resize operation
             * @param {SchedulerPanel} scheduler The scheduler object
             * @param {Record} record The updated record
             */
            'afterresize',
            // Resizing events end --------------------------
            
            // Dnd events start --------------------------
            /**
             * @event beforednd
             * Fires before a dnd operation is initiated, return false to cancel it
             * @param {SchedulerPanel} scheduler The scheduler object
             * @param {Ext.data.Record} record The record corresponding to the node that's about to be dragged
             * @param {EventObject} e The event object
             */ 
            'beforednd', 
            
            /**
             * @event dndstart
             * Fires when a dnd operation starts
             * @param {SchedulerPanel} plugin the dragdrop plugin
             * @param {Array} records the records being dragged
             */
            'dndstart',
            
            /**
             * @event drop
             * Fires after a succesful drag and drop operation
             * @param {SchedulerPanel} scheduler The scheduler object
             * @param {Array} records the affected records (if copies were made, they were not inserted into the store)
             * @param {Boolean} isCopy True if the records were copied instead of moved
             */
            'drop',
            
            /**
             * @event afterdnd
             * Fires when after a drag n drop operation, even when drop was performed on an invalid location
             * @param {SchedulerPanel} grid The grid object
             */
            'afterdnd',
            // Dnd events end --------------------------
            
            // Drag create events start --------------------------
            /**
             * @event beforedragcreate
             * Fires before a drag starts, return false to stop the execution
             * @param {SchedulerPanel} scheduler The scheduler object
             * @param {EventObject} e The event object
             */
            'beforedragcreate', 
            
            /**
             * @event dragcreatestart
             * Fires before a drag starts, return false to stop the execution
             * @param {SchedulerPanel} scheduler The scheduler object
             */
            'dragcreatestart', 
        
            /**
             * @event dragcreateend
             * Fires after a successful drag-create operation
             * @param {SchedulerPanel} scheduler The scheduler object
             * @param {Object} data An object containing three properties: record (meaning the resource record), startDate and endDate
             */
            'dragcreateend',
            
            /**
             * @event afterdragcreate
             * Always fires after a drag-create operation
             * @param {SchedulerPanel} scheduler The scheduler object
             */
            'afterdragcreate'
            // Drag create events end --------------------------
            );
        
        config = config || {};
        Ext.applyIf(config, {
            plugins : []
        });
        
        Ext.apply(this, config);
        
        if (!this.eventTemplate) {
            this.eventTemplate = new Ext.Template(
                '<div id="{id}" style="width:{width}px;left:{leftOffset}px" class="sch-event {cls}">',
                    (this.resizeHandles === 'both' || this.resizeHandles === 'left') ? String.format(this.resizeHandleHtml, 'west') : '',

                    '<div class="sch-event-inner">{text}</div>',
                    
                    // The html that makes up the resize handle
                    (this.resizeHandles === 'both' || this.resizeHandles === 'right') ? String.format(this.resizeHandleHtml, 'east') : '',
                '</div>'
            ).compile();
        }
        
        this.configureFunctionality();
        Sch.SchedulerPanel.superclass.constructor.call(this, config);
    },
    
    getView : function(){
        if(!this.view){
            this.view = new Sch.SchedulerView(this.viewConfig);
        }
        return this.view;
    },
    
    // private
    initComponent : function() {
        
        // Assign a default renderer to be used by all time column cells
        this.timeColumnDefaults.renderer = this.internalRenderer;
        
        // Required to be able to have multiple schedulers running at the same time, to not risk having multiple items in the DOM with the same id
        this.eventPrefix = Ext.id() + '-';
        
        if (this.autoViews && this.autoViews.length > 0) {
             this.eventStore.on({
                beforeload : this.onBeforeStoreLoad,
                scope : this
            });
            
            // If the store has a proxy, intercept its request method
            if (this.eventStore.proxy.request) {
              // Setup new view after a store load operation is initiated
              this.eventStore.proxy.request = this.eventStore.proxy.request.createInterceptor(function(a, b, params) {
                this.reconfigureInternal(params);
              }, this);
            } else if (this.eventStore.proxy.load) {
              // Ext 2.x intercept load method instead
              this.eventStore.proxy.load = this.eventStore.proxy.load.createInterceptor(function(params) {
                this.reconfigureInternal(params);
              }, this);
            }
        }
     
        this.on({
            resize : this.refreshView,
            afterrender : this.onRender_,
            beforedestroy : this.onBeforeDestroy_,
            columnresize : this.refreshView,
            dndstart : this.onDragDropStart,
            afterdnd : this.onDragDropEnd,
            dragcreatestart : this.onDragCreateStart,
            afterdragcreate : this.onAfterDragCreate,
            headerdblclick : this.onHeaderDoubleClick,
            
            cellclick : function(g, r, c, e) {
                var t = e.getTarget(this.eventSelector);
                if (t) {
                    this.fireEvent('eventclick', this, this.getEventRecordFromDomId(t.id), t, e);
                }
            },
            celldblclick : function(g, r, c, e) {
                var t = e.getTarget(this.eventSelector);
                if (t) {
                    this.fireEvent('eventdblclick', this, this.getEventRecordFromDomId(t.id), e);
                }
            },
            cellcontextmenu : function(g, r, c, e) {
                var t = e.getTarget(this.eventSelector);
                if (t) {
                    this.fireEvent('eventcontextmenu', this, this.getEventRecordFromDomId(t.id), e);
                }
            },
            scope : this
        });
        
        if(!this.selModel && !this.disableSelection){
            this.selModel = new Sch.EventSelectionModel();
        }
        
        Sch.SchedulerPanel.superclass.initComponent.call(this);
        var cm = this.getColumnModel();
        this.nbrStaticColumns = cm.getColumnCount();
        cm.on('hiddenchange', this.refreshView, this);
        
        // Initialize the cm to handle multiple row headers
        Ext.applyIf(cm, { rows : [] });
        
        this.on('render', function() {
            this.el.addClass('sch-schedulerpanel');
        }, this);
        
    },
    
    configureFunctionality : function() {
        var plugs = this.plugins,
            vfScope = this.validatorFnScope || this;
        
        if (this.resizeHandles !== 'none' && !this.resizePlug) {
            this.resizePlug = new Sch.plugins.Resize({
                validatorFn : function(resourceRecord, eventRecord, startDate, endDate) {
                    return (this.allowOverlap || this.isDateRangeAvailable(startDate, endDate, eventRecord.id, resourceRecord.get('Id'))) && 
                            this.resizeValidatorFn.apply(vfScope, arguments);
                },
                validatorFnScope : this
            });
            
            plugs.push(this.resizePlug);
        }
        
        if (this.enableEventDragDrop !== false && !this.dragdropPlug) {
                
            this.dragdropPlug = new Sch.plugins.DragDrop({
                validatorFn : function(dragRecords, targetResourceRecord, date, duration, e) {
                    return (this.allowOverlap || this.isDateRangeAvailable(date, date.add(Date.MINUTE, duration), dragRecords[0].id, targetResourceRecord.get('Id'))) && 
                           this.dndValidatorFn.apply(vfScope, arguments);
                },
                validatorFnScope : this
            });
            
            plugs.push(this.dragdropPlug);
        }
        
        if (this.enableDragCreation !== false && !this.dragCreatePlug) {
            this.dragCreatePlug = new Sch.plugins.DragCreator({
                validatorFn : function(resourceRecord, startDate, endDate) {
                    return (this.allowOverlap || this.isDateRangeAvailable(startDate, endDate, null, resourceRecord.get('Id'))) && 
                            this.createValidatorFn.apply(vfScope, arguments);
                },
                validatorFnScope : this
            });
            
            plugs.push(this.dragCreatePlug);
        }
    },
    
    // private
    reconfigureInternal : function(params) {
        var startDate = params[this.startParamName],
            endDate = params[this.endParamName],            
            timeSpan = Math.floor(Date.getDurationInDays(startDate, endDate)),
            i = 0;
        
        // Find the appropriate view by iterating the views array and comparing the requested timespan
        // to the timeSpan configured in the views arrays
        for (i = 0; i < this.autoViews.length; i++) {
            if (timeSpan <= this.autoViews[i].timeSpan) {
                break;
            }
        }
        var viewCfg = this.autoViews[Math.min(i, this.autoViews.length - 1)];
        
        this.setViewInternal(params[this.startParamName], params[this.endParamName], viewCfg.columnType, viewCfg.viewBehaviour, viewCfg.renderer);
    }, 
     
    // private, clean up
    onBeforeDestroy_ : function() {
        if (this.tip)  this.tip.destroy();
        if (this.eventStore.autoDestroy) this.eventStore.destroy();
    },
    
    // private
    refreshView : function (g) {
        if (this.viewReady && this.nbrStaticColumns < this.getColumnModel().getColumnCount()) {
            this.getView().refresh(true);
        }
    },
    
    // private
    onRender_ : function(g) {
        // Prevent moving time columns
        var view = this.getView();
        if (view.columnDrag) {  
            view.columnDrag.onBeforeDrag = function(data, e) {
                return !e.getTarget('.sch-timeheader');
            };
        }
        
        if (this.tooltipTpl) {
            this.setupTooltip();
        }
        
        if (this.viewModel) {
            var v = this.viewModel;
            this.setView(v.start, v.end, v.columnType, v.viewBehaviour, v.renderer);
        }
        
        if(this.overClass){
            this.mon(this.getView().mainBody, {
                "mouseover": this.onMouseOver,
                "mouseout": this.onMouseOut,
                scope:this
            });
        }
    },
    
    tipCfg : {
        mouseOffset : [5,-50],
        cls : 'sch-tip',
        trackMouse: true,
        showDelay:1000,
        autoHide : false,
        width : Ext.isIE ? 140 : undefined
    },
    
    // private
    setupTooltip : function() {
        var v = this.getView(),
            tipCfg = Ext.apply({
                renderTo: Ext.getBody(),
                delegate: this.eventSelector,
                target: v.mainBody,
                listeners: {
                    beforeshow : {
                        fn : function (tip) {
                            if (!tip.triggerElement.id) return false;
                            
                            var eRec = this.getEventRecordFromDomId(tip.triggerElement.id);
                            
                            if (!eRec || this.fireEvent('beforetooltipshow', this, eRec) === false) return false;
                            
                            tip.update(this.tooltipTpl.apply(eRec.data));
                            
                            return true;
                        },
                        scope : this
                    }
                }
            }, this.tipCfg);
        
        this.tip = new Ext.ToolTip(tipCfg);
    },
    
    /**
     * Convenience method to load a date span
     * @param {Date} start The start date
     * @return {String} end The end date
     */
    loadInterval : function(start, end) {
        var params = {};
        params[this.startParamName] = start;
        params[this.endParamName] = end;
        this.eventStore.load({
            params : params
        });
    },
    
    // private
    onBeforeStoreLoad : function(store, options) {
        if (this.tip) {
            this.tip.hide();
        }
    },
   
    // private
    setViewInternal : function(startDate, endDate, columnType, viewBehaviourConstructor, renderer) {
        var cm = this.getColumnModel();
        
        this.fireEvent('beforeviewchange', this);
        viewBehaviourConstructor = viewBehaviourConstructor || Sch.ViewBehaviour.Base;
        
        if (renderer) {
            this.eventRenderer = renderer;
        }
        if (!this.viewBehaviour) {
            this.viewBehaviour = new viewBehaviourConstructor(this);
        } else if (viewBehaviourConstructor && !(this.viewBehaviour instanceof viewBehaviourConstructor)) {
            // Destroy old view behaviour
            this.viewBehaviour.destroy();
            
            // If we're switching to a new view type, hide elements first since they may look weird in a different CSS class
            if (!(this.viewBehaviour instanceof viewBehaviourConstructor)) {
                this.el.select(this.eventSelector).hide();
            }
            this.viewBehaviour = new viewBehaviourConstructor(this);
        }
        
        // Save column type to be able to call setView using only start/end date, reusing all other configuration data
        if (columnType) {
            this.columnType = columnType;
        }
        
        // Create new time columns
        var columnConfig = Sch.ColumnFactory.createColumns(startDate, endDate, columnType || this.columnType, this.timeColumnDefaults);
        
        if (columnConfig.rows) {
            if (this.nbrStaticColumns > 0) {
                Ext.each(columnConfig.rows, function(a) {
                    a.unshift({colspan : this.nbrStaticColumns});
                }, this);
            }
        }
        
        Ext.apply(cm, {
		    rows: columnConfig.rows || []
	    });
        
        var staticColumns = cm.config.slice(0, this.nbrStaticColumns);
        
        // Append the new time columns to the columns supplied at create-time
        cm.setConfig(staticColumns.concat(columnConfig.columns), false, true);
        
        this.fireEvent('viewchange', this);
   },
    
    // private
    internalRenderer : function(v, m, rec, row, col, ds, events, grid) {
        var cellResult = '',
            viewStart = grid.getStart(),
            viewEnd = grid.getEnd(),
            colWidth = grid.getColumnModel().getColumnWidth(col),
            c = this;
        
        // Call timeCellRenderer to be able to set css/style properties on grid cells
        grid.timeCellRenderer.call(this, events, m, rec, row, col, ds, c.start, c.end, grid);
        
        // Iterate events (belonging to current row) passed by the schedulerview
        events.each(function(event) {
            var start = event.get('StartDate'),
                end = event.get('EndDate'),
                startsInsideCell = start.betweenLesser(c.start, c.end);
            
            // Determine if the event should be rendered or not
            if (startsInsideCell || (col == grid.nbrStaticColumns && start < c.start && end > c.start)) {
                
                var availableTimeInColumn = Date.getDurationInMinutes(c.start, c.end),
                    endsOutsideView = end > viewEnd,
                    leftOffset = (Date.getDurationInMinutes(c.start, startsInsideCell ? start : c.start) / availableTimeInColumn) * colWidth,
                    itemWidth = grid.getXFromDate(Date.min(end, viewEnd)) - grid.getXFromDate(startsInsideCell ? start : viewStart);
                
                // Get event data from user supplied "eventRenderer" 
                var eventData = grid.eventRenderer.call(this, event, rec, row, col, ds) || {};
                
                // Apply scheduler specific properties
                Ext.apply(eventData, {
                    id : grid.eventPrefix + event.id,
                    cls : (eventData.cls || '') + (event.dirty ? ' sch-dirty' : '') + (endsOutsideView ? ' sch-event-endsoutside ' : '') + (startsInsideCell ? '' : ' sch-event-startsoutside'),
                    width : Math.max(1, itemWidth - grid.eventBorderWidth),
                    leftOffset : leftOffset
                });
                
                eventData.text = eventData.text || '&#160;';
                cellResult += grid.eventTemplate.apply(eventData);
            }
        }, this);
        
        m.css += ' sch-timetd';
        
        // Z-index is trouble in IE, thanks Condor for this fix
        if (Ext.isIE) {
            m.attr += ' style="z-index:' + (grid.getColumnModel().getColumnCount() - col) + '"';
        }
        return cellResult;
    },
    
    /**
     * Method to get a the current start date of the scheduler view
     * @return {Date} The start date of the current view
     */
    getStart : function() {
        return this.getColumnStart(this.nbrStaticColumns);
    },
    
    /**
     * Method to get a the current end date of the scheduler view
     * @return {Date} The start date of the current view
     */
    getEnd : function() {
        return this.getColumnEnd(this.getColumnModel().getColumnCount() - 1);
    },
    
    /**
     * Method to get the current viewBehaviour object used by the scheduler
     * @return {Sch.ViewBehaviour} The viewBehaviour object
     */
    getViewBehaviour : function() {
        return this.viewBehaviour;
    },
    
    /**
     * Method to get a the start date of a time column
     * @param {Int} The column index
     * @return {Date} The start date of the time column
     */
    getColumnStart : function(i) {
        return this.getColumnModel().config[i].start;
    },
    
    /**
     * Method to get a the start date of a time column
     * @param {Int} The column index
     * @return {Date} The end date of the time column
     */
    getColumnEnd : function(i) {
        return this.getColumnModel().config[i].end;
    },
    
    onMouseOver : function(e){
        var item = e.getTarget(this.eventSelector, this.view.cellSelectorDepth);
        if(item && item !== this.lastItem){
            this.lastItem = item;
            Ext.fly(item).addClass(this.overClass);
        }
    },
    
    onMouseOut : function(e){
        if(this.lastItem){
            if(!e.within(this.lastItem, true, true)){
                Ext.fly(this.lastItem).removeClass(this.overClass);
                delete this.lastItem;
            }
        }
    },
    
    onDragDropStart : function() {
        if (this.dragCreatePlug) {
            this.dragCreatePlug.setDisabled(true);
        }
        
        if (this.tip) {
            this.tip.hide();
            this.tip.disable();
        }
    },
    
    onDragDropEnd : function() {
        if (this.dragCreatePlug) {
            this.dragCreatePlug.setDisabled(false);
        }
        
        if (this.tip) {
            this.tip.enable();
        }
    },
    
    onDragCreateStart : function() {
        if (this.tip) {
            this.tip.hide();
            this.tip.disable();
        }
    },
    
    onAfterDragCreate : function() {
        if (this.tip) {
            this.tip.enable();
        }
    },
    
    getResourceByEventRecord : function(eventRecord) {
        return this.store.getById(eventRecord.get('ResourceId'));
    },
    
    onHeaderDoubleClick : function(g, colIndex, e) {
         var t = e.getTarget('.sch-timeheader');
         
         if (!t) return;
         
         var isGroupRow = !!t.className.match('ux-grid-hd-group-cell'),
             headerCfg,
             cm = g.getColumnModel(),
             isLockingView = Sch.LockingSchedulerView && (this.view instanceof Sch.LockingSchedulerView);
            
        if (isGroupRow) {
            var rowGroupIndex = g.getView().getHeaderGroupRow(e.getTarget('.x-grid3-hd'));
            headerCfg = cm.rows[rowGroupIndex][isLockingView ? (colIndex + 1) : colIndex]; // Add one for locking scenario since first one is the locked header
        } else {
            headerCfg = cm.config[colIndex];
        }
        
        this.fireEvent('timeheaderdblclick', this, headerCfg.start, headerCfg.end, e);
    },
    
    // private
    getResourceRecordByElement : function(t) {
        var retval = null,
            index = this.getView().findRowIndex(t);
        if (index >= 0) {
            retval = this.store.getAt(index);
        }
        return retval;
    },
    
    isDateRangeAvailable : function(start, end, eventId, resourceId) {
        var available = true;
        this.eventStore.each(function(r) {
            if (Date.intersectSpans(start, end, r.get('StartDate'), r.get('EndDate')) && (resourceId === r.get('ResourceId') && (!eventId || eventId !== r.id))){
                available = false;
                return false;
            }
        });
        return available;
    }
}); 

Ext.reg('scheduler', Sch.SchedulerPanel);


