// ==UserScript==
// @name         CV Request Archiver
// @namespace    https://github.com/Tiny-Giant/
// @version      2.0.0.5
// @description  Scans the chat transcript and checks all cv+delete requests for status, then moves the closed/deleted ones.
// @author       @TinyGiant @rene @Tunaki
// @include      /https?:\/\/chat(\.meta)?\.stack(overflow|exchange).com\/rooms\/.*/
// @grant        none
// ==/UserScript==
/* jshint -W097 */
'use strict';

var me = (/\d+/.exec($('#active-user').attr('class'))||[false])[0];
if(!me) return false;

var fkey = $('#fkey');
if(!fkey.length) return false;
fkey = fkey.val();

var room = (/chat.stackoverflow.com.rooms.(\d+)/.exec(window.location.href)||[false,false])[1];
if(!room) return false;

$.ajax({
    type: 'POST',
    url: '/user/info?ids=' + me + '&roomId=' + room,
    success: CVRequestArchiver
});

function CVRequestArchiver(info){
    if(!info.users[0].is_owner && !info.users[0].is_moderator) {
        return false;
    }
    
    var count = 0, total = 0, num = 0, rnum = 0, rlen = 0;
    var requests = [];
    var messagesToMove = [];
    var events = [];
    var ids = [];
    
    var target = 90230;
    var nodes = {};

    nodes.scope = document.querySelector('#chat-buttons');

    nodes.startbtn = document.createElement('button');
    nodes.startbtn.className = 'button archiver-startbtn';
    nodes.startbtn.textContent = 'archiver';
    nodes.scope.appendChild(nodes.startbtn);

    nodes.scope.appendChild(document.createElement('br'));

    nodes.form = document.createElement('form');
    nodes.form.className = 'archiver-form';
    nodes.scope.appendChild(nodes.form);

    nodes.count = document.createElement('input');
    nodes.count.className = 'button archiver-count';
    nodes.count.placeholder = '#';
    nodes.count.type = 'text';
    nodes.count.style.display = 'none';
    nodes.form.appendChild(nodes.count);

    nodes.gobtn = document.createElement('button');
    nodes.gobtn.className = 'button archiver-gobtn';
    nodes.gobtn.textContent = 'scan';
    nodes.gobtn.style.display = 'none';
    nodes.form.appendChild(nodes.gobtn);

    nodes.scope.appendChild(document.createTextNode(' '));

    nodes.cancel = document.createElement('button');
    nodes.cancel.className = 'button archiver-cancel';
    nodes.cancel.textContent = 'cancel';
    nodes.cancel.style.display = 'none';
    nodes.scope.appendChild(nodes.cancel);

    nodes.scope.appendChild(document.createElement('br'));

    nodes.indicator = document.createElement('input');
    nodes.indicator.className = 'button archiver-indicator';
    nodes.indicator.type = 'text';
    nodes.indicator.readOnly = true;
    nodes.indicator.style.display = 'none';
    nodes.scope.appendChild(nodes.indicator);

    nodes.scope.appendChild(document.createTextNode(' '));

    nodes.movebtn = document.createElement('button');
    nodes.movebtn.className = 'button archiver-movebtn';
    nodes.movebtn.textContent = 'move';
    nodes.movebtn.style.display = 'none';
    nodes.scope.appendChild(nodes.movebtn);

    nodes.progresswrp = document.createElement('div');
    nodes.progresswrp.className = 'archive-progresswrp';
    nodes.progresswrp.style.display = 'none';
    nodes.scope.appendChild(nodes.progresswrp);

    nodes.progress = document.createElement('div');
    nodes.progress.className = 'archive-progress';
    nodes.progresswrp.appendChild(nodes.progress);

    nodes.style = document.createElement('style');
    nodes.style.type = 'text/css';
    nodes.style.textContent = [
        '#chat-buttons {',
        '    cursor: default;',
        '}',
        '.button {',
        '    margin: 1px !important;',
        '}',
        'button.button:disabled {',
        '    opacity: 0.8;',
        '}',
        'button.button:disabled:hover {',
        '   background: #ff7b18 !important;',
        '}',
        '.button:disabled {',
        '    cursor: default !important;',
        '}',
        '.archiver-count {',
        '    width: 78px;',
        '    border: 0px !important;',
        '    box-sizing: border-box;',
        '    margin-right: 0px !important;',
        '    border-top-right-radius: 0px;',
        '    border-bottom-right-radius: 0px;',
        '}',
        '.archiver-gobtn {',
        '    margin-left: 0px !important;',
        '    border-top-left-radius: 0px;',
        '    border-bottom-left-radius: 0px;',
        '}',
        '.archiver-count,',
        '.archiver-count:hover,',
        '.archiver-count:focus {',
        '    background: #eee;',
        '    outline: none;',
        '    color: #111;',
        '}',
        '.archiver-count:hover:not(:disabled),',
        '.archiver-count:focus:not(:disabled),',
        '.archiver-indicator:not(:disabled),',
        '.archiver-indicator:hover:not(:disabled),',
        '.archiver-indicator:focus:not(:disabled) {',
        '    color: #000;',
        '    outline: none;',
        '    background: #fff;',
        '    cursor: default;',
        '}',
        '.archiver-indicator {',
        '    border: 0px;',
        '    width: 162px;',
        '}',
        '.archiver-form {',
        '    display: inline-block;',
        '}',
        '.archive-progresswrp {',
        '    margin-top: 2px;',
        '    width: 185px;',
        '    background: #eee;',
        '    height: 5px;',
        '}',
        '.archive-progress {',
        '    width: 0%;',
        '    background: #ff7b18;',
        '    height: 100%;',
        '}'
    ].join('\n');
    nodes.scope.appendChild(nodes.style);

    nodes.startbtn.addEventListener('click', function(){
        nodes.startbtn.disabled = true;
        nodes.count.style.display = '';
        nodes.gobtn.style.display = '';
        nodes.cancel.style.display = '';
        nodes.count.focus();
    }, false);

    nodes.cancel.addEventListener('click', reset, false);

    nodes.form.addEventListener('submit', function(e){
        e.preventDefault();
        nodes.cancel.disabled = true;
        total = count = +nodes.count.value;
        nodes.count.disabled = true;
        nodes.gobtn.disabled = true;
        nodes.indicator.style.display = '';
        nodes.indicator.value = 'getting events... (0 / ' + count + ')';
        nodes.progresswrp.style.display = '';
        getEvents(count);
    }, false);

    nodes.movebtn.addEventListener('click', movePosts, false);

    function reset() {
        if(this.disabled) return false;
        rlen = 0;
        rnum = 0;
        total = 0;
        count = 0;
        num = 0;
        requests = [];
        messagesToMove = [];
        events = [];
        ids = [];
        nodes.count.style.display = 'none';
        nodes.count.value = '';
        nodes.count.disabled = false;
        nodes.gobtn.style.display = 'none';
        nodes.gobtn.disabled = false;
        nodes.cancel.style.display = 'none';
        nodes.indicator.style.display = 'none';
        nodes.indicator.textContent = '';
        nodes.movebtn.style.display = 'none';
        nodes.startbtn.disabled = false;
        nodes.progresswrp.style.display = 'none';
        nodes.progress.style.width = '';
    }

    function getEvents(count, before) {
        nodes.indicator.value = 'getting events... (' + (total - count) + ' / ' + total + ')';
        nodes.progress.style.width = Math.ceil(((total - count) * 100) / total) + '%';
        if (count <= 0) {
            scanEvents(events);
            return false;
        }
        var data = {
            fkey: fkey,
            msgCount: count > 500 ? 500 : count,
            mode: 'Messages',
        };
        if (before) data.before = before;
        $.ajax({
            type: 'POST',
            url: '/chats/' + room + '/events',
            data: data,
            success: function(response) {
                events.push(response.events);

                // no more events in the transcript
                if(!response.events[0]) {
                    scanEvents();
                    return false;
                }

                getEvents(count - 500, response.events[0].message_id);
            }
        });
    };

    function scanEvents(){
        nodes.progress.style.width = '';
        for(var i in events) for(var j in events[i]) checkEvent(events[i][j], i, events.length);
        nodes.progress.style.width = '';

        if(!requests.length) {
            nodes.indicator.value = 'no requests found';
            nodes.progresswrp.style.display = 'none';
            nodes.progress.style.width = '';
            nodes.cancel.disabled = false;
            return false;
        }

        nodes.indicator.value = 'chunking request array...';

        rlen = requests.length;
        requests = chunkArray(requests, 100);
        
        checkRequests();
    }
    
    var cvRegexes = [
        /(?:tagged\/cv-pl(?:ease|s|z)|\[cv-pl(?:ease|s|z)\]).*(?:q[^\/]*|posts)\/(\d+)/,
        /(?:q[^\/]*|posts)\/(\d+).*(?:tagged\/cv-pl(?:ease|s|z)|\[cv-pl(?:ease|s|z)\])/,
    ];
    
    var deleteRegexes = [
        /(?:tagged\/del(?:ete)-pl(?:ease|s|z)|\[del(?:ete)-pl(?:ease|s|z)\]).*(?:q[^\/]*|posts)\/(\d+)/,
        /(?:q[^\/]*|posts)\/(\d+).*(?:tagged\/del(?:ete)-pl(?:ease|s|z)|\[del(?:ete)-pl(?:ease|s|z)\])/,
    ];
    
    var RequestType = {
        CLOSE: 'close-vote',
        DELETE: 'delete-vote'
    }
    
    function matchesRegex(message, regexes) {
        for(var j in regexes) {
            if(regexes[j].test(message)) {
                return true;
            }
        }
        return false;
    }
    
    function checkEvent(event, current, total) {
        nodes.indicator.value = 'checking events... (' + current + ' / ' + total + ')';
        nodes.progress.style.width = Math.ceil((current * 100) / total) + '%';
        var message = event.content;
        var type = RequestType.CLOSE;
        var isCVReq = matchesRegex(message, cvRegexes), isDelReq = false;
        if (!isCVReq) {
            isDelReq = matchesRegex(message, deleteRegexes);
            if (!isDelReq) return false;
            type = RequestType.DELETE;
        }
        var matches = message.match(/http.*?(?:q[^\/]*|posts)\/(\d+)/g);
        var posts = [];
        // matches will be null if an user screws up the formatting
        if (matches !== null) {
            for(var k in Object.keys(matches)) {
                posts.push(/(?:q[^\/]*|posts)\/(\d+)/.exec(matches[k])[1]);
            }
        }
        for(var l in posts) requests.push({ msg: event.message_id, post: posts[l], time: event.time_stamp, type: type });
    }

    function checkRequests() {
        var currentreq = requests.pop();

        var left = [rlen,rlen - (requests.length * 100)][+(rlen > 100)];

        nodes.indicator.value = 'checking requests... (' + left + ' / ' + rlen + ')'; 
        nodes.progress.style.width = Math.ceil((left * 100) / rlen) + '%';

        var xhr = new XMLHttpRequest();

        xhr.addEventListener("load", function(){
            if(this.status !== 200) {
                console.log(this);
                checkDone();
                return false;
            }

            var response = JSON.parse(this.response);
            var items = response.items;

            for(var i in items) {
                if (!items[i].deleted_date) {
                    for(var j in currentreq) {
                        if(currentreq[j].post == items[i].question_id && currentreq[j].type == RequestType.DELETE) delete currentreq[j];
                    }
                }
                if(!items[i].closed_date) {
                    for(var j in currentreq) {
                        if(items[i].close_vote_count == 0 && ((Date.now() - (currentreq[j].time * 1000)) > (1000 * 60 * 60 * 24 * 3))) continue;
                        if(currentreq[j].post == items[i].question_id && currentreq[j].type == RequestType.CLOSE) delete currentreq[j];
                    }
                }
            }

            for(var i in currentreq) messagesToMove.push(currentreq[i]);

            if(!requests.length) {
                checkDone();
                return false;
            }

            setTimeout(checkRequests, response.backoff * 1000);
        });

        var url = '//api.stackexchange.com/2.2/questions/' + formatPosts(currentreq) + '?' + [
            'pagesize=100',
            'site=stackoverflow',
            'key=qhq7Mdy8)4lSXLCjrzQFaQ((',
            'filter=!gB66oJbwf2oAg19qt9k287Kesk6)y5u4M_f'
        ].join('&');

        xhr.open("GET", url);

        xhr.send();
    }

    function checkDone() {
        rnum = messagesToMove.length;
        
        if(!rnum) {
            nodes.indicator.value = 'no requests found';
            nodes.progresswrp.style.display = 'none';
            nodes.progress.style.width = '';
            nodes.cancel.disabled = false;
            return false;
        }
        
        ids = chunkArray(formatMsgs(messagesToMove), 100);

        nodes.indicator.value = messagesToMove.length + ' request' + ['','s'][+(messagesToMove.length > 1)] + ' found';
        nodes.movebtn.style.display = '';
        nodes.cancel.disabled = false;
        nodes.progresswrp.style.display = 'none';
        nodes.progress.style.width = '';
    }
    
    function movePosts() {
        var currentids = ids.pop();

        var left = [rnum,rnum - (ids.length * 100)][+(rnum > 100)];

        nodes.indicator.value = 'moving requests... (' + left + ' / ' + rnum + ')';
        nodes.progress.style.width = Math.ceil((left * 100) / rnum) + '%';
        
        $.ajax({
            type: 'POST',
            data: 'ids=' + currentids.join('%2C') + '&to=' + target + '&fkey=' + fkey,
            url: '/admin/movePosts/' + room,
            success: function(){
                if(!ids.length) {
                    nodes.progresswrp.style.display = 'none';
                    nodes.progress.style.width = '';
                    nodes.indicator.value = 'done';
                    nodes.movebtn.style.display = 'none';
                    return false;
                }
                    
                setTimeout(movePosts, 5000);
            }
        }); 
    }

    function formatPosts(arr) {
        var tmp = [];
        for(var i in arr) tmp.push(arr[i].post);
        return tmp.join(';');
    }

    function formatMsgs(arr) {
        var tmp = [];
        for(var i in arr) tmp.push(arr[i].msg);
        return tmp;
    }
    
    function chunkArray(arr, len) {
        var tmp = [];
        var num = Math.ceil(arr.length / len);
        for(var i = 0; i < num; ++i) {
            tmp.push([]);
        }
        var ind = 0;
        for(var j in arr) {
            if(j > 0 && !(j % len)) ++ind;
            tmp[ind].push(arr[j]);
        }
        return tmp;
    }
}
