"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.round = exports.traceFunction = exports.RegisterTraceClass = exports.RegisterTraceSetter = exports.RegisterTraceGetter = exports.isPromise = exports.traceManager = exports.TraceManager = exports.TraceAsync = exports.TraceCall = exports.TraceMethod = exports.TraceMethodType = void 0;
const async_hooks_1 = require("async_hooks");
const errors_1 = require("./errors");
const strackTrace_1 = require("./strackTrace");
const utils_1 = require("./utils");
// eslint-disable-next-line @typescript-eslint/no-var-requires
const async_hooks = require("async_hooks");
// forces PromiseHooks to be enabled.
// eslint-disable-next-line @typescript-eslint/no-empty-function
(0, async_hooks_1.createHook)({ init() { } }).enable();
async_hooks
    .createHook({
    // eslint-disable-next-line @typescript-eslint/no-explicit-any
    init(asyncId, type, triggerAsyncId) {
        if (TraceManager.enabled) {
            TraceManager.async_promise_map.set(asyncId, triggerAsyncId);
        }
    },
    // eslint-disable-next-line @typescript-eslint/no-unused-vars, @typescript-eslint/no-explicit-any
    before(asyncId) {
        //TraceManager.async_promise_map.set( asyncId , "before" );
    },
    // eslint-disable-next-line @typescript-eslint/no-unused-vars, @typescript-eslint/no-explicit-any
    after(asyncId) {
        //TraceManager.async_promise_map.set( asyncId , "after" );
    },
    // eslint-disable-next-line @typescript-eslint/no-unused-vars, @typescript-eslint/no-explicit-any
    destroy(asyncId) {
        //TraceManager.async_promise_map.set( asyncId , "destroy" );
    },
})
    .enable();
