//  listView.js
//
//  Created by David Back on 27 Aug 2018
//  Copyright 2018 High Fidelity, Inc.
//
//  Distributed under the Apache License, Version 2.0.
//  See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html

const SCROLL_ROWS = 2; // number of rows used as scrolling buffer, each time we pass this number of rows we scroll
const FIRST_ROW_INDEX = 2; // the first elRow element's index in the child nodes of the table body

function ListView(elTableBody, elTableScroll, elTableHeaderRow, createRowFunction, updateRowFunction, clearRowFunction,
                  preRefreshFunction, postRefreshFunction, preResizeFunction, WINDOW_NONVARIABLE_HEIGHT) {
    this.elTableBody = elTableBody;
    this.elTableScroll = elTableScroll;
    this.elTableHeaderRow = elTableHeaderRow;
    
    this.elTopBuffer = null;
    this.elBottomBuffer = null;
    
    this.createRowFunction = createRowFunction;
    this.updateRowFunction = updateRowFunction;
    this.clearRowFunction = clearRowFunction;
    this.preRefreshFunction = preRefreshFunction;
    this.postRefreshFunction = postRefreshFunction;
    this.preResizeFunction = preResizeFunction;
    
    // the list of row elements created in the table up to max viewable height plus SCROLL_ROWS rows for scrolling buffer
    this.elRows = [];
    // the list of all row item data to show in the scrolling table, passed to updateRowFunction to set to each row
    this.itemData = [];
    // the current index within the itemData list that is set to the top most elRow element
    this.rowOffset = 0;
    // height of the elRow elements
    this.rowHeight = 0;
    // the previous elTableScroll.scrollTop value when the elRows were last shifted for scrolling
    this.lastRowShiftScrollTop = 0;
    
    this.initialize();
}
    
