class GroupWorker {
    /**
     * constructor
     */
    constructor() {
        this.createCacheHandler = this.createCache.bind(this);
        this.createGroupPerspectiveHandler = this.createGroupPerspective.bind(this);
        this.disposeGroupPerspectiveHandler = this.disposeGroupPerspective.bind(this);
        this.disposeCacheHandler = this.disposeCache.bind(this);
        this.getGroupPerspectiveHandler = this.getGroupPerspective.bind(this);
        this.getRecordsForHandler = this.getRecordsFor.bind(this);
        this.appendCacheHandler = this.appendCache.bind(this);

        this.functionMap = new Map();
        this.functionMap.set("createCache", this.createCacheHandler);
        this.functionMap.set("appendCache", this.appendCacheHandler);
        this.functionMap.set("createGroupPerspective", this.createGroupPerspectiveHandler);
        this.functionMap.set("disposeGroupPerspective", this.disposeGroupPerspectiveHandler);
        this.functionMap.set("disposeCache", this.disposeCacheHandler);
        this.functionMap.set("getGroupPerspective", this.getGroupPerspectiveHandler);
        this.functionMap.set("getRecordsFor", this.getRecordsForHandler);
        this.dataCache = new Map();
    }

    /**
     * dispose
     */
    dispose() {
        this.createCacheHandler = null;
        this.createGroupPerspectiveHandler = null;
        this.disposeGroupPerspectiveHandler = null;
        this.disposeCacheHandler = null;
        this.getGroupPerspectiveHandler = null;
        this.getRecordsForHandler = null;

        this.functionMap.clear();
        this.functionMap = null;
    }

    /**
     * process all incomming messages mapping it to handlers
     * @param args
     */
    onMessage(args) {
        if (this.functionMap.has(args.msg)) {
            this.functionMap.get(args.msg)(args);
        }
    }

    /**
     * create cache from data sent to worker
     * @param args
     */
    createCache(args) {
        if (this.dataCache.has(args.id)) {
            const dataCache = this.dataCache.get(args.id);
            dataCache.data = args.data;
            dataCache.updateAllPerspectives();
        }
        else {
            this.dataCache.set(args.id, new DataCache(args.id, args.data));
        }

        postMessage({
            msg: "getRecordsForResponse",
            id: args.id,
            data: args.data
        })
    }

    /**
     * Append existing cache with new records
     * @param args
     */
    appendCache(args) {
        if (this.dataCache.has(args.id)) {
            this.dataCache.get(args.id).append(args.data);

            postMessage({
                msg: "appendRecordsForResponse",
                id: args.id,
                data: args.data
            })
        }
    }

    /**
     * create a group perspective for data cached
     * @param args
     */
    createGroupPerspective(args) {
        const id = args.id;
        const perspectiveId = args.perspectiveId;
        const fieldsToGroup = args.fieldsToGroup;
        const aggegateOptions = args.aggegateOptions;
        const sortOptions = args.sortOptions;
        const data = args.data;

        let dataCache = this.dataCache.get(id);

        if (dataCache == undefined) {
            dataCache = new DataCache(args.id, args.data);
            this.dataCache.set(id, dataCache);
        }
        else {
            if (data != undefined) {
                dataCache.setData(data);
            }
        }

        let perspective = dataCache.getPerspective(perspectiveId);

        if (perspective == null) {
            perspective = dataCache.createPerspective(perspectiveId, fieldsToGroup, aggegateOptions, sortOptions);
        }

        return perspective;
    }

    /**
     * remove perspective
     * @param args
     */
    disposeGroupPerspective(args) {
        const id = args.id;
        const perspectiveId = args.perspectiveId;

        if (this.dataCache.has(id)) {
            const cache = this.dataCache.get(id);
            cache.disposePerspective(perspectiveId);
        }
    }

    /**
     * remove perspectives and cache
     * @param args
     */
    disposeCache(args) {
        const id = args.id;

        if (this.dataCache.has(id)) {
            const cache = this.dataCache.get(id);
            cache.dispose();

            this.dataCache.delete(id);
        }
    }

