/* 
    PlotKit Canvas
    --------------
    
    Provides HTML Canvas Renderer. This is supported under:
    
    - Safari 2.0
    - Mozilla Firefox 1.5
    - Opera 9.0 preview 2
    - IE 6 (via VML Emulation)
    
    It uses DIVs for labels.
    
    Notes About IE Support
    ----------------------
    
    This class relies on iecanvas.htc for Canvas Emulation under IE[1].
    iecanvas.htc is included in the distribution of PlotKit for convenience. In order to enable IE support, you must set the following option when initialising the renderer:
    
    var renderOptions = {
        "IECanvasHTC": "contrib/iecanvas.htc"
    };
    var engine = new CanvasRenderer(canvasElement, layout, renderOptions);
    
    Where "contrib/iecanvas.htc" is the path to the htc behavior relative
    to where your HTML is.
    
    This is only needed for IE support.
    
    Copyright
    ---------
    Copyright 2005,2006 (c) Alastair Tse <alastair^liquidx.net>
    For use under the BSD license. <http://www.liquidx.net/plotkit>
    
*/
// --------------------------------------------------------------------
// Check required components
// --------------------------------------------------------------------

try {    
    if (typeof(PlotKit.Layout) == 'undefined')
    {
        throw "";    
    }
} 
catch (e) {    
    throw "PlotKit.Layout depends on MochiKit.{Base,Color,DOM,Format} and PlotKit.Base and PlotKit.Layout"
}


// ------------------------------------------------------------------------
//  Defines the renderer class
// ------------------------------------------------------------------------

if (typeof(PlotKit.CanvasRenderer) == 'undefined') {
    PlotKit.CanvasRenderer = {};
}

PlotKit.CanvasRenderer.NAME = "PlotKit.CanvasRenderer";
PlotKit.CanvasRenderer.VERSION = PlotKit.VERSION;

PlotKit.CanvasRenderer.__repr__ = function() {
    return "[" + this.NAME + " " + this.VERSION + "]";
};

PlotKit.CanvasRenderer.toString = function() {
    return this.__repr__();
}

PlotKit.CanvasRenderer = function(element, layout, options) {
    if (arguments.length  > 0)
        this.__init__(element, layout, options);
};

PlotKit.CanvasRenderer.prototype.__init__ = function(element, layout, options) {
    var isNil = MochiKit.Base.isUndefinedOrNull;
    var Color = MochiKit.Color.Color;
    
    // default options
    this.options = {
        "drawBackground": true,
        "backgroundColor": Color.whiteColor(),
        "padding": {left: 30, right: 30, top: 5, bottom: 10},
        "colorScheme": PlotKit.Base.palette(PlotKit.Base.baseColors()[0]),
        "strokeColor": Color.whiteColor(),
        "strokeColorTransform": "asStrokeColor",
        "strokeWidth": 0.5,
        "shouldFill": true,
        "shouldStroke": true,
        "drawXAxis": true,
        "drawYAxis": true,
        "axisLineColor": Color.blackColor(),
        "axisLineWidth": 0.5,
        "axisTickSize": 3,
        "axisLabelColor": Color.blackColor(),
        "axisLabelFont": "Arial",
        "axisLabelFontSize": 9,
		"axisLabelWidth": 50,
		"pieRadius": 0.4,
        "enableEvents": true,
        "IECanvasHTC": "PlotKit/iecanvas.htc"
    };
    MochiKit.Base.update(this.options, options ? options : {});

    // we need to refetch the element because of this horrible Canvas on IE
    // crap
    this.element_id = element.id ? element.id : element;

    // Stuff relating to Canvas on IE support
    var self = PlotKit.CanvasRenderer;
    this.isIE = self.IECanvasEmulationIfNeeded(this.options.IECanvasHTC);
    this.IEDelay = 0.5;
    this.maxTries = 5;
    this.renderDelay = null;
    this.clearDelay = null;

    this.layout = layout;
    this.style = layout.style;
    this.element = MochiKit.DOM.getElement(this.element_id);
    //this.element = element;
    this.container = this.element.parentNode;
    this.height = this.element.height;
    this.width = this.element.width;

    // --- check whether everything is ok before we return

    if (isNil(this.element))
        throw "CanvasRenderer() - passed canvas is not found";

    if (!this.isIE && !(PlotKit.CanvasRenderer.isSupported(this.element)))
        throw "CanvasRenderer() - Canvas is not supported.";

    if (isNil(this.container) || (this.container.nodeName.toLowerCase() != "div"))
        throw "CanvasRenderer() - <canvas> needs to be enclosed in <div>";

    // internal state
    this.xlabels = new Array();
    this.ylabels = new Array();
    this.isFirstRender = true;

    this.area = {
        x: this.options.padding.left,
        y: this.options.padding.top,
        w: this.width - this.options.padding.left - this.options.padding.right,
        h: this.height - this.options.padding.top - this.options.padding.bottom
    };

    MochiKit.DOM.updateNodeAttributes(this.container, 
    {"style":{ "position": "relative", "width": this.width + "px"}});

    // load event system if we have Signals
    try {
        this.event_isinside = null;
        if (MochiKit.Signal && this.options.enableEvents) {
            this._initialiseEvents();
        }
    }
    catch (e) {
        // still experimental
    }
};

