/* eslint camelcase: 0 */
import {
    EVENT_START_PLAY,
    EVENT_START_TASK,
    OUTPUT_ELEMENT_LAST,
    OUTPUT_PAGE_SIZE,
} from './constants';

let $q;
let $scope;
let $state;

let resource;
let render;
let scroll;
let status;
let slide;
let stream;
let page;

let vm;
const listeners = [];
let lockFrames = false;

function onFrames (events) {
    events = slide.pushFrames(events);

    if (lockFrames) {
        return $q.resolve();
    }

    const popCount = events.length - render.getCapacity();

    if (!vm.isFollowing && canStartFollowing()) {
        startFollowing();
    }

    if (!vm.isFollowing && popCount > 0) {
        return $q.resolve();
    }

    scroll.pause();

    if (vm.isFollowing) {
        scroll.scrollToBottom();
    }

    return render.popBack(popCount)
        .then(() => {
            if (vm.isFollowing) {
                scroll.scrollToBottom();
            }

            return render.pushFront(events);
        })
        .then(() => {
            if (vm.isFollowing) {
                scroll.scrollToBottom();
            }

            scroll.resume();

            return $q.resolve();
        });
}

//
// Menu Controls (Running)
//

function firstRange () {
    if (scroll.isPaused()) {
        return $q.resolve();
    }

    stopFollowing();
    lockFollow = true;

    if (slide.isOnFirstPage()) {
        scroll.resetScrollPosition();

        return $q.resolve();
    }

    scroll.pause();
    lockFrames = true;

    return render.clear()
        .then(() => slide.getFirst())
        .then(results => render.pushFront(results))
        .then(() => slide.getNext())
        .then(results => {
            const popCount = results.length - render.getCapacity();

            return render.popBack(popCount)
                .then(() => render.pushFront(results));
        })
        .finally(() => {
            render.compile();
            scroll.resume();
            lockFollow = false;
        });
}

function nextRange () {
    if (vm.isFollowing) {
        scroll.scrollToBottom();

        return $q.resolve();
    }

    if (scroll.isPaused()) {
        return $q.resolve();
    }

    scroll.pause();
    lockFrames = true;

    return slide.getNext()
        .then(results => {
            const popCount = results.length - render.getCapacity();

            return render.popBack(popCount)
                .then(() => render.pushFront(results));
        })
        .finally(() => {
            render.compile();
            scroll.resume();
            lockFrames = false;

            return $q.resolve();
        });
}

function previousRange () {
    if (scroll.isPaused()) {
        return $q.resolve();
    }

    scroll.pause();
    stopFollowing();
    lockFrames = true;

    let initialPosition;
    let popHeight;

    return slide.getPrevious()
        .then(results => {
            const popCount = results.length - render.getCapacity();
            initialPosition = scroll.getScrollPosition();

            return render.popFront(popCount)
                .then(() => {
                    popHeight = scroll.getScrollHeight();

                    return render.pushBack(results);
                });
        })
        .then(() => {
            const currentHeight = scroll.getScrollHeight();
            scroll.setScrollPosition(currentHeight - popHeight + initialPosition);

            return $q.resolve();
        })
        .finally(() => {
            render.compile();
            scroll.resume();
            lockFrames = false;

            return $q.resolve();
        });
}

function lastRange () {
    if (scroll.isPaused()) {
        return $q.resolve();
    }

    scroll.pause();
    lockFrames = true;

    return render.clear()
        .then(() => slide.getLast())
        .then(results => render.pushFront(results))
        .then(() => {
            stream.setMissingCounterThreshold(slide.getTailCounter() + 1);

            scroll.scrollToBottom();
            lockFrames = false;

            return $q.resolve();
        })
        .finally(() => {
            render.compile();
            scroll.resume();

            return $q.resolve();
        });
}

function menuLastRange () {
    if (vm.isFollowing) {
        lockFollow = true;
        stopFollowing();

        return $q.resolve();
    }

    lockFollow = false;

    return lastRange()
        .then(() => {
            startFollowing();

            return $q.resolve();
        });
}

let followOnce;
let lockFollow;
function canStartFollowing () {
    if (lockFollow) {
        return false;
    }

    if (slide.isOnLastPage() && scroll.isBeyondLowerThreshold()) {
        followOnce = false;

        return true;
    }

    if (followOnce && // one-time activation from top of first page
        scroll.isBeyondUpperThreshold() &&
        slide.getTailCounter() - slide.getHeadCounter() >= OUTPUT_PAGE_SIZE) {
        followOnce = false;

        return true;
    }

    return false;
}

function startFollowing () {
    if (vm.isFollowing) {
        return;
    }

    vm.isFollowing = true;
    vm.followTooltip = vm.strings.get('tooltips.MENU_FOLLOWING');
}

