 /**
* some timespan definitions
*/
var ESpans = {
    MINUTE:             60,
    FIVE_MINUTES:       60*5,
    FIFTEEN_MINUTES:    60*15,
    THIRTY_MINUTES:     60*30,
    HOUR:               60*60,
    DAY:                60*60*24,
    WEEK:               60*60*24*7,
    MONTH:              60*60*24*7*4,
    YEAR:              60*60*24*365
}; // ESpans

 function tickT(t,i) {
    if(i == ESpans.WEEK) {
        var d = new Date(t*1000);
        var w = d.getDay();
        return ((new Date(d.getFullYear(),d.getMonth(),d.getDate())).getTime()/1000) - (w==0?7:w-1)*ESpans.DAY;
    } else if(i == ESpans.MONTH) {
        var d = new Date(t*1000);
        return (new Date(d.getFullYear(),d.getMonth(),1)).getTime()/1000;
    } else if(i == ESpans.YEAR) {
        var d = new Date(t*1000);
        return (new Date(d.getFullYear(),1,1)).getTime()/1000;
    }
    return Math.ceil(t/i)*i;
 }
 
 function tickName(t) {
    if(t >= ESpans.MONTH) {
        var n = (t/ESpans.MONTH);
        return n + ' Monat' + (n>1?'e':'');
    } else if(t >= ESpans.WEEK) {
        var n = (t/ESpans.WEEK);
        return n + ' Woche' + (n>1?'n':'');  
    } else if(t >= ESpans.DAY) {
        var n = (t/ESpans.DAY);
        return n + ' Tag' + (n>1?'e':'');
    } else if(t >= ESpans.HOUR) {
        var n = (t/ESpans.HOUR);
        return n + ' Stunde' + (n>1?'n':'');
    } else {
        var n = (t/ESpans.MINUTE);
        return n + ' Minute' + (n>1?'n':'');
    }    
    return '';
 }
 
 function makeLabel(t1,t2,i) {
     var cy = 0,now = 0;
     if(!t2) {
        var d = new Date();
        now = Math.ceil(d.getTime()/(1000*ESpans.DAY))*ESpans.DAY + d.getTimezoneOffset()*60;
        cy = strftime(now,'%y');    
     }
     
     // intraday: one label per hour
     if(i<ESpans.DAY) {
        var tt1 = ((t1/3600)|0);
        var tt2 = ((t2/3600)|0);
        if(!t2 || tt1 != tt2) {
            return (now-t1>ESpans.DAY?strftime(t1,'%d.%m '):'')+strftime(t1,'%H:%M');
        }
        return null;
     }
     
     // label per week
     if(i<ESpans.WEEK) {
        var tt1 = strftime(t1,'%u');
        var tt2 = strftime(t2,'%u');
        if(!t2 || tt1 < tt2) {
            var y1 = strftime(t1,'%y');
            var y2 = strftime(t2,'%y');
            return strftime(t1,'%d.%m' + ((t2 && y1!=y2)||(!t2 && cy!=y1)?' \'%y':''));
        }
        return null;
     }
     
     // label per week
     var tt1 = strftime(t1,'%m');
     var tt2 = strftime(t2,'%m');
     if(!t2 || tt1 != tt2) {
         return strftime(t1,'%B'+ ((t2 && tt1 < tt2)||(!t2 && strftime(t1,'%y')!=cy) ? ' \'%y':''));
     }
     
     return null;    
 }
 

 function ChartValue(d) {
    /**
    * references
    * @var ChartValue
    */
    this.next   = null;
    this.prev   = null;
    this.parent = null;
    this.begin  = null;
    this.end    = null;
    this.index  = 0;
    this.labelIndex = 0;
    
    /**
    * quote values
    * @var number
    */
    this.indicator  = [];
    this.label      = null;
    this.timestamp  = 0;
    this.open       = 0;
    this.high       = 0;
    this.low        = 0;    
    this.close      = 0;
    this.volume     = 0;
};