PlotKit.CanvasRenderer.IECanvasEmulationIfNeeded = function(htc) {
    var ie = navigator.appVersion.match(/MSIE (\d\.\d)/);
    var opera = (navigator.userAgent.toLowerCase().indexOf("opera") != -1);
    if ((!ie) || (ie[1] < 6) || (opera))
        return false;

    if (isUndefinedOrNull(MochiKit.DOM.getElement('VMLRender'))) {
        // before we add VMLRender, we need to recreate all canvas tags
        // programmatically otherwise IE will not recognise it

        var nodes = document.getElementsByTagName('canvas');
        for (var i = 0; i < nodes.length; i++) {
            var node = nodes[i];
            if (node.getContext) { return; } // Other implementation, abort
            var newNode = MochiKit.DOM.CANVAS(
               {id: node.id, 
                width: "" + parseInt(node.width),
                height: "" + parseInt(node.height)}, "");
            newNode.style.width = parseInt(node.width) + "px";
            newNode.style.height = parseInt(node.height) + "px";
            node.id = node.id + "_old";
            MochiKit.DOM.swapDOM(node, newNode);
        }

        document.namespaces.add("v");
        var vmlopts = {'id':'VMLRender',
                       'codebase':'vgx.dll',
                       'classid':'CLSID:10072CEC-8CC1-11D1-986E-00A0C955B42E'};
        var vml = MochiKit.DOM.createDOM('object', vmlopts);
        document.body.appendChild(vml);
        var vmlStyle = document.createStyleSheet();
        vmlStyle.addRule("canvas", "behavior: url('" + htc + "');");
        vmlStyle.addRule("v\\:*", "behavior: url(#VMLRender);");
    }
    return true;
};

PlotKit.CanvasRenderer.prototype.render = function() {
    if (this.isIE) {
        // VML takes a while to start up, so we just poll every this.IEDelay
        try {
            if (this.renderDelay) {
                this.renderDelay.cancel();
                this.renderDelay = null;
            }
            var context = this.element.getContext("2d");
        }
        catch (e) {
            this.isFirstRender = false;
            if (this.maxTries-- > 0) {
                this.renderDelay = MochiKit.Async.wait(this.IEDelay);
                this.renderDelay.addCallback(bind(this.render, this));
            }
            return;
        }
    }

    if (this.options.drawBackground)
        this._renderBackground();

    if (this.style == "bar") {
        this._renderBarChart();
		this._renderBarAxis(); 
	}
    else if (this.style == "pie") {
        this._renderPieChart();
		this._renderPieAxis();
	}
    else if (this.style == "line") {
        this._renderLineChart();
		this._renderLineAxis();
	}
};