function stopFollowing () {
    if (!vm.isFollowing) {
        return;
    }

    scroll.unlock();
    scroll.unhide();

    vm.isFollowing = false;
    vm.followTooltip = vm.strings.get('tooltips.MENU_LAST');
}

//
// Menu Controls (Page Mode)
//

function firstPage () {
    if (scroll.isPaused()) {
        return $q.resolve();
    }

    scroll.pause();

    return render.clear()
        .then(() => page.getFirst())
        .then(results => render.pushFront(results))
        .then(() => page.getNext())
        .then(results => {
            const popCount = page.trimHead();

            return render.popBack(popCount)
                .then(() => render.pushFront(results));
        })
        .finally(() => {
            render.compile();
            scroll.resume();

            return $q.resolve();
        });
}

function lastPage () {
    if (scroll.isPaused()) {
        return $q.resolve();
    }

    scroll.pause();

    return render.clear()
        .then(() => page.getLast())
        .then(results => render.pushBack(results))
        .then(() => page.getPrevious())
        .then(results => {
            const popCount = page.trimTail();

            return render.popFront(popCount)
                .then(() => render.pushBack(results));
        })
        .then(() => {
            scroll.scrollToBottom();

            return $q.resolve();
        })
        .finally(() => {
            render.compile();
            scroll.resume();

            return $q.resolve();
        });
}

function nextPage () {
    if (scroll.isPaused()) {
        return $q.resolve();
    }

    scroll.pause();

    return page.getNext()
        .then(results => {
            const popCount = page.trimHead();

            return render.popBack(popCount)
                .then(() => render.pushFront(results));
        })
        .finally(() => {
            render.compile();
            scroll.resume();
        });
}

function previousPage () {
    if (scroll.isPaused()) {
        return $q.resolve();
    }

    scroll.pause();

    let initialPosition;
    let popHeight;

    return page.getPrevious()
        .then(results => {
            const popCount = page.trimTail();
            initialPosition = scroll.getScrollPosition();

            return render.popFront(popCount)
                .then(() => {
                    popHeight = scroll.getScrollHeight();

                    return render.pushBack(results);
                });
        })
        .then(() => {
            const currentHeight = scroll.getScrollHeight();
            scroll.setScrollPosition(currentHeight - popHeight + initialPosition);

            return $q.resolve();
        })
        .finally(() => {
            render.compile();
            scroll.resume();

            return $q.resolve();
        });
}

//
// Menu Controls
//

function first () {
    if (vm.isProcessingFinished) {
        return firstPage();
    }

    return firstRange();
}

function last () {
    if (vm.isProcessingFinished) {
        return lastPage();
    }

    return lastRange();
}

function next () {
    if (vm.isProcessingFinished) {
        return nextPage();
    }

    return nextRange();
}

function previous () {
    if (vm.isProcessingFinished) {
        return previousPage();
    }

    return previousRange();
}

function menuLast () {
    if (vm.isProcessingFinished) {
        return lastPage();
    }

    return menuLastRange();
}

function down () {
    scroll.moveDown();
}

function up () {
    scroll.moveUp();
}

function togglePanelExpand () {
    vm.isPanelExpanded = !vm.isPanelExpanded;
}

//
// Line Interaction
//

const iconCollapsed = 'fa-angle-right';
const iconExpanded = 'fa-angle-down';
const iconSelector = '.at-Stdout-toggle > i';
const lineCollapsed = 'hidden';

function toggleCollapseAll () {
    if (scroll.isPaused()) return;

    const records = Object.keys(render.records).map(key => render.records[key]);
    const plays = records.filter(({ name }) => name === EVENT_START_PLAY);
    const tasks = records.filter(({ name }) => name === EVENT_START_TASK);

    const orphanLines = records
        .filter(({ level }) => level === 3)
        .filter(({ parents }) => !records[parents[0]]);

    const orphanLineParents = orphanLines
        .map(({ parents }) => ({ uuid: parents[0] }));

    plays.concat(tasks).forEach(({ uuid }) => {
        const icon = $(`#${uuid} ${iconSelector}`);

        if (vm.isMenuCollapsed) {
            icon.removeClass(iconCollapsed);
            icon.addClass(iconExpanded);
        } else {
            icon.removeClass(iconExpanded);
            icon.addClass(iconCollapsed);
        }
    });

    tasks.concat(orphanLineParents).forEach(({ uuid }) => {
        const lines = $(`.child-of-${uuid}`);

        if (vm.isMenuCollapsed) {
            lines.removeClass(lineCollapsed);
        } else {
            lines.addClass(lineCollapsed);
        }
    });

    vm.isMenuCollapsed = !vm.isMenuCollapsed;
    render.setCollapseAll(vm.isMenuCollapsed);
}