function ChartBuffer(owner, i) {
    /**
    * interval of this buffer
    * @var int
    */    
    this.interval = i;
    
    /**
    * parent buffer
    * @var ChartBuffer
    */
    this.parent = null;
    
    /**
    * the owner of this buffer
    * @var ChartSource
    */
    this.owner = owner;
    
    /**
    * child buffer
    * @var ChartBuffer
    */
    this.child = null;
    
    /**
    * first value in this buffer
    * @var ChartValue
    */
    this.begin = null;
    
    /**
    * last value in this buffer
    * @var ChartValue
    */
    this.end = null;
    
    /**
    * size
    */
    this.size = 0;
    
    /**
    * labels
    */
    this.labels = 0;
    
    /**
    * high and low
    */
    this.high = null;
    this.low = null;
    
    /**
    * find timestamp
    */
    this.findTimestamp = function(timestamp) {
        begin = this.begin;
        while(begin && begin.next) {
            if(begin.timestamp >= timestamp) {
                break;
            }
            begin = begin.next;
        }
        return begin;
    }
    
    /**
    * get span
    */
    this.findSpan = function(spanStart, spanEnd) {
        var begin = null, end = null;
        
        // find start
        begin = this.begin;
        while(begin) {
            if(begin.timestamp >= spanStart) {
                break;
            }
            begin = begin.next;
        }
        // find end
        end = this.end;
        while(end) {
            if(end.timestamp == spanEnd || (end.prev && end.prev.timestamp < spanEnd)) {
                break;
            }
            end = end.prev;
        }
        
        // check if valid beginning and end were found
        if(!begin || !end) {
            if(!begin && !end) {
                return null;
            }
            if(!begin) begin = end;
            if(!end) end = begin;
        }
        
        // return span
        return [begin, end];
    };
    
    /**
    * return the first timestamp in this buffer
    */
    this.getFirstTimestamp = function() {
        if(this.begin) {
            return this.begin.timestamp;
        }
        return null;
    }
    
    /**
    * return the last timestamp in this buffer
    */
    this.getLastTimestamp = function() {
        if(this.end) {
            return this.end.timestamp;
        }
        return null;
    }
    
    /**
    * getParent
    */
    this.getParent = function() {
        if(!this.parent) {
            var intervals = [ESpans.MONTH, ESpans.WEEK, ESpans.DAY, ESpans.HOUR, ESpans.THIRTY_MINUTES, ESpans.FIFTEEN_MINUTES, ESpans.FIVE_MINUTES, ESpans.MINUTE];
            for(var i = 0;i<intervals.length;i++) {
                if(intervals[i] == this.interval) {
                    if(i == 0) {
                        break;
                    }
                    this.parent = new ChartBuffer(this.owner, intervals[i-1]);
                    this.parent.child = this;
                }
            }
        }
        return this.child;
    };
    
    /**
    * getChild
    */
    this.getChild = function() {
        if(!this.child) {
            var intervals = [ESpans.MONTH, ESpans.WEEK, ESpans.DAY, ESpans.HOUR, ESpans.THIRTY_MINUTES, ESpans.FIFTEEN_MINUTES, ESpans.FIVE_MINUTES, ESpans.MINUTE];
            for(var i = 0;i<intervals.length;i++) {
                if(intervals[i] == this.interval) {
                    if(i == intervals.length - 1) {
                        break;
                    }
                    this.child = new ChartBuffer(this.owner, intervals[i+1]);
                    this.child.parent = this;
                }
            }
        }
        return this.child;
    };
    
    /**
    * update data from child values
    */
    this.updateData = function(first, last) {
        var cur=null,next=null,begin=null;
        var t = 0,t2 = 0;
        
        // find insertion point
        if(this.begin) {
            var start = tickT(first.timestamp,this.interval);
            // add values before current data
            if(start < this.begin.timestamp) {
                next = this.begin;
            // add values after current data
            } else {
                begin = this.end;
                t = tickT(begin.timestamp,this.interval);
                cur = begin;
            }
        }
        
        var index = this.size;
        var p = first, q = null;
        do {        
            if(this.interval == ESpans.DAY) {
                t2 = Math.floor(p.timestamp/ESpans.DAY)*ESpans.DAY;
            } else {
                t2 = tickT(p.timestamp,this.interval);
            }
            
            if(next && t2 >= next.timestamp) {
                break;
            }
            if(t2 < t) {
                p = p.next;
                continue;
            }
            
            if(t2 != t) {
                this.size++;
                
                if(this.interval == ESpans.DAY) {
                    var i = 2;
                }
                q = new ChartValue();
                q.timestamp  = t2;
                q.index      = index++;
                q.open       = p.open;
                q.high       = p.high;
                q.low        = p.low;
                q.close      = p.close;
                q.volume     = p.volume;
                q.begin = q.end = p;
                
                // add to list
                if(cur == null) {
                    if(begin) {
                        begin.next = q;
                        q.prev = begin;
                    } else {
                        this.begin = q;
                    }
                    begin = cur = q;
                } else {
                    cur.next = q;
                    q.prev = cur;
                    cur = q;
                }
                
                if(cur.prev) {
                    cur.label = makeLabel(q.timestamp, cur.prev.timestamp, this.interval);
                    if(cur.label) {
                        cur.labelIndex = this.labels++;
                    }
                }
                
                t = t2;
            } else {
                cur.end = p;
                // use last close
                cur.close = p.close;
                cur.volume += p.volume;
                // check for hpgh and low
                if(p.high > cur.high) {
                    cur.high = p.high;
                }
                if(p.low < cur.low) {
                    cur.low = p.low;
                }    
            }
            
            // check for high and low
            if(this.high === null || cur.high > this.high.high) {
                this.high = cur;
            }
            if(this.low === null || cur.low < this.low.low) {
                this.low = cur;
            }
            
            p.parent = cur;
            p = p.next;
        } while(p && p.prev != last);
        
        // if nothing was updated, skip
        if(!cur) {
            return;
        }
        
        // update end
        if(!next) {
            this.end = cur;
        } else {
            cur.next = next;
            next.prev = cur;
        }

        // update indices
        var icur;
        if(next) {
            icur = this.begin;
            var index = 0;
            this.labels = 0;
            while(icur) {
                icur.index = index++;
                if(icur.label) {
                    icur.labelIndex = this.labels++;
                }
                icur = icur.next;
            }
        }
        
        // calculate indicator values
        if(this.owner._indicators.length > 0) {
            if(begin == cur) {
                for(i=0;i<this.owner._indicators.length;i++) {
                    this.owner._indicators[i].calculate(cur);
                }
            } else {
                icur = this.begin;
                while(icur) {
                    for(i=0;i<this.owner._indicators.length;i++) {
                        this.owner._indicators[i].calculate(icur);
                    }
                    icur = icur.next;
                }    
            }
        }
        
        this.getParent();
        if(this.parent) {
            this.parent.updateData(begin, cur);
        }
    }
    
    /**
    * add data to the buffer
    */
    this.addData = function(s,e,i,data) {
        // check for empty dataset
        if(typeof data == 'undefined' || !data || data.length == 0) {
            return;
        }
        
        this.getChild(); 
        if(i < this.interval) {
            if(this.child) {
                this.child.addData(s,e,i,data);
            }
            return;
        }
        var cur=null,next=null,begin=null;
        // find insertion point
        if(this.begin) {
            var start = data[2][0];
            // add values before current data
            if(start < this.begin.timestamp) {
                next = this.begin;
            // add values after current data
            } else {
                begin = this.end;
            }
        }
        
        // fill buffer
        var q = null;
        var index = this.size;
        var t = 0,t2 = 0;
        for(var p=2;p<data.length;p++) {
            
            t2 = tickT(data[p][0],this.interval);
            if(begin && t2 <= begin.timestamp) {
                continue;
            }
            if(next && t2 >= next.timestamp) {
                break;
            }            
            
            if(t2 != t) {
                this.size++;
                
                q = new ChartValue();
                q.index      = index++;
                q.timestamp  = t2;
                q.open       = data[p][2];
                q.high       = data[p][4];
                q.low        = data[p][3];
                q.close      = data[p][1];
                q.volume     = data[p][5];
                
                // add to list
                if(cur == null) {
                    if(begin) {
                        begin.next = q;
                        q.prev = begin;
                    } else {
                        this.begin = q;
                    }
                    begin = cur = q;
                } else {
                    cur.next = q;
                    q.prev = cur;
                    cur = q;
                }
                
                if(cur.prev) {
                    cur.label = makeLabel(q.timestamp, cur.prev.timestamp, this.interval);
                    if(cur.label) {
                        cur.labelIndex = this.labels++;
                    }
                }
                
                t = t2;
            } else {
                // use last close
                cur.close = data[p][1];
                cur.volume  += data[p][5];
                // check for hpgh and low
                if(data[p][4] > cur.high) {
                    cur.high = data[p][4];
                }
                if(data[p][3] < cur.low) {
                    cur.low = data[p][3];
                }
            }
            
            // check for hpgh and low
            if(this.high === null || cur.high > this.high.high) {
                this.high = cur;
            }
            if(this.low === null || cur.low < this.low.low) {
                this.low = cur;
            }
        }    
        
        // if nothing was updated, skip
        if(!cur) {
            return;
        }
        
        // update end
        if(!next) {
            this.end = cur;
        } else {
            cur.next = next;
            next.prev = cur;
        }

        // update indices
        var icur;
        if(next) {
            icur = this.begin;
            var index = 0;
            this.labels = 0;
            while(icur) {
                icur.index = index++;
                if(icur.label) {
                    icur.labelIndex = this.labels++;
                }
                icur = icur.next;
            }
        }
        
        // calculate indicator values
        if(this.owner._indicators.length > 0) {
            if(begin == cur) {
                for(i=0;i<this.owner._indicators.length;i++) {
                    this.owner._indicators[i].calculate(cur);
                }
            } else {
                icur = this.begin;
                while(icur) {
                    for(i=0;i<this.owner._indicators.length;i++) {
                        this.owner._indicators[i].calculate(icur);
                    }
                    icur = icur.next;
                }    
            }
        }
        
        this.getParent();
        if(this.parent) {
            this.parent.updateData(begin, cur);
        }
    };
};

