/**
 * donutty // Create SVG donut charts with Javascript
 * @author simeydotme <simey.me@gmail.com>
 * @version 2.0.0
 * @license MIT
 * @link http://simey.me
 * @preserve
 */

(function( doc, win ) {

    var donutty,
        namespace = "http://www.w3.org/2000/svg";

    function isDefined( input ) {
        return typeof input !== "undefined";
    }

    function float( input ) {
        return parseFloat( input, 10 );
    }

    function truth( input ) {
        return isDefined( input ) && ( input === true || input === "true" );
    }

    donutty = win.Donutty = function( el, options ) {

        if ( el && typeof el === "string" ) {
            this.$wrapper = doc.querySelectorAll( el )[0];
        } else if ( doc.querySelector( `.${el.className}` ) ) {
			this.$wrapper = el;
        } else {
            this.$wrapper = doc.body;
            options = el;
        }

        if ( !this.$wrapper ) {

            return this;

        }

        if ( !isDefined( options ) ) {

            options = this.getOptionsFromTag();

        }

        this.state =               {};
        this.options =             options || {};
        this.options.min =         isDefined( this.options.min ) ? float( this.options.min ) : 0;
        this.options.max =         isDefined( this.options.max ) ? float( this.options.max ) : 100;
        this.options.value =       isDefined( this.options.value ) ? float( this.options.value ) : 50;
        this.options.round =       isDefined( this.options.round ) ? truth( this.options.round ) : true;
        this.options.circle =      isDefined( this.options.circle ) ? truth( this.options.circle ) : true;
        this.options.padding =     isDefined( this.options.padding ) ? float( this.options.padding ) : 4;
        this.options.radius =      float( this.options.radius ) || 50;
        this.options.thickness =   float( this.options.thickness ) || 10;
        this.options.bg =          this.options.bg || "rgba(70, 130, 180, 0.15)";
        this.options.color =       this.options.color || "mediumslateblue";
        this.options.transition =  this.options.transition || "all 1.2s cubic-bezier(0.57, 0.13, 0.18, 0.98)";
        this.options.text =        isDefined( this.options.text ) ? this.options.text : false;

        this.init();

        return this;

    };

    donutty.prototype.getOptionsFromTag = function() {

        return JSON.parse(JSON.stringify(this.$wrapper.dataset));

    };

    donutty.prototype.init = function() {

        this.$wrapper.donutty = this;

        var values;

        // create the state object from the options,
        // and then get the dash values for use in element creation
        this.createState();
        values = this.getDashValues();

        this.createSvg();
        this.createBg( values );
        this.createDonut( values );
        this.createText();
        this.insertFragments( values );

        return this;

    };

    donutty.prototype.createState = function() {

        this.state.min = this.options.min;
        this.state.max = this.options.max;
        this.state.value = this.options.value;
        this.state.bg = this.options.bg;
        this.state.color = this.options.color;

        return this;

    };

    donutty.prototype.createText = function() {

        if ( typeof this.options.text === "function" ) {

            this.$text = doc.createElement( "span" );
            this.$text.setAttribute( "class", "donut-text" );
            this.$text.style.opacity = 0;
            this.updateText();

        }

        return this;

    };

    donutty.prototype.createBg = function( values ) {

        this.$bg = doc.createElementNS( namespace, "circle" );

        this.$bg.setAttribute( "cx", "50%" );
        this.$bg.setAttribute( "cy", "50%" );
        this.$bg.setAttribute( "r", this.options.radius );
        this.$bg.setAttribute( "fill", "transparent" );
        this.$bg.setAttribute( "stroke", this.state.bg );
        this.$bg.setAttribute( "stroke-width", this.options.thickness + this.options.padding );
        this.$bg.setAttribute( "stroke-dasharray", values.full * values.multiplier );
        this.$bg.setAttribute( "class", "donut-bg" );

        if ( this.options.round ) {
            this.$bg.setAttribute( "stroke-linecap", "round" );
        }

        return this;

    };

    donutty.prototype.createDonut = function( values ) {

        this.$donut = doc.createElementNS( namespace, "circle" );

        this.$donut.setAttribute( "fill", "transparent" );
        this.$donut.setAttribute( "cx", "50%" );
        this.$donut.setAttribute( "cy", "50%" );
        this.$donut.setAttribute( "r", this.options.radius );
        this.$donut.setAttribute( "stroke", this.state.color );
        this.$donut.setAttribute( "stroke-width", this.options.thickness );
        this.$donut.setAttribute( "stroke-dashoffset", values.full );
        this.$donut.setAttribute( "stroke-dasharray", values.full );
        this.$donut.setAttribute( "class", "donut-fill" );
        this.$donut.style.opacity = 0;

        if ( this.options.round ) {
            this.$donut.setAttribute( "stroke-linecap", "round" );
        }

        return this;

    };

    donutty.prototype.createSvg = function() {

        var viewbox = this.options.radius * 2 + this.options.thickness + ( this.options.padding * 2 ),
            rotateExtra = this.options.round ? this.options.thickness / 3 : 0,
            rotate = this.options.circle ? 90 + rotateExtra : -225;

        this.$html = doc.createDocumentFragment();
        this.$svg = doc.createElementNS( namespace, "svg" );

        this.$svg.setAttribute( "xmlns", namespace );
        this.$svg.setAttribute( "viewbox", "0 0 " + viewbox + " " + viewbox );
        this.$svg.setAttribute( "transform", "rotate( " + rotate +" )" );
        this.$svg.setAttribute( "preserveAspectRatio", "xMidYMid meet" );
        this.$svg.setAttribute( "class", "donut" );

        return this;

    };

    donutty.prototype.insertFragments = function( values ) {

        this.$svg.appendChild( this.$bg );
        this.$svg.appendChild( this.$donut );
        this.$html.appendChild( this.$svg );

        if ( this.$text ) {
            this.$html.appendChild( this.$text );
        }

        this.$wrapper.appendChild( this.$html );

        // because of a strange bug in browsers not updating
        // the "preserveAspectRatio" setting when applied programmatically,
        // we need to essentially delete the DOM fragment, and then
        // set the innerHTML of the parent so that it updates in browser.
        this.$wrapper.innerHTML = this.$wrapper.innerHTML;

        // and because we just destroyed the DOM fragment and all
        // the references to it, we now set all those references again.
        this.$svg = this.$wrapper.querySelector(".donut");
        this.$bg = this.$wrapper.querySelector(".donut-bg");
        this.$donut = this.$wrapper.querySelector(".donut-fill");
        if ( this.$text ) {
            this.$text = this.$wrapper.querySelector(".donut-text");
        }

        // now the references are re-set, we can go
        // ahead and animate the element again.
        this.animate( values.fill, values.full );

    };

    donutty.prototype.getDashValues = function() {

        var circumference,
            percentageFilled,
            absoluteFilled,
            multiplier;

        multiplier = this.options.circle ? 1 : 0.75;
        circumference = 2 * Math.PI * this.options.radius;
        percentageFilled = ( this.state.value - this.state.min ) / ( this.state.max - this.state.min ) * 100;
        absoluteFilled = circumference - ( ( circumference * multiplier ) / 100 * percentageFilled );

        if (
            this.options.round &&
            this.options.circle &&
            percentageFilled < 100 &&
            absoluteFilled < this.options.thickness
        ) {

            // when in circle mode, if the linecaps are "round"
            // then the circle would look complete if it is actually
            // only ~97% complete, this is because the linecaps
            // overhang the stroke.
            absoluteFilled = this.options.thickness;

        }

        return {
            fill: absoluteFilled,
            full: circumference,
            multiplier: multiplier
        };

    };

    donutty.prototype.animate = function( fill, full ) {

        var _this = this;

        // ensure the transition property is applied before
        // the actual properties are set, so that browser renders
        // the transition
        _this.$bg.style.transition = this.options.transition;
        _this.$donut.style.transition = this.options.transition;
        if ( _this.$text ) {
            _this.$text.style.transition = this.options.transition;
        }

        // use a short timeout (~60fps) to simulate a new
        // animation frame (not using rAF due to ie9 problems)
        window.setTimeout( function() {

            _this.$bg.setAttribute( "stroke", _this.state.bg );
            _this.$bg.style.opacity = 1;

            _this.$donut.setAttribute( "stroke-dashoffset", fill );
            _this.$donut.setAttribute( "stroke-dasharray", full );
            _this.$donut.setAttribute( "stroke", _this.state.color );
            _this.$donut.style.opacity = 1;

            if ( _this.$text ) {
                _this.$text.style.opacity = 1;
            }

        }, 16 );

    };

    /**
     * use the current state to set the text inside
     * the text element (only if option is provided);
     * @return {object} the donut instance
     */
    donutty.prototype.updateText = function() {

        if ( typeof this.options.text === "function" ) {

            this.$text.innerHTML = this.options.text( this.state );

        }

        return this;

    };

    /**
     * set an individual state property for the chart
     * @param  {string}         prop the property to set
     * @param  {string/number}  val  the value of the given property
     * @return {object}              the donut instance
     * @chainable
     */
    donutty.prototype.set = function( prop, val ) {

        var values;

        if ( isDefined( prop ) && isDefined( val ) ) {

            this.state[ prop ] = val;
            values = this.getDashValues();
            this.updateText();
            this.animate( values.fill, values.full );

        }

        return this;

    };

    /**
     * set multiple state properties with an object
     * @param  {object} newState a map of properties to set
     * @return {object}          the donut instance
     * @chainable
     */
    donutty.prototype.setState = function( newState ) {

        var values;

        if ( isDefined( newState.value ) ) {
            this.state.value = newState.value;
        }

        if ( isDefined( newState.min ) ) {
            this.state.min = newState.min;
        }

        if ( isDefined( newState.max ) ) {
            this.state.max = newState.max;
        }

        if ( isDefined( newState.bg ) ) {
            this.state.bg = newState.bg;
        }

        if ( isDefined( newState.color ) ) {
            this.state.color = newState.color;
        }

        values = this.getDashValues();
        this.updateText();
        this.animate( values.fill, values.full );

        return this;

    };

}( document, window ));

// jquery constructor

( function( Donutty, $ ) {

    if ( typeof window.$ !== "undefined" ) {

        $( function() {

            $.fn.donutty = function( options ) {

                return $( this ).each( function() {

                    new Donutty( this, options );

                });

            };

            $( "[data-donutty]" ).donutty();

        });

    } else {

        console.warn( "Can't find jQuery to attach Donutty" );

    }

}( Donutty, jQuery ));