ListView.prototype = {
    getNumRows: function() {
        return this.elRows.length;
    },
    
    getScrollHeight: function() {
        return this.rowHeight * SCROLL_ROWS;
    },
    
    getFirstVisibleRowIndex: function() {
        return this.rowOffset;
    },
    
    getLastVisibleRowIndex: function() {
        return this.getFirstVisibleRowIndex() + entityList.getNumRows() - 1;
    },
    
    resetToTop: function() {
        this.rowOffset = 0;
        this.lastRowShiftScrollTop = 0;
        this.refreshBuffers();
        this.elTableScroll.scrollTop = 0;
    },

    clear: function() {
        for (let i = 0; i < this.getNumRows(); i++) {
            let elRow = this.elTableBody.childNodes[i + FIRST_ROW_INDEX];
            this.clearRowFunction(elRow);
            elRow.style.display = "none"; // hide cleared rows
        }
    },

    scroll: function() {
        let scrollTop = this.elTableScroll.scrollTop;       
        let scrollHeight = this.getScrollHeight();
        let nextRowChangeScrollTop = this.lastRowShiftScrollTop + scrollHeight;
        let totalItems = this.itemData.length;
        let numRows = this.getNumRows();

        // if the top of the scroll area has past the amount of scroll row space since the last point of scrolling and there
        // are still more rows to scroll to then trigger a scroll down by the min of the scroll row space or number of
        // remaining rows below
        // if the top of the scroll area has gone back above the last point of scrolling then trigger a scroll up by min of
        // the scroll row space or number of rows above
        if (scrollTop >= nextRowChangeScrollTop && numRows + this.rowOffset < totalItems) {
            let numScrolls = Math.ceil((scrollTop - nextRowChangeScrollTop) / scrollHeight);
            let numScrollRows = numScrolls * SCROLL_ROWS;
            if (numScrollRows + this.rowOffset + numRows > totalItems) {
                numScrollRows = totalItems - this.rowOffset - numRows;
            }
            this.scrollRows(numScrollRows);
        } else if (scrollTop < this.lastRowShiftScrollTop) {
            let numScrolls = Math.ceil((this.lastRowShiftScrollTop - scrollTop) / scrollHeight);
            let numScrollRows = numScrolls * SCROLL_ROWS;
            if (this.rowOffset - numScrollRows < 0) {
                numScrollRows = this.rowOffset;
            }
            this.scrollRows(-numScrollRows);
        }
    },
    
    scrollRows: function(numScrollRows) {
        let numScrollRowsAbsolute = Math.abs(numScrollRows);
        if (numScrollRowsAbsolute === 0) {
            return;
        }
        
        let scrollDown = numScrollRows > 0; 
        
        let prevTopHeight = parseInt(this.elTopBuffer.getAttribute("height"));
        let prevBottomHeight =  parseInt(this.elBottomBuffer.getAttribute("height"));
        
        // if the number of rows to scroll at once is greater than the total visible number of row elements,
        // then just advance the rowOffset accordingly and allow the refresh below to update all rows
        if (numScrollRowsAbsolute > this.getNumRows()) {
            this.rowOffset += numScrollRows;
        } else {
            // for each row to scroll down, move the top row element to the bottom of the
            // table before the bottom buffer and reset it's row data to the new item
            // for each row to scroll up, move the bottom row element to the top of
            // the table before the top row and reset it's row data to the new item
            for (let i = 0; i < numScrollRowsAbsolute; i++) {
                let topRow = this.elTableBody.childNodes[FIRST_ROW_INDEX];
                let rowToMove = scrollDown ? topRow : this.elTableBody.childNodes[FIRST_ROW_INDEX + this.getNumRows() - 1];
                let rowIndex = scrollDown ? this.getNumRows() + this.rowOffset : this.rowOffset - 1;
                let moveRowBefore = scrollDown ? this.elBottomBuffer : topRow;
                this.elTableBody.removeChild(rowToMove);
                this.elTableBody.insertBefore(rowToMove, moveRowBefore);
                this.updateRowFunction(rowToMove, this.itemData[rowIndex]);
                this.rowOffset += scrollDown ? 1 : -1;
            }
        }
        
        // add/remove the row space that was scrolled away to the top buffer height and last scroll point
        // add/remove the row space that was scrolled away to the bottom buffer height      
        let scrolledSpace = this.rowHeight * numScrollRows;
        let newTopHeight = prevTopHeight + scrolledSpace;
        let newBottomHeight = prevBottomHeight - scrolledSpace;
        this.elTopBuffer.setAttribute("height", newTopHeight);
        this.elBottomBuffer.setAttribute("height", newBottomHeight);
        this.lastRowShiftScrollTop += scrolledSpace;
        
        // if scrolling more than the total number of visible rows at once then refresh all row data
        if (numScrollRowsAbsolute > this.getNumRows()) {
            this.refresh();
        }
    },

    /**
     * Scrolls firstRowIndex with least effort, also tries to make the window include the other selections in case lastRowIndex is set.
     * In the case that firstRowIndex and lastRowIndex are already within the visible bounds then nothing will happen.
     * @param {number} firstRowIndex - The row that will be scrolled to.
     * @param {number} lastRowIndex - The last index of the bound.
     */
    scrollToRow: function (firstRowIndex, lastRowIndex) {
        lastRowIndex = lastRowIndex ? lastRowIndex : firstRowIndex;
        let boundingTop = firstRowIndex * this.rowHeight;
        let boundingBottom = (lastRowIndex * this.rowHeight) + this.rowHeight;
        if ((boundingBottom - boundingTop) > this.elTableScroll.clientHeight) {
            boundingBottom = boundingTop + this.elTableScroll.clientHeight;
        }

        let currentVisibleAreaTop = this.elTableScroll.scrollTop;
        let currentVisibleAreaBottom = currentVisibleAreaTop + this.elTableScroll.clientHeight;

        if (boundingTop < currentVisibleAreaTop) {
            this.elTableScroll.scrollTop = boundingTop;
        } else if (boundingBottom > currentVisibleAreaBottom) {
            this.elTableScroll.scrollTop = boundingBottom - (this.elTableScroll.clientHeight);
        }
    },
    
    refresh: function() {
        this.preRefreshFunction();
        // block refreshing before rows are initialized
        let numRows = this.getNumRows();
        if (numRows === 0) {
            return;
        }
        
        let prevScrollTop = this.elTableScroll.scrollTop;
        
        // start with all row data cleared and initially set to invisible
        this.clear();
        
        // if we are at the bottom of the list adjust row offset to make sure all rows stay in view
        this.refreshRowOffset();
        
        // update all row data and set rows visible until max visible items reached
        for (let i = 0; i < numRows; i++) {
            let rowIndex = i + this.rowOffset;
            if (rowIndex >= this.itemData.length) {
                break;
            }
            let rowElementIndex = i + FIRST_ROW_INDEX;
            let elRow = this.elTableBody.childNodes[rowElementIndex];
            let itemData = this.itemData[rowIndex];
            this.updateRowFunction(elRow, itemData);
            elRow.style.display = ""; // make sure the row is visible
        }
        
        // update the top and bottom buffer heights to adjust for above changes
        this.refreshBuffers();

        // adjust the last row shift scroll point based on how much the current scroll point changed
        let scrollTopDifference =  this.elTableScroll.scrollTop - prevScrollTop;
        if (scrollTopDifference !== 0) {
            this.lastRowShiftScrollTop += scrollTopDifference;
            if (this.lastRowShiftScrollTop < 0) {
                this.lastRowShiftScrollTop = 0;
            }
        }
        this.postRefreshFunction();
    },
    
    refreshBuffers: function() {
        // top buffer height is the number of hidden rows above the top row
        let topHiddenRows = this.rowOffset;
        let topBufferHeight = this.rowHeight * topHiddenRows;
        this.elTopBuffer.setAttribute("height", topBufferHeight);
        
        // bottom buffer height is the number of hidden rows below the bottom row (last scroll buffer row)
        let bottomHiddenRows = this.itemData.length - this.getNumRows() - this.rowOffset;
        let bottomBufferHeight = this.rowHeight * bottomHiddenRows;
        if (bottomHiddenRows < 0) {
            bottomBufferHeight = 0;
        }
        this.elBottomBuffer.setAttribute("height", bottomBufferHeight);
    },
    
    refreshRowOffset: function() {
        // make sure the row offset isn't causing visible rows to pass the end of the item list and is clamped to 0
        let numRows = this.getNumRows();
        if (this.rowOffset + numRows > this.itemData.length) {
            this.rowOffset = this.itemData.length - numRows;
        }
        if (this.rowOffset < 0) {
            this.rowOffset = 0;
        }
    },

    resize: function() {        
        if (!this.elTableBody || !this.elTableScroll) {
            console.log("ListView.resize - no valid table body or table scroll element");
            return;
        }
        this.preResizeFunction();
        let prevScrollTop = this.elTableScroll.scrollTop;  

        // take up available window space
        this.elTableScroll.style.height = window.innerHeight - WINDOW_NONVARIABLE_HEIGHT;
        let viewableHeight = parseInt(this.elTableScroll.style.height) + 1;

        // remove all existing row elements and clear row list
        for (let i = 0; i < this.getNumRows(); i++) {
            let elRow = this.elRows[i];
            this.elTableBody.removeChild(elRow);
        }
        this.elRows = [];
        
        // create new row elements inserted between the top and bottom buffers up until the max viewable scroll area
        let usedHeight = 0;
        while (usedHeight < viewableHeight) {
            let newRow = this.createRowFunction();
            this.elTableBody.insertBefore(newRow, this.elBottomBuffer);
            this.rowHeight = newRow.offsetHeight;
            usedHeight += this.rowHeight;     
            this.elRows.push(newRow);
        }
        
        // add SCROLL_ROWS extras rows for scrolling buffer purposes
        for (let i = 0; i < SCROLL_ROWS; i++) {
            let scrollRow = this.createRowFunction();
            this.elTableBody.insertBefore(scrollRow, this.elBottomBuffer);
            this.elRows.push(scrollRow);
        }
        
        // restore the scroll point to the same scroll point from before above changes
        this.elTableScroll.scrollTop = prevScrollTop;
        
        this.refresh();
    },
    
    initialize: function() {
        if (!this.elTableBody || !this.elTableScroll) {
            console.log("ListView.initialize - no valid table body or table scroll element");
            return;
        }
        
        this.elTopBuffer = document.createElement("tr");
        this.elTableBody.appendChild(this.elTopBuffer);
        this.elTopBuffer.setAttribute("height", 0);
        
        this.elBottomBuffer = document.createElement("tr");
        this.elTableBody.appendChild(this.elBottomBuffer);
        this.elBottomBuffer.setAttribute("height", 0);

        this.onScroll = this.scroll.bind(this);
        this.elTableScroll.addEventListener("scroll", this.onScroll);
        this.onResize = this.resize.bind(this);
        window.addEventListener("resize", this.onResize);

        // initialize all row elements
        this.resize();
    }
};