function ChartSource(instrumentId, exchangeId, quoteSource) {
    /**
    * reference counter
    */
    this._referenceCounter = 0;
    
    /**
    * requested spans
    */
    this._requestedSpans = {};
    this._availableSpans = {};
    
    /**
    * indicators
    */
    this._indicators = [];
    this._indicatorId = 0;
    
    /**
    * quotesource
    * @var string
    * @access private
    */
    this._quoteSource = quoteSource;
    
    /**
    * instrument id
    * @var number
    * @access private
    */
    this._instrumentId = instrumentId;
    
    /**
    * exchange id 
    * @var number
    * @access private
    */
    this._exchangeId = exchangeId;
    
    /**
    * available buffer
    * @var ChartBuffer
    * @access private
    */
    this._buffer = null;
    
    /**
    * registered subscribers
    */
    this._subscriptions = [];
    
    /**
    * previous close
    */
    this._prevClose = null;
    
    /**
    * push variables
    */
    this._subscription = null;
    this._valueField = null;
    this._timeField = null;
    this._prevField = null;
    
    /**
    * get format string for a specific interval
    */
    this.getFormatString = function(i) {
        if(i<ESpans.DAY) {
            return '%H:%M';
        } else if(i == ESpans.WEEK) {
            return 'KW %V \'%y (%d.%m)';
        } else if(i == ESpans.MONTH) {
            return '%b \'%y';
        }
        return '%d.%m.%Y';
    }
    
    /**
    * increase reference count
    * @access public
    */
    this.acquire = function(c) {    
        if(typeof c != 'undefined') {
            this._subscriptions.push(c);
        }
        
        this._referenceCounter++;
        if(this._referenceCounter == 1) {
            this.registerPush();
        }
    };
    
    /**
    * decrease reference count
    * @access public
    */
    this.release = function(c) {
        if(typeof c != 'undefined') {
            for(var j=0;j<this._subscriptions.length;j++) {
                if(this._subscriptions[j] == c) {
                    this._subscriptions.splice(j,1);
                    break;
                }
            }
        }
        
        this._referenceCounter--;
        if(this._referenceCounter == 0) {
            this.unregisterPush();
        }                        
    };
    
    /**
    *  get last value
    */
    this.getLastValue = function() {
        var b = this.getBuffer(60);
        if(b.end) {
            return b.end.close;
        }
        return null;
    }
    
    /**
    * get previous close
    */
    this.getPreviousClose = function(i) {
        var value = this._prevClose;
        var buffer = this.getBuffer(60);
        
        if(typeof i == 'undefined' || i == 60) {
            if(typeof i == 'undefined' && value === null) {
                if(buffer && buffer.begin) {
                    value = buffer.begin;
                } else {
                    value = this._buffer.begin;   
                }
            }
            return value;
        }
        
        while(value && buffer && buffer.interval < i) {
            buffer = buffer.parent;
            if(value) {
                value = value.parent;    
            } else {
                value = buffer.begin;
            }
        }
        
        return value;
    }
    
    /**
    * get available span
    */
    this.getAvailableSpan = function() {
        return this._availableSpans[60];    
    }
    
    /**
    * get buffer for specific interval
    * @access public
    */
    this.getBuffer = function(i) {
        // no buffers available
        if(this._buffer == null) {
            return;
        }
        
        // return top buffer if no interval is specified
        if(typeof i == 'undefined') {
            var buffer = this._buffer;
            while(buffer.parent) {
                buffer = buffer.parent;
            }
            return buffer;
        }
        
        // search for matching buffer
        var buffer = this._buffer;
        if(buffer.interval > i) {
            while(buffer.child && buffer.interval > i) {
                buffer = buffer.child;
            }
        } else if(buffer.interval < i) {
            while(buffer.parent && buffer.interval < i) {
                buffer = buffer.parent;
            }
        }
        if(buffer.interval == i) {
            return buffer;
        }
        
        // no buffer found
        return null;
    };
    
    /**
    * request data for a specific timespan
    * @access public
    */
    this.requestData = function(s,e,i,c,f) {
        return this._loadData(s,e,i,c,f);    
    };
    
    /**
    * unregister from push quotes
    */
    this.unregisterPush = function() {
        if(!this._subscription) {
            return;
        }
        
        BG_QuotePush.unsubscribe(this._instrumentId, this._exchangeId, this._valueField | this._timeField | this._prevField);
        BG_AjaxPushHub.unsubscribe(this._subscription);    
    };
    
    /**
    * register for push quotes
    */
    this.registerPush = function() {
        if(!BG_QuotePush.isPushableExchange(this._exchangeId)) {
            return;
        }
        
        switch(this._quoteSource) {
            case 'bid':
            default:
                this._valueField = EQuoteFields.BID_LAST;
                this._timeField = EQuoteFields.BID_TIME;
                this._prevField = EQuoteFields.BID_CLOSE;
                break;
            case 'ask':
                this._valueField = EQuoteFields.ASK_LAST;
                this._timeField = EQuoteFields.ASK_TIME;
                this._prevField = EQuoteFields.ASK_CLOSE;
                break;
            case 'last':
                this._valueField = EQuoteFields.LAST_LAST;
                this._timeField = EQuoteFields.LAST_TIME;
                this._prevField = EQuoteFields.LAST_CLOSE;
                break;
        }
        var fields = this._valueField | this._timeField | this._prevField;
        var event = BG_QuotePush.subscribe(this._instrumentId, this._exchangeId, fields);
        var me = this;
        this._subscription = BG_AjaxPushHub.subscribe(event, function(e,d) { me.OnPush(e,d);});    
    };
    
    /**
    * receive quote pushes
    */
    this.OnPush = function(e,d) {
        if(!(d.fields & this._valueField)) {
            return;
        }
        
        var value   = parseFloat(d.values[this._valueField]);
        var time    = null;
        if(d.fields & this._timeField) {
            time = parseInt(d.values[this._timeField]);   
        } else {
            time = BG_QuotePush.get(this._instrumentId, this._exchangeId, this._timeField);
        }
        if(time === null) {
            return;
        }
        
        var buffer = this.getBuffer(60);
        if(!buffer) {
            buffer = this.getBuffer(ESpans.DAY);
            do {
                buffer = buffer.getChild();
            } while(buffer && buffer.interval != 60);
            if(!buffer) {
                return;
            }
        }
        
        if(buffer.interval == ESpans.DAY) {
            time = Math.floor(time/ESpans.DAY)*ESpans.DAY;
        }
        
        var t   = tickT(time, buffer.interval);
        var lt  = tickT(buffer.size == 0?0:buffer.end.timestamp, buffer.interval);
        
        var q;
        // update last tick
        if(t == lt) {
            q = buffer.end;
            q.close = value;
            if(value > q.high) {
                q.high = value;
            }
            if(value < q.low) {
                q.low = value;
            }
            if(!buffer.high || value > buffer.high.high) {
                buffer.high = q;
            }
            if(!buffer.low || value < buffer.low.low) {
                buffer.low = q;
            }
        // create new tick
        } else {
            q = new ChartValue();
            q.prev = buffer.end;
            if(!buffer.begin) {
                buffer.begin = q;
            }
            if(buffer.end) {
                buffer.end.next = q;
            }
            buffer.end = q;
            
            q.timestamp = time;
            q.open = value;
            q.close = value;
            q.high = value;
            q.low = value;
            q.volume = 0;
            q.index = buffer.size;
            if(q.prev) {
                q.index = q.prev.index+1;        
            } else {
                q.index = 0;
            }
            
            if(!buffer.high || value > buffer.high.high) {
                buffer.high = q;
            }
            if(!buffer.low || value < buffer.low.low) {
                buffer.low = q;
            }
            
            buffer.size++;
        }
        // update buffers
        if(buffer.parent) {
            buffer.parent.updateData(q,q);
        }
        
        // update indicators
        while(q) {
            for(i=0;i<this._indicators.length;i++) {
                this._indicators[i].calculate(q);
            }
            q = q.parent;
        } 
        
        // notify subscribers
        for(var i = 0;i<this._subscriptions.length;i++) {
            this._subscriptions[i](t != lt);
        }
    };
    
    /**
    * add an indicator
    */ 
    this.addIndicator = function(type, param1, param2, param3) {
        if(typeof param1 == 'undefined') {
            param1 = 0;
        }
        if(typeof param2 == 'undefined') {
            param2 = 0;
        }
        if(typeof param3 == 'undefined') {
            param3 = 0;
        }
        // check if this indicator is already registered
        var i;
        for(var j=0;j<this._indicators.length;j++) {
            i = this._indicators[j];
            if(i.type == type && i.param1 == param1 && i.param2 == param2 && i.param3 == param3) {
                i.referenceCount++;
                return i;
            }
        }
        
        // register it
        switch(type) {
            case 'sma': i = new IndicatorSMA(); break;
            case 'ema': i = new IndicatorEMA(); break;
            case 'tma': i = new IndicatorTMA(); break;
            case 'wma': i = new IndicatorWMA(); break;
            case 'bb':  i = new IndicatorBB();  break;
            default: return null;
        }
        
        var ids = [];
        for(var j=0;j<i.values;j++) {
            ids.push(this._indicatorId++);
        }
        i.id = ids;
        
        i.type = type;
        i.referenceCount = 1;
        i.param1 = param1;
        i.param2 = param2;
        i.param3 = param3;
        
        i.register(this);
        
        this._indicators.push(i);
        
        // calculate values
        var b = this.getBuffer(60);
        if(!b) {
            b = this.getBuffer(ESpans.DAY);
        }
        var cur;
        while(b) {
            cur = b.begin;
            while(cur) {
                i.calculate(cur);
                cur = cur.next;
            }    
            b = b.parent;
        }
        
        return i;
    };
    
    /**
    * remove an indicator
    */
    this.removeIndicator = function(indicator) {
        // check if this indicator is already registered
        for(var j=0;j<this._indicators.length;j++) {
            if(this._indicators[j] == indicator) {
                var i = this._indicators[j];
                if(--i.referenceCount <= 0) {
                    this._indicators.splice(j, 1);
                    i.unregister(this);
                    // TODO remove from quote values
                }
                return;
            }
        }    
    }
    
    /**
    * load quote data
    *
    * @param ChartBuffer buffer to load data for
    * @param number span start
    * @param number span end
    * @param number interval
    * @param function a callback to call when data is loaded
    *
    * @access private
    *
    * @return void
    */
    this._loadData = function(s,e,i,c,f) {
        var s2 = 0, e2 = 0, i2 = i < 86400 ? 60 : 86400;
        
        if(f!==true && i2 == 60 && typeof this._availableSpans[60] != 'undefined') {
            var requestedStart = Math.floor(s/ESpans.DAY)*ESpans.DAY;
            var availableStart = Math.floor(this._availableSpans[60][0]/ESpans.DAY)*ESpans.DAY;
            if(requestedStart < availableStart) {
                i = i2 = 86400;
            }
        }
        
        // try to preload more data
        if(i2 == 60) {
            s2 = ((s/86400)|0)*86400;
            e2 = (((e/86400)|0)+1)*86400;
        } else {
            s2  = s - ESpans.YEAR;
            e2  = e + ESpans.YEAR;
        }
                        
        // clamp request span
        if(typeof this._requestedSpans[i2] == 'undefined') {
            this._requestedSpans[i2] = [s2,e2];
        } else {
            if(this._requestedSpans[i2][0] <= s && this._requestedSpans[i2][1] >= e) {
                if(c) {
                    var b = this.getBuffer(i2);
                    if(b && b.size > 0) {
                        e = Math.min(b.getLastTimestamp(),e);
                        s = Math.min(e,Math.max(s,b.getFirstTimestamp()));
                        
                        c(s, e, i, false);
                    } else {
                        c(false, false, false, false);
                    }
                }
                return false;
            } else {
                this._requestedSpans[i2][0] = s2 = Math.min(this._requestedSpans[i2][1],s2);
                this._requestedSpans[i2][1] = e2 = Math.max(this._requestedSpans[i2][1],e2);
            }
        }
        
        // clip to current time
        var now = ((new Date()).getTime()/1000)|0;
        if(e2 > now) {
            e2 = now;
        }
        
        // send request        
        var xrequest = null;
        if (typeof XMLHttpRequest != 'undefined') {
            xrequest = new XMLHttpRequest();
        } else if (window.ActiveXObject) {
            if (navigator.userAgent.toLowerCase().indexOf("msie 5") != -1) {
                xrequest = new ActiveXObject("Microsoft.XMLHTTP");
            } else {
                xrequest = new ActiveXObject("Msxml2.XMLHTTP");
            }
        }
        
        var me = this;
        xrequest.onreadystatechange = function() {
            if(xrequest.readyState == 4) {
                if(xrequest.status != 200) {
                    if(c) {
                        c(false, false, false, true);
                    }
                    return;   
                }
                b = me.getBuffer();
                
                var data = eval(xrequest.responseText);
                b.addData(s2,e2,i2,data);
                
                // store intraday span
                if(!me._availableSpans[60]) {
                    me._availableSpans[60] = [0,0];
                }
                me._availableSpans[60][0] = data[0];
                me._availableSpans[60][1] = data[1];
                
                // check if historic data needs to be used
                if(f!==true && i2 == 60) {
                    var requestedStart = Math.floor(s2/ESpans.DAY)*ESpans.DAY;
                    var availableStart = Math.floor(me._availableSpans[60][0]/ESpans.DAY)*ESpans.DAY;
                    if(requestedStart < availableStart) {
                        me._loadData(s,e,ESpans.DAY,c);
                        return;
                    }
                }
                
                var today = Math.floor(((new Date()).getTime()/1000)/ESpans.DAY)*ESpans.DAY;
                if(!me._prevClose && i2 == 60 && s2 <= today && e2 >= today) {
                    b = me.getBuffer(i2);
                    var value = b.begin;
                    if(value) {
                        while(value.next && value.next.timestamp < today) {
                            value = value.next;
                        }
                        if(value.timestamp < today) {
                            me._prevClose = value;
                        }
                    }
                }
                if(c) {
                    b = me.getBuffer(i2);
                    if(b) {
                        e = Math.min(b.getLastTimestamp(),e);
                        s = Math.min(e,Math.max(s,b.getFirstTimestamp()));
                        if(s == 0) s = false;
                        if(e == 0) e = false;
                    } else {
                        s = e = i = false;
                    }
                    c(s, e, i, true);
                }
            }
        };
        
        // build cache parameters
        var cFrom   = Math.ceil(s2/i2);
        var cTo     = Math.ceil(e2/i2);
        var cacheTag = this._instrumentId + "o" + this._exchangeId + "o" + i2 + "o" + cFrom + "o" + cTo;
        
        xrequest.open("GET",this._baseURL + 'history.php?ctag='+cacheTag,true);
        xrequest.setRequestHeader("Content-type", "application/x-www-form-urlencoded");

        xrequest.send(null);
        
        return true;
    };
     
    // create a buffer
    this._buffer = new ChartBuffer(this,86400);
}

