"use strict";
var __importDefault = (this && this.__importDefault) || function (mod) {
    return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
const traverse_1 = require("@glas/traverse");
const ast_1 = require("../ast");
const runtimeTypeChecking_1 = require("./runtimeTypeChecking");
const inferTypes_1 = require("./inferTypes");
const common_1 = require("../common");
const createTypeCheck_1 = __importDefault(require("../../createTypeCheck"));
function createRuntime(root, options) {
    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 usesIonscript = false;
                node = traverse_1.traverse(node, {
                    enter(node) {
                        if (ast_1.VariableDeclaration.is(node) && node.kind === "type") {
                            return traverse_1.skip;
                        }
                        // cannot skip TypeExprssions as we need to convert 'is' operators within them to is calls.
                        // if (TypeExpression.is(node)) {
                        //     return skip
                        // }
                    },
                    leave(node, ancestors, path) {
                        //  types here.
                        if (ast_1.Reference.is(node)) {
                            if (node.name === common_1.runtimeModuleName) {
                                usesIonscript = true;
                            }
                        }
                        // add external count if needed to for arrays
                        if (ast_1.ForOfStatement.is(node) && node.count != null) {
                            let { location } = node;
                            return new ast_1.BlockStatement({
                                location,
                                body: [
                                    new ast_1.VariableDeclaration({ ...node.count, kind: "var", value: new ast_1.Literal({ location, value: 0 }) }),
                                    node.patch({
                                        count: null,
                                        body: node.body.patch({
                                            body: [
                                                ...node.body.body,
                                                new ast_1.ExpressionStatement({
                                                    location,
                                                    expression: new ast_1.UnaryExpression({
                                                        location,
                                                        operator: "++",
                                                        argument: new ast_1.Reference({
                                                            location: node.count.location,
                                                            name: node.count.id.name,
                                                        })
                                                    })
                                                })
                                            ]
                                        })
                                    }),
                                ]
                            });
                        }
                        if (ast_1.Range.is(node)) {
                            // replace with call to ionscript.range(start, end, inclusive, step)
                            usesIonscript = true;
                            let { location } = node;
                            let args = [node.start, node.end];
                            if (node.inclusive) {
                                args.push(new ast_1.Literal({ location, value: true }));
                            }
                            if (node.step) {
                                args.push(node.step);
                            }
                            return new ast_1.CallExpression({
                                location,
                                callee: new ast_1.MemberExpression({
                                    location,
                                    object: new ast_1.Reference({ location, name: common_1.runtimeModuleName }),
                                    property: new ast_1.Identifier({ location, name: "range" }),
                                }),
                                arguments: args,
                            });
                        }
                        if (ast_1.TypeExpression.is(node)) {
                            let last = path[path.length - 1];
                            if (inferTypes_1.typeProperties.has(last)) {
                                return null;
                            }
                            return node.value;
                        }
                        if (ast_1.FunctionType.is(node)) {
                            //  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 (DotExpression.is(node)) {
                        //     return new Reference({ name: "value" })
                        // }
                        if (ast_1.BinaryExpression.is(node) && node.operator === "is") {
                            if (ast_1.RuntimeType.is(node.right)) {
                                usesIonscript = true;
                                return new ast_1.CallExpression({
                                    callee: new ast_1.MemberExpression({
                                        object: new ast_1.Reference({ name: common_1.runtimeModuleName }),
                                        property: new ast_1.Identifier({ name: "is" }),
                                    }),
                                    arguments: [node.left, node.right],
                                });
                            }
                            else {
                                return runtimeTypeChecking_1.replaceNodes(node.right, ast_1.DotExpression.is, node.left);
                            }
                        }
                        //  checked variables and things.
                        if (ast_1.VariableDeclaration.is(node)) {
                            if (node.static || node.instance) {
                                if (node.kind === "let" && node.value != null && !ast_1.FunctionExpression.is(node.value)) {
                                    return node.patch({
                                        kind: "get",
                                        value: new ast_1.FunctionExpression({
                                            params: [],
                                            body: new ast_1.BlockStatement({
                                                body: [
                                                    new ast_1.ReturnStatement({ argument: node.value })
                                                ]
                                            })
                                        })
                                    });
                                }
                            }
                        }
                        if (ast_1.ClassDeclaration.is(node)) {
                            let result = node;
                            if (!node.isData) {
                                //  iterate and find var variables with a default value
                                let instanceVarsWithDefaults = node.instance.declarations.filter(d => d.kind === "var" && d.value != null);
                                if (instanceVarsWithDefaults.length > 0) {
                                    let ctor = node.instance.declarations.find(d => d.id.name === "constructor");
                                    let newCtor = ctor !== null && ctor !== void 0 ? ctor : new ast_1.VariableDeclaration({
                                        kind: "let",
                                        id: new ast_1.Declarator({ name: "constructor" }),
                                        value: new ast_1.FunctionExpression({
                                            params: [],
                                            body: new ast_1.BlockStatement({
                                                body: node.baseClasses.length
                                                    ? [
                                                        new ast_1.ExpressionStatement({
                                                            expression: new ast_1.CallExpression({
                                                                callee: new ast_1.Reference({ name: "super" }),
                                                                arguments: []
                                                            })
                                                        })
                                                    ]
                                                    : []
                                            })
                                        })
                                    });
                                    let newCtorBody = [
                                        ...instanceVarsWithDefaults.map(d => new ast_1.AssignmentStatement({
                                            left: new ast_1.MemberExpression({
                                                object: new ast_1.ThisExpression({}),
                                                property: new ast_1.Identifier(d.id)
                                            }),
                                            right: d.value
                                        })),
                                        ...newCtor.value.body.body
                                    ];
                                    // move any super call back to the top.
                                    {
                                        let isSuper = node => { var _a, _b; return ((_b = (_a = node === null || node === void 0 ? void 0 : node.expression) === null || _a === void 0 ? void 0 : _a.callee) === null || _b === void 0 ? void 0 : _b.name) === "super"; };
                                        let supers = newCtorBody.filter(isSuper);
                                        let nonSupers = newCtorBody.filter(n => !isSuper(n));
                                        newCtorBody = [...supers, ...nonSupers];
                                    }
                                    newCtor = newCtor.patch({
                                        value: newCtor.value.patch({
                                            body: new ast_1.BlockStatement({
                                                body: newCtorBody
                                            })
                                        })
                                    });
                                    let newInstances = [newCtor, ...node.instance.declarations.filter(d => d !== ctor)];
                                    result = result.patch({
                                        instance: new ast_1.InstanceDeclarations({
                                            declarations: newInstances
                                        }),
                                        instanceType: null
                                    });
                                }
                            }
                            // remove extends from data classes and insert single extends from ionscript.Data
                            if (node.isData) {
                                result = result.patch({
                                    baseClasses: [
                                        new ast_1.MemberExpression({
                                            object: new ast_1.Reference({ name: common_1.runtimeModuleName }),
                                            property: new ast_1.Identifier({ name: "Data" })
                                        })
                                    ],
                                    // also remove all the instance variable declarations
                                    instance: result.instance.patch({
                                        declarations: []
                                    })
                                });
                            }
                            //  handle static vars and typed vars
                            let staticVarsWithDefaults = node.static.filter(d => d.kind === "var" && d.value != null);
                            if (node.isData) {
                                usesIonscript = true;
                                staticVarsWithDefaults.push(new ast_1.VariableDeclaration({
                                    static: new ast_1.Identifier({ name: "static" }),
                                    kind: "var",
                                    id: new ast_1.Declarator({ name: "baseClasses" }),
                                    value: new ast_1.CallExpression({
                                        new: true,
                                        callee: new ast_1.Reference({ name: "Set" }),
                                        arguments: [
                                            new ast_1.ArrayExpression({
                                                elements: node.baseClasses
                                            })
                                        ]
                                    })
                                }));
                                staticVarsWithDefaults.push(new ast_1.VariableDeclaration({
                                    static: new ast_1.Identifier({ name: "static" }),
                                    kind: "var",
                                    id: new ast_1.Declarator({ name: "is" }),
                                    value: new ast_1.CallExpression({
                                        callee: new ast_1.MemberExpression({
                                            object: new ast_1.Reference({ name: common_1.runtimeModuleName }),
                                            property: new ast_1.Identifier({ name: createTypeCheck_1.default.name.valueOf() }),
                                        }),
                                        arguments: [new ast_1.Reference(node.id)],
                                    })
                                }));
                                // insert (ClassName).prototype.properties
                                const instancePropertiesLocation = "prototype.properties";
                                staticVarsWithDefaults.push(new ast_1.VariableDeclaration({
                                    static: new ast_1.Identifier({ name: "static" }),
                                    kind: "var",
                                    //  this prototype.properties hack works fine in generated javascript
                                    //  could also do "prototype[ionscript.symbols.properties]"
                                    id: new ast_1.Declarator({ name: instancePropertiesLocation }),
                                    value: new ast_1.CallExpression({
                                        new: true,
                                        callee: new ast_1.Reference({ name: "Map" }),
                                        arguments: [
                                            new ast_1.ArrayExpression({
                                                elements: [
                                                    ...node.baseClasses.map(base => new ast_1.SpreadElement({
                                                        argument: new ast_1.MemberExpression({
                                                            object: base,
                                                            property: new ast_1.Identifier({ name: instancePropertiesLocation })
                                                        })
                                                    })),
                                                    ...node.instance.declarations.filter(d => !d.inherited && d.instance && d.id.name !== "constructor").map(d => {
                                                        let name = d.id.name;
                                                        return new ast_1.ArrayExpression({
                                                            elements: [
                                                                new ast_1.Literal({ value: name }),
                                                                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: "Property" })
                                                                    }),
                                                                    arguments: [
                                                                        new ast_1.ObjectExpression({
                                                                            properties: (() => {
                                                                                let writable = d.kind === "var";
                                                                                let properties = new Array();
                                                                                // writable
                                                                                if (writable) {
                                                                                    properties.push(new ast_1.Property({
                                                                                        key: new ast_1.Identifier({ name: "writable" }),
                                                                                        value: new ast_1.Literal({ value: writable }),
                                                                                    }), 
                                                                                    // we also make any writable properties enumerable as well
                                                                                    new ast_1.Property({
                                                                                        key: new ast_1.Identifier({ name: "enumerable" }),
                                                                                        value: new ast_1.Literal({ value: writable }),
                                                                                    }));
                                                                                }
                                                                                if (d.type) {
                                                                                    properties.push(new ast_1.Property({
                                                                                        key: new ast_1.Identifier({ name: "type" }),
                                                                                        value: runtimeTypeChecking_1.toRuntimeType(d.type, `${node.id.name}.${name}.Type`),
                                                                                    }));
                                                                                }
                                                                                if (d.value) {
                                                                                    if (d.kind === "get") {
                                                                                        properties.push(new ast_1.Property({
                                                                                            key: new ast_1.Identifier({ name: "get" }),
                                                                                            method: true,
                                                                                            value: d.value,
                                                                                        }));
                                                                                    }
                                                                                    else {
                                                                                        properties.push(new ast_1.Property({
                                                                                            key: new ast_1.Identifier({ name: "value" }),
                                                                                            method: ast_1.FunctionExpression.is(d.value),
                                                                                            value: d.value,
                                                                                        }));
                                                                                    }
                                                                                }
                                                                                return properties;
                                                                            })()
                                                                        })
                                                                    ]
                                                                })
                                                                // need a new Property implementation.
                                                            ]
                                                        });
                                                    })
                                                ]
                                            })
                                        ],
                                    })
                                }));
                            }
                            if (staticVarsWithDefaults.length > 0) {
                                result = traverse_1.replace(result, ...staticVarsWithDefaults.map(d => new ast_1.AssignmentStatement({
                                    left: new ast_1.MemberExpression({
                                        object: new ast_1.Reference(node.id),
                                        property: new ast_1.Identifier(d.id)
                                    }),
                                    right: d.value
                                })));
                            }
                            return result;
                        }
                    }
                });
                // insert an import of ionscript
                if (usesIonscript && !common_1.hasDeclarator(node.body, common_1.runtimeModuleName)) {
                    node = node.patch({
                        body: [
                            new ast_1.ImportDeclaration({
                                specifiers: [
                                    new ast_1.ImportNamespaceSpecifier({
                                        local: new ast_1.Declarator({ name: common_1.runtimeModuleName })
                                    })
                                ],
                                source: new ast_1.Literal({ value: common_1.runtimeModuleName })
                            }),
                            ...node.body
                        ]
                    });
                }
                return node;
            }
        }
    });
}
exports.default = createRuntime;
