/**
    Model for the whole Queue with all it's items
**/
function QueueListModel(parent) {
    // Internal var's
    var self = this;
    self.parent = parent;
    self.dragging = false;

    // Because SABNZB returns the name
    // But when you want to set Priority you need the number..
    self.priorityName = [];
    self.priorityName["Force"] = 2;
    self.priorityName["High"] = 1;
    self.priorityName["Normal"] = 0;
    self.priorityName["Low"] = -1;
    self.priorityName["Stop"] = -4;
    self.priorityOptions = ko.observableArray([
        { value: 2,  name: glitterTranslate.priority["Force"] },
        { value: 1,  name: glitterTranslate.priority["High"] },
        { value: 0,  name: glitterTranslate.priority["Normal"] },
        { value: -1, name: glitterTranslate.priority["Low"] },
        { value: -4, name: glitterTranslate.priority["Stop"] }
    ]);
    self.processingOptions = ko.observableArray([
        { value: 0, name: glitterTranslate.pp["Download"] },
        { value: 1, name: glitterTranslate.pp["+Repair"] },
        { value: 2, name: glitterTranslate.pp["+Unpack"] },
        { value: 3, name: glitterTranslate.pp["+Delete"] }
    ]);

    // External var's
    self.queueItems = ko.observableArray([]);
    self.totalItems = ko.observable(0);
    self.deleteItems = ko.observableArray([]);
    self.isMultiEditing = ko.observable(false).extend({ persist: 'queueIsMultiEditing' });
    self.isLoading = ko.observable(false).extend({ rateLimit: 100 });
    self.multiEditItems = ko.observableArray([]);
    self.categoriesList = ko.observableArray([]);
    self.scriptsList = ko.observableArray([]);
    self.searchTerm = ko.observable('').extend({ rateLimit: { timeout: 400, method: "notifyWhenChangesStop" } });
    self.paginationLimit = ko.observable(20).extend({ persist: 'queuePaginationLimit' });
    self.pagination = new paginationModel(self);

    // Don't update while dragging
    self.shouldUpdate = function() {
        return !self.dragging;
    }
    self.dragStart = function() {
        self.dragging = true;
    }
    self.dragStop = function(event) {
        // Remove that extra label
        $(event.target).parent().removeClass('table-active-sorting')
        // Wait a little before refreshing again (prevents jumping)
        setTimeout(function() {
            self.dragging = false;
        }, 500)
    }

    // Update slots from API data
    self.updateFromData = function(data) {
        // Get all ID's
        var itemIds = $.map(self.queueItems(), function(i) {
            return i.id;
        });

        // Set limit
        self.totalItems(data.noofslots);

        // Container for new models
        var newItems = [];

        // Go over all items
        $.each(data.slots, function() {
            var item = this;
            var existingItem = ko.utils.arrayFirst(self.queueItems(), function(i) {
                return i.id === item.nzo_id;
            });

            if(existingItem) {
                existingItem.updateFromData(item);
                itemIds.splice(itemIds.indexOf(item.nzo_id), 1);
            } else {
                // Add new item
                newItems.push(new QueueModel(self, item))
            }
        });

        // Remove all items if there's any
        if(itemIds.length === self.paginationLimit()) {
            // Replace it, so only 1 Knockout DOM-update!
            self.queueItems(newItems);
            newItems = [];
        } else {
            // Remove items that don't exist anymore
            $.each(itemIds, function() {
                var id = this.toString();
                self.queueItems.remove(ko.utils.arrayFirst(self.queueItems(), function(i) {
                    return i.id === id;
                }));
            });
        }

        // New items, then add!
        if(newItems.length > 0) {
            ko.utils.arrayPushAll(self.queueItems, newItems);
            self.queueItems.valueHasMutated();
        }

        // Sort every time (takes just few msec)
        self.queueItems.sort(function(a, b) {
            return a.index() < b.index() ? -1 : 1;
        });
    };

    // Move in sortable
    self.move = function(event) {
        var itemMoved = event.item;
        // Up or down?
        var corTerm = event.targetIndex > event.sourceIndex ? -1 : 1;
        // See what the actual index is of the queue-object
        // This way we can see how we move up and down independent of pagination
        var itemReplaced = self.queueItems()[event.targetIndex+corTerm];
        callAPI({
            mode: "switch",
            value: itemMoved.id,
            value2: itemReplaced.index()
        }).then(self.parent.refresh);
    };

    // Move button clicked
    self.moveButton = function(event,ui) {
        var itemMoved = event;
        var targetIndex;
        if($(ui.currentTarget).is(".buttonMoveToTop")){
            //we want to move to the top
            targetIndex = 0;
        } else {
            // we want to move to the bottom
			targetIndex = self.totalItems() - 1;
        }
        callAPI({
            mode: "switch",
            value: itemMoved.id,
            value2: targetIndex
        }).then(self.parent.refresh);

    }

    self.triggerRemoveDownload = function(items) {
        // Show and fill modal
        self.deleteItems.removeAll()

        // Single or multiple items?
        if(items.length) {
            ko.utils.arrayPushAll(self.deleteItems, items)
        } else {
            self.deleteItems.push(items)
        }

        // Show modal or delete right away
        if(self.parent.confirmDeleteQueue()) {
            // Open modal if desired
            $('#modal-delete-queue-job').modal("show")
        } else {
            // Otherwise just submit right away
            $('#modal-delete-queue-job form').submit()
        }
    }

    // Save pagination state
    self.paginationLimit.subscribe(function(newValue) {
        // Save in config if global
        if(self.parent.useGlobalOptions()) {
            callAPI({
                mode: "set_config",
                section: "misc",
                keyword: "queue_limit",
                value: newValue
            })
        }
        // Update pagination and counters
        self.parent.refresh(true)
    });

    // Do we show search box. So it doesn't dissapear when nothing is found
    self.hasQueueSearch = ko.pureComputed(function() {
        return (self.pagination.hasPagination() || self.searchTerm() || (self.parent.hasQueue() && self.isMultiEditing()))
    })

    // Searching in queue (rate-limited in decleration)
    self.searchTerm.subscribe(function() {
        // Go back to page 1
        if(self.pagination.currentPage() !== 1) {
            // This forces a refresh
            self.pagination.moveToPage(1);
        } else {
            // Refresh now
            self.parent.refresh();
        }
    })

    // Clear searchterm
    self.clearSearchTerm = function(data, event) {
        // Was it escape key or click?
        if(event.type === 'mousedown' || (event.keyCode && event.keyCode === 27)) {
            self.isLoading(true)
            self.searchTerm('');
        }
        // Was it click and the field is empty? Then we focus on the field
        if(event.type === 'mousedown' && self.searchTerm() === '') {
            $(event.target).parents('.search-box').find('input[type="text"]').focus()
            return;
        }
        // Need to return true to allow typing
        return true;
    }

    /***
        Multi-edit functions
    ***/
    self.queueSorting = function(data, event) {
        // What action?
        var sort, dir;
        switch($(event.currentTarget).data('action')) {
            case 'sortRemainingAsc':
                sort = 'remaining';
                dir = 'asc';
                break;
            case 'sortAgeAsc':
                sort = 'avg_age';
                dir = 'desc';
                break;
            case 'sortAgeDesc':
                sort = 'avg_age';
                dir = 'asc';
                break;
            case 'sortNameAsc':
                sort = 'name';
                dir = 'asc';
                break;
            case 'sortNameDesc':
                sort = 'name';
                dir = 'desc';
                break;
            case 'sortSizeAsc':
                sort = 'size';
                dir = 'asc';
                break;
            case 'sortSizeDesc':
                sort = 'size';
                dir = 'desc';
                break;
        }

        // Show notification
        showNotification('.main-notification-box-sorting', 2000)

        // Send call
        callAPI({
            mode: 'queue',
            name: 'sort',
            sort: sort,
            dir: dir
        }).then(parent.refresh)
    }

    // Show the input box
    self.showMultiEdit = function() {
        // Update value
        self.isMultiEditing(!self.isMultiEditing())
        // Form
        var $form = $('form.multioperations-selector')

        // Reset form and remove all checked ones
        $form[0].reset();
        self.multiEditItems.removeAll();
        $('.queue-table input[name="multiedit"], #multiedit-checkall-queue').prop({'checked': false, 'indeterminate': false})

        // Is the multi-edit in view?
        if(($form.offset().top + $form.outerHeight(true)) > ($(window).scrollTop()+$(window).height())) {
            // Scroll to form
            $('html, body').animate({
                scrollTop: $form.offset().top + $form.outerHeight(true) - $(window).height() + 'px'
            }, 'fast')
        }
    }

    // Add to the list
    self.addMultiEdit = function(item, event) {
        // Is it a shift-click?
        if(event.shiftKey) {
            checkShiftRange('.queue-table input[name="multiedit"]');
        }

        // Add or remove from the list?
        if(event.currentTarget.checked) {
            // Add item
            self.multiEditItems.push(item);
            // Update them all
            self.doMultiEditUpdate();
        } else {
            // Go over them all to know which one to remove
            self.multiEditItems.remove(function(inList) { return inList.id == item.id; })
        }

        // Update check-all buton state
        setCheckAllState('#multiedit-checkall-queue', '.queue-table input[name="multiedit"]')
        return true;
    }

    // Check all
    self.checkAllJobs = function(item, event) {
        // Get which ones we care about
        var allChecks = $('.queue-table input[name="multiedit"]').filter(':not(:disabled):visible');

        // We need to re-evaltuate the state of this check-all
        // Otherwise the 'inderterminate' will be overwritten by the click event!
        setCheckAllState('#multiedit-checkall-queue', '.queue-table input[name="multiedit"]')

        // Now we can check what happend
        // For when some are checked, or all are checked (but not partly)
        if(event.target.indeterminate || (event.target.checked && !event.target.indeterminate)) {
            var allActive = allChecks.filter(":checked")
            // First remove the from the list
            if(allActive.length == self.multiEditItems().length) {
                // Just remove all
                self.multiEditItems.removeAll();
                // Remove the check
                allActive.prop('checked', false)
            } else {
                // Remove them seperate
                allActive.each(function() {
                    // Go over them all to know which one to remove
                    var item = ko.dataFor(this)
                    self.multiEditItems.remove(function(inList) { return inList.id == item.id; })
                    // Remove the check of this one
                    this.checked = false;
                })
            }
        } else {
            // None are checked, so check and add them all
            allChecks.prop('checked', true)
            allChecks.each(function() { self.multiEditItems.push(ko.dataFor(this)) })
            event.target.checked = true

            // Now we fire the update
            self.doMultiEditUpdate()
        }
        // Set state of all the check-all's
        setCheckAllState('#multiedit-checkall-queue', '.queue-table input[name="multiedit"]')
        return true;
    }

    // Do the actual multi-update immediatly
    self.doMultiEditUpdate = function() {
        // Anything selected?
        if(self.multiEditItems().length < 1) return;

        // Retrieve the current settings
        var newCat = $('.multioperations-selector select[name="Category"]').val()
        var newScript = $('.multioperations-selector select[name="Post-processing"]').val()
        var newPrior = $('.multioperations-selector select[name="Priority"]').val()
        var newProc = $('.multioperations-selector select[name="Processing"]').val()
        var newStatus = $('.multioperations-selector input[name="multiedit-status"]:checked').val()

        // List all the ID's
        var strIDs = '';
        $.each(self.multiEditItems(), function(index) {
            strIDs = strIDs + this.id + ',';
        })

        // All non-category updates need to only happen after a category update
        function nonCatUpdates() {
            if(newScript !== '') {
                callAPI({
                    mode: 'change_script',
                    value: strIDs,
                    value2: newScript
                })
            }
            if(newPrior !== '') {
                callAPI({
                    mode: 'queue',
                    name: 'priority',
                    value: strIDs,
                    value2: newPrior
                })
            }
            if(newProc !== '') {
                callAPI({
                    mode: 'change_opts',
                    value: strIDs,
                    value2: newProc
                })
            }
            if(newStatus) {
                callAPI({
                    mode: 'queue',
                    name: newStatus,
                    value: strIDs
                })
            }

            // Wat a little and do the refresh
            // Only if anything changed!
            if(newStatus || newProc !== '' || newPrior !== '' || newScript !== '' || newCat !== '') {
                setTimeout(parent.refresh, 100)
            }
        }

        // What is changed?
        if(newCat !== '') {
            callAPI({
                mode: 'change_cat',
                value: strIDs,
                value2: newCat
            }).then(nonCatUpdates)
        } else {
            nonCatUpdates()
        }

    }

    // Handle mousedown to capture state before change
    self.handleMultiEditStatusMouseDown = function(item, event) {
        var clickedValue = $(event.currentTarget).find("input").val();

        // If this radio was already selected (same value as previous), clear it
        if ($('.multioperations-selector input[name="multiedit-status"]:checked').val() === clickedValue) {
            // Clear all radio buttons in this group after the click finished
            // Hacky, but it works
            setTimeout(function () {
                $('.multioperations-selector input[name="multiedit-status"]').prop('checked', false);
            }, 200)
        }
        return true;
    }

    // Remove downloads from queue
    self.removeDownloads = function(form) {
        // Hide modal and show notification
        $('#modal-delete-queue-job').modal("hide")
        showNotification('.main-notification-box-removing')

        var strIDs = '';
        $.each(self.deleteItems(), function(index) {
            strIDs = strIDs + this.id + ',';
        })

        callAPI({
            mode: 'queue',
            name: 'delete',
            del_files: 1,
            value: strIDs
        }).then(function(response) {
            self.queueItems.removeAll(self.deleteItems());
            self.multiEditItems.removeAll(self.deleteItems())
            self.parent.refresh();
            hideNotification()
        });
    };

    // Delete all selected
    self.doMultiDelete = function() {
        // Anything selected?
        if(self.multiEditItems().length < 1) return;

        // Trigger modal
        self.triggerRemoveDownload(self.multiEditItems())
    }

    // Move all selected to top
    self.doMultiMoveToTop = function() {
        // Anything selected?
        if(self.multiEditItems().length < 1) return;

        // Move each item to the top, starting from the last one in the sorted list
        var arrayList = self.multiEditItems()
        var movePromises = [];
        for(var i = arrayList.length - 1; i >= 0; i--) {
            movePromises.push(callAPI({
                mode: "switch",
                value: arrayList[i].id,
                value2: 0
            }));
        }

        // Wait for all moves to complete then refresh
        Promise.all(movePromises).then(function() {
            self.parent.refresh();
        });
    }

    // Move all selected to bottom
    self.doMultiMoveToBottom = function() {
        // Anything selected?
        if(self.multiEditItems().length < 1) return;

        // Move each item to the bottom, starting from the first one in the sorted list
        var arrayList = self.multiEditItems()
        var movePromises = [];
        for(var i = 0; i < arrayList.length; i++) {
            movePromises.push(callAPI({
                mode: "switch",
                value: arrayList[i].id,
                value2: self.totalItems() - 1
            }));
        }

        // Wait for all moves to complete then refresh
        Promise.all(movePromises).then(function() {
            self.parent.refresh();
        });
    }

    // Focus on the confirm button
    $('#modal-delete-queue-job').on("shown.bs.modal", function() {
        $('#modal-delete-queue-job .btn[type="submit"]').focus()
    })

    // On change of page we need to check all those that were in the list!
    self.queueItems.subscribe(function() {
        // We need to wait until the unit is actually finished rendering
        setTimeout(function() {
            $.each(self.multiEditItems(), function(index) {
                $('#multiedit_' + this.id).prop('checked', true);
            })

            // Update check-all buton state
            setCheckAllState('#multiedit-checkall-queue', '.queue-table input[name="multiedit"]')
        }, 100)
    }, null, "arrayChange")
}