PlotKit.CanvasRenderer.prototype._renderBarChartWrap = function(data, plotFunc) {
    var context = this.element.getContext("2d");
    var colorCount = this.options.colorScheme.length;
    var colorScheme = this.options.colorScheme;
    var setNames = MochiKit.Base.keys(this.layout.datasets);
    var setCount = setNames.length;

    for (var i = 0; i < setCount; i++) {
        var setName = setNames[i];
        var color = colorScheme[i%colorCount];
        context.save();
        context.fillStyle = color.toRGBString();
        if (this.options.strokeColor)
            context.strokeStyle = this.options.strokeColor.toRGBString();
        else if (this.options.strokeColorTransform) 
            context.strokeStyle = color[this.options.strokeColorTransform]().toRGBString();
        
        context.lineWidth = this.options.strokeWidth;
        var forEachFunc = function(obj) {
            if (obj.name == setName)
                plotFunc(context, obj);
        };                

        MochiKit.Iter.forEach(data, bind(forEachFunc, this));
        context.restore();
    }
};

PlotKit.CanvasRenderer.prototype._renderBarChart = function() {
    var bind = MochiKit.Base.bind;

    var drawRect = function(context, bar) {
        var x = this.area.w * bar.x + this.area.x;
        var y = this.area.h * bar.y + this.area.y;
        var w = this.area.w * bar.w;
        var h = this.area.h * bar.h;       
        if ((w < 1) || (h < 1))
            return;
        if (this.options.shouldFill)
            context.fillRect(x, y, w, h);
        if (this.options.shouldStroke)
            context.strokeRect(x, y, w, h);                
    };
    this._renderBarChartWrap(this.layout.bars, bind(drawRect, this));
};

PlotKit.CanvasRenderer.prototype._renderLineChart = function() {
    var context = this.element.getContext("2d");
    var colorCount = this.options.colorScheme.length;
    var colorScheme = this.options.colorScheme;
    var setNames = MochiKit.Base.keys(this.layout.datasets);
    var setCount = setNames.length;
    var bind = MochiKit.Base.bind;
    var partial = MochiKit.Base.partial;

    for (var i = 0; i < setCount; i++) {
        var setName = setNames[i];
        var color = colorScheme[i%colorCount];
        var strokeX = this.options.strokeColorTransform;

        // setup graphics context
        context.save();
        context.fillStyle = color.toRGBString();
        if (this.options.strokeColor)
            context.strokeStyle = this.options.strokeColor.toRGBString();
        else if (this.options.strokeColorTransform) 
            context.strokeStyle = color[strokeX]().toRGBString();
        
        context.lineWidth = this.options.strokeWidth;
        
        // create paths
        var makePath = function() {
            context.beginPath();
            context.moveTo(this.area.x, this.area.y + this.area.h);
            var addPoint = function(context, point) {
            if (point.name == setName)
                context.lineTo(this.area.w * point.x + this.area.x,
                               this.area.h * point.y + this.area.y);
            };
            MochiKit.Iter.forEach(this.layout.points, partial(addPoint, context), this);
            context.lineTo(this.area.w + this.area.x,
                           this.area.h + this.area.y);
            context.lineTo(this.area.x, this.area.y + this.area.h);
            context.closePath();
        };

        if (this.options.shouldFill) {
            bind(makePath, this)();
            context.fill();
        }
        if (this.options.shouldStroke) {
            bind(makePath, this)();
            context.stroke();
        }

        context.restore();
    }
};