    /**
     * Return existing perspective
     * @param args
     */
    getGroupPerspective(args){
        const id = args.id;
        const perspectiveId = args.perspectiveId;

        if (this.dataCache.has(id)) {
            const cache = this.dataCache.get(id);
            const perspective = cache.getPerspective(perspectiveId);

            postMessage({
                msg: "getGroupPerspectiveResponse",
                id: id,
                perspectiveId: perspectiveId,
                data: perspective
            })
        }
    }

    /**
    * Filter records based on args argument
    * @params args
    */
    getRecordsFor(args){
        const id = args.id;
        const filters = args.filters;

        if (this.dataCache.has(id)) {
            const cache = this.dataCache.get(id);
            const items = cache.getRecordsFor(cache.data, filters);

            postMessage({
                msg: "getRecordsForResponse",
                id: id,
                data: items
            })
        }
    }

}

class DataCache {
    constructor(id, data) {
        this.id = id;
        this.data = data;
        this.perspectiveGrouping = new Map();
        this.sortDirectionMap = new Map([
            ["ascending", this.evaluateAscending],
            ["descending", this.evaluateDescending]
        ])
    }

    dispose() {
        this.data = null;

        this.perspectiveGrouping.clear();
        this.perspectiveGrouping = null;

        this.sortDirectionMap.clear();
        this.sortDirectionMap = null;
    }

    setData(data) {
        this.data = data;
        this.updateAllPerspectives();
    }

    append(data) {
        this.data = this.data.concat(data);
        this.updateAllPerspectives();
    }

    // process all perspectives again
    updateAllPerspectives() {
        this.perspectiveGrouping.forEach((value, key) => {
            const definition = value.__definition;
            this.createPerspective(definition.perspectiveId, definition.fieldsToGroup, definition.aggegateOptions, definition.sortOptions, true);
        })
    }

    /**
     * Remove a perspective from the cache
     * @param perspectiveId
     */
    disposePerspective(perspectiveId) {
        if (this.perspectiveGrouping.has(perspectiveId)) {
            this.perspectiveGrouping.delete(perspectiveId);
        }
    }

    /**
     * Return an existing perspective
     * @param perspectiveId
     */
    getPerspective(perspectiveId){
        if (this.perspectiveGrouping.has(perspectiveId)) {
            return this.perspectiveGrouping.get(perspectiveId);
        }

        return null;
    }

    /**
     * Filter records based on filter array argument
     * @param items
     * @param filters
     */
    getRecordsFor(items, filters)
    {
        if (!filters) {
            return items;
        }

        let result = items.slice(0);

        result = result.filter(function(el) {
            for(var j = 0; j < filters.length; j++){
                var fieldName = filters[j].fieldName;
                var value = filters[j].value;

                if(el[fieldName] != value){
                    return false;
                }
            }
            return true;
        });

        return result;
    }

    /**
     * Create a perspective and group
     * @param perspectiveId
     * @param fieldsToGroup
     * @param aggegateOptions
     */
    createPerspective(perspectiveId, fieldsToGroup, aggegateOptions, sortOptions, override) {
        let result = this.getPerspective(perspectiveId);

        if(!result || override == true)
        {
            const newPerspective = this.createPerspectiveGroup(fieldsToGroup, aggegateOptions, sortOptions);
            newPerspective.__definition = {
                perspectiveId: perspectiveId,
                fieldsToGroup: fieldsToGroup,
                aggegateOptions: aggegateOptions,
                sortOptions, sortOptions
            };

            this.perspectiveGrouping.set(perspectiveId, newPerspective);
            result = newPerspective;
        }

        postMessage({
            msg: "createGroupPerspectiveResponse",
            id: this.id,
            perspectiveId: perspectiveId,
            data: result
        });

        return result;
    }

    /**
     * Create a grouped and aggregated perspective from the data and store it with a key so that I can access it at any time from other views
     * More than one view can use the same perspective, a example of tha is the grid on master detail and the group chart on the list
     * @param fieldsToGroup: what fields are used in this grouping to define the perspective
     * @param aggegateOptions: what are the calculations that need to be made on the group
     */
    createPerspectiveGroup(fieldsToGroup, aggegateOptions, sortOptions) {
        const dataCopy = this.data.slice(0);

        const root = {
            level: 0,
            title: "None",
            items: dataCopy,
            isGroup: true
        };

        this.groupRecursive(root, fieldsToGroup, aggegateOptions, sortOptions);
        return root;
    }

