"use strict";
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
    if (k2 === undefined) k2 = k;
    Object.defineProperty(o, k2, { enumerable: true, get: function() { return m[k]; } });
}) : (function(o, m, k, k2) {
    if (k2 === undefined) k2 = k;
    o[k2] = m[k];
}));
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
    Object.defineProperty(o, "default", { enumerable: true, value: v });
}) : function(o, v) {
    o["default"] = v;
});
var __importStar = (this && this.__importStar) || function (mod) {
    if (mod && mod.__esModule) return mod;
    var result = {};
    if (mod != null) for (var k in mod) if (k !== "default" && Object.hasOwnProperty.call(mod, k)) __createBinding(result, mod, k);
    __setModuleDefault(result, mod);
    return result;
};
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 common_1 = require("../common");
const combineExpressions_1 = __importDefault(require("../analysis/combineExpressions"));
const types = __importStar(require("../types"));
const toCodeString_1 = __importDefault(require("../toCodeString"));
function toTypeCheck(type, value) {
    if (ast_1.Reference.is(type)) {
        return new ast_1.BinaryExpression({
            left: value,
            operator: "is",
            right: type,
        });
    }
    return runtimeTypeChecking_1.replaceNodes(type, ast_1.DotExpression.is, value);
}
function createDataClassConstructor(node, instanceVariables, isStruct, options) {
    return new ast_1.VariableDeclaration({
        kind: "let",
        instance: true,
        id: new ast_1.Declarator({ location: node.id.location, name: "constructor" }),
        value: new ast_1.FunctionExpression({
            returnType: new ast_1.Reference(node.id),
            params: isStruct ?
                instanceVariables.map(v => {
                    return new ast_1.Parameter({
                        id: common_1.clone(v.id),
                        value: common_1.clone(v.value),
                    });
                })
                : [
                    new ast_1.Parameter({
                        id: new ast_1.AssignmentPattern({
                            left: new ast_1.ObjectPattern({
                                properties: instanceVariables.map(v => {
                                    return new ast_1.Property({
                                        location: v.location,
                                        kind: "init",
                                        shorthand: true,
                                        key: common_1.clone(v.id),
                                        value: !v.value ? v.id : new ast_1.AssignmentPattern({
                                            location: v.location,
                                            left: common_1.clone(v.id),
                                            right: common_1.clone(v.value)
                                        }),
                                    });
                                })
                            }),
                            right: new ast_1.ObjectExpression({ properties: [] })
                        }),
                        type: new ast_1.TypeExpression({
                            value: combineExpressions_1.default([
                                new ast_1.BinaryExpression({
                                    left: new ast_1.DotExpression({}),
                                    operator: "is",
                                    right: types.Object,
                                }),
                                // Make these variable types optional...
                                ...instanceVariables.map(v => {
                                    var _a;
                                    let e = new ast_1.BinaryExpression({
                                        left: new ast_1.MemberExpression({
                                            object: new ast_1.DotExpression({}),
                                            property: new ast_1.Identifier(v.id)
                                        }),
                                        operator: "is",
                                        right: (_a = v.type) !== null && _a !== void 0 ? _a : types.Any,
                                    });
                                    // if this type is optional, then we add Undefined as an optional value
                                    let optional = v.type != null && toCodeString_1.default(v.type) !== toCodeString_1.default(types.Any) && v.value != null;
                                    if (optional) {
                                        e = new ast_1.BinaryExpression({
                                            left: e,
                                            operator: "||",
                                            right: new ast_1.BinaryExpression({
                                                left: new ast_1.MemberExpression({
                                                    object: new ast_1.DotExpression({}),
                                                    property: new ast_1.Identifier(v.id)
                                                }),
                                                operator: "is",
                                                right: types.Undefined,
                                            })
                                        });
                                    }
                                    return e;
                                })
                            ])
                        })
                    })
                ],
            body: new ast_1.BlockStatement({
                body: [
                    // first do type checks IF this is debug
                    ...(!options.debug ? [] : instanceVariables).map(v => {
                        return v.type == null ? null : new ast_1.IfStatement({
                            test: new ast_1.UnaryExpression({
                                prefix: true,
                                operator: "!",
                                argument: toTypeCheck(common_1.clone(v.type), new ast_1.Reference(v.id)),
                            }),
                            consequent: new ast_1.BlockStatement({
                                body: [
                                    runtimeTypeChecking_1.throwTypeError(v.type, v.id.name)
                                ]
                            })
                        });
                    }).filter(v => v != null),
                    ...instanceVariables.map(v => {
                        return new ast_1.AssignmentStatement({
                            left: new ast_1.MemberExpression({
                                object: new ast_1.ThisExpression({}),
                                property: new ast_1.Identifier(v.id),
                            }),
                            right: new ast_1.Reference(v.id),
                        });
                    }),
                    ...(!options.debug ? [] : [
                        new ast_1.ExpressionStatement({
                            expression: new ast_1.CallExpression({
                                callee: new ast_1.MemberExpression({
                                    object: new ast_1.Reference({ location: node.id.location, name: "Object" }),
                                    property: new ast_1.Identifier({ name: "freeze" }),
                                }),
                                arguments: [
                                    new ast_1.ThisExpression({})
                                ]
                            })
                        })
                    ])
                ]
            })
        })
    });
}
function getConstructor(declarations) {
    return declarations.find(d => ast_1.Identifier.is(d.id) && d.id.name === "constructor");
}
function addDataClassConstructors(root, options) {
    return traverse_1.traverse(root, {
        enter(node) {
            if (ast_1.ClassDeclaration.is(node) || ast_1.TypeExpression.is(node)) {
                return traverse_1.skip;
            }
        },
        leave(node) {
            if (ast_1.ClassDeclaration.is(node) && (node.isData || node.isStruct && getConstructor(node.instance.declarations) == null)) {
                return node.patch({
                    instance: node.instance.patch({
                        declarations: [
                            createDataClassConstructor(node, [...node.instance.declarations.values()].filter(a => a.kind === "var"), node.isStruct, options),
                            ...node.instance.declarations,
                        ]
                    })
                });
            }
        }
    });
}
exports.default = addDataClassConstructors;
