Source: helper.js

import { FieldType, FilteringMode } from './enums';
import Field from './fields/field';
import fieldStore from './field-store';
import Value from './value';
import {
    rowDiffsetIterator,
    groupByIterator,
    projectIterator,
    selectIterator,
    calculatedVariableIterator
} from './operator';
import { DM_DERIVATIVES, LOGICAL_OPERATORS } from './constants';
import createFields from './field-creator';
import defaultConfig from './default-config';
import * as converter from './converter';

/**
 * Prepares the selection data.
 */
function prepareSelectionData (fields, i) {
    const resp = {};
    for (let field of fields) {
        resp[field.name] = new Value(field.data[i], field);
    }
    return resp;
}

export function prepareJoinData (fields) {
    const resp = {};
    Object.keys(fields).forEach((key) => { resp[key] = new Value(fields[key], key); });
    return resp;
}

export const updateFields = ([rowDiffset, colIdentifier], partialFieldspace, fieldStoreName) => {
    let collID = colIdentifier.length ? colIdentifier.split(',') : [];
    let partialFieldMap = partialFieldspace.fieldsObj();
    let newFields = collID.map(coll => new Field(partialFieldMap[coll], rowDiffset));
    return fieldStore.createNamespace(newFields, fieldStoreName);
};

export const persistDerivation = (model, operation, config = {}, criteriaFn) => {
    let derivative;
    if (operation !== DM_DERIVATIVES.COMPOSE) {
        derivative = {
            op: operation,
            meta: config,
            criteria: criteriaFn
        };
        model._derivation.push(derivative);
    }
    else {
        derivative = [...criteriaFn];
        model._derivation.length = 0;
        model._derivation.push(...derivative);
    }
};

export const selectHelper = (rowDiffset, fields, selectFn, config) => {
    const newRowDiffSet = [];
    let lastInsertedValue = -1;
    let { mode } = config;
    let li;
    let checker = index => selectFn(prepareSelectionData(fields, index), index);
    if (mode === FilteringMode.INVERSE) {
        checker = index => !selectFn(prepareSelectionData(fields, index));
    }
    rowDiffsetIterator(rowDiffset, (i) => {
        if (checker(i)) {
            if (lastInsertedValue !== -1 && i === (lastInsertedValue + 1)) {
                li = newRowDiffSet.length - 1;
                newRowDiffSet[li] = `${newRowDiffSet[li].split('-')[0]}-${i}`;
            } else {
                newRowDiffSet.push(`${i}`);
            }
            lastInsertedValue = i;
        }
    });
    return newRowDiffSet.join(',');
};

export const filterPropagationModel = (model, propModels, config = {}) => {
    const operation = config.operation || LOGICAL_OPERATORS.AND;
    const filterByMeasure = config.filterByMeasure || false;
    let fns = [];
    if (propModels === null) {
        fns = [() => false];
    } else {
        fns = propModels.map(propModel => ((dataModel) => {
            const dataObj = dataModel.getData();
            const schema = dataObj.schema;
            const fieldsConfig = dataModel.getFieldsConfig();
            const fieldsSpace = dataModel.getFieldspace().fieldsObj();
            const data = dataObj.data;
            const domain = Object.values(fieldsConfig).reduce((acc, v) => {
                acc[v.def.name] = fieldsSpace[v.def.name].domain();
                return acc;
            }, {});

            return (fields) => {
                const include = !data.length ? false : data.some(row => schema.every((propField) => {
                    if (!(propField.name in fields)) {
                        return true;
                    }
                    const value = fields[propField.name].valueOf();
                    if (filterByMeasure && propField.type === FieldType.MEASURE) {
                        return value >= domain[propField.name][0] && value <= domain[propField.name][1];
                    }

                    if (propField.type !== FieldType.DIMENSION) {
                        return true;
                    }
                    const idx = fieldsConfig[propField.name].index;
                    return row[idx] === fields[propField.name].valueOf();
                }));
                return include;
            };
        })(propModel));
    }

    let filteredModel;
    if (operation === LOGICAL_OPERATORS.AND) {
        const clonedModel = model.clone(false, false);
        filteredModel = clonedModel.select(fields => fns.every(fn => fn(fields)), {
            saveChild: false,
            mode: FilteringMode.ALL
        });
    } else {
        filteredModel = model.clone(false, false).select(fields => fns.some(fn => fn(fields)), {
            mode: FilteringMode.ALL,
            saveChild: false
        });
    }

    return filteredModel;
};