PlotKit.CanvasRenderer.prototype._renderPieChart = function() {
    var context = this.element.getContext("2d");
    var colorCount = this.options.colorScheme.length;
    var slices = this.layout.slices;

    var centerx = this.area.x + this.area.w * 0.5;
    var centery = this.area.y + this.area.h * 0.5;
    var radius = Math.min(this.area.w * this.options.pieRadius, 
                          this.area.h * this.options.pieRadius);

    if (this.isIE) {
        centerx = parseInt(centerx);
        centery = parseInt(centery);
        radius = parseInt(radius);
    }


	// NOTE NOTE!! Canvas Tag draws the circle clockwise from the y = 0, x = 1
	// so we have to subtract 90 degrees to make it start at y = 1, x = 0

    for (var i = 0; i < slices.length; i++) {
        var color = this.options.colorScheme[i%colorCount];
        context.save();
        context.fillStyle = color.toRGBString();

        var makePath = function() {
            context.beginPath();
            context.moveTo(centerx, centery);
            context.arc(centerx, centery, radius, 
                        slices[i].startAngle - Math.PI/2,
                        slices[i].endAngle - Math.PI/2,
                        false);
            context.lineTo(centerx, centery);
            context.closePath();
        };

        if (Math.abs(slices[i].startAngle - slices[i].endAngle) > 0.001) {
            if (this.options.shouldFill) {
                makePath();
                context.fill();
            }
            
            if (this.options.shouldStroke) {
                makePath();
                context.lineWidth = this.options.strokeWidth;
                if (this.options.strokeColor)
                    context.strokeStyle = this.options.strokeColor.toRGBString();
                else if (this.options.strokeColorTransform)
                    context.strokeStyle = color[this.options.strokeColorTransform]().toRGBString();
                context.stroke();
            }
        }
        context.restore();
    }
};

PlotKit.CanvasRenderer.prototype._renderBarAxis = function() {
	this._renderAxis();
}

PlotKit.CanvasRenderer.prototype._renderLineAxis = function() {
	this._renderAxis();
};


PlotKit.CanvasRenderer.prototype._renderAxis = function() {
    if (!this.options.drawXAxis && !this.options.drawYAxis)
        return;

    var context = this.element.getContext("2d");

    var labelStyle = {"style":
         {"position": "absolute",
          "fontSize": this.options.axisLabelFontSize + "px",
          "zIndex": 10,
          "color": this.options.axisLabelColor.toRGBString(),
          "width": this.options.axisLabelWidth + "px",
          "overflow": "hidden"
         }
    };

    // axis lines
    context.save();
    context.strokeStyle = this.options.axisLineColor.toRGBString();
    context.lineWidth = this.options.axisLineWidth;


    if (this.options.drawYAxis) {
        if (this.layout.yticks) {
            var drawTick = function(tick) {
                var x = this.area.x;
                var y = this.area.y + tick[0] * this.area.h;
                context.beginPath();
                context.moveTo(x, y);
                context.lineTo(x - this.options.axisTickSize, y);
                context.closePath();
                context.stroke();

                var label = DIV(labelStyle, tick[1]);
                label.style.top = (y - this.options.axisLabelFontSize) + "px";
                label.style.left = (x - this.options.padding.left - this.options.axisTickSize) + "px";
                label.style.textAlign = "right";
                label.style.width = (this.options.padding.left - this.options.axisTickSize * 2) + "px";
                MochiKit.DOM.appendChildNodes(this.container, label);
                this.ylabels.push(label);
            };
            
            MochiKit.Iter.forEach(this.layout.yticks, bind(drawTick, this));
        }

        context.beginPath();
        context.moveTo(this.area.x, this.area.y);
        context.lineTo(this.area.x, this.area.y + this.area.h);
        context.closePath();
        context.stroke();
    }

    if (this.options.drawXAxis) {
        if (this.layout.xticks) {
            var drawTick = function(tick) {
                var x = this.area.x + tick[0] * this.area.w;
                var y = this.area.y + this.area.h;
                context.beginPath();
                context.moveTo(x, y);
                context.lineTo(x, y + this.options.axisTickSize);
                context.closePath();
                context.stroke();

                var label = DIV(labelStyle, tick[1]);
                label.style.top = (y + this.options.axisTickSize) + "px";
                label.style.left = (x - this.options.axisLabelWidth/2) + "px";
                label.style.textAlign = "center";
                label.style.width = this.options.axisLabelWidth + "px";
                MochiKit.DOM.appendChildNodes(this.container, label);
                this.xlabels.push(label);
            };
            
            MochiKit.Iter.forEach(this.layout.xticks, bind(drawTick, this));
        }

        context.beginPath();
        context.moveTo(this.area.x, this.area.y + this.area.h);
        context.lineTo(this.area.x + this.area.w, this.area.y + this.area.h);
        context.closePath();
        context.stroke();
    }

    context.restore();

};

