Source: visual-group/src/group-helper/matrix-resolver.js

import { VisualUnit } from '@chartshq/visual-unit';
import {
     initializeCacheMaps,
     headerCreator,
     extractUnitConfig,
     setFacetsAndProjections
} from './group-utils';
import {
     ROW, COL, LEFT, RIGHT, COLOR, SIZE, SHAPE, DETAIL, CELL, X_AXES, Y_AXES, ENTRY_CELLS, EXIT_CELLS, INITIALIZED,
     AXIS, UNIT, BEFORE_UPDATE, UPDATED, VALUE_MATRIX, FACET_HEADERS
} from '../enums/constants';
import { createValueCells, computeMatrices } from './cell-creator';
/**
 * Resolves the matrices from configuration provided
 *
 * @export
 * @class MatrixResolver
 */
export default class MatrixResolver {

    /**
     * Creates an instance of MatrixResolver.
     * @param {Object} dependencies needed to run the resolver
     * @memberof MatrixResolver
     */
    constructor (dependencies) {
        this._registry = {};
        this._layerConfig = [];
        this._matrixLayers = [];
        this._dependencies = dependencies;
        this._rowMatrix = [];
        this._columnMatrix = [];
        this._valueMatrix = [];
        this._facets = {};
        this._projections = {};
        this._datamodelTransform = {};
        this._units = [];
        this._cacheMaps = {};
        this._axes = {
            x: {},
            y: {},
            color: [],
            size: [],
            shape: []
        };

        this.cacheMaps(initializeCacheMaps());
    }

    /**
     * Set:  Registers placeholders, Get: return {Object} those placeholders
     *
     * @param {Object} placeholders cells that will construct the group
     * @return {Object} Either current instance or the set of placeholders
     * @memberof MatrixResolver
     */
    registry (...placeholders) {
        if (placeholders.length) {
            Object.entries(placeholders[0]).forEach((val) => {
                this._registry[val[0]] = val[1];
            });
            return this;
        }
        return this._registry;
    }

    /**
     * Used to set the layer config from outside or get current layer info
     *
     * @param {Object} type configuration of layer provided externally
     * @return {Object} either the layer or current instance
     * @memberof MatrixResolver
     */
    dependencies (...dep) {
        if (dep.length) {
            this._dependencies = dep[0];
            return this;
        }
        return this._dependencies;
    }

    /**
     * Used to set the layer config from outside or get current layer info
     *
     * @param {Object} type configuration of layer provided externally
     * @return {Object} either the layer or current instance
     * @memberof MatrixResolver
     */
    units (...unitArr) {
        if (unitArr.length) {
            this._units = unitArr[0];
            return this;
        }
        return this._units;
    }

    /**
     * Used to set the layer config from outside or get current layer info
     *
     * @param {Object} layer configuration of layer provided externally
     * @return {Object} either the layer or current instance
     * @memberof MatrixResolver
     */
    layerConfig (...config) {
        if (config.length) {
            this._layerConfig = config[0];
            return this;
        }
        return this._layerConfig;
    }

    /**
     * Used to set the layer config from outside or get current layer info
     *
     * @param {Object} type configuration of layer provided externally
     * @return {Object} either the layer or current instance
     * @memberof MatrixResolver
     */
    matrixLayers (...layers) {
        if (layers.length) {
            this._matrixLayers = layers[0];
            return this;
        }
        return this._matrixLayers;
    }

    /**
     * Used to set the layer config from outside or get current layer info
     *
     * @param {Object} type configuration of layer provided externally
     * @return {Object} either the layer or current instance
     * @memberof MatrixResolver
     */
    datamodelTransform (...transform) {
        if (transform.length) {
            this._datamodelTransform = transform[0];
            return this;
        }
        return this._datamodelTransform;
    }

    /**
     * Used to set the layer config from outside or get current layer info
     *
     * @param {Object} type configuration of layer provided externally
     * @return {Object} either the layer or current instance
     * @memberof MatrixResolver
     */
    cacheMaps (...maps) {
        if (maps.length) {
            [CELL, X_AXES, Y_AXES, ENTRY_CELLS, EXIT_CELLS].forEach((e) => {
                this._cacheMaps[`${e}Map`] = maps[0][`${e}Map`] || this._cacheMaps[`${e}Map`];
            });
            return this;
        }
        return this._cacheMaps;
    }