function toggleCollapse (uuid) {
    if (scroll.isPaused()) return;

    const record = render.records[uuid];

    if (record.name === EVENT_START_PLAY) {
        togglePlayCollapse(uuid);
    }

    if (record.name === EVENT_START_TASK) {
        toggleTaskCollapse(uuid);
    }
}

function togglePlayCollapse (uuid) {
    const record = render.records[uuid];
    const descendants = record.children || [];

    const icon = $(`#${uuid} ${iconSelector}`);
    const lines = $(`.child-of-${uuid}`);
    const taskIcons = $(`#${descendants.join(', #')}`).find(iconSelector);

    const isCollapsed = icon.hasClass(iconCollapsed);

    if (isCollapsed) {
        icon.removeClass(iconCollapsed);
        icon.addClass(iconExpanded);

        taskIcons.removeClass(iconExpanded);
        taskIcons.addClass(iconCollapsed);
        lines.removeClass(lineCollapsed);

        descendants
            .map(item => $(`.child-of-${item}`))
            .forEach(line => line.addClass(lineCollapsed));
    } else {
        icon.removeClass(iconExpanded);
        icon.addClass(iconCollapsed);

        taskIcons.removeClass(iconExpanded);
        taskIcons.addClass(iconCollapsed);

        lines.addClass(lineCollapsed);
    }

    descendants
        .map(item => render.records[item])
        .filter(({ name }) => name === EVENT_START_TASK)
        .forEach(rec => { render.records[rec.uuid].isCollapsed = true; });

    render.records[uuid].isCollapsed = !isCollapsed;
}

function toggleTaskCollapse (uuid) {
    const icon = $(`#${uuid} ${iconSelector}`);
    const lines = $(`.child-of-${uuid}`);

    const isCollapsed = icon.hasClass(iconCollapsed);

    if (isCollapsed) {
        icon.removeClass(iconCollapsed);
        icon.addClass(iconExpanded);
        lines.removeClass(lineCollapsed);
    } else {
        icon.removeClass(iconExpanded);
        icon.addClass(iconCollapsed);
        lines.addClass(lineCollapsed);
    }

    render.records[uuid].isCollapsed = !isCollapsed;
}

function showHostDetails (id, uuid) {
    $state.go('output.host-event.json', { eventId: id, taskUuid: uuid });
}

function showMissingEvents (uuid) {
    const record = render.records[uuid];

    const min = Math.min(...record.counters);
    const max = Math.min(Math.max(...record.counters), min + OUTPUT_PAGE_SIZE);

    const selector = `#${uuid}`;
    const clicked = $(selector);

    return resource.events.getRange([min, max])
        .then(results => {
            const counters = results.map(({ counter }) => counter);

            for (let i = min; i <= max; i++) {
                if (counters.indexOf(i) < 0) {
                    results = results.filter(({ counter }) => counter < i);
                    break;
                }
            }

            let lines = 0;
            let untrusted = '';

            for (let i = 0; i <= results.length - 1; i++) {
                const { html, count } = render.transformEvent(results[i]);

                lines += count;
                untrusted += html;

                const shifted = render.records[uuid].counters.shift();
                delete render.uuids[shifted];
            }

            const trusted = render.trustHtml(untrusted);
            const elements = angular.element(trusted);

            return render
                .requestAnimationFrame(() => {
                    elements.insertBefore(clicked);

                    if (render.records[uuid].counters.length === 0) {
                        clicked.remove();
                        delete render.records[uuid];
                    }
                })
                .then(() => render.compile())
                .then(() => lines);
        });
}

//
// Event Handling
//

let streaming;
function stopListening () {
    streaming = null;

    listeners.forEach(deregister => deregister());
    listeners.length = 0;
}

function startListening () {
    stopListening();

    listeners.push($scope.$on(resource.ws.events, (scope, data) => handleJobEvent(data)));
    listeners.push($scope.$on(resource.ws.status, (scope, data) => handleStatusEvent(data)));

    if (resource.model.get('type') === 'job') return;
    if (resource.model.get('type') === 'project_update') return;

    listeners.push($scope.$on(resource.ws.summary, (scope, data) => handleSummaryEvent(data)));
}

function handleJobEvent (data) {
    streaming = streaming || resource.events
        .getRange([Math.max(1, data.counter - 50), data.counter + 50])
        .then(results => {
            results.push(data);

            const counters = results.map(({ counter }) => counter);
            const min = Math.min(...counters);
            const max = Math.max(...counters);

            const missing = [];
            for (let i = min; i <= max; i++) {
                if (counters.indexOf(i) < 0) {
                    missing.push(i);
                }
            }

            if (missing.length > 0) {
                const maxMissing = Math.max(...missing);
                results = results.filter(({ counter }) => counter > maxMissing);
            }

            results.forEach(item => {
                stream.pushJobEvent(item);
                status.pushJobEvent(item);
            });

            stream.setMissingCounterThreshold(min);

            return $q.resolve();
        });

    streaming
        .then(() => {
            stream.pushJobEvent(data);
            status.pushJobEvent(data);
        });
}