export const cloneWithSelect = (sourceDm, selectFn, selectConfig, cloneConfig) => {
    const cloned = sourceDm.clone(cloneConfig.saveChild);
    const rowDiffset = selectHelper(
        cloned._rowDiffset,
        cloned.getPartialFieldspace().fields,
        selectFn,
        selectConfig
    );
    cloned._rowDiffset = rowDiffset;
    cloned.__calculateFieldspace().calculateFieldsConfig();
    // Store reference to child model and selector function
    if (cloneConfig.saveChild) {
        persistDerivation(cloned, DM_DERIVATIVES.SELECT, { config: selectConfig }, selectFn);
    }

    return cloned;
};

export const cloneWithProject = (sourceDm, projField, config, allFields) => {
    const cloned = sourceDm.clone(config.saveChild);
    let projectionSet = projField;
    if (config.mode === FilteringMode.INVERSE) {
        projectionSet = allFields.filter(fieldName => projField.indexOf(fieldName) === -1);
    }
    // cloned._colIdentifier = sourceDm._colIdentifier.split(',')
    //                         .filter(coll => projectionSet.indexOf(coll) !== -1).join();
    cloned._colIdentifier = projectionSet.join(',');
    cloned.__calculateFieldspace().calculateFieldsConfig();
    // Store reference to child model and projection fields
    if (config.saveChild) {
        persistDerivation(
            cloned,
            DM_DERIVATIVES.PROJECT,
            { projField, config, actualProjField: projectionSet },
            null
        );
    }

    return cloned;
};

export const updateData = (relation, data, schema, options) => {
    options = Object.assign(Object.assign({}, defaultConfig), options);
    const converterFn = converter[options.dataFormat];

    if (!(converterFn && typeof converterFn === 'function')) {
        throw new Error(`No converter function found for ${options.dataFormat} format`);
    }

    const [header, formattedData] = converterFn(data, options);
    const fieldArr = createFields(formattedData, schema, header);

    // This will create a new fieldStore with the fields
    const nameSpace = fieldStore.createNamespace(fieldArr, options.name);
    relation._partialFieldspace = nameSpace;
    // If data is provided create the default colIdentifier and rowDiffset
    relation._rowDiffset = formattedData.length && formattedData[0].length ? `0-${formattedData[0].length - 1}` : '';
    relation._colIdentifier = (schema.map(_ => _.name)).join();
    return relation;
};

export const fieldInSchema = (schema, field) => {
    let i = 0;

    for (; i < schema.length; ++i) {
        if (field === schema[i].name) {
            return {
                type: schema[i].subtype || schema[i].type,
                index: i
            };
        }
    }
    return null;
};