    /**
     * Used to set the layer config from outside or get current layer info
     *
     * @param {Object} type configuration of layer provided externally
     * @return {Object} either the layer or current instance
     * @memberof MatrixResolver
     */
    rowCells (...cells) {
        if (cells.length) {
            this._rowCells = cells[0];
            return this;
        }
        return this._rowCells;
    }

    /**
     * Used to set the layer config from outside or get current layer info
     *
     * @param {Object} type configuration of layer provided externally
     * @return {Object} either the layer or current instance
     * @memberof MatrixResolver
     */
    colCells (...cells) {
        if (cells.length) {
            this._colCells = cells[0];
            return this;
        }
        return this._colCells;
    }

    /**
     * Used to set the layer config from outside or get current layer info
     *
     * @param {Object} type configuration of layer provided externally
     * @return {Object} either the layer or current instance
     * @memberof MatrixResolver
     */
    axes (...axes) {
        if (axes.length) {
            this._axes = Object.assign({}, this._axes, axes[0]);
            return this;
        }
        return this._axes;
    }

    /**
     * Used to set the layer config from outside or get current layer info
     *
     * @param {Object} type configuration of layer provided externally
     * @return {Object} either the layer or current instance
     * @memberof MatrixResolver
     */
    rowMatrix (...rowMat) {
        if (rowMat.length) {
            this._rowMatrix = rowMat[0];
            return this;
        }
        return this._rowMatrix;
    }

    /**
     * Used to set the layer config from outside or get current layer info
     *
     * @param {Object} type configuration of layer provided externally
     * @return {Object} either the layer or current instance
     * @memberof MatrixResolver
     */
    columnMatrix (...colMat) {
        if (colMat.length) {
            this._columnMatrix = colMat[0];
            return this;
        }
        return this._columnMatrix;
    }

    /**
     * Used to set the layer config from outside or get current layer info
     *
     * @param {Object} type configuration of layer provided externally
     * @return {Object} either the layer or current instance
     * @memberof MatrixResolver
     */
    valueMatrix (...valMat) {
        if (valMat.length) {
            this._valueMatrix = valMat[0];
            return this;
        }
        return this._valueMatrix;
    }

    /**
     *
     *
     * @param {*} facets
     *
     * @memberof MatrixResolver
     */
    facets (...facets) {
        if (facets.length) {
            Object.entries(facets[0]).forEach((e) => {
                this._facets[e[0]] = e[1];
            });
            return this;
        }
        return this._facets;
    }

    /**
     *
     *
     * @param {*} projections
     *
     * @memberof MatrixResolver
     */
    projections (...projections) {
        if (projections.length) {
            Object.entries(projections[0]).forEach((e) => {
                this._projections[e[0]] = e[1];
            });
            return this;
        }
        return this._projections;
    }

    /**
     *
     *
     * @param {*} config
     * @param {*} layerConfig
     *
     * @memberof MatrixResolver
     */
    optionalProjections (config, layerConfig) {
        const otherEncodings = {};
        const optionalProjections = [];
        const otherEncodingTypes = [SIZE, COLOR, SHAPE];

        otherEncodingTypes.forEach((type) => {
            if (config[type] && config[type].field) {
                const enc = config[type];
                otherEncodings[type] = enc.field;
                optionalProjections.push(enc.field);
            }
        });

        if (config[DETAIL]) {
            optionalProjections.push(...config.detail);
        }

        if (layerConfig.length) {
            layerConfig.forEach((layer) => {
                if (layer.encoding) {
                    Object.values(layer.encoding).forEach((enc) => {
                        if (enc && optionalProjections.indexOf(enc.field) === -1) {
                            optionalProjections.push(enc.field ? enc.field : enc);
                        }
                    });
                }
            });
        }
        this.projections({ optionalProjections });
        return otherEncodings;
    }

    /**
     * return the normalized set of rows and facets and projections
     *
     * @param {Object} rows parameters needed to set horizontal axis consisiting of rows
     * @return {Object} facets, projections and normalized rows
     * @memberof MatrixResolver
     */
    horizontalAxis (rows, encoder) {
        if (rows) {
            this._horizontalAxis = setFacetsAndProjections(this, { type: ROW, fields: rows }, encoder);
            return this;
        }
        return this._horizontalAxis;
    }

