"use strict";
// 🙈
var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
    function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
    return new (P || (P = Promise))(function (resolve, reject) {
        function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
        function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
        function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
        step((generator = generator.apply(thisArg, _arguments || [])).next());
    });
};
var __generator = (this && this.__generator) || function (thisArg, body) {
    var _ = { label: 0, sent: function() { if (t[0] & 1) throw t[1]; return t[1]; }, trys: [], ops: [] }, f, y, t, g;
    return g = { next: verb(0), "throw": verb(1), "return": verb(2) }, typeof Symbol === "function" && (g[Symbol.iterator] = function() { return this; }), g;
    function verb(n) { return function (v) { return step([n, v]); }; }
    function step(op) {
        if (f) throw new TypeError("Generator is already executing.");
        while (_) try {
            if (f = 1, y && (t = op[0] & 2 ? y["return"] : op[0] ? y["throw"] || ((t = y["return"]) && t.call(y), 0) : y.next) && !(t = t.call(y, op[1])).done) return t;
            if (y = 0, t) op = [op[0] & 2, t.value];
            switch (op[0]) {
                case 0: case 1: t = op; break;
                case 4: _.label++; return { value: op[1], done: false };
                case 5: _.label++; y = op[1]; op = [0]; continue;
                case 7: op = _.ops.pop(); _.trys.pop(); continue;
                default:
                    if (!(t = _.trys, t = t.length > 0 && t[t.length - 1]) && (op[0] === 6 || op[0] === 2)) { _ = 0; continue; }
                    if (op[0] === 3 && (!t || (op[1] > t[0] && op[1] < t[3]))) { _.label = op[1]; break; }
                    if (op[0] === 6 && _.label < t[1]) { _.label = t[1]; t = op; break; }
                    if (t && _.label < t[2]) { _.label = t[2]; _.ops.push(op); break; }
                    if (t[2]) _.ops.pop();
                    _.trys.pop(); continue;
            }
            op = body.call(thisArg, _);
        } catch (e) { op = [6, e]; y = 0; } finally { f = t = 0; }
        if (op[0] & 5) throw op[1]; return { value: op[0] ? op[1] : void 0, done: true };
    }
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.keepEngineInSyncWithFileSystem2 = exports.createEngineDelegateSync = exports.createEngineDelegate = exports.EngineDelegate = exports.EngineDelegateEventType = exports.EngineMode = void 0;
var fs = require("fs");
var path = require("path");
var url = require("url");
var paperclip_utils_1 = require("paperclip-utils");
var utils_1 = require("./utils");
var EngineMode;
(function (EngineMode) {
    EngineMode[EngineMode["SingleFrame"] = 0] = "SingleFrame";
    EngineMode[EngineMode["MultiFrame"] = 1] = "MultiFrame";
})(EngineMode = exports.EngineMode || (exports.EngineMode = {}));
var mapResult = function (result) {
    if (!result) {
        return result;
    }
    if (result.Ok) {
        return result.Ok;
    }
    else {
        return { error: result.Err };
    }
};
var EngineDelegateEventType;
(function (EngineDelegateEventType) {
    EngineDelegateEventType["Loaded"] = "Loaded";
    EngineDelegateEventType["ChangedSheets"] = "ChangedSheets";
})(EngineDelegateEventType = exports.EngineDelegateEventType || (exports.EngineDelegateEventType = {}));
/*
Engine delegate is the bridge between JS and the rust engine. Primary reason
for this class instead of shoving functionality into the engine itself is for performance &
reducing amount of data being passed between Rust <-> JS
*/
var EngineDelegate = /** @class */ (function () {
    function EngineDelegate(_native, _onCrash) {
        var _this = this;
        if (_onCrash === void 0) { _onCrash = utils_1.noop; }
        this._native = _native;
        this._onCrash = _onCrash;
        this._listeners = [];
        this._rendered = {};
        this._onEngineDelegateEvent = function (event) {
            if (event.kind === paperclip_utils_1.EngineDelegateEventKind.Evaluated) {
                _this._rendered = paperclip_utils_1.updateAllLoadedData(_this._rendered, event);
                _this._dispatch({
                    kind: paperclip_utils_1.EngineDelegateEventKind.Loaded,
                    uri: event.uri,
                    data: _this._rendered[event.uri]
                });
            }
            else if (event.kind === paperclip_utils_1.EngineDelegateEventKind.Diffed) {
                var existingData = _this._rendered[event.uri];
                _this._rendered = paperclip_utils_1.updateAllLoadedData(_this._rendered, event);
                var newData = _this._rendered[event.uri];
                var removedSheetUris = [];
                for (var _i = 0, _a = existingData.importedSheets; _i < _a.length; _i++) {
                    var uri = _a[_i].uri;
                    if (!newData.allDependencies.includes(uri)) {
                        removedSheetUris.push(uri);
                    }
                }
                var addedSheets = [];
                for (var _b = 0, _c = event.data.allDependencies; _b < _c.length; _b++) {
                    var depUri = _c[_b];
                    // Note that we only do this if the sheet is already rendered -- engine
                    // doesn't fire an event in that scenario. So we need to notify any listener that a sheet
                    // has been added, including the actual sheet object.
                    if (!existingData.allDependencies.includes(depUri) &&
                        _this._rendered[depUri]) {
                        addedSheets.push({
                            uri: depUri,
                            sheet: _this._rendered[depUri].sheet
                        });
                    }
                }
                if (addedSheets.length || removedSheetUris.length) {
                    _this._dispatch({
                        uri: event.uri,
                        kind: paperclip_utils_1.EngineDelegateEventKind.ChangedSheets,
                        data: {
                            newSheets: addedSheets,
                            removedSheetUris: removedSheetUris,
                            allDependencies: event.data.allDependencies
                        }
                    });
                }
            }
        };
        this._tryCatch = function (fn) {
            try {
                return fn();
            }
            catch (e) {
                _this._onCrash(e);
                return null;
            }
        };
        this._dispatch = function (event) {
            // try-catch since engine will throw opaque error.
            for (var _i = 0, _a = _this._listeners; _i < _a.length; _i++) {
                var listener = _a[_i];
                listener(event);
            }
        };
        // only one native listener to for buffer performance
        this._native.add_listener(this._dispatch);
        this.onEvent(this._onEngineDelegateEvent);
        return this;
    }
    EngineDelegate.prototype.getGraphUris = function () {
        return this._native.get_graph_uris();
    };
    EngineDelegate.prototype.onEvent = function (listener) {
        var _this = this;
        if (listener == null) {
            throw new Error("listener cannot be undefined");
        }
        this._listeners.push(listener);
        return function () {
            var i = _this._listeners.indexOf(listener);
            if (i !== -1) {
                _this._listeners.splice(i, 1);
            }
        };
    };
    EngineDelegate.prototype.parseFile = function (uri) {
        return mapResult(this._native.parse_file(uri));
    };
    EngineDelegate.prototype.getLoadedAst = function (uri) {
        var _this = this;
        return this._tryCatch(function () { return _this._native.get_loaded_ast(uri); });
    };
    EngineDelegate.prototype.parseContent = function (content) {
        var _this = this;
        return this._tryCatch(function () { return mapResult(_this._native.parse_content(content)); });
    };
    EngineDelegate.prototype.updateVirtualFileContent = function (uri, content) {
        var _this = this;
        return this._tryCatch(function () {
            var ret = mapResult(_this._native.update_virtual_file_content(uri, content));
            return ret;
        });
    };
    EngineDelegate.prototype.getLoadedData = function (uri) {
        return this._rendered[uri];
    };
    EngineDelegate.prototype.open = function (uri) {
        var _this = this;
        var result = this._tryCatch(function () { return mapResult(_this._native.run(uri)); });
        if (result && result.error) {
            throw result.error;
        }
        return this._rendered[uri];
    };
    return EngineDelegate;
}());
exports.EngineDelegate = EngineDelegate;
var getIOOptions = function (options) {
    if (options === void 0) { options = {}; }
    return Object.assign({
        readFile: function (uri) {
            return fs.readFileSync(new URL(uri), "utf8");
        },
        fileExists: function (uri) {
            try {
                var url_1 = new URL(uri);
                // need to make sure that case matches _exactly_ since some
                // systems are sensitive to that.
                return existsSyncCaseSensitive(url_1) && fs.lstatSync(url_1).isFile();
            }
            catch (e) {
                console.error(e);
                return false;
            }
        },
        resolveFile: paperclip_utils_1.resolveImportUri(fs),
        mode: EngineMode.SingleFrame
    }, options.io, { mode: options.mode });
};
exports.createEngineDelegate = function (createNativeEngine) { return function (options, onCrash) { return __awaiter(void 0, void 0, void 0, function () {
    var _a, readFile, fileExists, resolveFile, mode, _b;
    return __generator(this, function (_c) {
        switch (_c.label) {
            case 0:
                _a = getIOOptions(options), readFile = _a.readFile, fileExists = _a.fileExists, resolveFile = _a.resolveFile, mode = _a.mode;
                _b = EngineDelegate.bind;
                return [4 /*yield*/, createNativeEngine(readFile, fileExists, resolveFile, mode)];
            case 1: return [2 /*return*/, new (_b.apply(EngineDelegate, [void 0, _c.sent(), onCrash]))()];
        }
    });
}); }; };
exports.createEngineDelegateSync = function (createNativeEngine) { return function (options, onCrash) {
    var _a = getIOOptions(options), readFile = _a.readFile, fileExists = _a.fileExists, resolveFile = _a.resolveFile, mode = _a.mode;
    return new EngineDelegate(createNativeEngine(readFile, fileExists, resolveFile, mode), onCrash);
}; };
var existsSyncCaseSensitive = function (uri) {
    var pathname = url.fileURLToPath(uri);
    var dir = path.dirname(pathname);
    var basename = path.basename(pathname);
    return fs.readdirSync(dir).includes(basename);
};
exports.keepEngineInSyncWithFileSystem2 = function (watcher, engine) {
    return watcher.onChange(function (kind, uri) {
        if (kind === paperclip_utils_1.ChangeKind.Changed) {
            engine.updateVirtualFileContent(uri, fs.readFileSync(new url.URL(uri), "utf8"));
        }
    });
};
