var oo = (function(document){

    var _serviceEndpoint = undefined;

    var _apiKey = undefined;

    var _defaultTimelineOptions = {};

    var _defaultPieOptions = {};

    var _defaultTableOptions = {};

    var _defaultBarOptions = {};

    var _defaultColumnOptions = {};

    /*
     * Binds OOcharts Objects to DOM Elements based on HTML attributes
     */
    var _autoBuild = function(){

        /*
         * Gets all elements containing value for specified attribute
         * @param {String} attribute
         * @return {Array} matchingElements
         */
        var getAllElementsWithAttribute = function(attribute){
            var matchingElements = [];
            var allElements = document.getElementsByTagName('*');
            for (var i = 0; i < allElements.length; i++){
                if (allElements[i].getAttribute(attribute)){
                    matchingElements.push(allElements[i]);
                }
            }
            return matchingElements;
        };

        // find all oocharts elements
        var elements = getAllElementsWithAttribute('data-oochart');

        for(var e = 0; e < elements.length; e++){

            var _element = elements[e];

            //get oocharts data
            var type  = _element.getAttribute('data-oochart');
            var startDate = _element.getAttribute('data-oochart-start-date');
            var endDate = _element.getAttribute('data-oochart-end-date');
            var profile = _element.getAttribute('data-oochart-profile');

            // if metric
            if(type.toLowerCase() === 'metric'){
                var metric = new _Metric(profile, startDate, endDate);
                metric.setMetric(_element.getAttribute('data-oochart-metric'));
                metric.draw(_element);

                //if column
            } else if (type.toLowerCase() === 'column'){
                var column = new _Column(profile, startDate, endDate);

                var metricsString = _element.getAttribute('data-oochart-metrics');
                var metrics = metricsString.split(',');

                for(var m = 0; m < metrics.length; m++){
                    column.addMetric(metrics[m], metrics[m+1]);
                    m=m+1;
                }

                var dimension = _element.getAttribute('data-oochart-dimension');

                column.setDimension(dimension);

                column.draw(_element);

                //if timeline
            } else if (type.toLowerCase() === 'timeline'){
                var timeline = new _Timeline(profile, startDate, endDate);

                var metricsString = _element.getAttribute('data-oochart-metrics');
                var metrics = metricsString.split(',');

                for(var m = 0; m < metrics.length; m++){
                    timeline.addMetric(metrics[m], metrics[m+1]);
                    m=m+1;
                }

                timeline.draw(_element);

            } else if (type.toLowerCase() === 'bar' ) {
                var bar = new _Bar(profile, startDate, endDate);

                var metricsString = _element.getAttribute('data-oochart-metrics');
                var metrics = metricsString.split(',');

                for(var m = 0; m < metrics.length; m++){
                    bar.addMetric(metrics[m], metrics[m+1]);
                    m=m+1;
                }

                var dimension = _element.getAttribute('data-oochart-dimension');

                bar.setDimension(dimension);

                bar.draw(_element);
                //if pie
            } else if (type.toLowerCase() === 'pie'){
                var pie = new _Pie(profile, startDate, endDate);

                var metricString = _element.getAttribute('data-oochart-metric');
                var dimension = _element.getAttribute('data-oochart-dimension');

                var metric = metricString.split(',');

                pie.setMetric(metric[0], metric[1]);
                pie.setDimension(dimension);

                pie.draw(_element);

                //if table
            } else if (type.toLowerCase() === 'table'){
                var table = new _Table(profile, startDate, endDate);

                var metricString = _element.getAttribute('data-oochart-metrics');
                var dimensionString = _element.getAttribute('data-oochart-dimensions');

                var metrics = metricString.split(',');
                var dimensions = dimensionString.split(',');

                for(var m = 0; m < metrics.length; m++){
                    table.addMetric(metrics[m], metrics[m+1]);
                    m += 1;
                }

                for(var d = 0; d < dimensions.length; d++){
                    table.addDimension(dimensions[d], dimensions[d+1]);
                    d += 1;
                }

                table.draw(_element);
            }

            //else ignore and continue
        }
    };

    var _setServiceEndpoint = function(endpoint){
      _serviceEndpoint = endpoint;
    };

    /*
     * Loads OOcharts dependencies
     * @param {Function} callback
     */
    var  _load = function(callback){
        if(!_apiKey) throw "Set APIKey with oo.setAPIKey before calling load.";
        if(!_serviceEndpoint) throw "Set service endpoint before calling load.";

        if (typeof google.visualization === 'undefined') {
            google.load("visualization", "1", { packages: ['corechart', 'table'], 'callback': callback });
        }
        else {
            var necpac = [];

            if (typeof google.visualization.corechart === 'undefined') {
                necpac.push('corechart');
            }

            if (typeof google.visualization.table === 'undefined') {
                necpac.push('table');
            }

            if (necpac.length > 0) {
                google.load("visualization", "1", { packages: necpac, 'callback': callback });
            }
        }

        _autoBuild();
    };

    /*
     * Sets API Key
     * @param {String} key
     */
    var _setAPIKey = function(key){
        _apiKey = key;
    };

    /*
     * Sets Column chart default options
     * @param {Object} opts
     */
    var _setColumnDefaults = function(opts){
        _defaultColumnOptions = opts;
    };

    /*
     * Sets Bar chart default options
     * @param {Object} opts
     */
    var _setBarDefaults = function(opts){
        _defaultBarOptions = opts;
    };


    /*
     * Sets Timeline chart default options
     * @param {Object} opts
     */
    var _setTimelineDefaults = function(opts){
        _defaultTimelineOptions = opts;
    };

    /*
     * Sets Pie chart default options
     * @param {Object} opts
     */
    var _setPieDefaults = function(opts){
        _defaultPieOptions = opts;
    };

    /*
     * Sets Table chart default options
     * @param {Object} opts
     */
    var _setTableDefaults = function(opts){
        _defaultTableOptions = opts;
    };

    /*
     * Helper to format Data object to GA standards
     * @param {Date} date
     * @returns {String} formatted date (YYYY-MM-DD)
     */
    var _formatDate = function(date){
        var year = date.getFullYear().toString();
        var month = (date.getMonth() + 1).toString();
        var date = date.getDate().toString();

        if(month.length === 1) month = "0" + month;

        if(date.length === 1) date = "0" + date;

        return  year + '-' + month + '-' + date;
    };

    /*
     * Helper to parse GA date to date object
     * @param {String} val
     * @returns {Date} parsed Date
     */
    var _parseDate = function(val){
        val = val.split('-');
        var newDate = new Date();
        newDate.setFullYear(val[0], val[1]-1, val[2]);
        newDate.setHours(0,0,0,0);
        return newDate;
    };

    /*------------------------------------------------------------
     Query (Core OOcharts Object)
     -------------------------------------------------------------*/

    /*
     * Query constructor
     * @param {String} profile
     * @param {Date or String} startDate
     * @param {Date or String} endDate
     */
    var _Query = function(profile, startDate, endDate) {
        this.metrics = [];
        this.dimensions = [];

        if(!profile) throw new Error("Profile argument required");

        this.profile = profile;

        // validate dates
        startDate = startDate || new Date();
        endDate = endDate || new Date();

        if(startDate instanceof Date){
            startDate = _formatDate(startDate);
        }

        if(endDate instanceof Date){
            endDate = _formatDate(endDate);
        }

        var _isRelativeDate = function(val){ return /^[0-9]+(m|d|w|y)$/.test(val); };
        var _isValidDate = function(val){ return /^([0-9]{4}-[0-9]{2}-[0-9]{2})$|^[0-9]+(m|d|w|y)$/.test(val); };

        if(!_isRelativeDate(startDate) && !_isValidDate(startDate)){
            throw new Error("startDate parameter invalid" );
        }

        if(!_isRelativeDate(endDate) && !_isValidDate(endDate)){
            throw new Error("endDate parameter invalid" );
        }

        if(_isRelativeDate(startDate) && _isRelativeDate(endDate)){
            throw new Error("startDate and endDate cannot both be relative dates");
        }

        this.startDate = startDate;
        this.endDate = endDate;
    };

    /*
     * Clears all metrics on Query
     */
    _Query.prototype.clearMetrics = function(){
        this.metrics = [];
    };

    /*
     * Clears all dimensions on Query
     */
    _Query.prototype.clearDimensions = function(){
        this.dimensions = [];
    };

    /*
     * Adds a metric to Query
     * @param {String} metric
     */
    _Query.prototype.addMetric = function(metric){
        this.metrics.push(metric);
    };

    /*
     * Adds a dimension to Query
     * @param {String} dimension
     */
    _Query.prototype.addDimension = function(dimension){
        this.dimensions.push(dimension);
    };

    /*
     * Sets filters string for Query
     * @param {String} filters
     */
    _Query.prototype.setFilter = function(filters){
        this.filters = filters;
    };

    /*
     * Sets sort string for Query
     * @param {String} sort
     */
    _Query.prototype.setSort = function(sort){
        this.sort = sort;
    };

    /*
     * Sets segment string for Query
     * @param {String} segment
     */
    _Query.prototype.setSegment = function(segment){
        this.segment = segment;
    };

    /*
     * Sets the beginning index for Query
     * @param {Number or String} index
     */
    _Query.prototype.setIndex = function(index){
        this.index = index;
    };

    /*
     * Sets max results for Query
     * @param {Number or String} maxResults
     */
    _Query.prototype.setMaxResults = function(maxResults){
        this.maxResults = maxResults;
    };

    /*
     * Executes query through JSONP
     * @param {Function} callback(data)
     */
    _Query.prototype.execute = function(callback){
        var query = {};

        query.key = _apiKey;
        query.profile = this.profile;

        if(this.metrics.length === 0) throw new Error("At least one metric is required");

        query.metrics = this.metrics.toString();
        query.start = this.startDate;
        query.end = this.endDate;

        if(this.dimensions.length > 0){
            query.dimensions = this.dimensions.toString();
        }

        if(this.filters){
            query.filters = this.filters;
        }

        if(this.sort){
            query.sort = this.sort;
        }

        if(this.index){
            query.index = this.index;
        }

        if(this.segment){
            query.segment = this.segment;
        }

        if(this.maxResults){
            query.maxResults = this.maxResults;
        }

        JSONP.get(_serviceEndpoint, query, function(data){
            if(data.rows.length === 0){
                data.rows = [[]];
            }

            callback(data);
        });
    };

    /*------------------------------------------------------------
     Metric
     -------------------------------------------------------------*/

    /*
     * Metric constructor
     * @param {String} profile
     * @param {Date or String} startDate
     * @param {Date or String} endDate
     */
    var _Metric = function(profile, startDate, endDate){
        this.query = new _Query(profile, startDate, endDate);
    };

    /*
     * Sets the metric
     * @param {String} metric
     */
    _Metric.prototype.setMetric = function(metric){
        this.query.clearMetrics();
        this.query.addMetric(metric);
    };

    /*
     * Draws metric result in container element
     * @param {Element or String} container (or id of container element)
     * @param {Function} fn
     */
    _Metric.prototype.draw = function(container, fn){
        this.query.execute(function(response){

            var element;

            if(typeof container === 'string'){
                element = document.getElementById(container);
            } else {
                element = container;
            }

            if(typeof response.rows[0][0] !== 'undefined'){
                element.innerHTML = response.rows[0][0].toString();
            }
            else {
                element.innerHTML = "0";
            }

            if(typeof fn !== 'undefined'){
                fn();
            }
        });
    };

    /*------------------------------------------------------------
     Column
     -------------------------------------------------------------*/

    /*
     * Column constructor
     * @param {String} profile
     * @param {Date or String} startDate
     * @param {Date or String} endDate
     */
    var _Column = function(profile, startDate, endDate){
        this.query = new _Query(profile, startDate, endDate);

        this.metricLabels = [];

        this.options = _defaultColumnOptions;
    };

    /*
     * Override default options
     * @param {Object} opts
     */
    _Column.prototype.setOptions = function(opts){
        this.options = opts;
    };

    /*
     * Adds a metric to Column
     * @param {String} metric
     * @param {String} label
     */
    _Column.prototype.addMetric = function(metric, label){
        this.metricLabels.push(label);
        this.query.addMetric(metric);
    };

    /*
     * Set dimension of Column
     * @param {String} dimension
     */
    _Column.prototype.setDimension = function(dimension){
        this.query.clearDimensions();
        this.query.dimensions.push(dimension);
        this.dimensionLabel = dimension;
    };

    /*
     * Draws Column in container element
     * @param {Element or String} container (or id of container element)
     * @param {Function} fn
     */
    _Column.prototype.draw = function(container, fn){
        var b = this;

        this.query.execute(function (response) {

            var data = response.rows;

            var dt = new google.visualization.DataTable();

            dt.addColumn('string', b.dimensionLabel);

            // Add data column labels
            for (var l = 0; l < b.metricLabels.length; l++) {
                dt.addColumn('number', b.metricLabels[l]);
            }

            // add data
            dt.addRows(data);

            var element;

            if(typeof container === 'string'){
                element = document.getElementById(container);
            } else {
                element = container;
            }

            var chart = new google.visualization.ColumnChart(element);
            chart.draw(dt, b.options);

            if (typeof fn != 'undefined') {
                fn();
            }
        });
    };

    /*------------------------------------------------------------
     Timeline
     -------------------------------------------------------------*/

    /*
     * Timeline constructor
     * @param {String} profile
     * @param {Date or String} startDate
     * @param {Date or String} endDate
     */
    var _Timeline = function(profile, startDate, endDate){
        this.query = new _Query(profile, startDate, endDate);
        this.query.addDimension('ga:date');

        this.labels = [];

        this.options = _defaultTimelineOptions;
    };

    /*
     * Override default options
     * @param {Object} opts
     */
    _Timeline.prototype.setOptions = function(opts){
        this.options = opts;
    };

    /*
     * Adds a metric to Timeline
     * @param {String} metric
     * @param {String} label
     */
    _Timeline.prototype.addMetric = function(metric, label){
        this.labels.push(label);
        this.query.addMetric(metric);
    };

    /*
     * Draws timeline in container element
     * @param {Element or String} container (or id of container element)
     * @param {Function} fn
     */
    _Timeline.prototype.draw = function(container, fn){
        var t = this;

        this.query.execute(function (response) {

            var data = response.rows;

            //Turn analytics date strings into date
            for (var r = 0; r < data.length; r++) {
                data[r][0] =_parseDate(data[r][0]);
            }

            var dt = new google.visualization.DataTable();

            dt.addColumn('date', 'Date');

            // Add data column labels
            for (var l = 0; l < t.labels.length; l++) {
                dt.addColumn('number', t.labels[l]);
            }

            // add data
            dt.addRows(data);

            var element;

            if(typeof container === 'string'){
                element = document.getElementById(container);
            } else {
                element = container;
            }

            var chart = new google.visualization.LineChart(element);
            chart.draw(dt, t.options);

            if (typeof fn != 'undefined') {
                fn();
            }
        });
    };

    /*------------------------------------------------------------
     Bar
     -------------------------------------------------------------*/

    /*
     * Bar constructor
     * @param {String} profile
     * @param {Date or String} startDate
     * @param {Date or String} endDate
     */
    var _Bar = function(profile, startDate, endDate){
        this.query = new _Query(profile, startDate, endDate);

        this.metricLabels = [];

        this.options = _defaultBarOptions;
    };

    /*
     * Override default options
     * @param {Object} opts
     */
    _Bar.prototype.setOptions = function(opts){
        this.options = opts;
    };

    /*
     * Adds a metric to Bar
     * @param {String} metric
     * @param {String} label
     */
    _Bar.prototype.addMetric = function(metric, label){
        this.metricLabels.push(label);
        this.query.addMetric(metric);
    };

    /*
     * Set dimension of Bar
     * @param {String} dimension
     */
    _Bar.prototype.setDimension = function(dimension){
        this.query.clearDimensions();
        this.query.dimensions.push(dimension);
        this.dimensionLabel = dimension;
    };

    /*
     * Draws Bar in container element
     * @param {Element or String} container (or id of container element)
     * @param {Function} fn
     */
    _Bar.prototype.draw = function(container, fn){
        var b = this;

        this.query.execute(function (response) {

            var data = response.rows;

            var dt = new google.visualization.DataTable();

            dt.addColumn('string', b.dimensionLabel);

            // Add data column labels
            for (var l = 0; l < b.metricLabels.length; l++) {
                dt.addColumn('number', b.metricLabels[l]);
            }

            // add data
            dt.addRows(data);

            var element;

            if(typeof container === 'string'){
                element = document.getElementById(container);
            } else {
                element = container;
            }

            var chart = new google.visualization.BarChart(element);
            chart.draw(dt, b.options);

            if (typeof fn != 'undefined') {
                fn();
            }
        });
    };

    /*------------------------------------------------------------
     Pie
     -------------------------------------------------------------*/

    /*
     * Pie constructor
     * @param {String} profile
     * @param {Date or String} startDate
     * @param {Date or String} endDate
     */
    var _Pie = function(profile, startDate, endDate){
        this.query = new _Query(profile, startDate, endDate);
        this.options = _defaultPieOptions;
    };

    /*
     * Set metric for Pie
     * @param {String} metric
     * @param {String} label
     */
    _Pie.prototype.setMetric = function(metric, label){
        this.metricLabel = label;
        this.query.clearMetrics();
        this.query.addMetric(metric);
    };

    /*
     * Set dimension for Pie
     * @param {String} dimension
     */
    _Pie.prototype.setDimension = function(dimension){
        this.query.clearDimensions();
        this.query.dimensionLabel = dimension;
        this.query.addDimension(dimension);
    };

    /*
     * Override default pie options
     * @param {Object} opts
     */
    _Pie.prototype.setOptions = function(opts){
        this.options = opts;
    };

    /*
     * Draws pie in container element
     * @param {Element or String} container (or id of container element)
     * @param {Function} fn
     */
    _Pie.prototype.draw = function(container, fn){

        var p = this;

        this.query.execute(function(response){

            var data = response.rows;

            var dt = new google.visualization.DataTable();

            dt.addColumn('string', p.dimensionLabel);
            dt.addColumn('number', p.metricLabel);
            dt.addRows(data);

            var element;

            if(typeof container === 'string'){
                element = document.getElementById(container);
            } else {
                element = container;
            }

            var chart = new google.visualization.PieChart(element);
            chart.draw(dt, p.options);

            if (typeof fn != 'undefined') {
                fn();
            }

        });

    };

    /*------------------------------------------------------------
     Table
     -------------------------------------------------------------*/

    /*
     * Table constructor
     * @param {String} profile
     * @param {Date or String} startDate
     * @param {Date or String} endDate
     */
    var _Table = function(profile, startDate, endDate){
        this.query = new _Query(profile, startDate, endDate);

        this.metricLabels = [];
        this.dimensionLabels = [];

        this.options = _defaultTableOptions;
    };

    /*
     * Adds a metric to table
     * @param {String} metric
     * @param {String} label
     */
    _Table.prototype.addMetric = function(metric, label){
        this.query.addMetric(metric);
        this.metricLabels.push(label);
    };

    /*
     * Adds a dimension to table
     * @param {String} dimension
     * @param {String} label
     */
    _Table.prototype.addDimension = function(dimension, label){
        this.query.addDimension(dimension);
        this.dimensionLabels.push(label);
    };

    /*
     * Override default table options
     * @param {Object} opts
     */
    _Table.prototype.setOptions = function(opts){
        this.options = opts;
    };

    /*
     * Draws table in container element
     * @param {Element or String} container (or id of container element)
     * @param {Function} fn
     */
    _Table.prototype.draw = function(container, fn){
        var t = this;

        this.query.execute(function (result) {

            var data = result.rows;

            var labelRow = [];

            for (var d = 0; d < t.dimensionLabels.length; d++) {
                labelRow.push(t.dimensionLabels[d]);
            }

            for (var m = 0; m < t.metricLabels.length; m++) {
                labelRow.push(t.metricLabels[m]);
            }

            data.splice(0, 0, labelRow);

            var dt = google.visualization.arrayToDataTable(data);

            var element;

            if(typeof container === 'string'){
                element = document.getElementById(container);
            } else {
                element = container;
            }

            var chart = new google.visualization.Table(element);
            chart.draw(dt, t.options);

            if (typeof fn != 'undefined') {
                fn();
            }
        });
    };

    /*------------------------------------------------------------
     Exports
     -------------------------------------------------------------*/

    return {
        Query : _Query,
        Timeline : _Timeline,
        Column : _Column,
        Bar : _Bar,
        Metric : _Metric,
        Pie : _Pie,
        Table : _Table,
        load : _load,
        setServiceEndpoint : _setServiceEndpoint,
        formatDate : _formatDate,
        parseDate : _parseDate,
        setAPIKey : _setAPIKey,
        setBarDefaults : _setBarDefaults,
        setColumnDefaults : _setColumnDefaults,
        setTimelineDefaults : _setTimelineDefaults,
        setPieDefaults : _setPieDefaults,
        setTableDefaults : _setTableDefaults
    };

})(document);