    /**
     * return the normalized set of columns and facets and projections
     *
     * @param {Object} columns parameters needed to set vertical axis consisiting of columns
     * @return {Object} facets, projections and normalized columns
     * @memberof MatrixResolver
     */
    verticalAxis (columns, encoder) {
        if (columns) {
            this._verticalAxis = setFacetsAndProjections(this, { type: COL, fields: columns }, encoder);
            return this;
        }
        return this._verticalAxis;
    }

    /**
     * Gets the class definition of a particular cell type(if the particular type has been extended, that particular
     * definition is returned)
     *
     * @param {Object} cell cell whose class definition is to be retrieved
     * @return {Object} cell definition
     * @memberof MatrixResolver
     */
    getCellDef (cell) {
        const registry = this.registry();

        Object.values(registry).forEach((e) => {
            if (e.prototype instanceof cell) {
                cell = e;
            }
        });
        return cell;
    }

    /**
     * return a visual cell creator along with its axis information to be injected to the datamodel creation
     * function
     *
     * @param {Object} GeomCell Type of cell to be created
     * @return {Object} Created cell
     * @memberof MatrixResolver
     */
    valueCellsCreator (context) {
        // reset matrix layers
        this.matrixLayers([]);

        return (datamodel, fieldInfo, facets) => createValueCells(context, datamodel, fieldInfo, facets);
    }

    /**
     * Callback to be applied on each cell of a matrix of a particular type
     *
     * @param {string} matrixType type of matrix on which callback is to be applied
     * @param {Function} callback function to be applied to each cell
     * @memberof MatrixResolver
     */
    forEach (matrixType, callback) {
        this[matrixType]().forEach((row, rIndex) => {
            row.forEach((col, cIndex) => {
                callback(rIndex, cIndex, col);
            });
        });
    }

    /**
     *
     *
     *
     * @memberof MatrixResolver
     */
    getAllFields () {
        const retObj = this.projections();

        Object.entries(this.facets()).forEach((e) => {
            retObj[e[0]] = e[1];
        });
        return retObj;
    }

    /**
     *
     *
     * @memberof MatrixResolver
     */
    resetSimpleAxes () {
        return this.axes({
            x: new Set(),
            y: new Set()
        });
    }

    /**
     *
     *
     * @param {*} componentRegistry
     * @param {*} config
     * @memberof MatrixResolver
     */
    createUnits (componentRegistry, config) {
        const {
            globalConfig,
            alias
        } = config;
        const {
            layerRegistry,
            sideEffectRegistry
        } = componentRegistry;
        const {
            smartlabel: smartLabel,
            lifeCycleManager
        } = this.dependencies();
        // Provide the source for the matrix
        const units = [];
        // Setting unit configuration
        const unitConfig = extractUnitConfig(globalConfig || {});

        this.forEach(VALUE_MATRIX, (i, j, el) => {
            let unit = el.source();
            if (!unit) {
                unit = VisualUnit.create({
                    layerRegistry,
                    sideEffectRegistry
                }, {
                    smartLabel,
                    lifeCycleManager
                });
                el.source(unit);
                units.push(unit);
            }
            unit.parentAlias(alias);
            el.config(unitConfig);
        });

        lifeCycleManager.notify({ client: units, action: INITIALIZED, formalName: UNIT });
        return this.units(units);
    }

    /**
     *
     *
     * @param {*} config
     * @memberof MatrixResolver
     */
    setDomains (config, datamodel, encoders) {
        const {
            color,
            shape,
            size,
            globalConfig
        } = config;
        const groupBy = globalConfig.autoGroupBy;
        const {
            rowFacets,
            colFacets
        } = this.getAllFields();
        const encoding = {
            color,
            shape,
            size
        };
        const facetFields = [...rowFacets.map(e => e.toString()), ...colFacets.map(e => e.toString())];
        const retContext = {
            domains: encoders.simpleEncoder.getRetinalFieldsDomain(datamodel, encoding, facetFields, groupBy),
            axes: this.axes(),
            encoding
        };
        encoders.retinalEncoder.setCommonDomain(retContext);
        return this;
    }

    /**
     *
     *
     *
     * @memberof MatrixResolver
     */
    getRetinalAxes () {
        const {
            color,
            shape,
            size
        } = this.axes();

        return {
            color: [...color],
            shape: [...shape],
            size: [...size]
        };
    }

