"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
const patch = require("fast-json-patch");
const json5_1 = require("json5");
require("rxjs/add/operator/distinctUntilChanged");
require("rxjs/add/operator/do");
require("rxjs/add/operator/filter");
require("rxjs/add/operator/map");
require("rxjs/add/operator/mergeAll");
const ds_1 = require("../../util/ds");
/**
 * Source represents source code for a resource in Interactive.
 */
class Source {
    /**
     * Returns a stream of events to send to the embedded frame. This shuld
     * generally emit whenever the source code is updated.
     */
    getEvents(src) {
        const prop = this.sourceProp();
        return src
            .map(state => state[prop].join('\n'))
            .distinctUntilChanged()
            .map(text => {
            try {
                return json5_1.parse(text);
            }
            catch (e) {
                return null;
            }
        })
            .filter(s => !!s)
            .map(parsed => {
            if (this.lastSrc) {
                const calls = this.compare(this.lastSrc, parsed);
                this.lastSrc = parsed;
                return calls;
            }
            this.lastSrc = parsed;
            return this.createPacket(this.lastSrc);
        });
    }
    /**
     * Returns a packet to recreate the resources this source owns, if possible.
     */
    getCreatePacket() {
        if (!this.lastSrc) {
            return [];
        }
        return this.createPacket(this.lastSrc);
    }
}
exports.Source = Source;
/**
 * ResourceComparator creates set set of create, update, and destroy
 * calls from a comparison between two resource types.
 */
class ResourceComparator {
    constructor(opts) {
        this.opts = opts;
        this.nested = Object.create(null);
        this.nestedKeys = [];
        this.nestedKeys = Object.keys(opts.nested || {});
        this.nestedKeys.forEach(key => {
            this.nested[key] = new ResourceComparator(opts.nested[key]);
        });
    }
    /**
     * Creates calls to update from A to B.
     */
    compare(previous, next, context) {
        let calls = this.compareTopLevel(previous, next, context);
        previous.forEach(nestedPrev => {
            // We only need to deep-diff resources that survived through the change.
            // If it only exists in the previous or next,
            // we don't need to do anything.
            const nestedNext = next.find(up => this.id(up) === this.id(nestedPrev));
            if (!nestedNext) {
                return;
            }
            Object.keys(this.nested).forEach((key) => {
                calls = calls.concat(this.nested[key].compare(nestedPrev[key] || [], nestedNext[key] || [], nestedNext));
            });
        });
        return calls;
    }
    compareTopLevel(previous, next, context) {
        const update = new Set();
        const create = new Set();
        const destroy = new Set();
        const working = patch.deepClone(previous);
        patch.compare(previous, next).forEach(op => {
            const path = op.path.split('/').slice(1);
            const index = Number(path[0]);
            const prop = path[1];
            if (typeof index !== 'number') {
                return; // todo(connor4312): print error
            }
            if (this.nested[prop]) {
                return; // we'll get to these later.
            }
            const id = index < working.length ? this.id(working[index]) : undefined;
            switch (op.op) {
                case 'replace':
                    if (prop !== this.opts.id) {
                        update.add(id);
                        break;
                    }
                    create.add(op.value);
                    destroy.add(id);
                    break;
                case 'remove':
                    if (prop === undefined || prop === this.opts.id) {
                        destroy.add(id);
                    }
                    else {
                        update.add(id);
                    }
                    break;
                case 'add':
                    if (prop === undefined) {
                        create.add(op.value[this.opts.id]);
                    }
                    else if (prop === this.opts.id) {
                        create.add(op.value);
                    }
                    else {
                        update.add(id);
                    }
                    break;
                default:
                    update.add(id);
            }
            patch.applyOperation(working, op);
        });
        let calls = [];
        if (destroy.size) {
            calls = calls.concat(ds_1.mapSet(destroy, id => this.opts.destroy(id, context)));
        }
        if (create.size) {
            const toCreate = ds_1.mapSet(create, id => next.find(x => this.id(x) === id));
            calls = calls.concat(this.opts.create(toCreate, context));
        }
        if (update.size) {
            const toUpdate = ds_1.setSub(update, destroy, create);
            const resources = ds_1.mapSet(toUpdate, id => next.find(x => this.id(x) === id));
            calls = calls.concat(this.opts.update(resources.map(r => ds_1.omit(r, this.nestedKeys)), context));
        }
        return calls.reduce((acc, c) => acc.concat(c), []);
    }
    id(instance) {
        return instance[this.opts.id];
    }
}
exports.ResourceComparator = ResourceComparator;