PlotKit.CanvasRenderer.prototype._renderPieAxis = function() {
    if (!this.options.drawXAxis)
        return;

	if (this.layout.xticks) {
		// make a lookup dict for x->slice values
		var lookup = new Array();
		for (var i = 0; i < this.layout.slices.length; i++) {
			lookup[this.layout.slices[i].xval] = this.layout.slices[i];
		}
		
		var centerx = this.area.x + this.area.w * 0.5;
	    var centery = this.area.y + this.area.h * 0.5;
	    var radius = Math.min(this.area.w * this.options.pieRadius,
	                          this.area.h * this.options.pieRadius);
		var labelWidth = this.options.axisLabelWidth;
		
		for (var i = 0; i < this.layout.xticks.length; i++) {
			var slice = lookup[this.layout.xticks[i][0]];
			if (MochiKit.Base.isUndefinedOrNull(slice))
				continue;
				
				
			var angle = (slice.startAngle + slice.endAngle)/2;
			// normalize the angle
			var normalisedAngle = angle;
			if (normalisedAngle > Math.PI * 2)
				normalisedAngle = normalisedAngle - Math.PI * 2;
			else if (normalisedAngle < 0)
				normalisedAngle = normalisedAngle + Math.PI * 2;
				
			var labelx = centerx + Math.sin(normalisedAngle) * (radius + 10);
	        var labely = centery - Math.cos(normalisedAngle) * (radius + 10);

			var attrib = {"position": "absolute",
	                      "zIndex": 11,
	                      "width": labelWidth + "px",
	                      "fontSize": this.options.axisLabelFontSize + "px",
	                      "overflow": "hidden",
						  "color": this.options.axisLabelColor.toHexString()
						};

			if (normalisedAngle <= Math.PI * 0.5) {
	            // text on top and align left
	            attrib["textAlign"] = "left";
	            attrib["verticalAlign"] = "top";
	            attrib["left"] = labelx + "px";
	            attrib["top"] = (labely - this.options.axisLabelFontSize) + "px";
	        }
	        else if ((normalisedAngle > Math.PI * 0.5) && (normalisedAngle <= Math.PI)) {
	            // text on bottom and align left
	            attrib["textAlign"] = "left";
	            attrib["verticalAlign"] = "bottom";     
	            attrib["left"] = labelx + "px";
	            attrib["top"] = labely + "px";

	        }
	        else if ((normalisedAngle > Math.PI) && (normalisedAngle <= Math.PI*1.5)) {
	            // text on bottom and align right
	            attrib["textAlign"] = "right";
	            attrib["verticalAlign"] = "bottom"; 
	            attrib["left"] = (labelx  - labelWidth) + "px";
	            attrib["top"] = labely + "px";
	        }
	        else {
	            // text on top and align right
	            attrib["textAlign"] = "right";
	            attrib["verticalAlign"] = "bottom";  
	            attrib["left"] = (labelx  - labelWidth) + "px";
	            attrib["top"] = (labely - this.options.axisLabelFontSize) + "px";
	        }
	
			var label = DIV({'style': attrib}, this.layout.xticks[i][1]);
			this.xlabels.push(label);
			MochiKit.DOM.appendChildNodes(this.container, label);
	  }
		
	}
};

PlotKit.CanvasRenderer.prototype._renderBackground = function() {
    var context = this.element.getContext("2d");
    context.save();
    context.fillStyle = this.options.backgroundColor.toRGBString();
    context.fillRect(0, 0, this.width, this.height);
    context.restore();
};

PlotKit.CanvasRenderer.prototype.clear = function() {
    if (this.isIE) {
        // VML takes a while to start up, so we just poll every this.IEDelay
        try {
            if (this.clearDelay) {
                this.clearDelay.cancel();
                this.clearDelay = null;
            }
            var context = this.element.getContext("2d");
        }
        catch (e) {
            this.isFirstRender = false;
            this.clearDelay = MochiKit.Async.wait(this.IEDelay);
            this.clearDelay.addCallback(bind(this.clear, this));
            return;
        }
    }

    var context = this.element.getContext("2d");
    context.clearRect(0, 0, this.width, this.height);

    
    for (var i = 0; i < this.xlabels.length; i++) {
        MochiKit.DOM.removeElement(this.xlabels[i]);
    }        
    for (var i = 0; i < this.ylabels.length; i++) {
        MochiKit.DOM.removeElement(this.ylabels[i]);
    }            
    this.xlabels = new Array();
    this.ylabels = new Array();
    
};