/**
 * simple JSONP support
 *
 *     JSONP.get('https://api.github.com/gists/1431613', function (data) { console.log(data); });
 *     JSONP.get('https://api.github.com/gists/1431613', {}, function (data) { console.log(data); });
 *
 * gist: https://gist.github.com/gists/1431613
 */
var JSONP = (function (document) {
    var requests = 0,
        callbacks = {};

    return {
        /**
         * makes a JSONP request
         *
         * @param {String} src
         * @param {Object} data
         * @param {Function} callback
         */
        get: function (src, data, callback) {
            // check if data was passed
            if (!arguments[2]) {
                callback = arguments[1];
                data = {};
            }

            // determine if there already are params
            src += (src.indexOf('?')+1 ? '&' : '?');

            var head = document.getElementsByTagName('head')[0],
                script = document.createElement('script'),
                params = [],
                requestId = requests,
                param;

            // increment the requests
            requests++;

            // create external callback name
            data.callback = 'JSONP.callbacks.request_' + requestId;

            // set callback function
            callbacks['request_' + requestId] = function (data) {
                // clean up
                head.removeChild(script);
                delete callbacks['request_' + requestId];

                // fire callback
                callback(data);
            };

            // traverse data
            for (param in data) {
                params.push(param + '=' + encodeURIComponent(data[param]));
            }

            // generate params
            src += params.join('&');

            // set script attributes
            script.type = 'text/javascript';
            script.src = src;

            // add to the DOM
            head.appendChild(script);
        },

        /**
         * keeps a public reference of the callbacks object
         */
        callbacks: callbacks
    };
}(document));