    /**
     * Recursivly group items of a group oject grouping it according to level and the field defined for that level.
     * @param group: the group to process
     * @param fieldsToGroup: what are the fields to use while grouping
     * @param aggegateOptions: what aggregate calculations should be used
     */
    groupRecursive(group, fieldsToGroup, aggegateOptions, sortOptions) {
        if (fieldsToGroup == undefined || fieldsToGroup == null) {
            return;
        }

        if (group.level > fieldsToGroup.length -1) {
            group.aggregate = {
                aggregate: aggegateOptions.aggregate,
                value: aggregator[aggegateOptions.aggregate](group.items, aggegateOptions.field)
            };

            group.lowestGroup = true;
            group.items = this.sortItems(group.items, sortOptions);
            return;
        }

        group.groups = this.group(group.items, fieldsToGroup[group.level], group.level + 1, sortOptions);

        const keys = group.groups.keys();

        for(let key of keys) {
            const childGroup = group.groups.get(key);
            this.groupRecursive(childGroup, fieldsToGroup, aggegateOptions, sortOptions);
        }

        group.aggregate = {
            aggregate: aggegateOptions.aggregate,
            value: aggregator[aggegateOptions.aggregate](group.items, aggegateOptions.field)
        };

        group.items = Array.from(group.groups, items => items[1]);
        delete group.groups;
    }

    /**
     * Create a
     * @param array
     * @param fieldName
     * @param level
     * @returns {any|*}
     */
    group(array, fieldName, level, sortOptions) {
        const result = array.reduce((groupMap, curr) => {
            const key = curr[fieldName];
            const id = curr[fieldName];
            const groupId = groupMap.size;

            if (groupMap.has(key)) {
                groupMap.get(key).items.push(curr);
            }
            else {
                groupMap.set(key, {
                    level: level,
                    field: fieldName,
                    title: key ? key.toString() : "none",
                    id: id,
                    items: [curr],
                    index: groupId,
                    isGroup: true
                })
            }

            return groupMap;
        }, new Map());

        if (sortOptions == undefined) {
            return result;
        }

        return this.sortGroup(result, sortOptions);
    }

    sortGroup(map, sortOptions) {
        const array = Array.from(map);
        const direction = sortOptions[array[0][1].field];

        if (direction == undefined) {
            return map;
        }

        const evaluation = this.sortDirectionMap.get(direction);

        array.sort((a, b) => {
            return evaluation(a[1].title, b[1].title);
        });

        return new Map(array);
    }

    sortItems(array, sortOptions) {
        if (sortOptions == undefined || array.length == 1) {
            return array;
        }

        const fields = Object.keys(sortOptions);
        return array.sort((a, b) => {
            for (let field of fields) {
                const direction = sortOptions[field];
                const result = this.sortDirectionMap.get(direction)(a[field], b[field]);

                if (result == 1) {
                    return 1;
                }
            }

            return 0;
        })
    }

    evaluateAscending(aValue, bValue) {
        if (aValue == bValue) {
            return 0;
        }

        return aValue < bValue ? -1 : 1;
    }

    evaluateDescending(aValue, bValue) {
        if (aValue == bValue) {
            return 0;
        }

        return aValue > bValue ? -1 : 1;
    }
}

const aggregator = {
    count(items) {
        return items.length;
    },

    sum(items, field) {
        let result = 0;

        for(let item of items) {
            result += item[field];
        }

        return result;
    },

    min(items, field) {
        let result = items[0][field];

        for(let item of items) {
            if (item[field] < result) {
                result = item[field];
            }
        }

        return result;
    },

    max(items, field) {
        let result = items[0][field];

        for(let item of items) {
            if (item[field] > result) {
                result = item[field];
            }
        }

        return result;
    },

    ave(items, field) {
        let result = this.sum(items, field);

        result = result / items.length;

        return result;
    }
};

const groupWorker = new GroupWorker();

onmessage = function(event) {
    groupWorker.onMessage(event.data);
};
