// ==UserScript==
// @name         Chaturbate Easy Tipping Fixed Version
// @namespace    madTipper
// @version      0.13.1
// @author       omgmikey - fixed on 1/21/2018 by tblopgreg
// @match        https://es.chaturbate.com/*
// @require      https://ajax.googleapis.com/ajax/libs/jquery/3.2.1/jquery.min.js
// @require      https://ajax.googleapis.com/ajax/libs/jqueryui/1.12.1/jquery-ui.min.js
// @require      https://cdnjs.cloudflare.com/ajax/libs/jquery-cookie/1.4.1/jquery.cookie.min.js
// @grant        GM_getValue
// @grant        GM_setValue
// @license      MIT
// @run-at       document-idle
// @description Adds a new tipping popup and modifies the existing one
// ==/UserScript==
var CSS_GREY  = {'color': 'rgb(88,141,61)'};
var CSS_WHITE = {'color': '#FFFFFF'};
var CSS_BLACK = {'color': '#000000'};
var ID_PREFIX    = '#madTipper'
var CLASS_PREFIX = '.madTipper'
var CLASS_INPUT  = CLASS_PREFIX + '_input';
var HTML_IDS = {
    'BUTTON': 'button',
    'POPUP': 'popup',
    'AMOUNT': 'amount',
    'COUNT': 'count',
    'INTERVAL': 'interval',
    'VARIANCE_LOWER': 'variance_lower',
    'VARIANCE_UPPER': 'variance_upper',
    'START': 'start',
    'STOP': 'stop',
    'TOTAL': 'total',
    'ETA': 'eta'
}
for (var key in HTML_IDS) {
    HTML_IDS[key] = ID_PREFIX + '_' + HTML_IDS[key];
}
var shell = $('.tip_shell');
var tipsLeft = 0;
var tipFunctionTimeout = null;
var juration = loadJuration();
(function initialize() {
    createTipperButton();
    createTipperPopup();
    injectCSS();
    improveDefaultTipPopup();
    loadPreviousSettings();
    initializeButtonCallbacks();
    updateTipperButton();
})();
function createTipperButton() {
    shell.append('<div id="madTipper_button_bg"><a href="#" id="madTipper_button"></a></div>');
}
function updateTipperButton() {
    if (tipsLeft == 0) {
        $(HTML_IDS['BUTTON']).html('MAD TIPPER').css({'width': '80px'});
    }
    else {
        $(HTML_IDS['BUTTON']).html('MAD TIPPER (' + tipsLeft + ')').css({'width': '120px'});
    }
}
function createTipperPopup() {
    shell.append(
        '<div class="overlay_popup" id="madTipper_popup">' +
            '<table width="100%" border="0" cellspacing="0" cellpadding="0">' +
                '<tbody>' +
                    '<tr>' +
                        '<td class="formborder">' +
                            '<div class="title">Mad Tipper</div>' +
                            '<div class="body">' +
                                '<form>' +
                                    '<label>Amount per tip:</label><br >' +
                                    '<input type="text" id="madTipper_amount" class="madTipper_input">' +
                                    '<br />' +
                                    '<label>Number of tips:</label><br >' +
                                    '<input type="text" id="madTipper_count" class="madTipper_input">' +
                                    '<br /><hr />' +
                                    '<label>Interval:</label><br >' +
                                    '<input type="text" id="madTipper_interval" class="madTipper_input">' +
                                    '<br />' +
                                    '<label>Interval variance lower (optional):</label><br >' +
                                    '<input type="text" id="madTipper_variance_lower" class="madTipper_input">' +
                                    '<br />' +
                                    '<label>Interval variance upper (optional):</label><br >' +
                                    '<input type="text" id="madTipper_variance_upper" class="madTipper_input">' +
                                    '<br /><hr />' +
                                    'Total tip:  ' + '<a id="madTipper_total"></a>' +
                                    '<br />' +
                                    'Estimated duration:  ' + '<a id="madTipper_eta"></a>' +
                                '</form>' +
                                '<hr />' +
                                '<button id="madTipper_start">Start</button>' +
                                '<button id="madTipper_stop" disabled="disabled">Stop</button>' +
                            '</div>' +
                        '</td>' +
                    '</tr>' +
                '</tbody>' +
            '</table>' +
        '</div>'
    );
}
function injectCSS() {
    var buttonBackgroundUrl =
        'url("https://ssl-ccstatic.highwebmedia.com/images/btn-sprites2.gif?ac5eba7d5cf3") no-repeat right';
    var buttonFontFamily =
        'UbuntuMedium,Arial,Helvetica,sans-serif';
    var genericButtonCSS = {
        'height':'21px',
        'width':'100px',
        'padding-left':'10px',
        'margin-right':'10px',
        'font-size':'12px',
        'text-shadow':'1px 1px 0 #588d3d',
        'color': '#FFFFFF'
    };
    genericButtonCSS['font-family'] = buttonFontFamily;
    genericButtonCSS['background'] = buttonBackgroundUrl + ' -84px';
    var mainButtonCSS = {
        'position': 'absolute',
        'z-index': 1000,
        'left': '500px',
        'top': '30px',
        'height': '18px',
        'padding': '3px 10px 0 0',
        'text-decoration': 'none',
        'text-align': 'center',
        'width': '80px'
    }
    for (var key in genericButtonCSS) {
        if (mainButtonCSS[key] === undefined) {
            mainButtonCSS[key] = genericButtonCSS[key];
        }
    }
    $(HTML_IDS['BUTTON']).css(mainButtonCSS);
    $(CLASS_INPUT).css({
        'width': 'auto',
        'margin-bottom': '10px'
    });
    $(HTML_IDS['POPUP']).css({
        'position': 'absolute',
        'z-index': 1000,
        'width': '280px',
        'top': '-456px',
        'left': '452px',
        'display': 'none'
    }).draggable();
    $(HTML_IDS['POPUP'] + ' .formborder').css({
        'border-bottom': '2px solid #0b5d81',
        'height': '420px'
    });
    $(HTML_IDS['START']).css(genericButtonCSS);
    genericButtonCSS['background'] = buttonBackgroundUrl + ' -42px';
    delete genericButtonCSS['color'];
    $(HTML_IDS['STOP']).css(genericButtonCSS);
}
function improveDefaultTipPopup() {
    $('.overlay_popup.tip_popup').css({
        'top': '-240px'
    }).draggable();
    $('#tip_message').css({
        'margin-bottom': '20px'
    })
    .append('<input type="checkbox" class="float_right" id="tip_keepopen"></input><br />')
    .append('<br /><label class="float_right" for="tip_keepopen">Keep this window open after tipping</label>');
    $('.float_right').css({
        'float': 'right'
    });
    setPopupHeight('250px');
    var tipPopup = $('.tip_popup');
    var keepOpenCheckbox = $('#tip_keepopen');
    var popupIsForcedOpen = false;
    /* use CB jquery to ensure correct callback execution order */
    var tipPopupForm = defchat_settings.domroot.find('.tip_popup form');
    tipPopupForm.submit(onFormSubmit);
    keepOpenCheckbox.css({
        'margin-top': '10px'
    });
    $('body').click(function(ev) {
        if ($('.tip_button').is(ev.target)) {
            popupIsForcedOpen = false;
            return;
        }
        if (!popupIsForcedOpen || tipPopup.has(ev.target).length) {
            return;
        }
        if (tipPopup.is(':visible')) {
            tipPopup.hide();
        }
        popupIsForcedOpen = false;
    });
    function onFormSubmit() {
        setPopupHeight('270px');
        if (!keepOpenCheckbox.is(':checked')) {
            return;
        }
        if (!tipPopup.is(':visible')) {
            tipPopup.show();
            popupIsForcedOpen = true;
        }
    }
    function setPopupHeight(value) {
        $('.overlay_popup.tip_popup .formborder').css({
            'height': value,
        });
    }
}
function startTipping() {
    var err = verifyTipperFields();
    if (err) {
        alert(err);
        stopTipping();
        return;
    }
    saveCurrentSettings();
    $(HTML_IDS['START']).prop('disabled', true).css(CSS_GREY);
    $(HTML_IDS['STOP']).prop('disabled', false).css(CSS_WHITE);
    $(CLASS_INPUT).prop('disabled', true).css(CSS_GREY);
    tipsLeft = getTipCount();
    /* we really want to send the first one immediately */
    sendTip();
    if (tipsLeft > 0) {
        chainQueueTips();
    }
}
function verifyTipperFields() {
    function isInt(value) {
        var regex = /^[0-9]+$/;
        return regex.test(String(value));
    }
    function isDuration(value) {
        try {
            juration.parse(value);
            return true;
        }
        catch(ex) {
            return false;
        }
    }
    function isDurationOrEmpty(value) {
        return value === '' || isDuration(value);
    }
    if (!isInt(getTipAmountRaw()) || getTipAmount() <= 0) {
        return 'Tip amount field should be a positive integer.';
    }
    if (!isInt(getTipCountRaw()) || getTipCount() <= 0) {
        return 'Tip count field should be a positive integer.';
    }
    if (!isDuration(getTipInterval())) {
        return 'Tip interval should contain a duration. E.g.: "2.5s", "1", "2min"';
    }
    if (!isDurationOrEmpty(getVarianceLowerRaw()) || !isDurationOrEmpty(getVarianceUpperRaw())) {
        return 'Variance fields should contain durations, or be left blank. E.g.: "", "2.5s"';
    }
}
function getSleepInterval() {
    var interval = juration.parse(getTipInterval());
    var lower_bound = interval - getVarianceLower();
    var upper_bound = interval + getVarianceUpper();
    return getRandomNumber(lower_bound, upper_bound) * 1000;
}
function getRandomNumber(min, max) {
    return Math.random() * (max - min) + min;
}
function chainQueueTips() {
    var sleepTime = getSleepInterval();
    tipFunctionTimeout = setTimeout(function() {
        sendTip(chainQueueTips);
    }, sleepTime);
}
function sendTip(queueNextTipFn) {
    var queryUrl = $('.tip_popup form').attr('action');
    var queryParams = $.param({
        'csrfmiddlewaretoken': $.cookie('csrftoken'),
        'tip_amount': getTipAmount(),
        'message': '',
        'tip_room_type': $('#id_tip_room_type').val(),
        'tip_v': defchat_settings.v_tip_vol,
    });
    $.ajax({
        url: queryUrl,
        data: queryParams,
        dataType: 'json',
        type: 'post',
        success: function(response) {
            if (response.error) {
                alert(response.error);
                stopTipping();
            }
        }
    });
    updateTipsLeft();
    if (tipsLeft === 0) {
        stopTipping();
    }
    else if (queueNextTipFn) {
        queueNextTipFn();
    }
}
function updateTipsLeft() {
    tipsLeft--;
    updateTipperButton();
}
function stopTipping() {
    clearTimeout(tipFunctionTimeout);
    tipFunctionTimeout = null;
    tipsLeft = 0;
    updateTipperButton();
    $(HTML_IDS['STOP']).prop('disabled', true).css(CSS_GREY);
    $(HTML_IDS['START']).prop('disabled', false).css(CSS_WHITE);
    $(CLASS_INPUT).prop('disabled', false).css(CSS_BLACK);
}
function initializeButtonCallbacks() {
    var popup = $(HTML_IDS['POPUP']);
    var button = $(HTML_IDS['BUTTON']);
    button.click(function(ev) {
        if (popup.is(':visible')) {
            popup.hide();
        }
        else {
            popup.show();
        }
    });
    popup.click(function(ev) {
        ev.stopPropagation();
    });
    $(HTML_IDS['START']).click(function() {
        startTipping();
        $(HTML_IDS['POPUP']).hide();
    });
    $(HTML_IDS['STOP']).click(function() {
        stopTipping();
    });
    $('body').click(function(ev) {
        if (ev.target.id != button.prop('id')) {
            $(HTML_IDS['POPUP']).hide();
        }
    });
    $(CLASS_INPUT).change(function() {
        calculateAndSetTotalTip();
        calculateAndSetETA();
    });
}
function calculateAndSetTotalTip() {
    var value = $(HTML_IDS['AMOUNT']).val() * $(HTML_IDS['COUNT']).val();
    $(HTML_IDS['TOTAL']).html(value + ' tokens');
}
function calculateAndSetETA() {
    var interval = juration.parse($(HTML_IDS['INTERVAL']).val());
    /* we're not counting the first tip */
    var count = getTipCount() - 1;
    var variance_lower = getVarianceLower();
    var variance_upper = getVarianceUpper();
    var eta = (interval + variance_upper - variance_lower) * count;
    $(HTML_IDS['ETA']).html(juration.stringify(eta, {'format': 'long', 'units': 2}));
}
function getTipAmount() {
    return parseInt(getTipAmountRaw());
}
function getTipAmountRaw() {
    return $(HTML_IDS['AMOUNT']).val();
}
function getTipInterval() {
    return $(HTML_IDS['INTERVAL']).val();
}
function getTipCount() {
    return parseInt(getTipCountRaw());
}
function getTipCountRaw() {
    return $(HTML_IDS['COUNT']).val();
}
function getVarianceLower() {
    return parseVariance(getVarianceLowerRaw());
}
function getVarianceLowerRaw() {
    return $(HTML_IDS['VARIANCE_LOWER']).val();
}
function getVarianceUpper() {
    return parseVariance(getVarianceUpperRaw());
}
function getVarianceUpperRaw() {
    return $(HTML_IDS['VARIANCE_UPPER']).val();
}
function parseVariance(variance) {
    if (variance == '0') {
        variance = 0;
    }
    variance = variance || 0;
    if (variance != 0) {
        variance = juration.parse(variance);
    }
    return variance;
}
function saveCurrentSettings() {
    GM_setValue('amount', getTipAmount());
    GM_setValue('interval', getTipInterval());
    GM_setValue('count', getTipCount());
    GM_setValue('variance_lower', getVarianceLower());
    GM_setValue('variance_upper', getVarianceUpper());
}
function loadPreviousSettings() {
    var amount = GM_getValue('amount', 1);
    $(HTML_IDS['AMOUNT']).val(amount);
    var count = GM_getValue('count', 10);
    $(HTML_IDS['COUNT']).val(count);
    var interval = GM_getValue('interval', '1s');
    $(HTML_IDS['INTERVAL']).val(interval);
    var variance_lower = GM_getValue('variance_lower', '');
    $(HTML_IDS['VARIANCE_LOWER']).val(variance_lower);
    var variance_upper = GM_getValue('variance_upper', '');
    $(HTML_IDS['VARIANCE_UPPER']).val(variance_upper);
    calculateAndSetTotalTip();
    calculateAndSetETA();
}
// Script ends here
// Libs included because they're not on a popular cdn 
/*
 * juration - a natural language duration parser
 * https://github.com/domchristie/juration
 *
 * Copyright 2011, Dom Christie
 * Licenced under the MIT licence
 *
 */
