
var nv = window.nv || {};

nv.version = '0.0.1a';
nv.dev = true //set false when in production

window.nv = nv;

nv.tooltip = {}; // For the tooltip system
nv.utils = {}; // Utility subsystem
nv.models = {}; //stores all the possible models/components
nv.charts = {}; //stores all the ready to use charts
nv.graphs = []; //stores all the graphs currently on the page
nv.logs = {}; //stores some statistics and potential error messages

nv.dispatch = d3.dispatch('render_start', 'render_end');

// *************************************************************************
//  Development render timers - disabled if dev = false

if (nv.dev) {
    nv.dispatch.on('render_start', function(e) {
        nv.logs.startTime = +new Date();
    });

    nv.dispatch.on('render_end', function(e) {
        nv.logs.endTime = +new Date();
        nv.logs.totalTime = nv.logs.endTime - nv.logs.startTime;
        nv.log('total', nv.logs.totalTime); // used for development, to keep track of graph generation times
    });
}

// ********************************************
//  Public Core NV functions

// Logs all arguments, and returns the last so you can test things in place
nv.log = function() {
    if (nv.dev && console.log && console.log.apply)
        console.log.apply(console, arguments)
    else if (nv.dev && console.log && Function.prototype.bind) {
        var log = Function.prototype.bind.call(console.log, console);
        log.apply(console, arguments);
    }
    return arguments[arguments.length - 1];
};


nv.render = function render(step) {
    step = step || 1; // number of graphs to generate in each timout loop

    render.active = true;
    nv.dispatch.render_start();

    setTimeout(function() {
        var chart, graph;

        for (var i = 0; i < step && (graph = render.queue[i]); i++) {
            chart = graph.generate();
            if (typeof graph.callback == typeof(Function)) graph.callback(chart);
            nv.graphs.push(chart);
        }

        render.queue.splice(0, i);

        if (render.queue.length) setTimeout(arguments.callee, 0);
        else { nv.render.active = false; nv.dispatch.render_end(); }
    }, 0);
};

nv.render.active = false;
nv.render.queue = [];

nv.addGraph = function(obj) {
    if (typeof arguments[0] === typeof(Function))
        obj = {generate: arguments[0], callback: arguments[1]};

    nv.render.queue.push(obj);

    if (!nv.render.active) nv.render();
};

nv.identity = function(d) { return d; };

nv.strip = function(s) { return s.replace(/(\s|&)/g,''); };

function daysInMonth(month,year) {
    return (new Date(year, month+1, 0)).getDate();
}

function d3_time_range(floor, step, number) {
    return function(t0, t1, dt) {
        var time = floor(t0), times = [];
        if (time < t0) step(time);
        if (dt > 1) {
            while (time < t1) {
                var date = new Date(+time);
                if ((number(date) % dt === 0)) times.push(date);
                step(time);
            }
        } else {
            while (time < t1) { times.push(new Date(+time)); step(time); }
        }
        return times;
    };
}

d3.time.monthEnd = function(date) {
    return new Date(date.getFullYear(), date.getMonth(), 0);
};

d3.time.monthEnds = d3_time_range(d3.time.monthEnd, function(date) {
        date.setUTCDate(date.getUTCDate() + 1);
        date.setDate(daysInMonth(date.getMonth() + 1, date.getFullYear()));
    }, function(date) {
        return date.getMonth();
    }
);


/*****
 * A no-frills tooltip implementation.
 *****/


