﻿(function () {

    // Globals
    this.options = {};
    this.ready = false;
    this.runningInterval = null;
    this.timerDisplay = null;

    // Internal Constants
    var aDay = 1000 * 60 * 60 * 24;
    var aHour = 1000 * 60 * 60;
    var aMinute = 1000 * 60;
    var aSecond = 1000;

    /**
     * @description
     * Constructor accepts an options object
     * @param {Object} options
     * @param {Object} options.targetElement  Element the timer will be appended to
     * @param {Boolean} options.emptyTargetElement If true will empty the target element before appending timer.
     * @param {String} options.cssClass  CSS class(es) you wish added to the block element
     * @param {Date} options.targetTime  The DateTime the timer will count down to
     * @param {String} options.duration  Alternate way to set target time example (1d 1h 1m)
     * @param {Function} options.callback Callback function will have scope of targetElement and receive options as argument.
     * @param {Boolean} options.removeOnComplete Will physically remove the display elements on completion of timer.
     * @param {Boolean} options.autoStart Will start the timer upon initialization if true
     * @param {Object[]} options.timerCallbacks 
     * @param {Int} options.timerCallbacks[].timeRemaining  The time remaining in seconds
     * @param {Function} options.timerCallbacks[].callback The callback method will have scope of the targetElement and will receive the seconds remaining.
     */
    this.PcsTimer = function () {

        // Define option defaults
        var defaultOptions = {
            targetElement: $j("body"),
            emptyTargetElement: false,
            cssClass: "",
            targetTime: null,
            duration: null,
            callback: null,
            removeOnComplete: true,
            autoStart: true,
            timerCallbacks: []
        }
        this.options = defaultOptions;
        if (arguments[0] && typeof arguments[0] === "object") {
            $j.extend(this.options, defaultOptions, arguments[0]);
        }
        configureTimer(this.options, this.options.duration);
        this.ready = true;
        if (this.options.autoStart) {
            this.start();
        }
    }

    /**
     * Start the Timer
     * @method start
     */
    PcsTimer.prototype.start = function () {
        if (!this.ready) {
            pcsTimerError("Timer is not ready to start.");
        }
        validateOptions(this.options);
        if (this.runningInterval === undefined || this.runningInterval == null) {
            if (this.timerDisplay === undefined || this.timerDisplay === null) {
                addTimeDisplay(this);
            }
            this.runningInterval = setInterval(updateDisplay, 500, this);
        }

        return this;
    }

    /** 
     * Stop the Timer
     * @method stop
     * @param {Boolean} removeDisplay  Will leave display at 0.00 if set to false
     */
    PcsTimer.prototype.stop = function (removeDisplay) {
        clearInterval(this.runningInterval);
        this.runningInterval = null;
        if (removeDisplay !== undefined && removeDisplay) {
            $j(this.timerDisplay).remove();
            this.timerDisplay = null;
        }

        return this;
    }

    /**
     * Removes the timer from the DOM
     * @method remove
     */
    PcsTimer.prototype.remove = function () {
        this.stop(true);
        return this;
    }

    /**
     * Set the target time to count down to
     * @method setTargetTime
     * @param {Date} targetTime  The DateTime the timer will count down to
     */
    PcsTimer.prototype.setTargetTime = function (targetTime) {
        if (Object.prototype.toString.call(targetTime) !== "[object Date]") {
            pcsTimerError("Invalid targetTime passed to setTargetTime.");
        }
        this.options.targetTime = targetTime;
        return this;
    }

    /**
     * Set the duration of the timer
     * @method setDuration
     * @param {String} duration  Alternate way to set target time example (1d 1h 1m)
     */
    PcsTimer.prototype.setDuration = function (duration) {
        var ms = convertDurationToMilliseconds(duration);
        this.setTargetTime(new Date(new Date().getTime() + ms));
        return this;
    }

    /**
     * Get the remaining time in Milliseconds
     * @method getRemainingTime
     */
    PcsTimer.prototype.getRemainingTime = function() {
        var now = new Date();
        return this.options.targetTime - now;
    }


    // Private Functions ===============================================================================================
    function validateOptions(options) {
        if (typeof options.targetElement !== "object") {
            pcsTimerError("Target element is required.");
        }
    }

    function configureTimer(options, duration) {
        if (Object.prototype.toString.call(options.targetTime) !== "[object Date]") {
            if (duration !== null) {
                var ms = convertDurationToMilliseconds(options.duration);
                options.targetTime = new Date(new Date().getTime() + ms);
            }
        }
    }

    function convertDurationToMilliseconds(duration) {
        if (typeof duration !== "string" || duration.length === 0) {
            pcsTimerError("Must set either the targetTime or the duration in options");
        }
        var dArray = duration.split(" ");
        var milliseconds = 0;
        $j.each(dArray, function (loc, element) {
            timeChar = String(element.substr(element.length - 1)).toLowerCase();
            if ("dhms".indexOf(timeChar) === -1) {
                pcsTimerError("Invalid duration. " + timeChar
                    + " is not recognized.  Must be 'd', 'h', 'm', or 's'. Day, Hour, Minute, Second");
            }
            timeNum = parseInt(element.replace(timeChar, ""));
            if (isNaN(timeNum)) {
                pcsTimerError("'" + element.replace(timeChar, "") + "' is not a valid number.");
            }
            switch (timeChar) {
                case "d":
                    milliseconds += timeNum * aDay;
                    break;
                case "h":
                    milliseconds += timeNum * aHour;
                    break;
                case "m":
                    milliseconds += timeNum * aMinute;
                    break;
                case "s":
                    milliseconds += timeNum * aSecond;
                    break;
            }
        });
        return milliseconds;
    }

    function pcsTimerError(message) {
        throw new Error("pscTimer error: " + message);
    }

    function addTimeDisplay(pcsTimer) {
        var options = pcsTimer.options;
        var td = $j("<div/>").addClass("PcsTimer");
        if (typeof options.cssClass === "string") {
            $j(td).addClass(options.cssClass);
        }
        if (options.emptyTargetElement) {
            $j(options.targetElement).html("");
        }
        $j(options.targetElement).append(td);
        pcsTimer.timerDisplay = td;
    }

    function updateDisplay(pcsTimer) {
        var options = pcsTimer.options;
        var now = new Date();
        var millisecondsBetween = options.targetTime - now;
        // Handle completion
        if (millisecondsBetween <= 0) {
            pcsTimer.stop(options.removeOnComplete);
            if (options.callback !== null) {
                options.callback.call(options.targetElement, options);
            }
            return;
        }
        var seconds = Math.round(millisecondsBetween / 1000);
        $j.each(options.timerCallbacks, function() {
            if (this.timeRemaining === seconds && !this.sent) {
                this.callback.call(options.targetElement, seconds);
                this.sent = true;
            }
        });

        var display = buildDisplay(millisecondsBetween);

        $j(pcsTimer.timerDisplay).html(display);
    }

    function buildDisplay(millisecondsBetween) {
        var days = 0;
        var hours = 0;
        var minutes = 0;
        var seconds = 0;
        if (millisecondsBetween > aDay) {
            days = Math.floor(millisecondsBetween / aDay);
            millisecondsBetween -= (aDay * days);
        }
        if (millisecondsBetween > aHour) {
            hours = Math.floor(millisecondsBetween / aHour);
            millisecondsBetween -= (aHour * hours);
        }
        if (millisecondsBetween > aMinute) {
            minutes = Math.floor(millisecondsBetween / aMinute);
            millisecondsBetween -= (aMinute * minutes);
        }
        if (millisecondsBetween > aSecond) {
            seconds = Math.floor(millisecondsBetween / aSecond);
        }
        var display = "";
        if (days > 0) {
            display += String(days) + " Days ";
        }
        if (hours > 0) {
            display += String(hours) + ":";
        }
        display += String(padTimeDisplay(minutes))
            + ":" + padTimeDisplay(seconds);
        return display;
    }

    function padTimeDisplay(timeSlice) {
        timeSlice = String(timeSlice);
        if (timeSlice.length === 1) {
            return "0" + timeSlice;
        }
        return timeSlice;
    }
}());