function handleStatusEvent (data) {
    status.pushStatusEvent(data);
}

function handleSummaryEvent (data) {
    if (resource.model.get('id') !== data.unified_job_id) return;
    if (!data.final_counter) return;

    stream.setFinalCounter(data.final_counter);
}

//
// Search
//

function reloadState (params) {
    params.isPanelExpanded = vm.isPanelExpanded;

    return $state.transitionTo($state.current, params, { inherit: false, location: 'replace' });
}

//
// Debug Mode
//

function clear () {
    stopListening();
    render.clear();

    followOnce = true;
    lockFollow = false;
    lockFrames = false;

    stream.bufferInit();
    status.init(resource);
    slide.init(resource.events, render);
    status.subscribe(data => { vm.status = data.status; });

    startListening();
}

function OutputIndexController (
    _$q_,
    _$scope_,
    _$state_,
    _resource_,
    _scroll_,
    _page_,
    _render_,
    _status_,
    _slide_,
    _stream_,
    $filter,
    strings,
    $stateParams,
) {
    const { isPanelExpanded, _debug } = $stateParams;
    const isProcessingFinished = !_debug && _resource_.model.get('event_processing_finished');

    $q = _$q_;
    $scope = _$scope_;
    $state = _$state_;

    resource = _resource_;
    scroll = _scroll_;
    render = _render_;
    status = _status_;
    stream = _stream_;
    slide = _slide_;
    page = _page_;

    vm = this || {};

    // Panel
    vm.title = $filter('sanitize')(resource.model.get('name'));
    vm.status = resource.model.get('status');
    vm.strings = strings;
    vm.resource = resource;
    vm.reloadState = reloadState;
    vm.isPanelExpanded = isPanelExpanded;
    vm.isProcessingFinished = isProcessingFinished;
    vm.togglePanelExpand = togglePanelExpand;

    // Stdout Navigation
    vm.menu = { last: menuLast, first, down, up, clear };
    vm.isMenuCollapsed = false;
    vm.isFollowing = false;
    vm.toggleCollapseAll = toggleCollapseAll;
    vm.toggleCollapse = toggleCollapse;
    vm.showHostDetails = showHostDetails;
    vm.showMissingEvents = showMissingEvents;
    vm.toggleLineEnabled = resource.model.get('type') === 'job';
    vm.followTooltip = vm.strings.get('tooltips.MENU_LAST');
    vm.debug = _debug;

    render.requestAnimationFrame(() => {
        render.init($scope, { toggles: vm.toggleLineEnabled });

        status.init(resource);
        page.init(resource.events);
        slide.init(resource.events, render);

        scroll.init({
            next,
            previous,
            onThresholdLeave () {
                followOnce = false;
                lockFollow = false;
                stopFollowing();

                return $q.resolve();
            },
        });

        let showFollowTip = true;
        const rates = [];
        stream.init({
            onFrames,
            onFrameRate (rate) {
                rates.push(rate);
                rates.splice(0, rates.length - 5);

                if (rates.every(value => value === 1)) {
                    scroll.unlock();
                    scroll.unhide();
                }

                if (rate > 1 && vm.isFollowing) {
                    scroll.lock();
                    scroll.hide();

                    if (showFollowTip) {
                        showFollowTip = false;
                        $(OUTPUT_ELEMENT_LAST).trigger('mouseenter');
                    }
                }
            },
            onStop () {
                lockFollow = true;
                stopFollowing();
                stopListening();
                status.updateStats();
                status.dispatch();
                status.sync();
                scroll.unlock();
                scroll.unhide();
                render.compile();
            }
        });

        if (isProcessingFinished) {
            followOnce = false;
            lockFollow = true;
            lockFrames = true;
            stopListening();
        } else {
            followOnce = true;
            lockFollow = false;
            lockFrames = false;
            resource.events.clearCache();
            status.subscribe(data => { vm.status = data.status; });
            startListening();
        }

        if (_debug) {
            return render.clear();
        }

        return last();
    });

    $scope.$on('$destroy', () => {
        stopListening();

        render.clear();
        render.el.remove();
        slide.clear();
        stream.bufferInit();
    });
}

OutputIndexController.$inject = [
    '$q',
    '$scope',
    '$state',
    'resource',
    'OutputScrollService',
    'OutputPageService',
    'OutputRenderService',
    'OutputStatusService',
    'OutputSlideService',
    'OutputStreamService',
    '$filter',
    'OutputStrings',
    '$stateParams',
];

module.exports = OutputIndexController;