(function() {

    var nvtooltip = window.nv.tooltip = {};

    nvtooltip.show = function(pos, content, gravity, dist, parentContainer, classes) {

        var container = document.createElement('div');
        container.className = 'nvtooltip ' + (classes ? classes : 'xy-tooltip');

        gravity = gravity || 's';
        dist = dist || 20;

        var body = parentContainer ? parentContainer : document.getElementsByTagName('body')[0];

        container.innerHTML = content;
        container.style.left = 0;
        container.style.top = 0;
        container.style.opacity = 0;

        body.appendChild(container);

        var height = parseInt(container.offsetHeight),
            width = parseInt(container.offsetWidth),
            windowWidth = nv.utils.windowSize().width,
            windowHeight = nv.utils.windowSize().height,
            scrollTop = window.scrollY,
            scrollLeft = window.scrollX,
            left, top;

        windowHeight = window.innerWidth >= document.body.scrollWidth ? windowHeight : windowHeight - 16;
        windowWidth = window.innerHeight >= document.body.scrollHeight ? windowWidth : windowWidth - 16;

        var tooltipTop = function ( Elem ) {
            var offsetTop = top;
            do {
                if( !isNaN( Elem.offsetTop ) ) {
                    offsetTop += (Elem.offsetTop);
                }
            } while( Elem = Elem.offsetParent );
            return offsetTop;
        }

        var tooltipLeft = function ( Elem ) {
            var offsetLeft = left;
            do {
                if( !isNaN( Elem.offsetLeft ) ) {
                    offsetLeft += (Elem.offsetLeft);
                }
            } while( Elem = Elem.offsetParent );
            return offsetLeft;
        }

        switch (gravity) {
            case 'e':
                left = pos[0] - width - dist;
                top = pos[1] - (height / 2);
                var tLeft = tooltipLeft(container);
                var tTop = tooltipTop(container);
                if (tLeft < scrollLeft) left = pos[0] + dist > scrollLeft ? pos[0] + dist : scrollLeft - tLeft + left;
                if (tTop < scrollTop) top = scrollTop - tTop + top;
                if (tTop + height > scrollTop + windowHeight) top = scrollTop + windowHeight - tTop + top - height;
                break;
            case 'w':
                left = pos[0] + dist;
                top = pos[1] - (height / 2);
                if (tLeft + width > windowWidth) left = pos[0] - width - dist;
                if (tTop < scrollTop) top = scrollTop + 5;
                if (tTop + height > scrollTop + windowHeight) top = scrollTop - height - 5;
                break;
            case 'n':
                left = pos[0] - (width / 2) - 5;
                top = pos[1] + dist;
                var tLeft = tooltipLeft(container);
                var tTop = tooltipTop(container);
                if (tLeft < scrollLeft) left = scrollLeft + 5;
                if (tLeft + width > windowWidth) left = left - width/2 + 5;
                if (tTop + height > scrollTop + windowHeight) top = scrollTop + windowHeight - tTop + top - height;
                break;
            case 's':
                left = pos[0] - (width / 2);
                top = pos[1] - height - dist;
                var tLeft = tooltipLeft(container);
                var tTop = tooltipTop(container);
                if (tLeft < scrollLeft) left = scrollLeft + 5;
                if (tLeft + width > windowWidth) left = left - width/2 + 5;
                if (scrollTop > tTop) top = scrollTop;
                break;
        }


        container.style.left = left+'px';
        container.style.top = top+'px';
        container.style.opacity = 1;
        container.style.position = 'absolute'; //fix scroll bar issue
        container.style.pointerEvents = 'none'; //fix scroll bar issue

        return container;
    };

    nvtooltip.cleanup = function() {

        // Find the tooltips, mark them for removal by this class (so others cleanups won't find it)
        var tooltips = document.getElementsByClassName('nvtooltip');
        var purging = [];
        while(tooltips.length) {
            purging.push(tooltips[0]);
            tooltips[0].style.transitionDelay = '0 !important';
            tooltips[0].style.opacity = 0;
            tooltips[0].className = 'nvtooltip-pending-removal';
        }


        setTimeout(function() {

            while (purging.length) {
                var removeMe = purging.pop();
                removeMe.parentNode.removeChild(removeMe);
            }
        }, 500);
    };


})();


nv.utils.windowSize = function() {
    // Sane defaults
    var size = {width: 640, height: 480};

    // Earlier IE uses Doc.body
    if (document.body && document.body.offsetWidth) {
        size.width = document.body.offsetWidth;
        size.height = document.body.offsetHeight;
    }

    // IE can use depending on mode it is in
    if (document.compatMode=='CSS1Compat' &&
        document.documentElement &&
        document.documentElement.offsetWidth ) {
        size.width = document.documentElement.offsetWidth;
        size.height = document.documentElement.offsetHeight;
    }

    // Most recent browsers use
    if (window.innerWidth && window.innerHeight) {
        size.width = window.innerWidth;
        size.height = window.innerHeight;
    }
    return (size);
};



// Easy way to bind multiple functions to window.onresize
// TODO: give a way to remove a function after its bound, other than removing alkl of them
nv.utils.windowResize = function(fun){
    var oldresize = window.onresize;

    window.onresize = function(e) {
        if (typeof oldresize == 'function') oldresize(e);
        fun(e);
    }
}

// Backwards compatible way to implement more d3-like coloring of graphs.
// If passed an array, wrap it in a function which implements the old default
// behavior
nv.utils.getColor = function(color) {
    if (!arguments.length) return nv.utils.defaultColor(); //if you pass in nothing, get default colors back

    if( Object.prototype.toString.call( color ) === '[object Array]' )
        return function(d, i) { return d.color || color[i % color.length]; };
    else
        return color;
    //can't really help it if someone passes rubbish as color
}

// Default color chooser uses the index of an object as before.
nv.utils.defaultColor = function() {
    var colors = d3.scale.category20().range();
    return function(d, i) { return d.color || colors[i % colors.length] };
}


// Returns a color function that takes the result of 'getKey' for each series and
// looks for a corresponding color from the dictionary,
nv.utils.customTheme = function(dictionary, getKey, defaultColors) {
    getKey = getKey || function(series) { return series.key }; // use default series.key if getKey is undefined
    defaultColors = defaultColors || d3.scale.category20().range(); //default color function

    var defIndex = defaultColors.length; //current default color (going in reverse)

    return function(series, index) {
        var key = getKey(series);

        if (!defIndex) defIndex = defaultColors.length; //used all the default colors, start over

        if (typeof dictionary[key] !== "undefined")
            return (typeof dictionary[key] === "function") ? dictionary[key]() : dictionary[key];
        else
            return defaultColors[--defIndex]; // no match in dictionary, use default color
    }
}



// From the PJAX example on d3js.org, while this is not really directly needed
// it's a very cool method for doing pjax, I may expand upon it a little bit,
// open to suggestions on anything that may be useful
nv.utils.pjax = function(links, content) {
    d3.selectAll(links).on("click", function() {
        history.pushState(this.href, this.textContent, this.href);
        load(this.href);
        d3.event.preventDefault();
    });

    function load(href) {
        d3.html(href, function(fragment) {
            var target = d3.select(content).node();
            target.parentNode.replaceChild(d3.select(fragment).select(content).node(), target);
            nv.utils.pjax(links, content);
        });
    }

    d3.select(window).on("popstate", function() {
        if (d3.event.state) load(d3.event.state);
    });
}

