@model Disco.Web.Areas.Public.Models.UserHeldDevices.NoticeboardModel
@{
    Layout = null;
    Html.BundleDeferred("~/ClientScripts/Modules/Knockout");
    Html.BundleDeferred("~/ClientScripts/Modules/jQuery-SignalR");
    Html.BundleDeferred("~/ClientScripts/Core");
    Html.BundleDeferred("~/Style/Public/HeldDevicesNoticeboard");
}
<!DOCTYPE html>
<html>
<head>
    <meta charset="utf-8" />
    <meta http-equiv="X-UA-Compatible" content="IE=edge" />
    <title>Disco ICT - Held Devices for Users</title>
    @Html.BundleRenderDeferred()
</head>
<body class="theme-@(Model.DefaultTheme) status-connecting">
    <div id="page">
        <header id="header">
            <div id="heading">Held Devices for Users</div>
            <div id="statusConnecting"><i class="fa fa-cog fa-spin"></i><span>connecting...</span></div>
            <div id="statusError"><i class="fa fa-cog fa-spin"></i><span>disconnected, reconnecting...</span></div>
            <div id="credits">
                powered by Disco ICT <i title="Disco ICT - Jobs"></i>
            </div>
        </header>
        <section id="mainSection">
            <div id="inProcess" class="list">
                <h3>In Process (<span data-bind="text: inProcess().length"></span>)
                </h3>
                <div class="content">
                    <!-- ko if: inProcess().length == 0 -->
                    <div class="noContent">&lt;None&gt;</div>
                    <!-- /ko -->
                    <ul data-bind="template: { name: 'item-template', foreach: inProcess, afterRender: onAdd, beforeRemove: onRemove }"></ul>
                </div>
            </div>
            <div id="readyForReturn" class="list">
                <h3>Ready for Return (<span data-bind="text: readyForReturn().length"></span>)
                </h3>
                <div class="content">
                    <!-- ko if: readyForReturn().length == 0 -->
                    <div class="noContent">&lt;None&gt;</div>
                    <!-- /ko -->
                    <ul data-bind="template: { name: 'item-template', foreach: readyForReturn, afterRender: onAdd, beforeRemove: onRemove }"></ul>
                </div>
            </div>
            <div id="waitingForUserAction" class="list">
                <h3>Waiting for User Action (<span data-bind="text: waitingForUserAction().length"></span>)
                </h3>
                <div class="content">
                    <!-- ko if: waitingForUserAction().length == 0 -->
                    <div class="noContent">&lt;None&gt;</div>
                    <!-- /ko -->
                    <ul data-bind="template: { name: 'item-template', foreach: waitingForUserAction, afterAdd: onAdd, beforeRemove: onRemove }"></ul>
                </div>
            </div>
            <footer id="footer">
            </footer>
        </section>
    </div>
    <script type="text/html" id="item-template">
        <li data-bind="css: { alert: IsAlert }">
            <span data-bind="text: UserIdFriendly + ' - ' + UserDisplayName"></span>
            <!-- ko if: !ReadyForReturn && EstimatedReturnTimeUnixEpoc -->
            <span class="small">(Expected <span data-bind="livestamp: EstimatedReturnTimeUnixEpoc"></span>)</span>
            <!-- /ko -->
            <!-- ko if: WaitingForUserAction -->
            <span class="small">(Since <span data-bind="livestamp: WaitingForUserActionSinceUnixEpoc"></span>)</span>
            <!-- /ko -->
            <!-- ko if: ReadyForReturn && !WaitingForUserAction -->
            <span class="small">(Ready <span data-bind="livestamp: ReadyForReturnSinceUnixEpoc"></span>)</span>
            <!-- /ko -->
        </li>
    </script>
    <script>
        ko.bindingHandlers.livestamp = {
            init: function (element, valueAccessor, allBindings, viewModel, bindingContext) {
                var value = valueAccessor();
                var valueUnwrapped = ko.unwrap(value);

                if (valueUnwrapped)
                    $(element).livestamp(valueUnwrapped);
                else
                    $(element).livestamp('destroy');
            }
        };
    </script>
    <script>
        $(function () {
            var hub;
            var viewModel;

            var rotateSpeed = 3000;
            var itemFilters;
            var fixedTheme = null;

            var $inProcessList = $('#inProcess').find('ul');
            var $readyForReturnList = $('#readyForReturn').find('ul');
            var $waitingForUserActionList = $('#waitingForUserAction').find('ul');

            function noticeboardViewModel(inProcess, readyForReturn, waitingForUserAction) {
                var self = this;

                self.initialized = false;

                self.inProcess = ko.observableArray(inProcess);
                self.readyForReturn = ko.observableArray(readyForReturn);
                self.waitingForUserAction = ko.observableArray(waitingForUserAction);

                self.onRemove = function (element, index, data) {
                    $(element).slideUp(400, function () {
                        $(this).remove();
                    });
                }
                self.onAdd = function (element, index, data) {
                    if (self.initialized)
                        $(element).hide().slideDown(400);
                }
            }

            function init() {
                monitorMouseMove();
                applyQueryString();

                // Connect to Hub
                hub = $.connection.noticeboardUpdates;

                // Map Functions
                hub.client.updateHeldDeviceForUser = updateHeldDevice;
                hub.client.setTheme = setTheme;

                $.connection.hub.qs = { Noticeboard: '@(Disco.Services.Jobs.Noticeboards.HeldDevicesForUsers.Name)' };
                $.connection.hub.error(connectionError);
                $.connection.hub.disconnected(connectionError);
                $.connection.hub.reconnected(connectionError);

                // Start Connection
                $.connection.hub.start().fail(connectionError).done(loadData);
            }

            // Called after SignalR is connected
            function loadData() {
                $.getJSON('@(Url.Action(MVC.Public.UserHeldDevices.UserHeldDevices()))', null, function (data) {

                    var inProcess = [];
                    var readyForReturn = [];
                    var waitingForUserAction = [];

                    data.filter(function (heldDeviceItem) {
                        return includeItem(heldDeviceItem)
                    }).forEach(function (heldDeviceItem) {
                        if (isWaitingForUserAction(heldDeviceItem))
                            waitingForUserAction.push(heldDeviceItem);
                        else if (isReadyForReturn(heldDeviceItem))
                            readyForReturn.push(heldDeviceItem);
                        else if (isInProcess(heldDeviceItem))
                            inProcess.push(heldDeviceItem);
                    });

                    inProcess.sort(sortFunction);
                    readyForReturn.sort(sortFunction);
                    waitingForUserAction.sort(sortFunction);

                    viewModel = new noticeboardViewModel(inProcess, readyForReturn, waitingForUserAction);

                    ko.applyBindings(viewModel);
                    viewModel.initialized = true;

                    $('body').removeClass('status-connecting');

                    window.setTimeout(scheduleRotation, rotateSpeed);
                });
            }

            // Called by SignalR
            function updateHeldDevice(updates) {
                if (viewModel) {

                    $.each(updates, function (UserId, heldDeviceItem) {
                        // Remove Existing
                        removeItem(UserId);

                        // Add Item
                        addItem(heldDeviceItem);
                    });
                }
            }

            function removeItem(UserId) {
                removeItemFromArray(viewModel.inProcess, UserId);
                removeItemFromArray(viewModel.readyForReturn, UserId);
                removeItemFromArray(viewModel.waitingForUserAction, UserId);
            }

            function addItem(heldDeviceItem) {
                if (heldDeviceItem !== null &&
                    heldDeviceItem !== undefined &&
                    includeItem(heldDeviceItem)) {

                    var array;

                    if (isWaitingForUserAction(heldDeviceItem))
                        array = viewModel.waitingForUserAction;
                    else if (isReadyForReturn(heldDeviceItem))
                        array = viewModel.readyForReturn;
                    else if (isInProcess(heldDeviceItem))
                        array = viewModel.inProcess;

                    if (array().length === 0) {
                        array.push(heldDeviceItem);
                    } else {
                        var index = findSortedInsertIndex(array, heldDeviceItem);
                        if (index === -1)
                            array.push(heldDeviceItem);
                        else
                            array.splice(index, 0, heldDeviceItem);
                    }
                }
            }

            function rotateArrays() {
                rotateArray(viewModel.inProcess, $inProcessList);
                rotateArray(viewModel.readyForReturn, $readyForReturnList);
                rotateArray(viewModel.waitingForUserAction, $waitingForUserActionList);
            }

            function scheduleRotation() {
                rotateArrays();

                window.setTimeout(scheduleRotation, rotateSpeed);
            }

            function includeItem(heldDeviceItem) {
                if (itemFilters == null || itemFilters.length == 0)
                    return true;

                return itemFilters.reduce(function (previousValue, currentValue, index, array) {
                    if (previousValue === false)
                        return false;
                    return currentValue(heldDeviceItem);
                }, true);
            }

            function setTheme(theme) {
                if (!!fixedTheme)
                    return;

                var $body = $(document.body);

                // Existing classes
                var c = $body.attr('class').split(' ');
                // Remove existing theme
                c = $.grep(c, function (i) { return (i.indexOf('theme-') !== 0) });

                c.push('theme-' + theme);

                $body.attr('class', c.join(' '));
            }

            function monitorMouseMove() {
                var token = null,
                    $body = $(document.body);

                $body.mousemove(function () {
                    if (!!token)
                        window.clearTimeout(token);
                    else if ($body.css('cursor') == 'none')
                        $body.css('cursor', 'auto');

                    token = window.setTimeout(function () {
                        $body.css('cursor', 'none');
                        token = null;
                    }, 3500);
                });

            }

            function applyQueryString() {
                var queryStringParameters = getQueryStringParameters();

                if (queryStringParameters !== null) {
                    var filters = [];

                    $.each(queryStringParameters, function (key, value) {
                        switch (key.toLowerCase()) {
                            case 'components':
                                const showComponents = value.split(",");
                                if (showComponents.length > 0) {
                                    const components = ['inProcess', 'readyForReturn', 'waitingForUserAction'];
                                    components.forEach(function (component) {
                                        if (!showComponents.includes(component)) {
                                            $('body').addClass('hide-' + component);
                                        }
                                    });
                                }
                                break;
                            case 'theme': // THEME
                                setTheme(value);
                                fixedTheme = value;
                                break;
                            case 'deviceaddressinclude': // FILTER: Device Address Include
                                var deviceAddresses = value.split(",").map(function (v) { return v.toLowerCase(); });
                                if (deviceAddresses.length > 0) {
                                    filters.push(function (heldDeviceItem) {
                                        // false if DeviceAddressShortName is null
                                        if (!heldDeviceItem.DeviceAddressShortName)
                                            return false;

                                        // true if DeviceAddressShortName is included
                                        return $.inArray(heldDeviceItem.DeviceAddressShortName.toLowerCase(), deviceAddresses) >= 0;
                                    });
                                }
                                break;
                            case 'deviceaddressexclude': // FILTER: Device Address Exclude
                                var deviceAddresses = value.split(",").map(function (v) { return v.toLowerCase(); });
                                if (deviceAddresses.length > 0) {
                                    filters.push(function (heldDeviceItem) {
                                        // true if DeviceAddressShortName is null
                                        if (!heldDeviceItem.DeviceAddressShortName)
                                            return true;

                                        // true if DeviceAddressShortName is excluded
                                        return $.inArray(heldDeviceItem.DeviceAddressShortName.toLowerCase(), deviceAddresses) < 0;
                                    });
                                }
                                break;
                            case 'deviceprofileinclude': // FILTER: Device Profile Include
                                var deviceProfiles = value.split(",").map(function (v) { return parseInt(v); });
                                if (deviceProfiles.length > 0) {
                                    filters.push(function (heldDeviceItem) {
                                        // true if DeviceProfileId is included
                                        return $.inArray(heldDeviceItem.DeviceProfileId, deviceProfiles) >= 0;
                                    });
                                }
                                break;
                            case 'deviceprofileexclude': // FILTER: Device Profile Exclude
                                var deviceProfiles = value.split(",").map(function (v) { return parseInt(v); });
                                if (deviceProfiles.length > 0) {
                                    filters.push(function (heldDeviceItem) {
                                        // true if DeviceProfileId is excluded
                                        return $.inArray(heldDeviceItem.DeviceProfileId, deviceProfiles) < 0;
                                    });
                                }
                                break;
                        }
                    });

                    if (filters.length > 0)
                        itemFilters = filters;
                    else
                        itemFilters = null;
                }
            }

            function connectionError() {
                try {
                    $('body').addClass('status-error');
                    $.connection.hub.stop();
                } catch (e) {
                    // Ignore
                }

                window.setTimeout(function () {
                    window.location.reload(true);
                }, 10000);
            }

            // Helpers
            function rotateArray(koArray, element) {
                var items = koArray();

                if (items.length <= 1)
                    return 0;

                if (element.height() < (element.parent().height() - 30)) {

                    if (findUnsortedArrayTopIndex(items) !== 0)
                        koArray.sort(sortFunction);

                    // Don't rotate if small & sorted correctly
                    return;
                }

                // Move Last Item to Top
                var item = koArray.pop();
                koArray.unshift(item);
            }
            function removeItemFromArray(koArray, UserId) {
                var items = koArray();
                for (var i = 0; i < items.length; i++) {
                    if (items[i].UserId == UserId) {
                        koArray.splice(i, 1);
                        items = koArray();
                        i--;
                    }
                }
            }
            function findUnsortedArrayTopIndex(items) {
                // Only one Item
                if (items.length <= 1)
                    return 0;

                for (var i = 1; i < items.length; i++) {
                    var s = sortFunction(items[i - 1], items[i]);
                    if (s > 0)
                        return i;
                }

                return 0;
            }
            function findSortedInsertIndex(koArray, heldDeviceItem) {
                var items = koArray();
                var startIndex = findUnsortedArrayTopIndex(items);
                for (var i = startIndex; i < items.length; i++) {
                    var s = sortFunction(heldDeviceItem, items[i]);
                    if (s <= 0)
                        return i;
                }
                if (startIndex !== 0) {
                    for (var i = 0; i < startIndex; i++) {
                        var s = sortFunction(heldDeviceItem, items[i]);
                        if (s <= 0)
                            return i;
                    }
                    return startIndex;
                } else {
                    return -1;
                }
            }
            function sortFunction(l, r) {
                return l.UserIdFriendly.toLowerCase() == r.UserIdFriendly.toLowerCase() ? 0 : (l.UserIdFriendly.toLowerCase() < r.UserIdFriendly.toLowerCase() ? -1 : 1)
            }
            function isInProcess(i) {
                return !i.ReadyForReturn && !i.WaitingForUserAction;
            }
            function isReadyForReturn(i) {
                return i.ReadyForReturn && !i.WaitingForUserAction;
            }
            function isWaitingForUserAction(i) {
                return i.WaitingForUserAction;
            }
            function getQueryStringParameters() {

                if (window.location.search.length === 0)
                    return null;

                var params = {};
                window.location.search.substr(1).split("&").forEach(function (pair) {
                    if (pair === "") return;
                    var parts = pair.split("=");
                    params[parts[0]] = parts[1] && decodeURIComponent(parts[1].replace(/\+/g, " "));
                });
                return params;
            }

            init();
        });
    </script>
</body>
</html>