/**
    Model for each Queue item
**/
function QueueModel(parent, data) {
    var self = this;
    self.parent = parent;
    self.rawLabels = []

    // Job info
    self.id = data.nzo_id;
    self.name = ko.observable($.trim(data.filename));
    self.password = ko.observable(data.password);
    self.index = ko.observable(data.index);
    self.status = ko.observable(data.status);
    self.labels = ko.observableArray(data.labels);
    self.isGrabbing = ko.observable(data.status === 'Grabbing' || data.avg_age === '-')
    self.isFetchingBlocks = data.status === 'Fetching' || data.priority === 'Repair' // No need to update
    self.totalMB = ko.observable(parseFloat(data.mb));
    self.remainingMB = ko.observable(parseFloat(data.mbleft))
    self.missingMB = ko.observable(parseFloat(data.mbmissing))
    self.percentage = ko.observable(parseInt(data.percentage))
    self.avg_age = ko.observable(data.avg_age)
    self.direct_unpack = ko.observable(data.direct_unpack)
    self.category = ko.observable(data.cat);
    self.priority = ko.observable(parent.priorityName[data.priority]);
    self.script = ko.observable(data.script);
    self.unpackopts = ko.observable(parseInt(data.unpackopts)) // UnpackOpts fails if not parseInt'd!
    self.pausedStatus = ko.observable(data.status === 'Paused');
    self.timeLeft = ko.observable(data.timeleft);

    // Initially empty
    self.nameForEdit = ko.observable();
    self.editingName = ko.observable(false);
    self.hasDropdown = ko.observable(false);

    // Color of the progress bar
    self.progressColor = ko.computed(function() {
        // Checking
        if(self.status() === 'Checking') {
            return '#58A9FA'
        }
        // Check for missing data, the value is arbitrary! (2%)
        if(self.missingMB()/self.totalMB() > 0.02) {
            return '#F8A34E'
        }
        // Set to grey, only when not Force download
        if((self.parent.parent.downloadsPaused() && self.priority() !== 2) || self.pausedStatus()) {
            return '#B7B7B7'
        }
        // Nothing
        return '';
    });

    // MB's
    self.progressText = ko.pureComputed(function() {
        if(self.isGrabbing()) {
            return glitterTranslate.fetchingURL
        }
        return (self.totalMB() - self.remainingMB()).toFixed(0) + " MB / " + (self.totalMB() * 1).toFixed(0) + " MB";
    })

    // Texts
    self.name_title = ko.pureComputed(function() {
        // When hovering over the job
        if(self.direct_unpack()) {
            return self.name() + ' - ' + glitterTranslate.status['DirectUnpack'] + ': ' + self.direct_unpack()
        }
        return self.name()
    })
    self.missingText = ko.pureComputed(function() {
        // Check for missing data, can show 0 if article-size is smaller than 500K, but we accept that
        if(self.missingMB()) {
            return self.missingMB().toFixed(0) + ' MB ' + glitterTranslate.misingArt
        }
    })
    self.statusText = ko.computed(function() {
        // Checking
        if(self.status() === 'Checking') {
            return glitterTranslate.checking
        }
        // Grabbing
        if(self.status() === 'Grabbing') {
            return glitterTranslate.fetch
        }
        // Pausing status
        if((self.parent.parent.downloadsPaused() && self.priority() !== 2) || self.pausedStatus()) {
            return glitterTranslate.paused;
        }
        // Just the time
        return rewriteTime(self.timeLeft());
    });

    // Icon to better show force-priority
    self.queueIcon = ko.computed(function() {
        // Force comes first
        if(self.priority() === 2) {
            return 'glyphicon-forward'
        }
        if(self.pausedStatus()) {
            return 'glyphicon-play'
        }
        return 'glyphicon-pause'
    })

    // Extra queue columns
    self.showColumn = function(param) {
        switch(param) {
            case 'category':
                // Exception for *
                if(self.category() === "*")
                    return glitterTranslate.defaultText
                return self.category();
            case 'priority':
                // Onload-exception
                if(self.priority() === undefined) return;
                return ko.utils.arrayFirst(self.parent.priorityOptions(), function(item) { return item.value === self.priority()}).name;
            case 'processing':
                // Onload-exception
                if(self.unpackopts() === undefined) return;
                return ko.utils.arrayFirst(self.parent.processingOptions(), function(item) { return item.value === self.unpackopts()}).name;
            case 'scripts':
                return self.script();
            case 'age':
                return self.avg_age();
        }
        return;
    };

    // Every update
    self.updateFromData = function(data) {
        // Update job info
        self.name($.trim(data.filename));
        self.password(data.password);
        self.index(data.index);
        self.status(data.status)
        self.isGrabbing(data.status === 'Grabbing' || data.avg_age === '-')
        self.totalMB(parseFloat(data.mb));
        self.remainingMB(parseFloat(data.mbleft));
        self.missingMB(parseFloat(data.mbmissing))
        self.percentage(parseInt(data.percentage))
        self.avg_age(data.avg_age)
        self.direct_unpack(data.direct_unpack)
        self.category(data.cat);
        self.priority(parent.priorityName[data.priority]);
        self.script(data.script);
        self.unpackopts(parseInt(data.unpackopts)) // UnpackOpts fails if not parseInt'd!
        self.pausedStatus(data.status === 'Paused');
        self.timeLeft(data.timeleft);

        // Did the label-list change?
        // Otherwise KO will send updates to all texts during refresh()
        if(self.rawLabels !== data.labels.toString()) {
            // Update
            self.labels(data.labels);
            self.rawLabels = data.labels.toString();
        }
    };

    // Pause individual download
    self.pauseToggle = function() {
        callAPI({
            mode: 'queue',
            name: (self.pausedStatus() ? 'resume' : 'pause'),
            value: self.id
        }).then(self.parent.parent.refresh);
    };

    // Edit name
    self.editName = function(data, event) {
        // Not when still grabbing
        if(self.isGrabbing()) return false;

        // Change status and fill
        self.editingName(true)
        self.nameForEdit(self.name())

        // Select the input
        const $input = $(event.target).parents('.name').find('input');
        $input.select();

        // Add Tab/Shift+Tab navigation
        $input.off('keydown.tabnav').on('keydown.tabnav', function (e) {
            if (e.key === 'Tab') {
                e.preventDefault();

                // Find all rename inputs that are currently visible
                const inputs = $('.queue-table input[type="text"]');
                const currentIndex = inputs.index(this);
                let nextIndex = e.shiftKey ? currentIndex - 1 : currentIndex + 1;

                // Wrap around
                if (nextIndex >= inputs.length) nextIndex = 0;
                if (nextIndex < 0) nextIndex = inputs.length - 1;

                // Simulate clicking Rename on the next row
                const $nextRow = inputs.eq(nextIndex).closest('tr');
                $nextRow.find('.hover-button[title="Rename"]').click();

                // Delay focusing to wait for new input to appear
                setTimeout(() => {
                    const $nextInput = $('.queue-table input[type="text"]').eq(nextIndex);
                    $nextInput.focus().select();
                }, 50);
            }
        });
    };


    // Catch the submit action
    self.editingNameSubmit = function() {
        self.editingName(false)
    }

    // Do on change
    self.nameForEdit.subscribe(function(newName) {
        // Anything change or empty?
        if(!newName || self.name() === newName) return;

        // Rename would abort Direct Unpack, so ask if user is sure
        if(self.direct_unpack() && !confirm(glitterTranslate.renameAbort)) return;

        // Send rename
        callAPI({
                mode: 'queue',
                name: 'rename',
                value: self.id,
                value2: newName
            }).then(self.parent.parent.refresh)
    })

    // See items
    self.showFiles = function() {
        // Not when still grabbing
        if(self.isGrabbing()) return false;
        // Trigger update
        parent.parent.filelist.loadFiles(self)
    }

    // Toggle calculation of dropdown
    // Turns out that the <select> in the dropdown are a hugggeeee slowdown on initial load!
    // Only loading on click cuts half the speed (especially on large queues)
    self.toggleDropdown = function(item, event) {
        self.hasDropdown(true)
        // Keep it open!
        keepOpen(event.target)
    }

    // Change of settings
    self.changeCat = function(item, event) {
        callAPI({
            mode: 'change_cat',
            value: item.id,
            value2: item.category()
        }).then(function() {
            // Hide all tooltips before we refresh
            $('.queue-item-settings li').filter('[data-tooltip="true"]').tooltip('hide')
            self.parent.parent.refresh()
        })
    }
    self.changeScript = function(item) {
        callAPI({
            mode: 'change_script',
            value: item.id,
            value2: item.script()
        })
    }
    self.changeProcessing = function(item) {
        callAPI({
            mode: 'change_opts',
            value: item.id,
            value2: item.unpackopts()
        })
    }
    self.changePriority = function(item, event) {
        // Not if we are fetching extra blocks for repair!
        if(item.isFetchingBlocks) return
        callAPI({
            mode: 'queue',
            name: 'priority',
            value: item.id,
            value2: item.priority()
        }).then(function() {
            // Hide all tooltips before we refresh
            $('.queue-item-settings li').filter('[data-tooltip="true"]').tooltip('hide')
            self.parent.parent.refresh()
        })
    }


}