ChartSource.prototype._baseURL = '/js/core/chart/';

/**
* chart source cache
*/
var ChartSourceCache = {
    _instrumentNames: {},
    _sources: [],
    
    /**
    * store an instrument name
    */
    addName: function(id, name) {
        ChartSourceCache._instrumentNames[id] = name;
    },
    
    /**
    * preload some data
    */
    preload: function(instrumentId, exchangeId, quoteSource, start, end) {
        if(ChartCache._staticCharts) {
            return;
        }
        var source = ChartSourceCache.get(instrumentId, exchangeId, quoteSource);
        var interval = end-start > 86400 ? 86400 : 60;
        if(typeof end == 'undefined') {
            var now = ((new Date()).getTime()/1000)|0;    
            end     = Math.ceil(now / ESpans.DAY)*ESpans.DAY;
            start   = Math.ceil((now - start)/ ESpans.DAY)*ESpans.DAY + 1;
        }
        source.requestData(start,end,interval);
    },
    
    /**
    * get/create a source
    */
    get: function(instrumentId, exchangeId, quoteSource) {
        var s = null;
        for(var i=0;i<ChartSourceCache._sources.length;i++) {
            s = ChartSourceCache._sources[i];
            if(s._instrumentId == instrumentId && s._exchangeId == exchangeId && s._quoteSource == quoteSource) {
                return s;
            }
        }
        
        s = new ChartSource(instrumentId, exchangeId, quoteSource);
        s.acquire();
        if(typeof ChartSourceCache._instrumentNames[instrumentId] != 'undefined') {
            s._instrumentName = ChartSourceCache._instrumentNames[instrumentId];        
        }
        ChartSourceCache._sources.push(s);
        
        return s;
    },
    
    /**
    * remove a source
    */
    remove: function(instrumentId, exchangeId, quoteSource) {
        var s = null;
        for(var i=0;i<ChartSourceCache._sources.length;i++) {
            s = ChartSourceCache._sources[i];
            if(s._instrumentId == instrumentId && s._exchangeId == exchangeId && s._quoteSource == quoteSource) {
                s.release();
                ChartSourceCache._sources.splice(i,1);
                return;
            }
        }    
    }
}