PlotKit.CanvasRenderer.prototype._initialiseEvents = function() {
    var connect = MochiKit.Signal.connect;
    var bind = MochiKit.Base.bind;
    MochiKit.Signal.registerSignals(this, ['onmouseover', 'onclick', 'onmouseout', 'onmousemove']);
    //connect(this.element, 'onmouseover', bind(this.onmouseover, this));
    //connect(this.element, 'onmouseout', bind(this.onmouseout, this));
    //connect(this.element, 'onmousemove', bind(this.onmousemove, this));
    connect(this.element, 'onclick', bind(this.onclick, this));
};

PlotKit.CanvasRenderer.prototype._resolveObject = function(e) {
    // does not work in firefox
	//var x = (e.event().offsetX - this.area.x) / this.area.w;
	//var y = (e.event().offsetY - this.area.y) / this.area.h;

    var x = (e.mouse().page.x - PlotKit.Base.findPosX(this.element) - this.area.x) / this.area.w;
    var y = (e.mouse().page.y - PlotKit.Base.findPosY(this.element) - this.area.y) / this.area.h;
	
    //log(x, y);

    var isHit = this.layout.hitTest(x, y);
    if (isHit)
        return isHit;
    return null;
};

PlotKit.CanvasRenderer.prototype._createEventObject = function(layoutObj, e) {
    if (layoutObj == null) {
        return null;
    }

    e.chart = layoutObj
    return e;
};


PlotKit.CanvasRenderer.prototype.onclick = function(e) {
    var layoutObject = this._resolveObject(e);
    var eventObject = this._createEventObject(layoutObject, e);
    if (eventObject != null)
        MochiKit.Signal.signal(this, "onclick", eventObject);
};

PlotKit.CanvasRenderer.prototype.onmouseover = function(e) {
    var layoutObject = this._resolveObject(e);
    var eventObject = this._createEventObject(layoutObject, e);
    if (eventObject != null) 
        signal(this, "onmouseover", eventObject);
};

PlotKit.CanvasRenderer.prototype.onmouseout = function(e) {
    var layoutObject = this._resolveObject(e);
    var eventObject = this._createEventObject(layoutObject, e);
    if (eventObject == null)
        signal(this, "onmouseout", e);
    else 
        signal(this, "onmouseout", eventObject);

};

PlotKit.CanvasRenderer.prototype.onmousemove = function(e) {
    var layoutObject = this._resolveObject(e);
    var eventObject = this._createEventObject(layoutObject, e);

    if ((layoutObject == null) && (this.event_isinside == null)) {
        // TODO: should we emit an event anyway?
        return;
    }

    if ((layoutObject != null) && (this.event_isinside == null))
        signal(this, "onmouseover", eventObject);

    if ((layoutObject == null) && (this.event_isinside != null))
        signal(this, "onmouseout", eventObject);

    if ((layoutObject != null) && (this.event_isinside != null))
        signal(this, "onmousemove", eventObject);

    this.event_isinside = layoutObject;
    //log("move", x, y);    
};

PlotKit.CanvasRenderer.isSupported = function(canvasName) {
    var canvas = null;
    try {
        if (MochiKit.Base.isUndefinedOrNull(canvasName)) 
            canvas = MochiKit.DOM.CANVAS({});
        else
            canvas = MochiKit.DOM.getElement(canvasName);
        var context = canvas.getContext("2d");
    }
    catch (e) {
        var ie = navigator.appVersion.match(/MSIE (\d\.\d)/);
        var opera = (navigator.userAgent.toLowerCase().indexOf("opera") != -1);
        if ((!ie) || (ie[1] < 6) || (opera))
            return false;
        return true;
    }
    return true;
};