function loadJuration() {
  var UNITS = {
    seconds: {
      patterns: ['second', 'sec', 's'],
      value: 1,
      formats: {
        'chrono': '',
        'micro':  's',
        'short':  'sec',
        'long':   'second'
      }
    },
    minutes: {
      patterns: ['minute', 'min', 'm(?!s)'],
      value: 60,
      formats: {
        'chrono': ':',
        'micro':  'm',
        'short':  'min',
        'long':   'minute'
      }
    },
    hours: {
      patterns: ['hour', 'hr', 'h'],
      value: 3600,
      formats: {
        'chrono': ':',
        'micro':  'h',
        'short':  'hr',
        'long':   'hour'
      }
    },
    days: {
      patterns: ['day', 'dy', 'd'],
      value: 86400,
      formats: {
        'chrono': ':',
        'micro':  'd',
        'short':  'day',
        'long':   'day'
      }
    },
    weeks: {
      patterns: ['week', 'wk', 'w'],
      value: 604800,
      formats: {
        'chrono': ':',
        'micro':  'w',
        'short':  'wk',
        'long':   'week'
      }
    },
    months: {
      patterns: ['month', 'mon', 'mo', 'mth'],
      value: 2628000,
      formats: {
        'chrono': ':',
        'micro':  'm',
        'short':  'mth',
        'long':   'month'
      }
    },
    years: {
      patterns: ['year', 'yr', 'y'],
      value: 31536000,
      formats: {
        'chrono': ':',
        'micro':  'y',
        'short':  'yr',
        'long':   'year'
      }
    }
  };
  var stringify = function(seconds, options) {
    if(!_isNumeric(seconds)) {
      throw "juration.stringify(): Unable to stringify a non-numeric value";
    }
    if((typeof options === 'object' && options.format !== undefined) && (options.format !== 'micro' && options.format !== 'short' && options.format !== 'long' && options.format !== 'chrono')) {
      throw "juration.stringify(): format cannot be '" + options.format + "', and must be either 'micro', 'short', or 'long'";
    }
    var defaults = {
      format: 'short',
      units: undefined
    };
    var opts = _extend(defaults, options);
    var units = ['years', 'months', 'days', 'hours', 'minutes', 'seconds'], values = [];
    var remaining = seconds;
    var activeUnits = 0;
    for(var i = 0, len = units.length;
        i < len && (opts.units == undefined || activeUnits < opts.units);
        i++) {
      var unit = UNITS[units[i]];
      values[i] = Math.floor(remaining / unit.value);
      if (values[i] > 0 || activeUnits > 0)
        activeUnits++;
      if(opts.format === 'micro' || opts.format === 'chrono') {
        values[i] += unit.formats[opts.format];
      }
      else {
        values[i] += ' ' + _pluralize(values[i], unit.formats[opts.format]);
      }
      remaining = remaining % unit.value;
    }
    var output = '';
    for(i = 0, len = values.length; i < len; i++) {
      if(values[i].charAt(0) !== "0" && opts.format != 'chrono') {
        output += values[i] + ' ';
      }
      else if (opts.format == 'chrono') {
        output += _padLeft(values[i]+'', '0', i==values.length-1 ? 2 : 3);
      }
    }
    return output.replace(/\s+$/, '').replace(/^(00:)+/g, '').replace(/^0/, '');
  };
  var parse = function(string) {
    // returns calculated values separated by spaces
    for(var unit in UNITS) {
      for(var i = 0, mLen = UNITS[unit].patterns.length; i < mLen; i++) {
        var regex = new RegExp("((?:\\d+\\.\\d+)|\\d+)\\s?(" + UNITS[unit].patterns[i] + "s?(?=\\s|\\d|\\b))", 'gi');
        string = string.replace(regex, function(str, p1, p2) {
          return " " + (p1 * UNITS[unit].value).toString() + " ";
        });
      }
    }
    var sum = 0,
        numbers = string
                    .replace(/(?!\.)\W+/g, ' ')                       // replaces non-word chars (excluding '.') with whitespace
                    .replace(/^\s+|\s+$|(?:and|plus|with)\s?/g, '')   // trim L/R whitespace, replace known join words with ''
                    .split(' ');
    for(var j = 0, nLen = numbers.length; j < nLen; j++) {
      if(numbers[j] && isFinite(numbers[j])) {
         sum += parseFloat(numbers[j]);
      } else if(!numbers[j]) {
        throw "juration.parse(): Unable to parse: a falsey value";
      } else {
        // throw an exception if it's not a valid word/unit
        throw "juration.parse(): Unable to parse: " + numbers[j].replace(/^\d+/g, '');
      }
    }
    return sum;
  };
  // _padLeft('5', '0', 2); // 05
  var _padLeft = function(s, c, n) {
      if (! s || ! c || s.length >= n) {
        return s;
      }
      var max = (n - s.length)/c.length;
      for (var i = 0; i < max; i++) {
        s = c + s;
      }
      return s;
  };
  var _pluralize = function(count, singular) {
    return count == 1 ? singular : singular + "s";
  };
  var _isNumeric = function(n) {
    return !isNaN(parseFloat(n)) && isFinite(n);
  };
  var _extend = function(obj, extObj) {
    for (var i in extObj) {
      if(extObj[i] !== undefined) {
        obj[i] = extObj[i];
      }
    }
    return obj;
  };
  var juration = {
    parse: parse,
    stringify: stringify,
    humanize: stringify
  };
  return juration;
};