(function(exports) {
    var sparky = exports.sparky = {
        version: "0.2.1"
    };

    var lib = sparky.lib = (typeof d3 === "object")
        ? d3
        : (function() {
            var shim = {};

            // like d3.keys(), returns an array of the object's keys
            shim.keys = function(obj) {
                var keys = [];
                for (var k in obj) keys.push(k);
                return keys;
            };

            // returns the minimum value in an array, optionally derived from
            // an accessor function
            shim.min = function(values, accessor) {
                var min = Number.POSITIVE_INFINITY,
                    len = values.length;
                for (var i = 0; i < len; i++) {
                    var val = accessor ? accessor(values[i]) : values[i];
                    if (val < min) min = val;
                }
                return min;
            };

            // returns the maximum value in an array, optionally derived from
            // an accessor function
            shim.max = function(values, accessor) {
                var max = Number.NEGATIVE_INFINITY,
                    len = values.length;
                for (var i = 0; i < len; i++) {
                    var val = accessor ? accessor(values[i]) : values[i];
                    if (val > max) max = val;
                }
                return max;
            };

            shim.scale = {};

            // our linear scale is simpler in that it only uses one value
            shim.scale.linear = function() {
                var dmin = 0, dmax = 1,
                    rmin = 0, rmax = 1,
                    clamp = false,
                    scale = function(val) {
                        if (clamp) {
                            if (val < dmin) val = dmin;
                            if (val > dmax) val = dmax;
                        }
                        return rmin + (rmax - rmin) * (val - dmin) / (dmax - dmin);
                    };

                scale.clamp = function(c) {
                    if (arguments.length) {
                        clamp = c;
                        return scale;
                    } else {
                        return clamp;
                    }
                };

                scale.domain = function(domain) {
                    if (arguments.length) {
                        dmin = domain[0];
                        dmax = domain[1];
                        return scale;
                    } else {
                        return [dmin, dmax];
                    }
                };

                scale.range = function(range) {
                    if (arguments.length) {
                        rmin = range[0];
                        rmax = range[1];
                        return scale;
                    } else {
                        return [rmin, rmax];
                    }
                };

                return scale;
            };

            // the identity function returns the value provided
            shim.identity = function(v) {
                return v;
            };

            // coerce a value into the identity function if it's not a
            // function already
            shim.functor = function(v) {
                return (typeof v === "function")
                    ? v
                    : function() { return v; };
            };

            return shim;
        })();

    sparky.sparkline = function(parent, data, config, overrides) {

        // attempt to query the document for the provided selector
        if (typeof parent === "string") {
            var id = parent;
            parent = document.getElementById(id) || document.querySelector(id);
            if (!parent) {
                throw 'No element found for "' + id + '"';
            }
        }

        // merge defaults and options, or fetch presets
        var options = (typeof config === "string")
            ? sparky.presets.get(config, overrides)
            : _extend(sparky.sparkline.defaults, config || {});

        // remember the length of the data array
        var data_len = data.length;
        // get_val() is a value getter for each datum
        var get_val = lib.functor(options.value);
        // figure out the minimum and maximum values
        var dmin = isNaN(options.min) ? lib.min(data, get_val) : options.min,
            dmax = isNaN(options.max) ? lib.max(data, get_val) : options.max;

        // determine the sparkline's dimensions
        var size = _size(parent),
            // padding is the number of pixels to inset from the edges
            padding = options.padding || 0,
            width = options.width || size.width,
            height = options.height || size.height;

        // create the x and y scales
        var XX = lib.scale.linear()
                .domain([0, data_len - 1])
                .range([padding, width - padding]),
            YY = lib.scale.linear()
                .domain([dmin, dmax])
                .range([height - padding, padding]);

        // create our Raphael surface
        var paper = Raphael(parent, width, height);

        if (options.range_fill && options.range_fill != "none") {
            // FIXME: complain if range_min and range_max aren't defined?
            var ry1 = YY(options.range_max),
                ry2 = YY(options.range_min);
            // only create a rect
            if (ry1 != ry2) {
                rect = paper.rect(padding, ry1, width - padding * 2, ry2 - ry1)
                    .attr({
                        "class":    "range",
                        "stroke":   "none",
                        "fill":     options.range_fill
                    });
            }
        }

        // bars and dots are mutually exclusive;
        // if there's a bar_fill option, assume they want bars
        if (options.bar_fill && options.bar_fill != "none") {
            var baseline = isNaN(options.baseline) ? 0 : options.baseline,
                actual_min = Math.min(dmin, baseline),
                spread = dmax - dmin;

            var avail_height = (height - padding * 2),
                avail_width = (width - padding * 2);

            // define our bar fill and positioning parameters
            var bar_fill = lib.functor(options.bar_fill || "black"),
                bar_spacing = isNaN(options.bar_spacing) ? 0 : options.bar_spacing,
                bar_width = (avail_width - bar_spacing * (data_len - 1)) / data_len;

            // proportional height
            var BH = function(val) {
                return avail_height * ((val >= baseline)
                    ? (val - baseline) / spread
                    : (baseline - val) / spread);
            };
            var BY = lib.scale.linear()
                .domain([baseline, dmax])
                .range([height - padding - BH(actual_min), padding])
                .clamp(true);
            var BX = lib.scale.linear()
                .domain([0, data_len - 1])
                .range([padding, padding + avail_width - bar_width]);

            var y0 = BY(baseline);

            // create a Raphael set for the bars
            var bars = paper.set();
            // (and stash it on the paper object for later use)
            paper.bars = bars;
            var did_min = false,
                did_max = false;
            for (var i = 0; i < data_len; i++) {
                // get the screen coordinate and the value,
                var val = get_val(data[i]),
                    x = BX(i),
                    y = BY(val),
                    h = BH(val),
                    // generate some metadata:
                    meta = {
                        // true if it's first in the list
                        first: i == 0,
                        // true if it's last in the list
                        last: i == data_len - 1,
                        // true if it's >= maximum value
                        max: did_max ? false : (did_max = val >= dmax),
                        // true if it's <= minimum value
                        min: did_min ? false : (did_min = val <= dmin),
                        // true if it's above the baseline
                        above: val >= baseline,
                        // true if it's below the baseline
                        below: val <= baseline
                    };
                // create the dot
                var bar = paper.rect(x, y, bar_width, h)
                    .attr({
                        "class":    "bar",
                        "stroke":   "none",
                        "fill":     bar_fill.call(meta, data[i], i)
                    });
                bars.push(bar);
            }

        // otherwise, do the dots
        } else {

            // create an array of screen coordinates for each datum
            var points = [];
            for (var i = 0; i < data_len; i++) {
                points.push({
                    x: XX(i),
                    y: YY(data[i])
                });
            }

            // if "area_fill" was provided, push some more points onto the array
            if (options.area_fill && options.area_fill !== "none") {
                var bottom = YY.range()[0],
                    br = {x: XX(data_len - 1), y: bottom},
                    bl = {x: XX(0), y: bottom};
                points.push(br);
                points.push(bl);
                points.push(points[0]);
            }

            var path = [];
            for (var i = 0; i < points.length; i++) {
                var p = points[i];
                path.push((i === 0) ? "M" : "L", p.x, ",", p.y);
            }
            // path.push("Z");
            // generate the path, and set its fill and stroke attributes
            var line = paper.path(path.join(" "))
                .attr({
                    "class":        "line",
                    "fill":         options.area_fill || "none",
                    "stroke":       options.line_stroke || "black",
                    "stroke-width": options.line_stroke_width || 1.5
                });

            // define our radius and color getters for dots
            var dot_radius = lib.functor(options.dot_radius),
                dot_fill = lib.functor(options.dot_fill || "black"),
                dot_stroke = lib.functor(options.dot_stroke || "none"),
                dot_stroke_width = lib.functor(options.dot_stroke_width || 0);

            // create a Raphael set for the dots
            var dots = paper.set();
            // (and stash it on the paper object for later use)
            paper.dots = dots;
            var did_min = false,
                did_max = false;
            for (var i = 0; i < data_len; i++) {
                // get the screen coordinate and the value,
                var point = points[i],
                    val = get_val(data[i]),
                    // generate some metadata:
                    meta = {
                        // true if it's first in the list
                        first: i == 0,
                        // true if it's last in the list
                        last: i == data_len - 1,
                        // true if it's >= maximum value
                        max: did_max ? false : (did_max = val >= dmax),
                        // true if it's <= minimum value
                        min: did_min ? false : (did_min = val <= dmin)
                    },
                    // get the radius
                    r = dot_radius.call(meta, data[i], i);
                // only create the dot if the radius > 0
                if (r > 0 && !isNaN(r)) {
                    // create the dot
                    var dot = paper.circle(point.x, point.y)
                        .attr({
                            "r":            r,
                            "class":        "dot",
                            "stroke":       dot_stroke.call(meta, data[i], i),
                            "stroke-width": dot_stroke_width.call(meta, data[i], i),
                            "fill":         dot_fill.call(meta, data[i], i)
                        });
                    dots.push(dot);
                }
            }
        }

        return paper;
    };

    // sparkline() option defaults
    sparky.sparkline.defaults = {
        width:              0, // 0 means "use the intrinsic width"
        height:             0, // 0 means "use the intrinsic height"
        // increase the padding to avoid cutting off dots with larger radii.
        padding:            2,
        // "area_fill" enables area rendering and defines the area's fill color
        area_fill:          null,
        // TODO: document
        range_min:          0,
        range_max:          0,
        range_fill:         null,
        // the value function (or key string) tells sparkline() how to extract
        // values from the data array. _identity() returns the value provided,
        // so it acts like a passthru for array values. See also: d3.identity()
        value:              lib.identity,
        // the color of the sparkline's line
        line_stroke:        "black",
        // the stroke width of the sparkline's line
        line_stroke_width:  1,
        // the fill color of the sparkline's dots, or a function that returns a
        // color for each datum. The function receives two arguments:
        // function(datum, index) { }
        // and the "this" context is a metadata object with properties that let
        // you know if this datum is the first, last, min or max value in the
        // data array.
        dot_fill:           null,
        // the radius of the sparkline's dots, or a function that returns the
        // radius for each datum, as above with "dot_fill".
        dot_radius:         0,

        // bar fill, defined either as a color function(datum, index)
        bar_fill:           null,
        // spacing between bars, in pixels
        bar_spacing:        1,
        // baseline value below which bars will also be drawn below
        baseline:           0
    };

    // Utility parsing functions
    sparky.parse = {};
    (function() {

        var split = sparky.parse.split = function(str) {
            return str.split(/\s*,\s*/);
        };

        sparky.parse.numbers = function(str, parser) {
            var numbers = split(str),
                len = numbers.length;
            if (!parser) parser = Number;
            for (var i = 0; i < len; i++) {
                numbers[i] = parser(numbers[i]);
            }
            return numbers;
        };

    })();

    sparky.util = {};

    sparky.util.getElementOptions = function(element, defaults, keys) {
        var options = {};

        function _option(key) {
            var value = element.getAttribute("data-" + key);
            if (value) {
                var num = Number(value);
                return isNaN(num) ? value : num;
            } else {
                return null;
            }
        }

        if (!keys) keys = lib.keys(sparky.sparkline.defaults);
        var len = keys.length;
        for (var i = 0; i < len; i++) {
            var key = keys[i],
                val = _option(key);
            if (val !== null) {
                options[key] = val;
            }
        }
        return defaults ? _extend(defaults, options) : options;
    };

    // Presets!
    sparky.presets = {};

    /**
     * Register a named preset:
     * sparky.presets.set("big-blue", {
     *   line_stroke: "blue",
     *   line_stroke_width: 2
     * });
     */
    sparky.presets.set = function(id, options) {
        sparky.presets[id] = options;
    };

    /**
     * Get a named preset:
     * sparky.presets.get("big-blue");
     */
    sparky.presets.get = function(id, options) {
        return _extend(sparky.presets[id], options || {});
    };

    /**
     * Copy a named preset and override select options:
     * sparky.sparkline.presets.extend("big-blue", "big-green", {
     *   line_stroke: "green"
     * });
     */
    sparky.presets.extend = function(base, id, options) {
        return sparky.presets[id] = _extend(sparky.presets[base], options);
    };

    // defaults
    sparky.presets.set("default", sparky.sparkline.defaults);

    // a nice preset for fill
    sparky.presets.set("gray-area", {
        min:            0,
        dot_radius:     0,
        padding:        0,
        area_fill:      "#999",
        line_stroke:    "none"
    });

    /*
     * Tufte-esque presets inspired by:
     * http://www.edwardtufte.com/bboard/q-and-a-fetch-msg?msg_id=0001OR
     */

    sparky.presets.set("hilite-last", {
        line_stroke:        "#888",
        line_stroke_width:  1,
        range_fill:         "#ddd",
        dot_fill:           "#f00",
        dot_radius: function(d, i) {
            return this.last ? 2 : 0;
        }
    });

    sparky.presets.extend("hilite-last", "hilite-peaks", {
        dot_fill: function(d, i) {
            return (this.first || this.last)
                ? "#f00"
                : (this.min || this.max)
                  ? "#339ACF"
                  : null;
        },
        dot_radius: function(d, i) {
            return (this.first || this.last || this.min || this.max)
                ? 2
                : 0;
        }
    });

    sparky.presets.set("zero-bars", {
        padding:            0,
        line_stroke:        "none",
        dot_fill:           "none",
        bar_fill: function(d, i) {
            return this.above ? "black" : "red";
        }
    });

    sparky.presets.set("binary", {
        padding:        0,
        line_stroke:    "none",
        dot_fill:       "none",
        bar_fill:       "#333",
        bar_spacing:    .5,
        baseline:       .5,
        min:            0,
        max:            1
    });

    // internal utility functions:

    /**
     * Get the intrinsic size ({width, height}) of an element in round pixels.
     */
    function _size(el) {
        return {
            width: ~~el.offsetWidth,
            height: ~~el.offsetHeight
        };
    }

    /**
     * Override all of the iterable properties in the first object so that they
     * contain the values of the second, and return it as a new object.
     */
    function _extend(defaults, options) {
        var o = {};
        for (var k in defaults) {
            o[k] = defaults[k];
        }
        for (var k in options) {
            o[k] = options[k];
        }
        return o;
    }

})(this);