Source: layout/src/visual-matrix/visual-matrix.js

import { mergeRecursive, generateGetterSetters } from 'muze-utils';
import {
    createTree,
    extraCellsRemover,
    combineMatrices,
    spaceTakenByColumn,
    getDistributedHeight,
    getDistributedWidth,
    spaceTakenByRow,
    computeLogicalSpace,
    createMatrixEachLevel,
    createMatrixInstances
  } from '../utils';
import { PROPS } from './props';
import { COLUMN_ROOT, ROW_ROOT, HEIGHT, WIDTH } from '../enums/constants';
import { defaultConfig } from './default-config';

/**
 * This class used to create column / row matrix for GridLayout
 *
 * @class VisualMatrix
 */
export default class VisualMatrix {

    /**
     *Creates an instance of VisualMatrix.
     * @param {any} matrix Two set of matrices
     * @param {any} [config={}] Configuration for VisualMatrix
     * @memberof VisualMatrix
     */
    constructor (matrix, config = {}) {
        // Prepare matrices
        this._lastLevelKey = 0;
        this._primaryMatrix = matrix[0] || [];
        this._secondaryMatrix = matrix[1] || [];
        this._maxMeasures = [];
        this._availableSpace = {};

        // Store the config
        generateGetterSetters(this, PROPS);
        const defCon = Object.assign({}, this.constructor.defaultConfig());
        this.config(mergeRecursive(defCon, config));

        this._breakPointer = this.config().isTransposed ? matrix[0].length :
            (matrix[0].length > 0 ? matrix[0][0].length : 0);
        this._layoutMatrix = combineMatrices([matrix[0] || [], matrix[1] || []], this.config());

        // Create Tree
        this._tree = {
            key: this.config().isTransposed ? COLUMN_ROOT : ROW_ROOT,
            values: this.createTree()
        };
        this._logicalSpace = this.setLogicalSpace();
    }


    primaryMatrix (...params) {
        if (params.length) {
            return this;
        }
        return this._primaryMatrix;
    }


    secondaryMatrix (...params) {
        if (params.length) {
            return this;
        }
        return this._secondaryMatrix;
    }


    tree (...params) {
        if (params.length) {
            return this;
        }
        return this._tree;
    }


    static defaultConfig () {
        return defaultConfig;
    }


    createTree () {
        const { tree, lastLevelKey } = createTree(this);
        this._lastLevelKey = lastLevelKey;
        return tree;
    }

    /**
     * Computes the logical space taken by the entire matrixTree
     *
     * @return {Object} Logical space taken
     * @memberof VisualMatrix
     */
    setLogicalSpace () {
        const {
            isTransposed
        } = this.config();
        const matrixTree = this.tree();
        createMatrixEachLevel(matrixTree, isTransposed);
        return computeLogicalSpace(matrixTree, this.config(), this.maxMeasures());
    }

    /**
     * Returns the space taken by visual matrix
     *
     * @return {Object} space taken by the matrix
     * @memberof VisualMatrix
     */
    getLogicalSpace () {
        return this.logicalSpace();
    }

    /**
     * Sets the provied space to the visual matrix
     *
     * @param {number} width width provided
     * @param {number} height height provided
     * @memberof VisualMatrix
     */
    setAvailableSpace (width, height) {
        this.availableSpace({ width, height });
        const tree = this.tree();
        const heightMeasures = this.populateMaxMeasures(HEIGHT, tree);
        const widthMeasures = this.populateMaxMeasures(WIDTH, tree);
        const depth = this.calculateDepth(widthMeasures, heightMeasures);

        this.viewableMatrix = this.createViewPortMatrix(depth);
        this.viewableMeasures = this.redistribute(this.viewableMatrix, width, height);
        return this;
    }

    /**
     * Populate the max measures in the array
     *
     * @param {Array} measures array to be filled with max measures
     * @param {Object} matrixTree matrix tree of visual matrix
     * @param {number} measure width or height
     * @param {number} [depth=0] depth of the tree that to be calculated
     * @memberof VisualMatrix
     */
    populateMaxMeasures (type, matrixTree, depth = 0, measures = []) {
        measures[depth] = Math.max(measures[depth] || 0, matrixTree.space[type]);
        if (matrixTree.values) {
            const childDepth = depth + 1;
            matrixTree.values.forEach((child) => {
                if (child.space) {
                    measures = this.populateMaxMeasures(type, child, childDepth, measures);
                }
            });
        }
        return measures;
    }

