/* eslint-disable no-unused-vars */
/* global toast, cacheVideos, gtag */

const SECONDS_DAY   = 86400;
const SECONDS_MONTH = 2629800;
const SECONDS_YEAR  = SECONDS_MONTH * 12;

// Basic REGEX to validate emails.
const RE_EMAIL = /^(?!\.)[a-zA-Z0-9!#$%&'*+/=?^_`{|}~.-]{1,64}(?<!\.)@[a-zA-Z0-9-]{1,63}(?:\.[a-zA-Z0-9-]{2,63})+$/;
function isEmail(e) {
    return RE_EMAIL.test(e);
}


Array.prototype.last = function() {
    return this[this.length - 1];
};

Array.prototype.toObject = function() {
    return this.reduce((obj, item) => {
        obj[item.id] = item;
        return obj
    }, {});
};

Array.prototype.groupBy = function(keyFn) {
    return this.reduce((obj, x) => {
        const key = keyFn(x);
        (obj[key] = obj[key] || []).push(x);
        return obj;
    }, {});
};

Array.prototype.maximize = function(fn) {
    let maxValue = -Infinity;
    let maximizer = null;
    this.forEach(elem => {
        const value = fn(elem);
        if (value > maxValue) {
            maxValue = value;
            maximizer = elem;
        }
    });
    return maximizer;
};
Array.prototype.minimize = function(fn) {
    let minValue = +Infinity;
    let minimizer = null;
    this.forEach(elem => {
        const value = fn(elem);
        if (value < minValue) {
            minValue = value;
            minimizer = elem;
        }
    });
    return minimizer;
};

Array.prototype.closestIndex = function (num)
{
    let idx = 0, closet = 0;
    let curr = this[idx], diff = Math.abs(num - curr);
    this.forEach(elem =>
    {
      let newdiff = Math.abs(num - elem);
      if (newdiff < diff)
      {
         diff = newdiff;
         closet = idx;
      }
      idx++;
   });
   return closet;
}


// perform actions until a promise is complete (spinner, loading thumbnails, etc.)
jQuery.fn.extend({
    spin: function(promise) {
        const $elem = this;

        if ($elem.data('currentSpin') && $elem.data('currentSpin').abort) {
            $elem.data('currentSpin').abort();
        }

        $elem.data('currentSpin', promise);
        $elem.addClass('loading');
        $.when(promise).always(
            () => $elem.removeClass('loading')
        );
        return promise;
    }
});

jQuery.fn.extend({
    selectNode: function () {
        const elem = this[0];
        if (document.body.createTextRange) {
            const range = document.body.createTextRange();
            range.moveToElementText(elem);
            range.select();
        } else if (window.getSelection) {
            const selection = window.getSelection();
            const range = document.createRange();
            range.selectNodeContents(elem);
            selection.removeAllRanges();
            selection.addRange(range);
        }
    },
    selectNodeText: function (startOffset, endOffset) {
        const elem = this[0];
        let textNode = Array.from(elem.childNodes).filter(e => e.nodeType === e.TEXT_NODE)[0];
        if (textNode == null) {
            textNode = document.createTextNode("");
            elem.appendChild(textNode);
        }
        if (document.body.createTextRange) {
            const range = document.body.createTextRange();
            range.moveToElementText(elem);
            range.select();
        } else if (window.getSelection) {
            const selection = window.getSelection();
            const range = document.createRange();
            range.setStart(textNode, startOffset);
            range.setEnd(textNode, endOffset);
            selection.removeAllRanges();
            selection.addRange(range);
        }
    }
});

function deepCopy(x) {
    return jQuery.extend(true, {}, x);
}

function randomId() {
    return Math.random().toString(36).substr(2, 16);
}

function range(n) {
    return [...Array(n).keys()]
}

function between(a, x, b) {
    return x >= a && x <= b
}

function relativeValue(start, x, end) {
    return (x - start) / (end - start);
}

function throttle(fn, limitMs) {
    let timeout;
    let lastRunTimestamp;
    return function() {
        if (! lastRunTimestamp) {
            fn.apply(this, arguments);
            lastRunTimestamp = Date.now();
        } else {
            let timeRemaining = limitMs - (Date.now() - lastRunTimestamp);
            clearTimeout(timeout);
            timeout = setTimeout(() => {
                if ((Date.now() - lastRunTimestamp) >= limitMs) {
                    fn.apply(this, arguments);
                    lastRunTimestamp = Date.now();
                }
            }, timeRemaining);
        }
    }
}

function capitalize(str) {
    return str.charAt(0).toUpperCase() + str.slice(1);
}

function toSI(v, unit, precision, significant) {
    const prec = typeof precision == 'undefined' ? 2 : precision;
    const prefixes = ['', 'K', 'M', 'G', 'T', 'P', 'E', 'Z', 'Y'];
    const order = (v === 0) ? 0 : Math.floor(Math.log(v) / Math.log(1000));
    const prefix = prefixes[order];
    const reduced = v / (1000**order);
    const rounded = significant ? parseFloat(reduced.toPrecision(prec)) : reduced.toFixed(prec);
    return rounded + ' ' + prefix + unit
}

function round(n, prec) {
    return Math.round(n * 10 ** prec) / (10 ** prec);
}

function toPercentage(n) {
    return (n * 100.0).toString() + '%'
}

function toLocalDate(ts, {time = true} = {}) {
    const dt = new Date(ts*1000);
    const
        y = dt.getFullYear(),
        m = String(dt.getMonth()+1).padStart(2, '0'),
        d = String(dt.getDate()).padStart(2, '0'),
        h = String(dt.getHours()).padStart(2, '0'),
        min = String(dt.getMinutes()).padStart(2, '0'),
        s = String(dt.getSeconds()).padStart(2, '0');
    return `${y}/${m}/${d}` + (time ? ` ${h}:${min}:${s}` : '');
}

function toTimeDuration(period) {
    if (isNaN(period)) return '';
    const pad = n => n.toString().padStart(2, '0');
    const seconds = Math.floor(period) % 60,
          minutes = Math.floor(period / 60) % 60,
          hours   = Math.floor(period / (60 * 60));
    const secsStr  = pad(seconds),
          minsStr  = (hours > 0) ? `${pad(minutes)}:` : `${minutes}:`,
          hoursStr = (hours > 0) ? `${hours}:` : '';
    return hoursStr + minsStr + secsStr
}

function toDurationHuman(period, shorten) {
    if (isNaN(period)) return '';
    let amount, unit;
    if (period >= 365 * 24 * 3600) {
        [amount, unit] = [Math.floor(period / (365 * 24 * 3600)), 'year'];
    } else if (period >= 30 * 24 * 3600) {
        [amount, unit] = [Math.floor(period / (30 * 24 * 3600)), 'month'];
    } else if (period >= 7 * 24 * 3600) {
        [amount, unit] = [Math.floor(period / (7 * 24 * 3600)), 'week'];
    } else if (period >= 24 * 3600) {
        [amount, unit] = [Math.floor(period / (24 * 3600)), 'day'];
    } else if (period >= 3600) {
        [amount, unit] = [Math.floor(period / 3600), 'hour'];
    } else if (period >= 60) {
        [amount, unit] = [Math.floor(period / 60), 'minute'];
    } else {
        [amount, unit] = [Math.floor(period), 'second'];
    }
    unit = shorten ? unit[0] : unit + (amount == 1 ? '' : 's');
    return amount + ' ' + unit;
}

function formatCurrency(amount, currency, noRounding) {
    currency = currency || 'USD';

    if (amount == Math.round(amount) && !noRounding) {
        return new Intl.NumberFormat(
            'en-US', {style: 'currency', currency: currency, minimumFractionDigits: 0}).format(amount);
    }
    return new Intl.NumberFormat(
        'en-US', {style: 'currency', currency: currency}).format(amount);
}

function formatViews(v, expand, units) {
    const unit = units === false ? '' : (v == 1 ? ' view' : ' views');
    if (v == 1) {
        return '1' + unit;
    } else if (v < 10000 || expand) {
        let res = [];
        let chars = v.toString().split('');
        chars.reverse().forEach((c, i) => {
            res.push(c);
            if (i % 3 == 2 && i < chars.length - 1) {
                res.push(' ');
            }
        });
        return res.reverse().join('') + unit;
    } else {
        const suffixes = ['', 'K', 'M', 'B', 'T'];
        const order = (v === 0) ? 0 : Math.floor(Math.log(v) / Math.log(1000));
        const suffix = suffixes[order];
        return (v / (1000**order)).toFixed(0) + suffix + unit;
    }
}

function formatDuration(t) {
    if (t >= 3600) {
      return Math.round(t / 360) / 10 + ' hr';
    } else if (t >= 60) {
      return Math.round(t / 6) / 10 + ' min';
    } else if (t) {
      return t + ' sec';
    }
    return '0 sec';
}

function videoThumbnailURL(video) {
    if (!video.url && video.cut_svid) {
        const cutSource = cacheVideos.get(video.cut_svid);
        if (cutSource) {
            const cutThumb = Math.floor(video.cut_start + Math.floor((video.cut_end - video.cut_start) / 3)).toString().padStart(5, '0');
            return makeURL(`thumbnails/${cutThumb}.jpg`, cutSource.url);
        }
        return '';
    }
    let args = {};
    if (video.nocache) args.nocache = Math.round(new Date().getTime() / 1000);
    return makeURL('thumbnails/thumbnail.jpg', video.url, args);
}

function momentThumbnailURL(baseUrl, start, {duration = 0} = {}) {
    const thumbnailN = Math.ceil(start + duration/2);
    const filename = `thumbnails/${thumbnailN.toString().padStart(5,'0')}.jpg`;
    return makeURL(filename, baseUrl)
}

function extractDifferences(original, edited) {
    let leftMatch = 0, rightMatch = 0;
    const len = original.length;

    // Find how many words match from the start.
    while (leftMatch < len && original[leftMatch] === edited[leftMatch]) {
        leftMatch++;
    }

    // Find how many words match from the end.
    while (rightMatch < (len - leftMatch) && original[len - 1 - rightMatch] === edited[edited.length - 1 - rightMatch]) {
        rightMatch++;
    }

    const diffStart = leftMatch;
    const deleteCount = len - leftMatch - rightMatch;
    const newWords = edited.slice(leftMatch, edited.length - rightMatch);

    return [diffStart, deleteCount, newWords];
}

function copyToClipboard(str, isHTML) {
    let el;
    if (isHTML) {
        elParent = document.createElement('div');
        elParent.classList.add('copy-tmp-box');
        el = document.createElement('div');
        el.innerHTML = str;
        elParent.appendChild(el);
        document.body.appendChild(elParent);
        let range = document.createRange();
        let selection = window.getSelection();
        range.selectNodeContents(el);
        selection.removeAllRanges();
        selection.addRange(range);
    } else {
        el = document.createElement('input');
        document.body.appendChild(el);
        el.value = str;
        el.select();
    }
    document.execCommand('copy', false);
    el.remove();
    toast("Copied to clipboard");
}

function parseHTML(string) {
    return document.createRange().createContextualFragment(string);
}

function parseDuration(timeStr) {
    const parts = timeStr.split(":").slice(0, 3);
    let s = Number.parseFloat(parts[parts.length - 1]) || 0;
    let m = Number.parseFloat(parts[parts.length - 2]) || 0;
    let h = Number.parseFloat(parts[parts.length - 3]) || 0;
    return h * 3600 + m * 60 + s;
}

function mtag(action, args) {
    if (typeof gtag != 'undefined') {
        gtag('event', action, args);
    } else {
        console.warn('gtag unavailable for event:', action, args || null);
    }
}

function scrolledToBottom(element, padding) {
    const scrollMax = element.scrollHeight - element.clientHeight;
    return element.scrollTop > scrollMax - (typeof padding != 'undefined' ? padding : 200);
}

function makeURL(url, base, args) {
    const argsBase = new URL(base).searchParams;
    const urlNew = new URL(url, base);
    argsBase.forEach((v, k) => urlNew.searchParams.append(k, v));
    if (args) new URLSearchParams(args).forEach((v, k) => urlNew.searchParams.append(k, v));
    return urlNew.toString();
}

const jGet = (url, args = {}) =>
    $.ajax({
        url: url,
        type: 'GET',
        xhrFields: { withCredentials: true },
        ...args,
    });

const jPost = (url, data) =>
    $.ajax({
        url: url,
        type: 'POST',
        data: JSON.stringify(data),
        contentType: "application/json; charset=utf-8",
        dataType: "json",
        xhrFields: { withCredentials: true }
    });

const jDelete = (url, data) =>
    $.ajax({
        url: url,
        type: 'DELETE',
        data: JSON.stringify(data),
        contentType: "application/json; charset=utf-8",
        dataType: "json",
        xhrFields: { withCredentials: true }
    });

const jAll = (...requests) => {
    const response = $.when(...requests).then((...response) => {
        if (requests.length === 1) {
            return response;
        } else {
            const responses = response.map(r => r[0]);
            return $.when(...responses);
        }
        requests.length === 1 ? r : $.when(...r)
    });
    response.abort = () => requests.forEach(r => r.abort());
    return response;
}

const tNow = () => Math.round(new Date().getTime() / 1000);
