/**
 * Fuzzy Moment - convert an absolute date text into a fuzzy moment
 *
 * containerClass string The class name of dom element to convert (default: 'fuzzydate')
 * dateHasTime boolean Whether containerClass contains a time (default: false)
 * dateFormat string The python token date formatting
 * timeFormat string The python token time formatting
 * trimZero Whether to trim leading "0"s (default : false)
 * dtGlue string To insert between the output of date and time (default: '<br />')
 */
 function fuzzyMoment(fmConfig) {

    var containerClass = (/undefined/i.test(typeof(fmConfig)) || /undefined/i.test(typeof(fmConfig.containerClass)) ? '.fuzzydate' : fmConfig.containerClass),
        dateWithTime = (/undefined/i.test(typeof(fmConfig)) || /undefined/i.test(typeof(fmConfig.dateHasTime)) ? false : !!fmConfig.dateHasTime),
        dateFormat = (/undefined/i.test(typeof(fmConfig)) || /undefined/i.test(typeof(fmConfig.dateFormat)) ? '' : fmConfig.dateFormat),
        timeFormat = (/undefined/i.test(typeof(fmConfig)) || /undefined/i.test(typeof(fmConfig.timeFormat)) ? '' : fmConfig.timeFormat),
        trimZero = (/undefined/i.test(typeof(fmConfig)) || /undefined/i.test(typeof(fmConfig.trimZero)) ? false : !!fmConfig.trimZero),
        dtGlue = (/undefined/i.test(typeof(fmConfig)) || /undefined/i.test(typeof(fmConfig.dtGlue)) ? '<br />' : fmConfig.dtGlue),

        jd = (function (str) {
            var token_map = ['a', 'ddd', 'A', 'dddd', 'b', 'MMM', 'B', 'MMMM', 'd', 'DD', 'm', 'MM', 'y', 'YY', 'Y', 'YYYY', 'x', 'L',
                             'H', 'HH', 'I', 'hh', 'M', 'mm', 'S', 'ss', 'p', 'A'],
                result = '';

            for (var i = 0; i < str.length; i++)
                if (/[aAbBdmyYxHIMSp]/.test(str[i])) {
                    for (var t = 0; t < token_map.length; t = t + 2)
                        if (str[i] == token_map[t]) {
                            result += token_map[t + 1];
                            break;
                        }
                } else if ('%' != str[i])
                    result += str[i];

            return result;
        }),
        dateToken = jd(dateFormat),
        timeToken = jd(timeFormat),

        addQTip = (function() {
            $(this).css('cursor', 'help');
            $(this).qtip({
                show: {
                    solo: true
                },
                position: {
                    viewport: $(window),
                    my: 'left center',
                    adjust: {
                        y: -10,
                        x: 2
                    }
                },
                style: {
                    tip: {
                        corner: true,
                        method: 'polygon'
                    },
                    classes: 'qtip-rounded qtip-dark qtip-shadow ui-tooltip-sb'
                }
            });
        });

    if (trimZero) {
        timeToken = timeToken.replace(/hh/ig, 'h');
        dateToken = dateToken.replace(/\bDD\b/g, 'D');
    }

    $(containerClass).each(function() {
        var input = $(this).text(),
            dateA = '[<span class="fd">',
            dtSeparator = ' ',
            timeA = '</span>]', timeB = '[' + timeA;

        if (dateWithTime) {
            var timeMeta = input.match(/^.{6,}?([,\s]+)(\d{1,2}).(?:\d{2,2})(?:.(\d{2,2}))?(?:\s([ap]m))?$/im);
            if (null != timeMeta) {
                dtSeparator = (! /undefined/i.test(typeof(timeMeta[1])) ? timeMeta[1] : dtSeparator);
                // adjust timeToken to num digits of input hours
                timeToken = (! /undefined/i.test(typeof(timeMeta[2])) && 1 == timeMeta[2].length ? timeToken.replace(/hh/ig, 'h') : timeToken);
                // adjust timeToken to use seconds if input has them
                timeToken = (! /undefined/i.test(typeof(timeMeta[3])) && 2 == timeMeta[3].length ? timeToken : timeToken.replace(/.ss/, ''));
                // adjust timeToken to am/pm or AM/PM if input has it
                timeToken = (! /undefined/i.test(typeof(timeMeta[4])) && 2 == timeMeta[4].length ? timeToken.replace(/A$/, (/[ap]m/.test(timeMeta[4]) ? 'a' : 'A')) : timeToken);
            }
            timeA = '</span>' + dtGlue + '<span class="ft">]' + timeToken + '[' + timeA;
            timeB = '[</span>' + dtGlue + '<span class="ft">]' + timeToken + timeB;
        }

        var inputTokens = dateToken + dtSeparator + (dateWithTime ?  timeToken : 'HH:mm:ss');

        if (! moment(input + (dateWithTime ? '' : dtSeparator + '00:00:00'), inputTokens).isValid())
            return;

        moment.lang('en', {
            calendar: {
                lastDay:dateA + 'Yesterday' + timeA, sameDay:dateA + 'Today' + timeA, nextDay:dateA + 'Tomorrow' + timeA,
                lastWeek:dateA + 'last] ddd' + timeB, nextWeek:dateA + 'on] ddd' + timeB,
                sameElse:dateA + ']ddd, MMM D YYYY[' + timeA
            },
            relativeTime: {
                future:'in %s', past:'%s ago', s:'seconds', m:'a minute', mm:'%d minutes', h:'an hour', hh:'%d hours',
                d:'a day', dd:'%d days', M:'a month', MM:'%d months', y:'a year', yy:'%d years'
            }
        });

        var airdatetime = moment(input + (dateWithTime ? '' : dtSeparator + '00:00:00'), inputTokens),
            airdate = airdatetime.clone().hour(0).minute(0).second(0).millisecond(0),
            today = moment({}),
            day = Math.abs(airdate.diff(today, 'days')),
            week = airdate.diff(today, 'week'), isPast = week < 0, week = Math.abs(week),
            titleThis = false, qTipTime = false
            result = (0 == week ? airdatetime.calendar() : '');

        if (/\bOn\b/i.test(result)) {
           var fuzzer = false, weekday = today.format('dd');
            if (/we/i.test(weekday))
                fuzzer = (5 <= day);
            else if (/(?:th|fr)/i.test(weekday))
                fuzzer = (4 <= day);
            else
                fuzzer = (6 == day);
            if (fuzzer)
                result = result.replace(/\bOn\b/i, 'Next');

        } else if (! /\b((yester|to)day\b|tomo|last\b)/i.test(result)) {
            if (14 > day)
                result = airdate.from(today) + (dateWithTime ? dtGlue + airdatetime.format(timeToken) : '');
            else if (4 > week) {
                result = (isPast ? '' : 'in ') + (1 == week ? 'a' : week) + ' week' + (1 == week ? '' : 's') + (isPast ? ' ago' : '');
                qTipTime = true;
            } else {
                result = airdate.from(today);
                qTipTime = true;
                var month = airdate.diff(today, 'month');
                if (1 == parseInt(airdate.year() - today.year()))
                    result += '<br />(Next Year)';
            }
            titleThis = true;
        }

        var n = false; // disable for prod
        $(this).html(result);
        if (dateWithTime && /(yester|to)day/i.test(result))
            $(this).find('.fd').attr('title',(n?'1) ':'') + moment.duration(airdatetime.diff(moment(),'seconds'),'seconds').humanize(true)).each(addQTip);
        else if (dateWithTime)
            $(this).find('.fd').attr('title',(n?'2) ':'') + airdate.from(today)).each(addQTip);
        else if (! /today/i.test(result))
            $(this).find('.fd').attr('title',(n?'3) ':'') + airdate.from(today)).each(addQTip);
        else
            titleThis = false;

        if (titleThis)
            if (dateWithTime && qTipTime)
                $(this).attr('title',(n?'4) ':'') + airdatetime.format(inputTokens)).each(addQTip);
            else
                $(this).attr('title',(n?'5) ':'') + airdate.format(dateToken)).each(addQTip);
        else
            if (dateWithTime && qTipTime)
                $(this).find('.ft').attr('title',(n?'6) ':'') + airdatetime.format(inputTokens)).each(addQTip);
            else
                $(this).find('.ft').attr('title',(n?'7) ':'') + airdate.format(dateToken)).each(addQTip);
    });
}