    /**
     * Calculates the depth of the tree that can be viewed
     *
     * @param {Array} widthMeasures array of widths
     * @param {Array} heightMeasures array of heights
     * @return {number} depth of the tree
     * @memberof VisualMatrix
     */
    calculateDepth (widthMeasures, heightMeasures) {
        let i;
        let j;
        const { height, width } = this.availableSpace();

        for (i = 0; i < heightMeasures.length; i++) {
            if (heightMeasures[i] <= height) break;
        }
        for (j = 0; j < widthMeasures.length; j++) {
            if (widthMeasures[j] <= width) break;
        }
        return Math.min(widthMeasures.length - 1, Math.max(i, j));
    }

    /**
     * Redistributes the provied space to all cells
     *
     * @param {*} viewableMatrix current viewport matrix
     * @param {*} width provied width
     * @param {*} height provied height
     * @return {Object} current viewports matrixes with measures
     * @memberof VisualMatrix
     */
    redistribute (viewableMatrix, width, height) {
        let maxHeights = [];
        let maxWidths = [];
        const {
            isTransposed
        } = this.config();

        viewableMatrix.forEach((matrixInst) => {
            const matrix = matrixInst.matrix;
            const mWidth = 0;
            const mHeight = 0;
            const options = { mWidth, mHeight, matrix, width, height, maxHeights, maxWidths };
            const maxMeasures = isTransposed ? this.redistributeColumnWise(options) : this.redistributeRowWise(options);
            maxWidths = maxMeasures.maxWidths;
            maxHeights = maxMeasures.maxHeights;
        });

        const measurements = viewableMatrix.map((matrixInst, i) => {
            let heightMeasures;
            let columnMeasures;
            const cellDimOptions = { matrixInst, maxWidths, maxHeights, matrixIndex: i };
            const { heights, widths, rowHeights, columnWidths } = this.getCellDimensions(cellDimOptions);

            if (!isTransposed) {
                heightMeasures = [height, height];
                columnMeasures = widths;
            } else {
                heightMeasures = heights;
                columnMeasures = [width, width];
            }
            return {
                rowHeights: {
                    primary: rowHeights[0],
                    secondary: rowHeights[1]
                },
                columnWidths: {
                    primary: columnWidths[0],
                    secondary: columnWidths[1]
                },
                height: {
                    primary: heightMeasures[0],
                    secondary: heightMeasures[1]
                },
                width: {
                    primary: columnMeasures[0],
                    secondary: columnMeasures[1]
                }
            };
        });

        return measurements;
    }

    /**
     * Gets the viewable measures for the current viewable matrix
     *
     * @return {Object} Set of viewable measures
     * @memberof VisualMatrix
     */
    getViewableSpaces () {
        return this.viewableMeasures;
    }

    /**
     * Returns the matrix that can be viewed in the current viewport
     *
     * @return {Array} Set of matrices that can be viewed
     * @memberof VisualMatrix
     */
    getViewableData () {
        return this.viewableMatrix;
    }

    removeExtraCells () {
        const {
            isTransposed,
            extraCellLengths
        } = this.config();
        const matrix = this._layoutMatrix;
        const tree = mergeRecursive({}, this.tree());
        const begCellLen = extraCellLengths[0];
        const endCellLen = extraCellLengths[1] || Number.NEGATIVE_INFINITY;
        const layoutMatrix = !isTransposed ? extraCellsRemover(matrix, begCellLen, endCellLen) :
        matrix.slice(0).map(e => extraCellsRemover(e, begCellLen, endCellLen));

        tree.values = extraCellsRemover(tree.values, begCellLen, endCellLen);
        if (!isTransposed) {
            tree.matrix = extraCellsRemover(tree.matrix, begCellLen, endCellLen);
        } else {
            tree.matrix = tree.matrix.map(e => extraCellsRemover(e, begCellLen, endCellLen));
        }
        return {
            tree,
            layoutMatrix
        };
    }

    /**
     * Creates the viewport that can be viewed together
     *
     * @param {number} depth maxDepth that can be viewed in the viewport
     * @return {Array<Object>} Set of matrices that can be viewed
     * @memberof VisualMatrix
     */
    createViewPortMatrix (depth) {
        const arr = [];
        createMatrixInstances(arr, depth, this.removeExtraCells(), this);
        return arr;
    }

