define(function(require) {

var kpoint = require("./kpoint.js");
var kvector = require("./kvector.js");
require("./tex.js");  // for graphie.label()
require("./tmpl.js");

var Graphie = KhanUtil.Graphie = function() {
};

/* Convert cartesian coordinates [x, y] to polar coordinates [r,
 * theta], with theta in degrees, or in radians if angleInRadians is
 * specified.
 */
function cartToPolar(coord, angleInRadians) {
    var r = Math.sqrt(Math.pow(coord[0], 2) + Math.pow(coord[1], 2));
    var theta = Math.atan2(coord[1], coord[0]);
    // convert angle range from [-pi, pi] to [0, 2pi]
    if (theta < 0) {
        theta += 2 * Math.PI;
    }
    if (!angleInRadians) {
        theta = theta * 180 / Math.PI;
    }
    return [r, theta];
}

function polar(r, th) {
    if (typeof r === "number") {
        r = [r, r];
    }
    th = th * Math.PI / 180;
    return [r[0] * Math.cos(th), r[1] * Math.sin(th)];
}

// Keep track of all the intervalIDs created by setInterval.
// This lets us cancel all the intervals when cleaning up.
var intervalIDs = [];
function cleanupIntervals() {
    _.each(intervalIDs, function(intervalID) {
        window.clearInterval(intervalID);
    });
    intervalIDs.length = 0;
}

$.extend(KhanUtil, {
    unscaledSvgPath: function(points) {
        // If this is an empty closed path, return "" instead of "z", which
        // would give an error
        if (points[0] === true) {
            return "";
        }
        return $.map(points, function(point, i) {
            if (point === true) {
                return "z";
            }
            return (i === 0 ? "M" : "L") + point[0] + " " + point[1];
        }).join("");
    },

    getDistance: function(point1, point2) {
        return kpoint.distanceToPoint(point1, point2);
    },

    /**
    * Return the difference between two sets of coordinates
    */
    coordDiff: function(startCoord, endCoord) {
        return _.map(endCoord, function(val, i) {
            return endCoord[i] - startCoord[i];
        });
    },

    /**
    * Round the given coordinates to a given snap value
    * (e.g., nearest 0.2 increment)
    */
    snapCoord: function(coord, snap) {
        return _.map(coord, function(val, i) {
            return KhanUtil.roundToNearest(snap[i], val);
        });
    },

    // Find the angle in degrees between two or three points
    findAngle: function(point1, point2, vertex) {
        if (vertex === undefined) {
            var x = point1[0] - point2[0];
            var y = point1[1] - point2[1];
            if (!x && !y) {
                return 0;
            }
            return (180 + Math.atan2(-y, -x) * 180 / Math.PI + 360) % 360;
        } else {
            return KhanUtil.findAngle(point1, vertex) - KhanUtil.findAngle(point2, vertex);
        }
    },

    graphs: {}
});


_.extend(Graphie.prototype, {
    cartToPolar: cartToPolar,
    polar: polar
});

var labelDirections = {
    "center": [-0.5, -0.5],
    "above": [-0.5, -1.0],
    "above right": [0.0, -1.0],
    "right": [0.0, -0.5],
    "below right": [0.0, 0.0],
    "below": [-0.5, 0.0],
    "below left": [-1.0, 0.0],
    "left": [-1.0, -0.5],
    "above left": [-1.0, -1.0]
};

KhanUtil.createGraphie = function(el) {
    var xScale = 40, yScale = 40, xRange, yRange;

    $(el).css("position", "relative");
    var raphael = Raphael(el);

    // For a sometimes-reproducible IE8 bug; doesn't affect SVG browsers at all
    $(el).children("div").css("position", "absolute");

    // Set up some reasonable defaults
    var currentStyle = {
        "stroke-width": 2,
        "fill": "none"
    };

    var scaleVector = function(point) {
        if (typeof point === "number") {
            return scaleVector([point, point]);
        }

        var x = point[0], y = point[1];
        return [x * xScale, y * yScale];
    };

    var scalePoint = function scalePoint(point) {
        if (typeof point === "number") {
            return scalePoint([point, point]);
        }

        var x = point[0], y = point[1];
        return [(x - xRange[0]) * xScale, (yRange[1] - y) * yScale];
    };

    var unscalePoint = function(point) {
        if (typeof point === "number") {
            return unscalePoint([point, point]);
        }

        var x = point[0], y = point[1];
        return [x / xScale + xRange[0], yRange[1] - y / yScale];
    };

    var unscaleVector = function(point) {
        if (typeof point === "number") {
            return unscaleVector([point, point]);
        }

        return [point[0] / xScale, point[1] / yScale];
    };

    var setLabelMargins = function(span, size) {
        var $span = $(span);
        var direction = $span.data("labelDirection");
        $span.css("visibility", "");

        if (typeof direction === "number") {
            var x = Math.cos(direction);
            var y = Math.sin(direction);

            var scale = Math.min(
                size[0] / 2 / Math.abs(x),
                size[1] / 2 / Math.abs(y));

            $span.css({
                marginLeft: (-size[0] / 2) + x * scale,
                marginTop: (-size[1] / 2) - y * scale
            });
        } else {
            var multipliers = labelDirections[direction || "center"];
            $span.css({
                marginLeft: Math.round(size[0] * multipliers[0]),
                marginTop: Math.round(size[1] * multipliers[1])
            });
        }
    };

    var svgPath = function(points, alreadyScaled) {
        return $.map(points, function(point, i) {
            if (point === true) {
                return "z";
            } else {
                var scaled = alreadyScaled ? point : scalePoint(point);
                return (i === 0 ? "M" : "L") + KhanUtil.bound(scaled[0]) + " " + KhanUtil.bound(scaled[1]);
            }
        }).join("");
    };

    var svgParabolaPath = function(a, b, c) {
        var computeParabola = function(x) {
            return (a * x + b) * x + c;
        };

        // If points are collinear, plot a line instead
        if (a === 0) {
            var points = _.map(xRange, function(x) {
                return [x, computeParabola(x)];
            });
            return svgPath(points);
        }

        // Calculate x coordinates of points on parabola
        var xVertex = -b / (2 * a);
        var distToEdge = Math.max(
            Math.abs(xVertex - xRange[0]),
            Math.abs(xVertex - xRange[1])
        );

        // To guarantee that drawn parabola to spans the viewport, use a point
        // on the edge of the graph furtherest from the vertex
        var xPoint = xVertex + distToEdge;

        // Compute parabola and other point on the curve
        var vertex = [xVertex, computeParabola(xVertex)];
        var point = [xPoint, computeParabola(xPoint)];

        // Calculate SVG 'control' point, defined by spec
        var control = [vertex[0], vertex[1] - (point[1] - vertex[1])];

        // Calculate mirror points across parabola's axis of symmetry
        var dx = Math.abs(vertex[0] - point[0]);
        var left = [vertex[0] - dx, point[1]];
        var right = [vertex[0] + dx, point[1]];

        // Scale and bound
        var points = _.map([left, control, right], scalePoint);
        var values = _.map(_.flatten(points), KhanUtil.bound);
        return "M" + values[0] + "," + values[1] + " Q" + values[2] + "," +
            values[3] + " " + values[4] + "," + values[5];
    };

    var svgSinusoidPath = function(a, b, c, d) {
        // Plot a sinusoid of the form: f(x) = a * sin(b * x - c) + d
        var quarterPeriod = Math.abs(Math.PI / (2 * b));

        var computeSine = function(x) {
            return a * Math.sin(b * x - c) + d;
        };

        var computeDerivative = function(x) {
            return a * b * Math.cos(c - b * x);
        };

        var coordsForOffset = function(initial, i) {
            // Return the cubic coordinates (including the two anchor and two
            // control points) for the ith portion of the sinusoid.
            var x0 = initial + quarterPeriod * i;
            var x1 = x0 + quarterPeriod;

            // Interpolate using derivative technique
            // See: http://stackoverflow.com/questions/13932704/how-to-draw-sine-waves-with-svg-js
            var xCoords = [
                x0,
                x0 * 2/3 + x1 * 1/3,
                x0 * 1/3 + x1 * 2/3,
                x1
            ];
            var yCoords = [
                computeSine(x0),
                computeSine(x0) + computeDerivative(x0) * (x1 - x0)/3,
                computeSine(x1) - computeDerivative(x1) * (x1 - x0)/3,
                computeSine(x1)
            ];

            // Zip and scale
            return _.map(_.zip(xCoords, yCoords), scalePoint);
        };

        // How many quarter-periods do we need to span the graph?
        var extent = xRange[1] - xRange[0];
        var numQuarterPeriods = Math.ceil(extent / quarterPeriod) + 1;

        // Find starting coordinate: first anchor point curve left of xRange[0]
        var initial = c / b;
        var distToEdge = initial - xRange[0];
        initial -= quarterPeriod * Math.ceil(distToEdge / quarterPeriod);

        // First portion of path is special-case, requiring move-to ('M')
        var coords = coordsForOffset(initial, 0);
        var path = "M" + coords[0][0] + "," + coords[0][1] + " C" +
            coords[1][0] + "," + coords[1][1] + " " + coords[2][0] + "," +
            coords[2][1] + " " + coords[3][0] + "," + coords[3][1];
        for (var i = 1; i < numQuarterPeriods; i++) {
            coords = coordsForOffset(initial, i);
            path += " C" + coords[1][0] + "," + coords[1][1] + " " +
                coords[2][0] + "," + coords[2][1] + " " + coords[3][0] + "," +
                coords[3][1];
        }

        return path;
    };

    // `svgPath` is independent of graphie range, so we export on KhanUtil
    $.extend(KhanUtil, { svgPath: svgPath });

    var processAttributes = function(attrs) {
        var transformers = {
            scale: function(scale) {
                if (typeof scale === "number") {
                    scale = [scale, scale];
                }

                xScale = scale[0];
                yScale = scale[1];

                // Update the canvas size
                raphael.setSize((xRange[1] - xRange[0]) * xScale, (yRange[1] - yRange[0]) * yScale);
            },

            clipRect: function(pair) {
                var point = pair[0], size = pair[1];
                point[1] += size[1]; // because our coordinates are flipped

                return { "clip-rect": scalePoint(point).concat(scaleVector(size)).join(" ") };
            },

            strokeWidth: function(val) {
                return { "stroke-width": parseFloat(val) };
            },

            rx: function(val) {
                return { rx: scaleVector([val, 0])[0] };
            },

            ry: function(val) {
                return { ry: scaleVector([0, val])[1] };
            },

            r: function(val) {
                var scaled = scaleVector([val, val]);
                return { rx: scaled[0], ry: scaled[1] };
            }
        };

        var processed = {};
        $.each(attrs || {}, function(key, value) {
            var transformer = transformers[key];

            if (typeof transformer === "function") {
                $.extend(processed, transformer(value));
            } else {
                var dasherized = key.replace(/([A-Z]+)([A-Z][a-z])/g, "$1-$2")
                    .replace(/([a-z\d])([A-Z])/g, "$1-$2")
                    .toLowerCase();
                processed[dasherized] = value;
            }
        });

        return processed;
    };

    var addArrowheads = function arrows(path) {
        var type = path.constructor.prototype;

        if (type === Raphael.el) {
            if (path.type === "path" && typeof path.arrowheadsDrawn === "undefined") {
                var w = path.attr("stroke-width"), s = 0.6 + 0.4 * w;
                var l = path.getTotalLength();

                if (l < 0.75 * s) {
                    // You're weird because that's a *really* short path
                    // Giving up now before I get more confused

                } else {
                    // This makes a lot more sense
                    var set = raphael.set();
                    var head = raphael.path("M-3 4 C-2.75 2.5 0 0.25 0.75 0C0 -0.25 -2.75 -2.5 -3 -4");
                    var end = path.getPointAtLength(l - 0.4);
                    var almostTheEnd = path.getPointAtLength(l - 0.75 * s);
                    var angle = Math.atan2(end.y - almostTheEnd.y, end.x - almostTheEnd.x) * 180 / Math.PI;
                    var attrs = path.attr();
                    delete attrs.path;

                    var subpath = path.getSubpath(0, l - 0.75 * s);
                    subpath = raphael.path(subpath).attr(attrs);
                    subpath.arrowheadsDrawn = true;
                    path.remove();

                    head.rotate(angle, 0.75, 0).scale(s, s, 0.75, 0)
                        .translate(almostTheEnd.x, almostTheEnd.y).attr(attrs)
                        .attr({ "stroke-linejoin": "round", "stroke-linecap": "round" });
                    head.arrowheadsDrawn = true;
                    set.push(subpath);
                    set.push(head);
                    return set;
                }
            }
        } else if (type === Raphael.st) {
            for (var i = 0, l = path.items.length; i < l; i++) {
                arrows(path.items[i]);
            }
        }
        return path;
    };

    var drawingTools = {
        circle: function(center, radius) {
            return raphael.ellipse.apply(raphael, scalePoint(center).concat(scaleVector([radius, radius])));
        },

        // (x, y) is coordinate of bottom left corner
        rect: function(x, y, width, height) {
            // Raphael needs (x, y) to be coordinate of upper left corner
            var corner = scalePoint([x, y + height]);
            var dims = scaleVector([width, height]);
            return raphael.rect.apply(raphael, corner.concat(dims));
        },

        ellipse: function(center, radii) {
            return raphael.ellipse.apply(raphael, scalePoint(center).concat(scaleVector(radii)));
        },

        fixedEllipse: function(center, radii, maxScale) {
            // Scale point and radius
            var scaledPoint = scalePoint(center);
            var scaledRadii = scaleVector(radii);

            // Padding protects against clipping at the edges
            var padding = 2;
            var width = 2 * scaledRadii[0] * maxScale + padding;
            var height = 2 * scaledRadii[1] * maxScale + padding;

            // Calculate absolute left, top
            var left = scaledPoint[0] - width / 2;
            var top = scaledPoint[1] - height / 2;

            // Wrap in <div>
            var wrapper = document.createElement("div");
            $(wrapper).css({
                position: "absolute",
                width: width + "px",
                height: height + "px",
                left: left + "px",
                top: top + "px"
            });

            // Create Raphael canvas
            var localRaphael = Raphael(wrapper, width, height);
            var visibleShape = localRaphael.ellipse(
                width / 2,
                height / 2,
                scaledRadii[0],
                scaledRadii[1]
            );

            return {
                wrapper: wrapper,
                visibleShape: visibleShape
            };
        },

        arc: function(center, radius, startAngle, endAngle, sector) {
            startAngle = (startAngle % 360 + 360) % 360;
            endAngle = (endAngle % 360 + 360) % 360;

            var cent = scalePoint(center);
            var radii = scaleVector(radius);
            var startVector = polar(radius, startAngle);
            var endVector = polar(radius, endAngle);

            var startPoint = scalePoint([center[0] + startVector[0], center[1] + startVector[1]]);
            var endPoint = scalePoint([center[0] + endVector[0], center[1] + endVector[1]]);

            var largeAngle = ((endAngle - startAngle) % 360 + 360) % 360 > 180;

            return raphael.path(
                "M" + startPoint.join(" ") +
                "A" + radii.join(" ") +
                " 0 " + // ellipse rotation
                (largeAngle ? 1 : 0) +
                " 0 " + // sweep flag
                endPoint.join(" ") +
                (sector ? "L" + cent.join(" ") + "z" : ""));
        },

        path: function(points) {
            var p = raphael.path(svgPath(points));
            p.graphiePath = points;
            return p;
        },

        fixedPath: function(points, center, createPath) {
            points = _.map(points, scalePoint);
            center = center ? scalePoint(center) : null;
            createPath = createPath || svgPath;

            var pathLeft = _.min(_.pluck(points, 0));
            var pathRight = _.max(_.pluck(points, 0));
            var pathTop = _.min(_.pluck(points, 1));
            var pathBottom = _.max(_.pluck(points, 1));

            // Apply padding to line
            var padding = [4, 4];

            // Calculate and apply additional offset
            var extraOffset = [pathLeft, pathTop];

            // Apply padding and offset to points
            points = _.map(points, function(point) {
                return kvector.add(
                    kvector.subtract(
                        point,
                        extraOffset
                    ),
                    kvector.scale(padding, 0.5)
                );
            });

            // Calculate <div> dimensions
            var width = (pathRight - pathLeft) + padding[0];
            var height = (pathBottom - pathTop) + padding[1];
            var left = extraOffset[0] - padding[0]/2;
            var top = extraOffset[1] - padding[1]/2;

            // Create <div>
            var wrapper = document.createElement("div");
            $(wrapper).css({
                position: "absolute",
                width: width + "px",
                height: height + "px",
                left: left + "px",
                top: top + "px",
                // If user specified a center, set it
                transformOrigin: center ? (width/2 + center[0]) + "px " +
                                          (height/2 + center[1]) + "px"
                                        : null
            });

            // Create Raphael canvas
            var localRaphael = Raphael(wrapper, width, height);

            // Calculate path
            var visibleShape = localRaphael.path(createPath(points));

            return {
                wrapper: wrapper,
                visibleShape: visibleShape
            };
        },

        scaledPath: function(points) {
            var p = raphael.path(svgPath(points, /* alreadyScaled */ true));
            p.graphiePath = points;
            return p;
        },

        line: function(start, end) {
            return this.path([start, end]);
        },

        parabola: function(a, b, c) {
            // Plot a parabola of the form: f(x) = (a * x + b) * x + c
            return raphael.path(svgParabolaPath(a, b, c));
        },

        fixedLine: function(start, end, thickness) {
            // Apply padding to line
            var padding = [thickness, thickness];

            // Scale points to get values in pixels
            start = scalePoint(start);
            end = scalePoint(end);

            // Calculate and apply additional offset
            var extraOffset = [
                Math.min(start[0], end[0]),
                Math.min(start[1], end[1])
            ];

            // Apply padding and offset to start, end points
            start = kvector.add(
                kvector.subtract(
                    start,
                    extraOffset
                ),
                kvector.scale(padding, 0.5)
            );
            end = kvector.add(
                kvector.subtract(
                    end,
                    extraOffset
                ),
                kvector.scale(padding, 0.5)
            );

            // Calculate <div> dimensions
            var left = extraOffset[0] - padding[0]/2;
            var top = extraOffset[1] - padding[1]/2;
            var width = Math.abs(start[0] - end[0]) + padding[0];
            var height = Math.abs(start[1] - end[1]) + padding[1];

            // Create <div>
            var wrapper = document.createElement("div");
            $(wrapper).css({
                position: "absolute",
                width: width + "px",
                height: height + "px",
                left: left + "px",
                top: top + "px",
                // Outsiders should feel like the line's 'origin' (i.e., for
                // rotation) is the starting point
                transformOrigin: start[0] + "px " + start[1] + "px"
            });

            // Create Raphael canvas
            var localRaphael = Raphael(wrapper, width, height);

            // Calculate path
            var path = "M" + start[0] + " " + start[1] + " " +
                       "L" + end[0] + " " + end[1];
            var visibleShape = localRaphael.path(path);
            visibleShape.graphiePath = [start, end];

            return {
                wrapper: wrapper,
                visibleShape: visibleShape
            };
        },

        sinusoid: function(a, b, c, d) {
            // Plot a sinusoid of the form: f(x) = a * sin(b * x - c) + d
            return raphael.path(svgSinusoidPath(a, b, c, d));
        },

        grid: function(xr, yr) {
            var step = currentStyle.step || [1, 1];
            var set = raphael.set();

            var x = step[0] * Math.ceil(xr[0] / step[0]);
            for (; x <= xr[1]; x += step[0]) {
                set.push(this.line([x, yr[0]], [x, yr[1]]));
            }

            var y = step[1] * Math.ceil(yr[0] / step[1]);
            for (; y <= yr[1]; y += step[1]) {
                set.push(this.line([xr[0], y], [xr[1], y]));
            }

            return set;
        },

        label: function(point, text, direction, latex) {
            latex = (typeof latex === "undefined") || latex;

            var $span = $("<span>").addClass("graphie-label");

            if (!latex) {
                $span.html(text);
            }

            var pad = currentStyle["label-distance"];

            // TODO(alpert): Isn't currentStyle applied afterwards
            // automatically since this is a 'drawing tool'?
            $span
                .css($.extend({}, currentStyle, {
                        position: "absolute",
                        padding: (pad != null ? pad : 7) + "px"
                    }))
                .data("labelDirection", direction)
                .appendTo(el);

            $span.setPosition = function(point) {
                var scaledPoint = scalePoint(point);
                $span.css({
                    left: scaledPoint[0],
                    top: scaledPoint[1]
                });
            };

            $span.setPosition(point);

            var span = $span[0];

            $span.processMath = function(math, force) {
                KhanUtil.processMath(span, math, force, function() {
                    var width = span.scrollWidth;
                    var height = span.scrollHeight;
                    setLabelMargins(span, [width, height]);
                });
            };

            if (latex) {
                $span.processMath(text, /* force */ false);
            } else {
                var width = span.scrollWidth;
                var height = span.scrollHeight;
                setLabelMargins(span, [width, height]);
            }

            return $span;
        },

        plotParametric: function(fn, range, shade, fn2) {
            // Note: fn2 should only be set if 'shade' is true, as it denotes
            // the function between which fn should have its area shaded.
            // In general, plotParametric shouldn't be used to shade the area
            // between two arbitrary parametrics functions over an interval,
            // as the method assumes that fn and fn2 are both of the form
            // fn(t) = (t, fn'(t)) for some initial fn'.
            fn2 = fn2 || function(t) { return [t, 0]; };

            currentStyle.strokeLinejoin || (currentStyle.strokeLinejoin = "round");
            currentStyle.strokeLinecap || (currentStyle.strokeLinecap = "round");

            var min = range[0], max = range[1];
            var step = (max - min) / (currentStyle["plot-points"] || 800);
            if (step === 0) {
                step = 1;
            }

            var paths = raphael.set();
            var points = [];
            var lastDiff = KhanUtil.coordDiff(fn(min), fn2(min));

            var lastFlip = min;
            for (var t = min; t <= max; t += step) {
                var top = fn(t);
                var bottom = fn2(t);
                var diff = KhanUtil.coordDiff(top, bottom);

                // Find points where it flips
                // Create path that sketches area between the two functions
                if (
                    // if there is an asymptote here, meaning that the graph switches signs and has a large difference
                    ((diff[1] < 0) !== (lastDiff[1] < 0)) && Math.abs(diff[1] - lastDiff[1]) > 2 * yScale ||
                    // or the function value gets really high (which breaks raphael)
                    Math.abs(diff[1]) > 1e7 ||
                    // or the function is undefined
                    isNaN(diff[1])
                   ) {
                    // split the path at this point, and draw it
                    if (shade) {
                        points.push(top);

                        // backtrack to draw paired function
                        for (var u = t - step; u >= lastFlip; u -= step) {
                            points.push(fn2(u));
                        }
                        lastFlip = t;
                    }
                    paths.push(this.path(points));
                    // restart the path, excluding this point
                    points = [];
                    if (shade) {
                        points.push(top);
                    }
                } else {
                    // otherwise, just add the point to the path
                    points.push(top);
                }

                lastDiff = diff;
            }

            if (shade) {
                // backtrack to draw paired function
                for (var u = max - step; u >= lastFlip; u -= step) {
                    points.push(fn2(u));
                }
            }
            paths.push(this.path(points));

            return paths;
        },

        plotPolar: function(fn, range) {
            var min = range[0], max = range[1];

            // There is probably a better heuristic for this
            currentStyle["plot-points"] || (currentStyle["plot-points"] = 2 * (max - min) * xScale);

            return this.plotParametric(function(th) {
                return polar(fn(th), th * 180 / Math.PI);
            }, range);
        },

        plot: function(fn, range, swapAxes, shade, fn2) {
            var min = range[0], max = range[1];
            currentStyle["plot-points"] || (currentStyle["plot-points"] = 2 * (max - min) * xScale);

            if (swapAxes) {
                if (fn2) {
                    // TODO(charlie): support swapped axis area shading
                    throw new Error(
                        "Can't shade area between functions with swapped axes."
                    );
                }
                return this.plotParametric(function(y) {
                    return [fn(y), y];
                }, range, shade);
            } else {
                if (fn2) {
                    if (shade) {
                        return this.plotParametric(function(x) {
                            return [x, fn(x)];
                        }, range, shade, function(x) {
                            return [x, fn2(x)];
                        });
                    } else {
                        throw new Error(
                            "fn2 should only be set when 'shade' is True."
                        );
                    }
                }
                return this.plotParametric(function(x) {
                    return [x, fn(x)];
                }, range, shade);
            }
        },

        /**
         * Given a piecewise function, return a Raphael set of paths that
         * can be used to draw the function, e.g. using style().
         * Calls plotParametric.
         *
         * @param  {[]} fnArray    array of functions which when called
         *                         with a parameter i return the value of
         *                         the function at i
         * @param  {[]} rangeArray array of ranges over which the
         *                         corresponding functions are defined
         * @return {Raphael set}
         */
        plotPiecewise: function(fnArray, rangeArray) {
            var paths = raphael.set();
            var self = this;
            _.times(fnArray.length, function(i) {
                var fn = fnArray[i];
                var range = rangeArray[i];
                var fnPaths = self.plotParametric(function(x) {
                    return [x, fn(x)];
                }, range);
                _.each(fnPaths, function(fnPath) {
                    paths.push(fnPath);
                });
            });

            return paths;
        },

        /**
         * Given an array of coordinates of the form [x, y], create and
         * return a Raphael set of Raphael circle objects at those
         * coordinates
         *
         * @param  {Array of arrays} endpointArray
         * @return {Raphael set}
         */
        plotEndpointCircles: function(endpointArray) {
            var circles = raphael.set();
            var self = this;

            _.each(endpointArray, function(coord, i) {
                circles.push(self.circle(coord, 0.15));
            });

            return circles;
        },

        plotAsymptotes: function(fn, range) {
            var min = range[0], max = range[1];
            var step = (max - min) / (currentStyle["plot-points"] || 800);

            var asymptotes = raphael.set(), lastVal = fn(min);

            for (var t = min; t <= max; t += step) {
                var funcVal = fn(t);

                if (((funcVal < 0) !== (lastVal < 0)) && Math.abs(funcVal - lastVal) > 2 * yScale) {
                    asymptotes.push(
                        this.line([t, yScale], [t, -yScale])
                    );
                }

                lastVal = funcVal;
            }

            return asymptotes;
        }
    };

    var graphie = new Graphie();
    _.extend(graphie, {
        raphael: raphael,

        init: function(options) {
            var scale = options.scale || [40, 40];
            scale = (typeof scale === "number" ? [scale, scale] : scale);

            xScale = scale[0];
            yScale = scale[1];

            if (options.range == null) {
                return Khan.error("range should be specified in graph init");
            }

            xRange = options.range[0];
            yRange = options.range[1];

            var w = (xRange[1] - xRange[0]) * xScale, h = (yRange[1] - yRange[0]) * yScale;
            raphael.setSize(w, h);

            $(el).css({
                "width": w,
                "height": h
            });

            this.range = options.range;
            this.scale = scale;
            this.dimensions = [w, h];
            this.xpixels = w;
            this.ypixels = h;

            return this;
        },

        // Wrap window.setInterval to keep track of all the intervalIDs.
        setInterval: function() {
            var intervalID = Function.prototype.apply.call(window.setInterval,
                                                           window,
                                                           arguments);
            intervalIDs.push(intervalID);
            return intervalID;
        },

        style: function(attrs, fn) {
            var processed = processAttributes(attrs);

            if (typeof fn === "function") {
                var oldStyle = currentStyle;
                currentStyle = $.extend({}, currentStyle, processed);
                var result = fn.call(graphie);
                currentStyle = oldStyle;
                return result;
            } else {
                $.extend(currentStyle, processed);
            }
        },

        scalePoint: scalePoint,
        scaleVector: scaleVector,

        unscalePoint: unscalePoint,
        unscaleVector: unscaleVector,

        // Custom SVG path functions that are dependent on graphie range
        // `svgPath`, while independent of range, is exported for consistency
        svgPath: svgPath,
        svgParabolaPath: svgParabolaPath,
        svgSinusoidPath: svgSinusoidPath
    });

    $.each(drawingTools, function(name) {
        graphie[name] = function() {
            var last = arguments[arguments.length - 1];
            var oldStyle = currentStyle;
            var result;

            // The last argument is probably trying to change the style
            if (typeof last === "object" && !_.isArray(last)) {
                currentStyle = $.extend({}, currentStyle, processAttributes(last));

                var rest = [].slice.call(arguments, 0, arguments.length - 1);
                result = drawingTools[name].apply(drawingTools, rest);
            } else {
                currentStyle = $.extend({}, currentStyle);

                result = drawingTools[name].apply(drawingTools, arguments);
            }

            // Bad heuristic for recognizing Raphael elements and sets
            var type = result.constructor.prototype;
            if (type === Raphael.el || type === Raphael.st) {
                result.attr(currentStyle);

                if (currentStyle.arrows) {
                    result = addArrowheads(result);
                }
            } else if (result instanceof $) {
                result.css(currentStyle);
            }

            currentStyle = oldStyle;
            return result;
        };
    });


    // Initializes graphie settings for a graph and draws the basic graph
    // features (axes, grid, tick marks, and axis labels)
    // Options expected are:
    // - range: [[a, b], [c, d]] or [a, b]
    // - scale: [a, b] or number
    // - gridOpacity: number (0 - 1)
    // - gridStep: [a, b] or number (relative to units)
    // - tickStep: [a, b] or number (relative to grid steps)
    // - tickLen: [a, b] or number (in pixels)
    // - labelStep: [a, b] or number (relative to tick steps)
    // - yLabelFormat: fn to format label string for y-axis
    // - xLabelFormat: fn to format label string for x-axis
    // - smartLabelPositioning: true or false to ignore minus sign
    graphie.graphInit = function(options) {

        options = options || {};

        $.each(options, function(prop, val) {

            // allow options to be specified by a single number for shorthand if
            // the horizontal and vertical components are the same
            if (!prop.match(/.*Opacity$/) && prop !== "range" &&
                typeof val === "number") {
                options[prop] = [val, val];
            }

            // allow symmetric ranges to be specified by the absolute values
            if (prop === "range" || prop === "gridRange") {
                if (val.constructor === Array) {
                    if (val[0].constructor !== Array) {  // but don't mandate symmetric ranges
                        options[prop] = [[-val[0], val[0]], [-val[1], val[1]]];
                    }
                } else if (typeof val === "number") {
                    options[prop] = [[-val, val], [-val, val]];
                }
            }

        });

        var range = options.range || [[-10, 10], [-10, 10]],
            gridRange = options.gridRange || options.range,
            scale = options.scale || [20, 20],
            grid = options.grid != null ? options.grid : true,
            gridOpacity = options.gridOpacity || 0.1,
            gridStep = options.gridStep || [1, 1],
            axes = options.axes != null ? options.axes : true,
            axisArrows = options.axisArrows || "",
            axisOpacity = options.axisOpacity || 1.0,
            axisCenter = options.axisCenter || [
                Math.min(Math.max(range[0][0], 0), range[0][1]),
                Math.min(Math.max(range[1][0], 0), range[1][1])
            ],
            axisLabels = options.axisLabels != null ? options.axisLabels : false,
            ticks = options.ticks != null ? options.ticks : true,
            tickStep = options.tickStep || [2, 2],
            tickLen = options.tickLen || [5, 5],
            tickOpacity = options.tickOpacity || 1.0,
            labels = options.labels || options.labelStep || false,
            labelStep = options.labelStep || [1, 1],
            labelOpacity = options.labelOpacity || 1.0,
            unityLabels = options.unityLabels || false,
            labelFormat = options.labelFormat || function(a) { return a; },
            xLabelFormat = options.xLabelFormat || labelFormat,
            yLabelFormat = options.yLabelFormat || labelFormat,
            smartLabelPositioning = options.smartLabelPositioning != null ?
                options.smartLabelPositioning : true,
            realRange = [
                [range[0][0] - (range[0][0] > 0 ? 1 : 0),
                 range[0][1] + (range[0][1] < 0 ? 1 : 0)],
                [range[1][0] - (range[1][0] > 0 ? 1 : 0),
                 range[1][1] + (range[1][1] < 0 ? 1 : 0)]
            ];

        if (!_.isArray(unityLabels)) {
            unityLabels = [unityLabels, unityLabels];
        }

        if (smartLabelPositioning) {
            var minusIgnorer = function(lf) { return function(a) {
                return (lf(a) + "").replace(/-(\d)/g, "\\llap{-}$1");
            }; };

            xLabelFormat = minusIgnorer(xLabelFormat);
            yLabelFormat = minusIgnorer(yLabelFormat);
        }

        this.init({
            range: realRange,
            scale: scale
        });

        // draw grid
        if (grid) {
            this.grid(gridRange[0], gridRange[1], {
                stroke: "#000000",
                opacity: gridOpacity,
                step: gridStep
            });
        }

        // draw axes
        if (axes) {

            // this is a slight hack until <-> arrowheads work
            if (axisArrows === "<->" || true) {
                this.style({
                    stroke: "#000000",
                    opacity: axisOpacity,
                    strokeWidth: 2,
                    arrows: "->"
                }, function() {
                    if (range[1][0] < 0 && range[1][1] > 0) {
                        this.path([axisCenter, [gridRange[0][0], axisCenter[1]]]);
                        this.path([axisCenter, [gridRange[0][1], axisCenter[1]]]);
                    }
                    if (range[0][0] < 0 && range[0][1] > 0) {
                        this.path([axisCenter, [axisCenter[0], gridRange[1][0]]]);
                        this.path([axisCenter, [axisCenter[0], gridRange[1][1]]]);
                    }
                });

            // also, we don't support "<-" arrows yet, but why you
            // would want that on your graph is beyond me.
            } else if (axisArrows === "->" || axisArrows === "") {
                this.style({
                    stroke: "#000000",
                    opacity: axisOpacity,
                    strokeWidth: 2,
                    arrows: axisArrows
                }, function() {
                    this.path([[gridRange[0][0], axisCenter[1]], [gridRange[0][1], axisCenter[1]]]);
                    this.path([[axisCenter[0], gridRange[1][0]], [axisCenter[0], gridRange[1][1]]]);
                });

            }

            if (axisLabels && axisLabels.length === 2) {
                this.label([gridRange[0][1], axisCenter[1]], axisLabels[0], "right");
                this.label([axisCenter[0], gridRange[1][1]], axisLabels[1], "above");
            }

        }

        // draw tick marks
        if (ticks) {
            this.style({
                stroke: "#000000",
                opacity: tickOpacity,
                strokeWidth: 1
            }, function() {

                // horizontal axis
                var step = gridStep[0] * tickStep[0],
                    len = tickLen[0] / scale[1],
                    start = gridRange[0][0],
                    stop = gridRange[0][1];

                if (range[1][0] < 0 && range[1][1] > 0) {
                    for (var x = step + axisCenter[0]; x <= stop; x += step) {
                        if (x < stop || !axisArrows) {
                            this.line([x, -len + axisCenter[1]], [x, len + axisCenter[1]]);
                        }
                    }

                    for (var x = -step + axisCenter[0]; x >= start; x -= step) {
                        if (x > start || !axisArrows) {
                            this.line([x, -len + axisCenter[1]], [x, len + axisCenter[1]]);
                        }
                    }
                }

                // vertical axis
                step = gridStep[1] * tickStep[1];
                len = tickLen[1] / scale[0];
                start = gridRange[1][0];
                stop = gridRange[1][1];

                if (range[0][0] < 0 && range[0][1] > 0) {
                    for (var y = step + axisCenter[1]; y <= stop; y += step) {
                        if (y < stop || !axisArrows) {
                            this.line([-len + axisCenter[0], y], [len + axisCenter[0], y]);
                        }
                    }

                    for (var y = -step + axisCenter[1]; y >= start; y -= step) {
                        if (y > start || !axisArrows) {
                            this.line([-len + axisCenter[0], y], [len + axisCenter[0], y]);
                        }
                    }
                }

            });
        }

        // draw axis labels
        if (labels) {
            this.style({
                stroke: "#000000",
                opacity: labelOpacity
            }, function() {

                // horizontal axis
                var step = gridStep[0] * tickStep[0] * labelStep[0],
                    start = gridRange[0][0],
                    stop = gridRange[0][1],
                    xAxisPosition = (axisCenter[0] < 0) ? "above" : "below",
                    yAxisPosition = (axisCenter[0] < 0) ? "right" : "left",
                    xShowZero = axisCenter[0] === 0 && axisCenter[1] !== 0,
                    yShowZero = axisCenter[0] !== 0 && axisCenter[1] === 0,
                    axisOffCenter = axisCenter[0] !== 0 || axisCenter[1] !== 0,
                    showUnityX = unityLabels[0] || axisOffCenter,
                    showUnityY = unityLabels[1] || axisOffCenter;

                // positive x-axis
                for (var x = (xShowZero ? 0 : step) + axisCenter[0]; x <= stop; x += step) {
                    if (x < stop || !axisArrows) {
                        this.label([x, axisCenter[1]], xLabelFormat(x), xAxisPosition);
                    }
                }

                // negative x-axis
                for (var x = -step * (showUnityX ? 1 : 2) + axisCenter[0]; x >= start; x -= step) {
                    if (x > start || !axisArrows) {
                        this.label([x, axisCenter[1]], xLabelFormat(x), xAxisPosition);
                    }
                }

                step = gridStep[1] * tickStep[1] * labelStep[1];
                start = gridRange[1][0];
                stop = gridRange[1][1];

                // positive y-axis
                for (var y = (yShowZero ? 0 : step) + axisCenter[1]; y <= stop; y += step) {
                    if (y < stop || !axisArrows) {
                        this.label([axisCenter[0], y], yLabelFormat(y), yAxisPosition);
                    }
                }

                // negative y-axis
                for (var y = -step * (showUnityY ? 1 : 2) + axisCenter[1]; y >= start; y -= step) {
                    if (y > start || !axisArrows) {
                        this.label([axisCenter[0], y], yLabelFormat(y), yAxisPosition);
                    }
                }
            });
        }
    };

    return graphie;
};

$.fn.graphie = function(problem) {
    if (Khan.query.nographie != null) {
        return;
    }
    return this.find(".graphie, script[type='text/graphie']").addBack().filter(".graphie, script[type='text/graphie']").each(function() {
        // Grab code for later execution
        var code = $(this).text(), graphie;

        // Ignore graphie elements that have already been processed
        if ($(this).data("graphie") != null) {
            return;
        }

        // Remove any of the code that's in there
        $(this).empty();

        // Initialize the graph
        if ($(this).data("update")) {
            var id = $(this).data("update");
            $(this).remove();

            // Graph could be in either of these
            var area = $("#problemarea").add(problem);
            graphie = area.find("#" + id + ".graphie").data("graphie");
        } else {
            var el = this;
            if ($(this).filter("script")[0] != null) {
                el = $("<div>").addClass("graphie")
                    .attr("id", $(this).attr("id")).insertAfter(this)[0];
                $(this).remove();
            }
            graphie = KhanUtil.createGraphie(el);
            $(el).data("graphie", graphie);

            var id = $(el).attr("id");
            if (id) {
                KhanUtil.graphs[id] = graphie;
            }
        }

        // So we can write graph.bwahahaha = 17 to save stuff between updates
        if (typeof graphie.graph === "undefined") {
            graphie.graph = {};
        }

        // Add newline in case code ends with a // comment
        code = "(function() {" + code + "\n})()";

        // Execute the graph-specific code
        KhanUtil.currentGraph = graphie;
        $.tmpl.getVAR(code, graphie);
        // delete KhanUtil.currentGraph;
    }).end();
};

$.fn.graphieCleanup = function(problem) {
    cleanupIntervals();
};

});