export const propagateIdentifiers = (dataModel, propModel, config = {}, nonTraversingModel, grouped) => {
    // function to propagate to target the DataModel instance.
    const forwardPropagation = (targetDM, propagationData, hasGrouped) => {
        propagateIdentifiers(targetDM, propagationData, config, nonTraversingModel, hasGrouped);
    };

    dataModel !== nonTraversingModel && dataModel.handlePropagation({
        payload: config.payload,
        data: propModel,
        sourceIdentifiers: config.sourceIdentifiers,
        sourceId: config.propagationSourceId,
        groupedPropModel: !!grouped
    });

    // propagate to children created by SELECT operation
    selectIterator(dataModel, (targetDM, criteria) => {
        if (targetDM !== nonTraversingModel) {
            const selectionModel = propModel[0].select(criteria, {
                saveChild: false
            });
            const rejectionModel = propModel[1].select(criteria, {
                saveChild: false
            });

            forwardPropagation(targetDM, [selectionModel, rejectionModel], grouped);
        }
    });
    // propagate to children created by PROJECT operation
    projectIterator(dataModel, (targetDM, projField) => {
        if (targetDM !== nonTraversingModel) {
            const projModel = propModel[0].project(projField, {
                saveChild: false
            });
            const rejectionProjModel = propModel[1].project(projField, {
                saveChild: false
            });

            forwardPropagation(targetDM, [projModel, rejectionProjModel], grouped);
        }
    });

    // propagate to children created by groupBy operation
    groupByIterator(dataModel, (targetDM, conf) => {
        if (targetDM !== nonTraversingModel) {
            const {
                    reducer,
                    groupByString
                } = conf;
                // group the filtered model based on groupBy string of target
            const selectionGroupedModel = propModel[0].groupBy(groupByString.split(','), reducer, {
                saveChild: false
            });
            const rejectionGroupedModel = propModel[1].groupBy(groupByString.split(','), reducer, {
                saveChild: false
            });
            forwardPropagation(targetDM, [selectionGroupedModel, rejectionGroupedModel], true);
        }
    });

    calculatedVariableIterator(dataModel, (targetDM, ...params) => {
        if (targetDM !== nonTraversingModel) {
            const entryModel = propModel[0].clone(false, false).calculateVariable(...params, {
                saveChild: false,
                replaceVar: true
            });
            const exitModel = propModel[1].clone(false, false).calculateVariable(...params, {
                saveChild: false,
                replaceVar: true
            });
            forwardPropagation(targetDM, [entryModel, exitModel], grouped);
        }
    });
};

export const getRootGroupByModel = (model) => {
    if (model._parent && model._derivation.find(d => d.op !== 'group')) {
        return getRootGroupByModel(model._parent);
    }
    return model;
};

export const getRootDataModel = (model) => {
    if (model._parent) {
        return getRootDataModel(model._parent);
    }
    return model;
};

export const propagateToAllDataModels = (identifiers, rootModels, config) => {
    let criteria;
    let propModel;
    const propagationNameSpace = config.propagationNameSpace;
    const payload = config.payload;
    const propagationSourceId = config.propagationSourceId;

    if (identifiers === null) {
        criteria = null;
    } else {
        const filteredCriteria = Object.entries(propagationNameSpace.mutableActions)
            .filter(d => d[0] !== propagationSourceId)
            .map(d => Object.values(d[1]).map(action => action.criteria));
        criteria = [].concat(...[...filteredCriteria, identifiers]);
    }

    const rootGroupByModel = rootModels.groupByModel;
    const rootModel = rootModels.model;
    const propConfig = {
        payload,
        propagationSourceId,
        sourceIdentifiers: identifiers
    };

    if (rootGroupByModel) {
        propModel = filterPropagationModel(rootGroupByModel, criteria, {
            filterByMeasure: true
        });
        propagateIdentifiers(rootGroupByModel, propModel, propConfig);
    }

    propModel = filterPropagationModel(rootModel, criteria, {
        filterByMeasure: !rootGroupByModel
    });
    propagateIdentifiers(rootModel, propModel, propConfig, rootGroupByModel);
};

export const propagateImmutableActions = (propagationNameSpace, rootModels, propagationSourceId) => {
    const rootGroupByModel = rootModels.groupByModel;
    const rootModel = rootModels.model;
    const immutableActions = propagationNameSpace.immutableActions;
    for (const sourceId in immutableActions) {
        const actions = immutableActions[sourceId];
        for (const action in actions) {
            const criteriaModel = actions[action].criteria;
            propagateToAllDataModels(criteriaModel, {
                groupByModel: rootGroupByModel,
                model: rootModel
            }, {
                propagationNameSpace,
                payload: actions[action].payload,
                propagationSourceId
            });
        }
    }
};