var TraceMethodType;
(function (TraceMethodType) {
    TraceMethodType[TraceMethodType["ClassGetter"] = 0] = "ClassGetter";
    TraceMethodType[TraceMethodType["ClassSetter"] = 1] = "ClassSetter";
    TraceMethodType[TraceMethodType["ClassMethod"] = 2] = "ClassMethod";
    TraceMethodType[TraceMethodType["Function"] = 3] = "Function";
})(TraceMethodType = exports.TraceMethodType || (exports.TraceMethodType = {}));
class TraceMethod {
    constructor(className, methodName, methodType) {
        this.className = "";
        this.methodName = "";
        this.source = "";
        this.calls = 0;
        this.inclusiveTime = 0;
        this.innerTime = 0;
        this.methodsCalled = [];
        this.className = className;
        this.methodName = methodName;
        this.calls = 0;
        this.methodType = methodType;
    }
}
exports.TraceMethod = TraceMethod;
class TraceCall {
    // eslint-disable-next-line @typescript-eslint/no-explicit-any
    constructor(async, method, args, isAsync, isAwait) {
        // eslint-disable-next-line @typescript-eslint/no-explicit-any
        this.ret = null;
        // timing
        this.completed = false;
        this.startTime = 0;
        this.endTime = 0;
        // stack
        this.stack = "";
        this.level = 0;
        this.async = async;
        this.method = method;
        this.method.calls++;
        this.callIndex = method.calls;
        this.args = args;
        this.isAsync = isAsync;
        this.isAwait = isAwait;
    }
    get systemTime() {
        return new Date().getTime();
    }
    get traceTime() {
        return this.endTime - this.startTime;
    }
    start() {
        this.startTime = this.systemTime;
        // get source line
        // todo: not working properly because of Stub (trace line offset 7 is not correct - it changes)
        const stack = new strackTrace_1.StackTrace();
        if (stack.stackTrace.length > 7) {
            const traceSourceInfo = stack.stackTrace[7];
            this.method.source = traceSourceInfo.source;
        }
    }
    // eslint-disable-next-line @typescript-eslint/no-explicit-any
    complete(ret) {
        this.endTime = this.systemTime;
        this.method.inclusiveTime += this.traceTime;
        this.completed = true;
        this.ret = ret;
    }
    completeException(error) {
        this.complete(undefined);
        this.error = error;
    }
    get argsToString() {
        return this.args.map((val) => this.stringifyObject(val)).join(",");
    }
    // eslint-disable-next-line @typescript-eslint/no-explicit-any
    stringifyObject(obj) {
        let text = "";
        if (typeof obj === "string") {
            text = obj;
        }
        else {
            try {
                text = (0, utils_1.mccJsonStringify)(obj);
            }
            catch (_a) {
                text = "error stringifying object";
            }
        }
        return text;
    }
    // eslint-disable-next-line @typescript-eslint/no-explicit-any
    verbose(obj, verbose = true, maxlength = 32) {
        const text = this.stringifyObject(obj);
        if (verbose)
            return text;
        if (text.length <= maxlength)
            return text;
        const halve = maxlength / 2 - 2;
        return text.substring(0, halve) + "..." + text.substring(text.length - halve);
    }
    toString(showIndent = false, showSource = true, showTiming = false, showVerbose = true) {
        // todo: show if method is in error
        const args = this.verbose(this.argsToString, showVerbose);
        let status = " ";
        if (this.isAsync) {
            if (this.isAwait) {
                status = "▼"; // await
            }
            else {
                status = "▲"; // new async
            }
        }
        if (this.completed) {
            if (this.error) {
                status += "‼";
            }
            else {
                status += "◼";
            }
        }
        else {
            status += "▶";
        }
        const source = showSource ? `[${this.method.source}] ` : ``;
        const indent = "".padEnd(showIndent ? this.level * 2 : 0);
        const timing = showTiming ? (this.completed ? `${this.traceTime}ms ` : `not completed `) : ``;
        if (this.ret) {
            return `${status} ${indent}${this.method.className}.${this.method.methodName}(${args})=${this.verbose(this.ret, showVerbose)} ${source}${timing}`;
        }
        else {
            return `${status} ${indent}${this.method.className}.${this.method.methodName}(${args}) ${source}${timing}`;
        }
    }
    toShortString(showVerbose = false) {
        const args = this.verbose(this.argsToString, showVerbose);
        if (this.ret) {
            return `${this.method.className}.${this.method.methodName}(${args})=${this.verbose(this.ret, showVerbose)}`;
        }
        else {
            return `${this.method.className}.${this.method.methodName}(${args})`;
        }
    }
}
exports.TraceCall = TraceCall;
class TraceAsync {
    constructor(id) {
        this.stack = [];
        this.trace = [];
        this.id = id;
        this.eid = (0, async_hooks_1.executionAsyncId)();
        this.tid = (0, async_hooks_1.triggerAsyncId)();
    }
}
exports.TraceAsync = TraceAsync;
class TraceManager {
    constructor() {
        this.displayRuntimeTrace = false;
        this.displayStateOnException = true;
        // eslint-disable-next-line @typescript-eslint/no-explicit-any, @typescript-eslint/no-unused-vars, @typescript-eslint/no-empty-function
        this.onException = function (error) { };
        this.nextAsyncId = 0;
        this.methods = [];
        this.asyncs = [];
    }
    createMethod(className, methodName, methodType) {
        let method = this.methods.find((x) => x.className == className && x.methodName == methodName);
        if (method)
            return method;
        method = new TraceMethod(className, methodName, methodType);
        this.methods.push(method);
        return method;
    }
    createAsync(await) {
        if (await) {
            const async = new TraceAsync(this.nextAsyncId++);
            this.asyncs.push(async);
            return async;
        }
        // find parent
        const tid = (0, async_hooks_1.triggerAsyncId)();
        const parents = [];
        // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
        for (let i = tid; i; i = TraceManager.async_promise_map.get(i)) {
            parents.push(i);
        }
        let async = this.asyncs.reverse().find((x) => parents.find((y) => y === x.tid) != null);
        if (async)
            return async;
        async = new TraceAsync(this.nextAsyncId++);
        this.asyncs.push(async);
        return async;
    }
    // eslint-disable-next-line @typescript-eslint/no-explicit-any
    start(className, methodName, args, methodType, isAsync, isAwait) {
        const method = this.createMethod(className, methodName, methodType);
        //const tid = triggerAsyncId();
        const eid = (0, async_hooks_1.executionAsyncId)();
        const async = this.createAsync(isAsync && !isAwait);
        const trace = new TraceCall(async, method, args, isAsync, isAwait);
        // add methods called
        const top = trace.async.stack[trace.async.stack.length - 1];
        if (top) {
            if (!top.method.methodsCalled.find((x) => x.className == className && x.methodName == methodName)) {
                top.method.methodsCalled.push(method);
            }
        }
        trace.start();
        trace.level = trace.async.stack.length;
        trace.async.stack.push(trace);
        trace.async.trace.push(trace);
        if (this.displayRuntimeTrace) {
            console.log(`trace|${trace.async.id}|${eid}| ${trace.toString(true, false, false, false)}`);
        }
        return trace;
    }
    // eslint-disable-next-line @typescript-eslint/no-explicit-any
    complete(trace, ret) {
        if (!trace)
            return;
        const last = trace.async.stack.pop();
        //const async_hooks = require('async_hooks')
        //const asyncId = async_hooks.executionAsyncId();
        //trace.async.asyncId = asyncId;
        // if (this.displayRuntimeTrace) {
        //    console.log(`trace|${trace.async.id}| completed`);
        // }
        if (!last)
            return;
        last.complete(ret);
        // update inner time
        const top = trace.async.stack[trace.async.stack.length - 1];
        if (top) {
            top.method.innerTime += last.traceTime;
        }
    }
    getAsync(id) {
        return this.asyncs.find((x) => x.id === id);
    }
    completeError(trace, error) {
        var _a;
        if (!trace)
            return;
        (_a = trace.async.stack.pop()) === null || _a === void 0 ? void 0 : _a.completeException(error);
    }
    // eslint-disable-next-line @typescript-eslint/no-explicit-any
    showException(error) {
        // todo: user exception callback
        //this.onException?(error);
        if (!this.displayStateOnException) {
            console.error(error);
            return;
        }
        console.log(`EXCEPTION name='${error.name}' message='${error.message}'`);
        if (error.name === `mccError` && error.message === `OutsideError`) {
            const innerError = error.innerError;
            if (innerError) {
                console.log(`INNER EXCEPTION name='${innerError.name}' message='${innerError.message}'`);
            }
        }
        this.showState();
    }
    // eslint-disable-next-line @typescript-eslint/no-explicit-any
    showState(stack = undefined) {
        if (stack) {
            console.log(`NODE STACK ${stack.stack}`);
        }
        this.showStack();
        this.showTrace(true, false, true, false);
    }
    showStack() {
        for (const async of this.asyncs) {
            console.log(`\nTRACE ASYNC STACK ${async.id} #${async.eid}`);
            for (const trace of async.stack) {
                console.log(trace.toString());
            }
        }
    }
    showTrace(indent = false, source = true, timing = false, verbose = false) {
        for (const async of this.asyncs) {
            console.log(`\nTRACE ASYNC ${async.id} #${async.eid}`);
            for (const trace of async.trace) {
                console.log(trace.toString(indent, source, timing, verbose));
            }
        }
    }
    // eslint-disable-next-line @typescript-eslint/no-unused-vars
    showMethods(indent = false, source = true, timing = false) {
        console.log("\nMETHODS");
        for (const method of this.methods) {
            const exclusiveTime = Math.max(0, method.inclusiveTime - method.innerTime);
            console.log(`◼ ${method.className}.${method.methodName}  ${method.calls} ${method.inclusiveTime}ms ${exclusiveTime}ms`);
            for (const inner of method.methodsCalled) {
                console.log(`    ${inner.className}.${inner.methodName}`);
            }
        }
    }
    get main() {
        if (this.asyncs.length === 0) {
            return undefined;
        }
        return this.asyncs[0];
    }
    clearTrace() {
        this.asyncs = [];
        this.nextAsyncId = 0;
    }
    get firstTrace() {
        // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
        if (this.main.trace.length === 0)
            return "";
        // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
        return this.main.trace[0].toShortString();
    }
    get lastTrace() {
        // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
        if (this.main.trace.length === 0)
            return "";
        // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
        return this.main.trace[this.main.trace.length - 1].toShortString();
    }
}
exports.TraceManager = TraceManager;
TraceManager.enabled = true;
TraceManager.async_promise_map = new Map();
exports.traceManager = new TraceManager();
const awaitSourceMap = new Map();
// eslint-disable-next-line @typescript-eslint/no-explicit-any
function Stub(className, name, funct, cx, args, methodType) {
    const isAsync = funct.constructor.name === "AsyncFunction";
    let isAwait = false;
    if (TraceManager.enabled) {
        const stack = new strackTrace_1.StackTrace();
        const stackTrace = stack.find("Stub", +2);
        if (stackTrace) {
            // check if cache exists
            const mapKey = `${stackTrace.source}:${stackTrace.line}`;
            // cache
            const cached = awaitSourceMap.get(mapKey);
            if (cached) {
                isAwait = cached;
            }
            else {
                // check if that source file has 'await'
                // eslint-disable-next-line @typescript-eslint/no-var-requires
                const fs = require("fs");
                const data = fs.readFileSync(stackTrace.source).toString();
                const lines = data.split("\n");
                if (lines && lines.length > stackTrace.line - 1) {
                    isAwait = lines[stackTrace.line - 1].indexOf(`await `) >= 0;
                }
                // cache
                awaitSourceMap.set(mapKey, isAwait);
            }
        }
    }
    // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
    const trace = TraceManager.enabled ? exports.traceManager.start(className, name, args, methodType, isAsync, isAwait) : undefined;
    try {
        const res = methodType === TraceMethodType.ClassGetter ? funct.apply(cx) : funct.apply(cx, args);
        if (!isPromise(res)) {
            exports.traceManager.complete(trace, res);
            return res;
        }
        return new Promise((resolve, reject) => {
            // eslint-disable-next-line @typescript-eslint/no-explicit-any
            res.then((result) => {
                exports.traceManager.complete(trace, result);
                resolve(result);
                // eslint-disable-next-line @typescript-eslint/no-explicit-any
            }).catch((error) => {
                if ((error === null || error === void 0 ? void 0 : error.name) === errors_1.MCC_ERROR) {
                    exports.traceManager.showException(error);
                    exports.traceManager.completeError(trace, error);
                    reject(error);
                }
                else {
                    const newError = new errors_1.mccOutsideError(error);
                    exports.traceManager.showException(newError);
                    exports.traceManager.completeError(trace, newError);
                    reject(newError);
                }
            });
        });
        // eslint-disable-next-line @typescript-eslint/no-explicit-any
    }
    catch (error) {
        if ((error === null || error === void 0 ? void 0 : error.name) === errors_1.MCC_ERROR) {
            exports.traceManager.showException(error);
            exports.traceManager.completeError(trace, error);
            throw error;
        }
        else {
            const newError = new errors_1.mccOutsideError(error);
            exports.traceManager.showException(newError);
            exports.traceManager.completeError(trace, newError);
            throw new errors_1.mccOutsideError(newError);
        }
    }
}
// eslint-disable-next-line @typescript-eslint/no-explicit-any
function isPromise(p) {
    if (typeof p === "object" && typeof p.then === "function") {
        return true;
    }
    return false;
}
exports.isPromise = isPromise;
// export function RegisterTraceValue(target: any, name?: string, descriptor?: any) {
//    //  decorating a method
//    if (descriptor && descriptor.value) {
//       let original = descriptor.value;
//       descriptor.value = function (...args: any[]) {
//          return Stub(target, name!, original, this, args, TraceMethodType.Descriptor);
//       };
//       return descriptor;
//    }
//    return null;
// }
// eslint-disable-next-line @typescript-eslint/no-explicit-any
function RegisterTraceGetter(target, name, descriptor) {
    if (descriptor && descriptor.get) {
        const original = descriptor.get;
        descriptor.get = function () {
            // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
            return Stub(target.name, name, original, this, [], TraceMethodType.ClassGetter);
        };
        return descriptor;
    }
    return null;
}
exports.RegisterTraceGetter = RegisterTraceGetter;
// eslint-disable-next-line @typescript-eslint/no-explicit-any
function RegisterTraceSetter(target, name, descriptor) {
    if (descriptor && descriptor.set) {
        const original = descriptor.set;
        // eslint-disable-next-line @typescript-eslint/no-explicit-any
        descriptor.set = function (...args) {
            // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
            return Stub(target.name, name, original, this, args, TraceMethodType.ClassSetter);
        };
        return descriptor;
    }
    return null;
}
exports.RegisterTraceSetter = RegisterTraceSetter;
// eslint-disable-next-line @typescript-eslint/no-explicit-any
function RegisterTraceClass(targetClass) {
    // add tracing capability to all own methods, setters and getters
    Object.getOwnPropertyNames(targetClass.prototype).forEach((methodName) => {
        const desc = Object.getOwnPropertyDescriptor(targetClass.prototype, methodName);
        // getter
        if (desc === null || desc === void 0 ? void 0 : desc.get) {
            RegisterTraceGetter(targetClass, methodName, desc);
            Object.defineProperty(targetClass.prototype, methodName, desc);
            return;
        }
        // setter
        if (desc === null || desc === void 0 ? void 0 : desc.set) {
            RegisterTraceSetter(targetClass, methodName, desc);
            Object.defineProperty(targetClass.prototype, methodName, desc);
            return;
        }
        const original = targetClass.prototype[methodName];
        // constructor
        if (methodName === "constructor") {
            // todo: sort out constructor managed wrapping (next lines are commented because they ruin constructor)
            // RegisterTraceValue(targetClass, methodName, desc);
            // Object.defineProperty(targetClass.prototype, methodName, desc!);
            return;
        }
        // skip non functions
        if (typeof original !== "function") {
            return;
        }
        // an arrow function can't be used while we have to preserve right 'this'
        // eslint-disable-next-line @typescript-eslint/no-explicit-any
        targetClass.prototype[methodName] = function (...args) {
            return Stub(targetClass.name, methodName, original, this, args, TraceMethodType.ClassMethod);
        };
    });
    return targetClass;
}
exports.RegisterTraceClass = RegisterTraceClass;
// eslint-disable-next-line @typescript-eslint/no-explicit-any
function traceFunction(funct, ...args) {
    return Stub("", funct.name, funct, null, args, TraceMethodType.Function);
}
exports.traceFunction = traceFunction;
function round(x, decimal = 0) {
    if (decimal === 0)
        return Math.round(x);
    const dec10 = 10 ** decimal;
    return Math.round(x * dec10) / dec10;
}
exports.round = round;
//# sourceMappingURL=trace.js.map