/**
* chart cache
*/
var ChartCache = {
    _charts: [],
    _works: null,
    _staticCharts: null,
    
    getUseStaticCharts: function() {
        if(ChartCache._staticCharts === null) {
            ChartCache._staticCharts = BG_AjaxPush._readSetting('BG_ChartsStatic',0) == 1 ? true : false;
        }
        return ChartCache._staticCharts;
    },
    
    setUseStaticCharts: function(flag) {
        BG_AjaxPush._writeSetting('BG_ChartsStatic',flag?1:0,86400*364);
        ChartCache._staticCharts = flag;   
    },
    
    works: function() {
        if(ChartCache._works === null) {
            if(typeof window.G_vmlCanvasManager != 'undefined' && !Silverlight.isInstalled()) {
                ChartCache._works = false;
            } else {
                ChartCache._works = true;
            }
        }
        return ChartCache._works;
    },
    
    remove: function(id) {
        var chart = ChartCache._charts[id];
        if(chart) {
            delete ChartCache._charts[id];
            chart._delete();
            
            return true;
        }
        return false;
    },
    
    set: function(id,chart) {
        ChartCache._charts[id] = chart;
    },
    
    get: function(id) {
        if(typeof ChartCache._charts[id] != 'undefined') {
            return ChartCache._charts[id];
        }
        return null;
    }
};

