"use strict";
var __importDefault = (this && this.__importDefault) || function (mod) {
    return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.toRuntimeType = exports.replaceNodes = exports.typeCheckOrThrow = exports.throwTypeError = void 0;
const traverse_1 = require("@glas/traverse");
const ast_1 = require("../ast");
const common_1 = require("../common");
const combineExpressions_1 = __importDefault(require("../analysis/combineExpressions"));
// let Vector_x = Symbol("Vector_x")
function getSymbolName(c, d) {
    return `${c.id.name}_${d.id.name}`;
}
function throwTypeError(type, valueName) {
    let typeName = ast_1.Reference.is(type) ? type.name : "Valid";
    let stringValue = combineExpressions_1.default([
        new ast_1.Literal({ value: `${valueName} is not ${typeName}: ` }),
        new ast_1.Reference({ name: valueName })
    ], "+");
    return new ast_1.ThrowStatement({
        argument: new ast_1.CallExpression({
            new: true,
            callee: new ast_1.Reference({ name: "Error" }),
            arguments: [stringValue]
        })
    });
}
exports.throwTypeError = throwTypeError;
function typeCheckOrThrow(value, type, name) {
    return new ast_1.IfStatement({
        test: new ast_1.UnaryExpression({
            operator: "!",
            argument: new ast_1.BinaryExpression({ left: value, operator: "is", right: type })
        }),
        consequent: new ast_1.BlockStatement({
            body: [
                throwTypeError(type, name)
            ]
        })
    });
}
exports.typeCheckOrThrow = typeCheckOrThrow;
/**
 * Replaces nodes but returns original if none were replaced.
 */
function replaceNodes(root, match, replacement) {
    let replaceCount = 0;
    let result = traverse_1.traverse(root, {
        leave(node) {
            if (match(node)) {
                replaceCount++;
                return replacement;
            }
        }
    });
    return replaceCount > 0 ? result : root;
}
exports.replaceNodes = replaceNodes;
function replaceTypedVarsWithProperties(clas, options) {
    return traverse_1.traverse(clas, {
        enter(node) {
            if (!ast_1.ClassDeclaration.is(node) && ast_1.Declaration.is(node)) {
                return traverse_1.skip;
            }
        },
        leave(node, ancestors) {
            if (ast_1.VariableDeclaration.is(node)) {
                if (node.static || node.instance) {
                    if (node.kind === "var" && node.type != null) {
                        let classDeclaration = common_1.getLast(ancestors, ast_1.ClassDeclaration.is);
                        let name = getSymbolName(classDeclaration, node);
                        // can convert to get/set with types here, BUT that would mean suckitude.
                        // convert to type and shit
                        return traverse_1.replace(
                        // EITHER provide default here OR retain property for later createRuntime usage initializing in constructor and then remove.
                        node.patch({ inherited: true }), // we flag inherited so it can be removed 
                        new ast_1.VariableDeclaration({
                            static: node.static,
                            kind: "get",
                            id: node.id,
                            value: new ast_1.FunctionExpression({
                                params: [],
                                body: new ast_1.BlockStatement({
                                    body: [
                                        new ast_1.ReturnStatement({
                                            argument: new ast_1.MemberExpression({
                                                object: new ast_1.ThisExpression({}),
                                                property: new ast_1.Reference({ name })
                                            })
                                        })
                                    ]
                                })
                            })
                        }), new ast_1.VariableDeclaration({
                            static: node.static,
                            kind: "set",
                            id: node.id,
                            value: new ast_1.FunctionExpression({
                                params: [new ast_1.Parameter({ id: new ast_1.Declarator({ name: "value" }) })],
                                body: new ast_1.BlockStatement({
                                    body: [
                                        typeCheckOrThrow(new ast_1.Reference({ name: "value" }), node.type, "value"),
                                        new ast_1.AssignmentStatement({
                                            left: new ast_1.MemberExpression({
                                                object: new ast_1.ThisExpression({}),
                                                property: new ast_1.Reference({ name })
                                            }),
                                            right: new ast_1.Reference({ name: "value" }),
                                        })
                                    ]
                                })
                            })
                        }));
                    }
                }
            }
        }
    });
}
function toRuntimeType(type, name) {
    var _a;
    if (ast_1.FunctionType.is(type)) {
        //  we do NOT have runtime function type checking yet
        //  so we at least just verify that it is in fact a function.
        return new ast_1.Reference({ name: "Function" });
    }
    if (ast_1.RuntimeType.is(type)) {
        return type;
    }
    return new ast_1.CallExpression({
        new: true,
        callee: new ast_1.MemberExpression({
            object: new ast_1.Reference({ name: common_1.runtimeModuleName }),
            property: new ast_1.Identifier({ name: "Type" })
        }),
        arguments: [
            ast_1.FunctionExpression.is(type)
                ? type
                : new ast_1.FunctionExpression({
                    params: [
                        new ast_1.Parameter({ id: new ast_1.Declarator({ name: "_" }) })
                    ],
                    body: new ast_1.BlockStatement({
                        body: [
                            new ast_1.ReturnStatement({
                                argument: traverse_1.traverse((_a = type.value) !== null && _a !== void 0 ? _a : type, {
                                    leave(node) {
                                        if (ast_1.DotExpression.is(node)) {
                                            return new ast_1.Reference({ name: "_" });
                                        }
                                        // type checks should NOT throw null so we implicitly convert all dot expressions to optional
                                        if (ast_1.MemberExpression.is(node) && !node.optional) {
                                            return node.patch({ optional: true });
                                        }
                                    }
                                })
                            })
                        ]
                    })
                }),
            new ast_1.Literal({ value: name }),
        ]
    });
}
exports.toRuntimeType = toRuntimeType;
// also adds ion import if there are any references to ion
function runtimeTypeChecking(root, options) {
    if (!options.debug) {
        // skip all automatic runtime type checks if this is a release build
        return root;
    }
    return traverse_1.traverse(root, {
        enter(node) {
            if (ast_1.Program.is(node)) {
                return traverse_1.skip;
            }
        },
        leave(node) {
            if (ast_1.Program.is(node)) {
                // let hasIonReference = false
                let result = traverse_1.traverse(node, {
                    enter(node) {
                        // if (Reference.is(node) && node.name === runtimeModuleName) {
                        //     hasIonReference = true
                        // }
                        if (ast_1.TypeExpression.is(node)) {
                            return traverse_1.skip;
                        }
                    },
                    leave(node, ancestors, path) {
                        if (ast_1.VariableDeclaration.is(node)) {
                            if (node.kind === "type") {
                                return node.patch({
                                    kind: "const",
                                    value: toRuntimeType(node.value, node.id.name)
                                });
                            }
                        }
                        if (ast_1.FunctionExpression.is(node)) {
                            //  only works without destructuring for now
                            let typedParams = node.params.filter(p => p.type != null && ast_1.Identifier.is(p.id));
                            if (typedParams != null) {
                                return node.patch({
                                    body: node.body.patch({
                                        body: [
                                            ...typedParams.map(p => typeCheckOrThrow(new ast_1.Reference(p.id), p.type, p.id.name)),
                                            ...node.body.body,
                                        ]
                                    })
                                });
                            }
                        }
                        if (ast_1.ClassDeclaration.is(node) && !node.isData) {
                            // first find any typed vars we need to create symbols for
                            //  handle typed variables by adding symbols to use for storing values
                            let typedVars = [...node.static, ...node.instance.declarations].filter(d => d.type != null);
                            if (typedVars.length > 0) {
                                return traverse_1.replace(...typedVars.map(d => {
                                    let name = getSymbolName(node, d);
                                    return new ast_1.VariableDeclaration({
                                        kind: "let",
                                        id: new ast_1.Declarator({ name }),
                                        value: new ast_1.CallExpression({
                                            callee: new ast_1.Reference({ name: "Symbol" }),
                                            arguments: options.debug
                                                ? [new ast_1.Literal({ value: name })]
                                                : []
                                        })
                                    });
                                }), replaceTypedVarsWithProperties(node, options));
                            }
                        }
                    }
                });
                // if (hasIonReference) {
                //     result = result.patch({
                //         body: [
                //             new ImportDeclaration({
                //                 specifiers: [
                //                     new ImportNamespaceSpecifier({
                //                         local: new Declarator({ name: runtimeModuleName })
                //                     })
                //                 ],
                //                 source: new Literal({ value: runtimeModuleName }),
                //             }),
                //             ...result.body
                //         ]
                //     })
                // }
                return result;
            }
        }
    });
}
exports.default = runtimeTypeChecking;
