// This should work both in node and in the browsers, so that's what this wrapper is about
;(function(root, undefined) {


    /**
     * Timecode object constructor
     * @param {number|String|Date|Object} timeCode Frame count as number, "HH:MM:SS(:|;|.)FF", Date(), or object.
     * @param {number} [frameRate=29.97] Frame rate
     * @param {boolean} [dropFrame=true] Whether the timecode is drop-frame or not
     * @constructor
     * @returns {Timecode} timecode
     */
    var Timecode = function ( timeCode, frameRate, dropFrame ) {

        // Make this class safe for use without "new"
        if (!(this instanceof Timecode)) return new Timecode( timeCode, frameRate, dropFrame);

        // Get frame rate
        if (typeof frameRate === 'undefined') this.frameRate = 29.97;
        else if (typeof frameRate === 'number' && frameRate>0) this.frameRate = frameRate;
        else throw new Error('Number expected as framerate');
        if (this.frameRate!==23.976 && this.frameRate!==24 && this.frameRate!==25 && this.frameRate!==29.97 && this.frameRate!==30 &&
            this.frameRate!==50 && this.frameRate!==59.94 && this.frameRate!==60
        ) throw new Error('Unsupported framerate');

        // If we are passed dropFrame, we need to use it
        if (typeof dropFrame === 'boolean') this.dropFrame = dropFrame;
        else this.dropFrame = (this.frameRate===29.97 || this.frameRate===59.94); // by default, assume DF for 29.97 and 59.94, NDF otherwise

        // Now either get the frame count, string or datetime        
        if (typeof timeCode === 'number') {
            this.frameCount = Math.round(timeCode);
            this._frameCountToTimeCode();
        }
        else if (typeof timeCode === 'string') {
            // pick it apart
            var parts = timeCode.match('^([012]\\d):(\\d\\d):(\\d\\d)(:|;|\\.)(\\d\\d)$');
            if (!parts) throw new Error("Timecode string expected as HH:MM:SS:FF or HH:MM:SS;FF");
            this.hours = parseInt(parts[1]);
            this.minutes = parseInt(parts[2]);
            this.seconds = parseInt(parts[3]);
            // do not override input parameters
            if (typeof dropFrame !== 'boolean') {
                this.dropFrame = parts[4]!==':';
            }
            this.frames = parseInt(parts[5]);
            this._timeCodeToFrameCount();
        }
        else if (typeof timeCode === 'object' && timeCode instanceof Date) {
            var midnight = new Date(timeCode.getFullYear(), timeCode.getMonth(), timeCode.getDate(),0,0,0);
            var midnight_tz = midnight.getTimezoneOffset() * 60 * 1000;
            var timecode_tz = timeCode.getTimezoneOffset() * 60 * 1000;
            this.frameCount = Math.round(((timeCode-midnight + (midnight_tz - timecode_tz))*this.frameRate)/1000);
            this._frameCountToTimeCode();
        }
        else if (typeof timeCode === 'object' && timeCode.hours >= 0) {
            this.hours = timeCode.hours;
            this.minutes = timeCode.minutes;
            this.seconds = timeCode.seconds;
            this.frames = timeCode.frames;
            this._timeCodeToFrameCount();
        }
        else if (typeof timeCode === 'undefined') {
            this.frameCount = 0;
        }
        else {
            throw new Error('Timecode() constructor expects a number, timecode string, or Date()');
        }

        this._validate(timeCode);

        return this;
    };

    /**
     * Validates timecode
     * @private
     * @param {number|String|Date|Object} timeCode for the reference
     */
    Timecode.prototype._validate = function (timeCode) {

        // Make sure dropFrame is only for 29.97 & 59.94
        if (this.dropFrame && this.frameRate!==29.97 && this.frameRate!==59.94) {
            throw new Error('Drop frame is only supported for 29.97 and 59.94 fps');
        }

        // make sure the numbers make sense
        if (this.hours > 23 || this.minutes > 59 || this.seconds > 59 || this.frames >= this.frameRate ||
            (this.dropFrame && this.seconds === 0 && this.minutes % 10 && this.frames < 2 * (this.frameRate / 29.97))) {
            throw new Error("Invalid timecode" + JSON.stringify(timeCode));
        }
    };

    /**
     * Calculate timecode based on frame count
     * @private
     */
    Timecode.prototype._frameCountToTimeCode = function() {
        var fc = this.frameCount;
        // adjust for dropFrame
        if (this.dropFrame) {
            var df = this.frameRate===29.97 ? 2 : 4; // 59.94 skips 4 frames
            var d = Math.floor(this.frameCount / (17982*df/2));
            var m = this.frameCount % (17982*df/2);
            if (m<df) m=m+df;
            fc += 9*df*d + df*Math.floor((m-df)/(1798*df/2));
        }
        var fps = Math.round(this.frameRate);
        this.frames = fc % fps;
        this.seconds = Math.floor(fc/fps) % 60;
        this.minutes = Math.floor(fc/(fps*60)) % 60;
        this.hours = Math.floor(fc/(fps*3600)) % 24;
    };

    /**
     * Calculate frame count based on time Timecode
     * @private
     */
    Timecode.prototype._timeCodeToFrameCount = function() {
        this.frameCount = (this.hours*3600 + this.minutes*60 + this.seconds) * Math.round(this.frameRate) + this.frames;
        // adjust for dropFrame
        if (this.dropFrame) {
            var totalMinutes = this.hours*60 + this.minutes;
            var df = this.frameRate === 29.97 ? 2 : 4;
            this.frameCount -= df * (totalMinutes - Math.floor(totalMinutes/10));    
        }
    };

    /**
     * Convert Timecode to String
     * @param {String} format output format
     * @returns {string} timecode
     */
    Timecode.prototype.toString = function TimeCodeToString(format) {
        var frames = this.frames;
        var field = '';
        if (typeof format === 'string') {
            if (format === 'field') {
                if (this.frameRate<=30) field = '.0';
                else {
                    frames = Math.floor(frames/2);
                    field = '.'.concat((this.frameCount%2).toString());
                };
            }
            else throw new Error('Unsupported string format');
        };
        return "".concat(
            this.hours<10 ? '0' : '',
            this.hours.toString(),
            ':',
            this.minutes<10 ? '0' : '',
            this.minutes.toString(),
            ':',
            this.seconds<10 ? '0' : '',
            this.seconds.toString(),
            this.dropFrame ? ';' : ':',
            frames<10 ? '0' : '',
            frames.toString(),
            field
        );
    };

    /**
     * @returns {Number} the frame count when Timecode() object is used as a number
     */
    Timecode.prototype.valueOf = function() {
        return this.frameCount;
    };

    /**
     * Adds t to timecode, in-place (i.e. the object itself changes)
     * @param {number|string|Date|Timecode} t How much to add
     * @param {boolean} [negative=false] Whether we are adding or subtracting
     * @param {Number} [rollOverMaxHours] allow rollovers
     * @returns {Timecode} timecode
     */
    Timecode.prototype.add = function (t, negative, rollOverMaxHours) {
        if (typeof t === 'number') {
            var newFrameCount = this.frameCount + Math.round(t) * (negative?-1:1);
            if (newFrameCount<0 && rollOverMaxHours > 0) {
                newFrameCount = (Math.round(this.frameRate*86400)) + newFrameCount;
                if (((newFrameCount / this.frameRate) / 3600) > rollOverMaxHours) {
                    throw new Error('Rollover arithmetic exceeds max permitted');
                }
            }
            if (newFrameCount<0) {
                throw new Error("Negative timecodes not supported");
            }
            this.frameCount = newFrameCount;
        }
        else {
            if (!(t instanceof Timecode)) t = new Timecode(t, this.frameRate, this.dropFrame);
            return this.add(t.frameCount,negative,rollOverMaxHours);
        }
        this.frameCount = this.frameCount % (Math.round(this.frameRate*86400)); // wraparound 24h
        this._frameCountToTimeCode();
        return this;
    };


    Timecode.prototype.subtract = function(t, rollOverMaxHours) {
        return this.add(t,true,rollOverMaxHours);
    };

    /**
     * Converts timecode to a Date() object
     * @returns {Date} date
     */
    Timecode.prototype.toDate = function() {
        var ms = this.frameCount/this.frameRate*1000;
        var midnight = new Date();
        midnight.setHours(0);
        midnight.setMinutes(0);
        midnight.setSeconds(0);
        midnight.setMilliseconds(0);

        var d = new Date( midnight.valueOf() + ms );
        var midnight_tz = midnight.getTimezoneOffset() * 60 * 1000;
        var timecode_tz = d.getTimezoneOffset() * 60 * 1000;
        return new Date( midnight.valueOf() + ms + (timecode_tz-midnight_tz));
    };

    // Export it for Node or attach to root for in-browser
    /* istanbul ignore else */
    if (typeof exports === 'object' && typeof exports.nodeName !== 'string') {
        module.exports = Timecode;
    } else if (root) {
        root.Timecode = Timecode;
    }


}(this));