/**
* initialization routines
*/
function staticLoadScript(url) {
    if(navigator.appVersion.indexOf("MSIE")   != -1) {
        document.write('<script type="text/javascript" src="'+url+'"></script>');
    } else {
        var script = document.createElement('script');
        script.src = url;
        document.body.appendChild(script);
    }
}


if(!window.Silverlight) {
   
    var Silverlight = {
        available: null,
        
        isInstalled: function(d) {
            if(typeof d == 'undefined') {
                d = '2.0';
            }
            if(Silverlight.available!==null) {
                return Silverlight.available;
            }
            
            var c = false, a = null;
            try
            {
                var b = null;
                if(navigator.appVersion.indexOf("MSIE") != -1) { 
                    b = new ActiveXObject("AgControl.AgControl");
                } else {
                    if(navigator.plugins["Silverlight Plug-In"]) {
                        a = document.createElement("div");
                        document.body.appendChild(a);
                        a.innerHTML = '<embed type="application/x-silverlight" />';
                        b = a.childNodes[0]
                    }
                }
                if(b.IsVersionSupported(d)) {
                    c = true;
                }
                b = null;
                Silverlight.available = true
            }
            catch(e)
            {
                c=false
            }
            
            if(a) {
                document.body.removeChild(a);
            }
            
            return c
        }
    }
}

function useStaticCharts() {
    XYChart     = StaticCharts.XYChart;
    NavBar      = StaticCharts.NavBar;
    Chart       = StaticCharts.Chart;
    Sparkline   = StaticCharts.Sparkline;
}

if(ChartCache.getUseStaticCharts() == 1) {
    useStaticCharts();
}
    
function initCharts(path) {   
    if(ChartCache.getUseStaticCharts() == 1) {
        return;
    }
    
    var c = document.createElement('canvas');
    var ctx;
    if (!c.getContext) {
        if(!Silverlight.isInstalled()) {
            useStaticCharts();
            staticLoadScript(path+'chart_fallback.js');
        } else {
            staticLoadScript(path+'excanvas.js');
        }
    } else {
        ctx = c.getContext('2d');
        if(!ctx.fillText) {
           staticLoadScript(path+'canvas.text.js');
        } else if(navigator.appVersion.indexOf('iPhone') != -1) {
            staticLoadScript(path+'canvas.text.js?reimplement=true');
        }
    }
    ChartSource.prototype._baseURL = path;
};