    /**
     * Distibutes the given space row wisely
     *
     * @param {Object} options Redistribution information
     * @memberof VisualMatrix
     */
    redistributeRowWise (options) {
        let cWidths = [];
        let rHeights = [];
        let mHeight = 0;
        const maxMeasures = this.maxMeasures();
        const {
            isDistributionEqual,
            distribution,
            isTransposed,
            gutter
        } = this.config();
        const { matrix, width, height, maxHeights, maxWidths } = options;

        mHeight = spaceTakenByColumn(matrix, this._lastLevelKey).height;

        const maxWidth = maxMeasures.reduce((t, n) => {
            t += n;
            return t;
        });
        if (maxWidth > 0) {
            cWidths = maxMeasures.map(space => space + (width - maxWidth) * (space / maxWidth));
        } else {
            cWidths = maxMeasures.map(() => 0);
        }
        rHeights = getDistributedHeight({
            matrix,
            cIdx: this._lastLevelKey,
            height: mHeight,
            availableHeight: height,
            isDistributionEqual,
            distribution,
            isTransposed,
            gutter
        });
        maxWidths.push(cWidths);
        maxHeights.push(rHeights);
        return { maxWidths, maxHeights };
    }

    /**
     * Distibutes the given space column wisely
     *
     * @param {Object} options Redistribution information
     * @memberof VisualMatrix
     */
    redistributeColumnWise (options) {
        let rHeights = [];
        const { matrix, width, maxHeights, maxWidths } = options;
        const borderWidth = this.config().unitMeasures.border;
        const mWidth = spaceTakenByRow(matrix[this._lastLevelKey]).width;
        const cWidths = getDistributedWidth({
            row: matrix[this._lastLevelKey],
            width: mWidth,
            availableWidth: width
        }, this.config());

        matrix.forEach((row, rIdx) => row.forEach((col, cIdx) => {
            const oldLogicalSpace = col.getLogicalSpace().height;
            col.setAvailableSpace(cWidths[cIdx] - borderWidth, oldLogicalSpace);
            rHeights[rIdx] = Math.max(rHeights[rIdx] || 0, col.getLogicalSpace().height);
        }));

        if (maxHeights.length > 0) {
            rHeights = rHeights.map((e, i) => Math.max(e, maxHeights[0][i]));
        }

        maxHeights.push(rHeights);

        for (let x = 0; x < maxHeights.length; x++) {
            maxHeights[x] = rHeights;
        }
        maxWidths.push(cWidths);
        return { maxHeights, maxWidths };
    }

    /**
     * Dispatch the calculated cell dimensions to all the cells
     *
     * @param {Object} options cell dimension information
     * @return {Object} row and column heights / widths
     * @memberof VisualMatrix
     */
    getCellDimensions (options) {
        let indices = [];
        let unitMeasures = [];
        let mainMeasures = [];
        let computedMeasures = [];
        const {
            isTransposed,
            unitMeasures: measures
        } = this.config();
        const borderWidth = measures.border;
        const { matrixInst, maxWidths, maxHeights, matrixIndex } = options;
        const matrix = matrixInst.matrix;
        const rowHeights = [[0], [0]];
        const columnWidths = [[0], [0]];
        const heights = [0, 0];
        const widths = [0, 0];
        const breakPointer = this._breakPointer;

        matrix.forEach((row, rIdx) => {
            row.forEach((cell, cIdx) => {
                const colHeight = maxHeights[matrixIndex][rIdx] || 0;
                const colWidth = maxWidths[matrixIndex][cIdx];

                if (!isTransposed) {
                    cell.setAvailableSpace(colWidth, colHeight - borderWidth);
                    indices = [rIdx, cIdx];
                    unitMeasures = [columnWidths, rowHeights];
                    mainMeasures = [widths, heights];
                    computedMeasures = [colWidth, colHeight];
                } else {
                    cell.setAvailableSpace(colWidth - borderWidth, colHeight);
                    indices = [cIdx, rIdx];
                    unitMeasures = [rowHeights, columnWidths];
                    mainMeasures = [heights, widths];
                    computedMeasures = [colHeight, colWidth];
                }

                if (indices[0] === 0 && indices[1] < breakPointer) {
                    unitMeasures[0][0][indices[1]] = computedMeasures[0];
                    mainMeasures[0][0] = (mainMeasures[0][0] || 0) + computedMeasures[0];
                } else if (indices[0] === 0 && indices[1] >= breakPointer) {
                    unitMeasures[0][1][indices[1] - breakPointer] = computedMeasures[0];
                    mainMeasures[0][1] = (mainMeasures[0][1] || 0) + computedMeasures[0];
                }
                if (indices[1] === this._lastLevelKey) {
                    unitMeasures[1][0][indices[0]] = computedMeasures[1];
                    unitMeasures[1][1][indices[0]] = computedMeasures[1];
                }
            });
        });
        return {
            heights,
            widths,
            rowHeights,
            columnWidths
        };
    }
}