    /**
     *
     *
     * @param {*} type
     *
     * @memberof MatrixResolver
     */
    getSimpleAxes (type) {
        return this.axes()[`${type}`];
    }

    /**
     *
     *
     * @param {*} datamodel
     * @param {*} config
     * @memberof MatrixResolver
     */
    createRetinalAxes (fieldsConfig, config, encoders) {
        const layerConfig = this.layerConfig();
        this.optionalProjections(config, layerConfig);
        const retinalAxes = encoders.retinalEncoder.createAxis({
            fieldsConfig,
            config,
            axes: this.axes()
        });
        const {
            lifeCycleManager
        } = this.dependencies();

        [COLOR, SHAPE, SIZE].forEach((e) => {
            this.axes()[e] = retinalAxes[e];
        });

        lifeCycleManager.notify({ client: this.axes(), action: INITIALIZED, formalName: AXIS });
        lifeCycleManager.notify({ client: this.units(), action: BEFORE_UPDATE, formalName: UNIT });

        const units = [];
        const matrixLayers = this.matrixLayers();

        this.forEach(VALUE_MATRIX, (i, j, el) => {
            el.axes(retinalAxes);
            el.source() && el.source().retinalFields(config);
            el.layerDef(encoders.retinalEncoder.getLayerConfig(config, matrixLayers[i][j]));
            el.updateModel();

            units.push(el.source());
        });

        lifeCycleManager.notify({ client: units, action: UPDATED, formalName: UNIT });
        return this;
    }

    /**
     *
     *
     * @param {*} placeholders
     * @param {*} fieldNames
     *
     * @memberof MatrixResolver
     */
    createHeaders (placeholders, fieldNames, config) {
        let bottomLeft = [];
        let bottomRight = [];
        const {
            rows,
            columns
        } = placeholders;
        const {
            smartlabel: labelManager,
            lifeCycleManager
        } = this.dependencies();
        const TextCell = this.getCellDef(this.registry().TextCell);
        const BlankCell = this.getCellDef(this.registry().BlankCell);
        const [leftRows, rightRows] = rows;
        const [topCols, bottomCols] = columns;
        const rowHeaders = fieldNames.rows;
        const blankCellCreator = cell => new BlankCell().config({ show: cell.config().show });

        // Headers and footers are created based on the rows. Thereafter, using the column information
        // they are tabularized into the current structure
        const headers = {
            left: headerCreator(leftRows, rowHeaders[0], config.showHeaders ? TextCell : BlankCell, labelManager),
            right: headerCreator(rightRows, rowHeaders[1], config.showHeaders ? TextCell : BlankCell, labelManager)
        };
        const footers = {
            left: leftRows.length > 0 ? leftRows[0].map(blankCellCreator) : [],
            right: rightRows.length > 0 ? rightRows[0].map(blankCellCreator) : []
        };
        const [topLeft, topRight] = [LEFT, RIGHT].map(type => topCols.map((col, i) => {
            if (i === topCols.length - 1) {
                return headers[type];
            }
            return footers[type];
        }));

        // Creating only bottom matrices if there is no information on the top
        if (topCols.length === 0) {
            [bottomLeft, bottomRight] = [LEFT, RIGHT].map(type => bottomCols.map((col, i) => {
                if (i === 0) {
                    return headers[type];
                }
                return footers[type];
            }));
        } else {
            bottomLeft = bottomCols.map(() => (leftRows.length > 0 ? leftRows[0].map(blankCellCreator) : []));
            bottomRight = bottomCols.map(() => (rightRows.length > 0 ? rightRows[0].map(blankCellCreator) : []));
        }

        lifeCycleManager.notify({
            client: [topLeft, topRight, bottomLeft, bottomRight],
            action: INITIALIZED,
            formalName: FACET_HEADERS
        });

        return { topLeft, topRight, bottomLeft, bottomRight };
    }

    /**
     *
     *
     * @param {*} datamodel
     * @param {*} config
     * @param {*} componentRegistry
     * @param {*} encoders
     *
     * @memberof MatrixResolver
     */
    getMatrices (datamodel, config, componentRegistry, encoders) {
        const context = {
            datamodel,
            componentRegistry,
            encoders,
            resolver: this
        };

        return computeMatrices(context, config);
    }
}