"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 });
exports.typeProperties = exports.inferType = void 0;
const traverse_1 = require("@glas/traverse");
const ast_1 = require("../ast");
const ast = __importStar(require("../ast"));
const types = __importStar(require("../types"));
const createScopeMaps_1 = require("../createScopeMaps");
const getSortedTypedNodes_1 = __importStar(require("../analysis/getSortedTypedNodes"));
const evaluate_1 = __importDefault(require("../analysis/evaluate"));
const common_1 = require("../common");
const simplify_1 = __importDefault(require("../analysis/simplify"));
const toCodeString_1 = __importDefault(require("../toCodeString"));
const getLeftMostMemberObject_1 = __importDefault(require("../analysis/getLeftMostMemberObject"));
const splitExpressions_1 = __importDefault(require("../analysis/splitExpressions"));
const combineExpressions_1 = __importDefault(require("../analysis/combineExpressions"));
const getMemberTypeExpression_1 = __importDefault(require("../analysis/getMemberTypeExpression"));
const getFinalStatements_1 = __importDefault(require("../analysis/getFinalStatements"));
const combineTypeExpression_1 = require("../analysis/combineTypeExpression");
const negate_1 = __importDefault(require("../analysis/negate"));
const runtimeTypeChecking_1 = require("./runtimeTypeChecking");
const literalTypes = {
    boolean: types.Boolean,
    number: types.Number,
    object: types.Object,
    string: types.String,
};
const binaryOperationsType = {
    "<": types.Boolean,
    ">": types.Boolean,
    "<=": types.Boolean,
    ">=": types.Boolean,
    "==": types.Boolean,
    "!=": types.Boolean,
    "is": types.Boolean,
    "isnt": types.Boolean,
    "&&": types.Boolean,
    "&": types.Number,
    "||": types.Boolean,
    "|": types.Number,
    "^": types.Number,
    "+": types.Number,
    "-": types.Number,
    "*": types.Number,
    "**": types.Number,
    "/": types.Number,
    "%": types.Number,
};
const unaryOperationsType = {
    "!": types.Boolean,
    "+": types.Number,
    "-": types.Number,
    "typeof": types.String,
    "delete": types.Void,
    "++": types.Number,
    "--": types.Number,
};
function is(type, left = new ast.DotExpression({})) {
    return new ast.BinaryExpression({
        location: type.location,
        left,
        operator: "is",
        right: type
    });
}
function getImpliedType(type, assertion, name) {
    //  we cannot further refine the type of function types or literals
    //  only references and type expressions
    if (ast.FunctionType.is(type) || ast.Literal.is(type)) {
        return type;
    }
    type = toTypeExpression(type);
    let found = 0;
    let assertType = assertion == null ? null : traverse_1.traverse(assertion, {
        leave(node) {
            if (ast.Reference.is(node) && node.name === name) {
                found++;
                return new ast.DotExpression({});
            }
        }
    });
    // didn't find any means the expression was irrelevant to the type so we can ignore it
    if (found > 0) {
        type = combineTypeExpression_1.and(type, assertType);
    }
    type = combineTypeExpression_1.simplifyType(type);
    return type;
}
/**
 * Returns any Type object other than References which it will traverse.
 */
function getType(node, c) {
    if (ast.Reference.is(node)) {
        let declarator = getDeclarator(node, c);
        if (declarator != null) {
            let declaration = c.getResolved(c.getParent(declarator));
            if (ast.ClassDeclaration.is(declaration)) {
                // we want a type and that type will be the instanceType
                return declaration.instanceType;
            }
            let result = getType(declarator.type, c);
            return result;
        }
    }
    if (ast.Type.is(node)) {
        return node;
    }
    return null;
}
function getClassDeclaration(node, c) {
    let declarator = getDeclarator(node, c);
    if (declarator) {
        let baseDeclaration = c.getParent(declarator);
        baseDeclaration = c.getResolved(baseDeclaration);
        if (ast.ClassDeclaration.is(baseDeclaration)) {
            return baseDeclaration;
        }
    }
    return null;
}
function getFunctionType(node, c) {
    let type = getType(node, c);
    if (ast.FunctionType.is(type)) {
        return type;
    }
    if (ast.TypeExpression.is(type)) {
        // look inside and see if it implements a FunctionType
        for (let e of splitExpressions_1.default(type.value)) {
            if (ast.BinaryExpression.is(e)
                && e.operator === "is"
                && ast.DotExpression.is(e.left)
                && ast.FunctionType.is(e.right)) {
                return e.right;
            }
        }
    }
    return null;
}
function getConstructorType(node, c, returnType = null) {
    // if (node.isData) {
    //     console.log("------ not implemented data constructor types")
    //     // crap... we cannot actually
    //     // throw new Error("Not implemented data class constructor types")
    // }
    //  returnType is null on the first call, recursive calls pass in the original class type
    //  which should be the returnType patched onto any inherited constructor
    //  constructors can be inherited from base classes when not specified in subclasses
    let returnDefault = returnType == null;
    if (returnType == null) {
        returnType = new ast.Reference(node.id);
    }
    let ctor = node.instance.declarations.find(d => ast.VariableDeclaration.is(d) && d.id.name === "constructor");
    if (ctor == null) {
        for (let base of node.baseClasses) {
            let baseDeclaration = getClassDeclaration(base, c);
            if (baseDeclaration != null) {
                let baseType = getConstructorType(baseDeclaration, c, returnType);
                if (baseType != null) {
                    return baseType.patch({ returnType });
                }
            }
        }
    }
    else {
        // return the type of the ctor.
        let { value } = ctor;
        value = c.getResolved(value);
        if (value != null && ast.FunctionType.is(value === null || value === void 0 ? void 0 : value.type)) {
            return value.type;
        }
    }
    // default constructor
    return returnDefault ? new ast.FunctionType({
        params: [
            new ast.SpreadElement({ argument: new ast.Reference({ name: "unknown" }) })
        ],
        returnType: new ast.Reference(node.id)
    }) : null;
}
function getDeclarator(node, c) {
    node = c.getResolved(node);
    let referencedNode = c.getDeclarator(node);
    if (ast.Reference.is(referencedNode)) {
        return c.getResolved(getDeclarator(referencedNode, c));
    }
    else if (ast.Declarator.is(referencedNode)) {
        while (referencedNode != null) {
            let declaration = c.getAncestor(referencedNode, ast.Declaration.is);
            // we traverse ACROSS conditional variables to get the original declaration
            if (ast.VariableDeclaration.is(declaration) && declaration.kind === "conditional") {
                let before = referencedNode;
                let refDeclaratorParent = c.ancestors.get(c.getAncestor(referencedNode, ast.Declaration.is));
                if (refDeclaratorParent != null) {
                    referencedNode = c.getDeclarator(refDeclaratorParent, referencedNode.name);
                }
                if (before === referencedNode) {
                    throw common_1.SemanticError(`Conditional referencing itself?`, referencedNode);
                }
            }
            else {
                break;
            }
        }
        return c.getResolved(referencedNode);
    }
    else {
        return null;
        // console.error(`Referenced node is not a declaration ${node.name}`, referencedNode)
        // throw new Error("Referenced node is not a declaration")
    }
}
function getChainedConditionalTypeAssertion(c, type, node, operator, negateValue) {
    let alist = common_1.getAncestorsAndSelfList(node, c.ancestors);
    let expressionIndex = alist.findIndex(node => {
        if (operator === "?") {
            return ast.ConditionalExpression.is(node);
        }
        return ast.BinaryExpression.is(node) && node.operator === operator;
    });
    if (expressionIndex >= 0) {
        let parent = c.getResolved(alist[expressionIndex]);
        if (ast.BinaryExpression.is(parent)) {
            //  check if we are the right side.
            //  the parent expression cannot have been resolved yet so we don't have to use resolved.
            if (parent.right === alist[expressionIndex - 1]) {
                // OK, now we just have to check the left side and find a reference with same name.
                // we can then definitely assert that the left expression is true
                let assertion = parent.left;
                if (negateValue) {
                    assertion = negate_1.default(assertion);
                }
                return getImpliedType(type, assertion, node.name);
            }
        }
        else if (ast.ConditionalExpression.is(parent)) {
            let isConsequent = parent.consequent === alist[expressionIndex - 1];
            let isAlternate = parent.alternate === alist[expressionIndex - 1];
            // type is only implied if we are in the consequent or alternate path
            //  otherwise we are in the test which implies nothing
            if (isConsequent || isAlternate) {
                let assertion = parent.test;
                if (isAlternate) {
                    assertion = negate_1.default(assertion);
                }
                return getImpliedType(type, assertion, node.name);
            }
        }
    }
    return type;
}
function toTypeExpression(type) {
    if (ast.TypeExpression.is(type)) {
        return type;
    }
    if (ast.Reference.is(type) || ast.Literal.is(type)) {
        return new ast.TypeExpression({
            value: new ast.BinaryExpression({
                left: new ast.DotExpression({}),
                operator: ast.Reference.is(type) ? "is" : "==",
                right: type
            })
        });
    }
    if (ast.Expression.is(type)) {
        return new ast.TypeExpression({ value: type });
    }
    return null;
    // throw new Error("getTypeExpression not implemented for " + type.constructor.name)
}
exports.inferType = {
    BinaryExpression(node, c) {
        var _a;
        // for now just use the left type
        let type = binaryOperationsType[node.operator];
        let left = c.getResolved(node.left);
        let right = c.getResolved(node.right);
        if (node.operator === "??") {
            type = (_a = toTypeExpression(combineTypeExpression_1.or(left.type, right.type))) !== null && _a !== void 0 ? _a : types.Any;
        }
        if (type == null) {
            throw common_1.SemanticError(`Could not find type for operator: ${node.operator}`, node);
        }
        return { type };
    },
    UnaryExpression(node) {
        // for now just use the left type
        let type = unaryOperationsType[node.operator];
        if (type == null) {
            throw common_1.SemanticError(`Could not find type for operator: ${node.operator}`, node);
        }
        return { type };
    },
    RegularExpression(node) {
        return { type: types.RegExp };
    },
    Literal(node) {
        // literals are their own type
        let literalType = literalTypes[typeof node.value];
        let baseType;
        if (Number.isInteger(node.value)) {
            literalType = types.Integer;
        }
        let type = toTypeExpression(combineTypeExpression_1.and(toTypeExpression(literalType), toTypeExpression(new ast.Literal({ value: node.value }))));
        return { type };
    },
    ObjectExpression(node, c) {
        let expressions = [
            new ast.BinaryExpression({
                left: new ast.DotExpression({}),
                operator: "is",
                right: types.Object
            })
        ];
        for (let p of node.properties) {
            if (ast.Property.is(p)) {
                if (p.key == null) {
                    throw common_1.SemanticError("Key is required", p);
                }
                let pkey = c.getResolved(p.key);
                let pvalue = c.getResolved(p.value);
                if (pvalue.type == null) {
                    // console.log({ pkey, pvalue })
                }
                else {
                    expressions.push(new ast.BinaryExpression({
                        left: new ast.MemberExpression({ object: new ast.DotExpression({}), property: pkey }),
                        operator: "is",
                        right: pvalue.type
                    }));
                }
            }
            else {
                // Spread Element
                let arg = c.getResolved(p.argument);
                if (ast.TypeExpression.is(arg.type)) {
                    for (let e of splitExpressions_1.default(arg.type.value, "&&")) {
                        // ignore everything except member expressions
                        if (ast.BinaryExpression.is(e) && ast.MemberExpression.is(e.left) && ast.DotExpression.is(getLeftMostMemberObject_1.default(e.left))) {
                            let { left } = e;
                            // remove any other BinaryExpressions with the same left value
                            expressions = expressions.filter(check => !(ast.BinaryExpression.is(check) && toCodeString_1.default(check.left) == toCodeString_1.default(left)));
                            // value = combineExpressions()
                            // need ability to override properties in left type
                            expressions.push(e);
                        }
                    }
                }
            }
        }
        let type = new ast.TypeExpression({ value: combineExpressions_1.default(expressions) });
        return { type };
    },
    ArrayExpression(node, c) {
        let expressions = [
            new ast.BinaryExpression({
                left: new ast.DotExpression({}),
                operator: "is",
                right: types.Array
            })
        ];
        let length = 0;
        let lengthKnown = true;
        let index = 0;
        for (let p of node.elements) {
            p = c.getResolved(p);
            if (ast.Expression.is(p)) {
                length++;
                if (p.type) {
                    expressions.push(new ast.BinaryExpression({
                        left: new ast.MemberExpression({ object: new ast.DotExpression({}), property: new ast.Literal({ value: index }) }),
                        operator: "is",
                        right: p.type
                    }));
                }
            }
            else if (ast.SpreadElement.is(p)) {
                let arg = c.getResolved(p.argument);
                let argType = getType(arg.type, c);
                if (ast.TypeExpression.is(argType)) {
                    let foundLength = false;
                    let baseLength = length; // save length before we add so we can offset indices
                    for (let e of splitExpressions_1.default(argType.value)) {
                        // ignore everything except member expressions
                        if (ast.BinaryExpression.is(e)
                            && ast.MemberExpression.is(e.left)
                            && ast.DotExpression.is(e.left.object)) {
                            if (ast.Identifier.is(e.left.property)
                                && e.left.property.name === "length"
                                && ast.Literal.is(e.right)
                                && typeof e.right.value === "number") {
                                if (e.operator === "==" || e.operator == ">=") {
                                    if (e.operator === "==") {
                                        foundLength = true;
                                    }
                                    length += e.right.value;
                                }
                            }
                            //  if length is not known for sure, then we cannot assert index types
                            //  theoretically... we COULD assert offset from right end of array
                            if (lengthKnown && e.operator === "is" && ast.Literal.is(e.left.property) && typeof e.left.property.value === "number") {
                                let { left } = e;
                                // remove any other BinaryExpressions with the same left value
                                // expressions = expressions.filter(check => !(ast.BinaryExpression.is(check) && toCodeString(check.left) == toCodeString(left)))
                                // value = combineExpressions()
                                // need ability to override properties in left type
                                let newIndex = baseLength + e.left.property.value;
                                // the length must be at least large enough to hold this new index
                                length = Math.max(length, newIndex + 1);
                                expressions.push(e.patch({
                                    left: e.left.patch({
                                        property: new ast.Literal({
                                            value: newIndex
                                        })
                                    })
                                }));
                            }
                        }
                    }
                    if (!foundLength) {
                        lengthKnown = false;
                    }
                }
                else {
                    console.log("ArrayExpression: Unexpected object type: " + toCodeString_1.default(arg.type));
                }
            }
            index++;
        }
        //  finally, insert a length property as a minimally known value
        //  if there are spread elements it could be larger
        expressions.splice(1, 0, new ast.BinaryExpression({
            left: new ast.MemberExpression({ object: new ast.DotExpression({}), property: new ast.Identifier({ name: "length" }) }),
            operator: lengthKnown ? "==" : ">=",
            right: new ast.Literal({ value: length })
        }));
        let type = new ast.TypeExpression({ value: combineExpressions_1.default(expressions) });
        return { type };
        // Type of ArrayExpression
        // For now... just Array reference?
        // we would need to find the common base type of multiple type expressions or references.
        return { type: types.Array };
    },
    ConditionalDeclaration(node, c) {
        var _a;
        const name = node.id.name;
        let [newKnownType, containingVarDeclarator] = getSortedTypedNodes_1.getContainingIfTestAndOriginalDeclarator(node, c.scopes, c.ancestors);
        newKnownType = c.getResolved(newKnownType);
        containingVarDeclarator = c.getResolved(containingVarDeclarator);
        if (node.negate) {
            newKnownType = negate_1.default(newKnownType);
        }
        let type = getImpliedType((_a = containingVarDeclarator === null || containingVarDeclarator === void 0 ? void 0 : containingVarDeclarator.type) !== null && _a !== void 0 ? _a : null, newKnownType, name);
        return { type };
    },
    ClassDeclaration(node, c) {
        // calculate a TypeExpression that can be used to compare these instances
        let instanceExpressions = new Array();
        instanceExpressions.push(is(new ast.Reference({ name: node.id.path })));
        // with normal (non-data) classes, we should combine actual instanceType from class 
        for (let base of node.baseClasses) {
            if (!node.isData) {
                let baseDeclaration = getClassDeclaration(base, c);
                if (baseDeclaration != null) {
                    instanceExpressions.push(baseDeclaration.instanceType);
                    // SKIP adding the reference is check beneath since this already contains it
                    continue;
                }
            }
            instanceExpressions.push(is(base));
        }
        function* getAssertions(declarations) {
            for (let d of declarations) {
                d = c.getResolved(d);
                if (ast.VariableDeclaration.is(d)) {
                    let dotMember = new ast.MemberExpression({ object: new ast.DotExpression({}), property: d.id });
                    switch (d.kind) {
                        case "var":
                            if (d.type) {
                                if (ast.TypeExpression.is(d.type)) {
                                    yield* [...splitExpressions_1.default(d.type.value)].map(term => runtimeTypeChecking_1.replaceNodes(term, ast.DotExpression.is, dotMember));
                                }
                                else {
                                    yield is(d.type, dotMember);
                                }
                            }
                            break;
                        case "let":
                            yield new ast.BinaryExpression({
                                left: dotMember,
                                operator: "==",
                                right: d.value
                            });
                            break;
                    }
                }
            }
        }
        instanceExpressions.push(...getAssertions(node.instance.declarations));
        let instanceType = new ast.TypeExpression({ location: node.location, value: combineExpressions_1.default(instanceExpressions) });
        // infer static types
        let type = new ast.TypeExpression({
            location: node.location,
            value: combineExpressions_1.default([
                is(getConstructorType(node, c)),
                is(types.Class),
                is(types.Function),
                ...getAssertions(node.static)
            ])
        });
        return { instanceType, type };
    },
    Parameter(node, c) {
        var _a;
        return (_a = exports.inferType.VariableDeclaration) === null || _a === void 0 ? void 0 : _a.apply(this, arguments);
    },
    FunctionExpression(func, c) {
        var _a;
        // traverse and find all return types
        let returnType = func.returnType;
        if (returnType == null) {
            // see if we are a constructor...
            if (((_a = func.id) === null || _a === void 0 ? void 0 : _a.name) === "constructor") {
                let parent = c.getParent(func);
                if (ast.VariableDeclaration.is(parent) && parent.instance) {
                    let clas = c.getAncestor(parent, ast.ClassDeclaration.is);
                    // returnType is the containing class
                    if (clas) {
                        returnType = new ast.Reference({ name: clas === null || clas === void 0 ? void 0 : clas.id.path });
                    }
                }
            }
            if (returnType == null) {
                let returnTypes = [];
                traverse_1.traverse(func.body, {
                    enter(node) {
                        if (ast.ReturnStatement.is(node)) {
                            if (node.argument == null) {
                                returnTypes.push(types.Undefined);
                            }
                            else {
                                let resolvedValue = c.getResolved(node.argument);
                                if (resolvedValue.type != null) {
                                    returnTypes.push(resolvedValue.type);
                                }
                                else {
                                    // throw SemanticError(`Return Value type not resolved`, node)
                                    // console.log("type not resolved ===>", toCodeString(node.argument))
                                    if (returnTypes.indexOf(types.Any) < 0) {
                                        returnTypes.push(types.Any);
                                    }
                                }
                                return traverse_1.skip;
                            }
                        }
                    }
                });
                //  technically, IF the last statement of (every last branch of) a function
                //  is not a return statement then the function could return void.
                let finalStatements = [...getFinalStatements_1.default(func.body)];
                if (finalStatements.find(s => !ast.ReturnStatement.is(s)) != null) {
                    returnTypes.push(types.Undefined);
                }
                if (returnTypes.length > 1) {
                    let expressions = new Array();
                    for (let i = returnTypes.length - 1; i >= 0; i--) {
                        let type = returnTypes[i];
                        expressions.push(ast.TypeExpression.is(type)
                            ? type.value
                            : new ast.BinaryExpression({ left: new ast.DotExpression({}), operator: "is", right: type, location: type.location }));
                    }
                    returnType = simplify_1.default(new ast.TypeExpression({ location: func.body.location, value: combineExpressions_1.default(expressions, "||") }));
                }
                else if (returnTypes.length === 0) {
                    returnType = types.Undefined;
                }
                else if (returnTypes.length === 1) {
                    returnType = returnTypes[0];
                }
            }
        }
        // we also need to infer the function signature type
        let type = func.type != null
            ? func.type
            : new ast.FunctionType({
                params: func.params.map(p => {
                    var _a, _b;
                    let ptype = (_b = (_a = c.getResolved(p)) === null || _a === void 0 ? void 0 : _a.type) !== null && _b !== void 0 ? _b : null;
                    return ast.RestElement.is(p.id) ? new ast.SpreadElement({ location: p.location, argument: ptype !== null && ptype !== void 0 ? ptype : types.Array }) : ptype;
                }),
                returnType
            });
        return { returnType, type };
    },
    VariableDeclaration(node, c) {
        let value = c.getResolved(node.value);
        if (node.type == null) {
            //  the "type" of a type declaration is the value
            //  otherwise the type is the values type
            let type;
            if ((value === null || value === void 0 ? void 0 : value.type) != null) {
                type = value === null || value === void 0 ? void 0 : value.type;
                //  if this variable is reassignable, then we remove the initial literal value
                //  from the variable type. So String & . == "foo" becomes => String
                if (c.isAssignable(node) && ast.TypeExpression.is(type)) {
                    type = type.patch({
                        value: combineExpressions_1.default([...splitExpressions_1.default(type.value)].filter(term => {
                            if (ast.BinaryExpression.is(term) && term.operator === "==" && ast.DotExpression.is(term.left) && ast.Literal.is(term.right)) {
                                return false;
                            }
                            return true;
                        }))
                    });
                }
            }
            else if (ast.Type.is(value)) {
                type = value;
            }
            return { type };
        }
        else if ((value === null || value === void 0 ? void 0 : value.type) != null) {
            // the variable has a type, so let's double check that the value type matches it.
            let check = c.isConsequent(toTypeExpression(value.type), toTypeExpression(node.type));
            if (check === false) {
                //  we only throw an error if we KNOW that a value is invalid.
                //  if it only might be invalid then we
                throw common_1.SemanticError(`Cannot assign type (${toCodeString_1.default(value.type)}) to type (${toCodeString_1.default(node.type)})`, node.value);
                c.semanticError(`Cannot assign type (${toCodeString_1.default(value.type)}) to type (${toCodeString_1.default(node.type)})`, node.value);
            }
        }
    },
    Declarator(node, c) {
        let parent = c.getResolved(c.getParent(node));
        if (ast_1.Typed.is(parent)) {
            return { type: parent.type };
        }
    },
    Reference(node, c) {
        var _a;
        let declarator = getDeclarator(node, c);
        let type = (_a = declarator === null || declarator === void 0 ? void 0 : declarator.type) !== null && _a !== void 0 ? _a : null;
        if (type == null) {
            // check if this is a reference to a global type
            let globalType = types[node.name];
            if ((globalType === null || globalType === void 0 ? void 0 : globalType.path) === node.path) {
                type = globalType;
            }
        }
        // Infer in chained conditionals here.
        // if we are the right side of a A & B conditional then that implies A
        type = getChainedConditionalTypeAssertion(c, type, node, "&&", false);
        // if we are the right side of a A | B optional then that implies not A
        type = getChainedConditionalTypeAssertion(c, type, node, "||", true);
        // if we are a consequent or alternate of an A ? B : C conditional that implies either A or not A
        type = getChainedConditionalTypeAssertion(c, type, node, "?");
        return { type };
    },
    AssignmentStatement(node, c) {
        let left = c.getResolved(node.left);
        let right = c.getResolved(node.right);
        if (!c.isAssignable(left)) {
            let name = ast.Reference.is(left) ? left.name
                : ast.MemberExpression.is(left) && ast.Identifier.is(left.property) ? left.property.name
                    : "variable";
            c.semanticError(`${name} is not assignable`, node.left);
        }
        if (left.type != null && right.type != null) {
            let check = c.isConsequent(toTypeExpression(right.type), toTypeExpression(left.type));
            if (check === false) {
                //  we only throw an error if we KNOW that a value is invalid.
                //  if it only might be invalid then we
                c.semanticError(`Cannot assign type (${toCodeString_1.default(right.type)}) to type (${toCodeString_1.default(left.type)})`, left);
            }
        }
    },
    CallExpression(node, c) {
        var _a, _b;
        let callee = c.getResolved(node.callee);
        let calleeType = getFunctionType(callee.type, c);
        if (!ast.FunctionType.is(calleeType)) {
            // we don't know the type of the function so we won't type check
            return;
        }
        // console.log("......... CHECK PARAMETERS! " + toCodeString(calleeType))
        for (let i = 0; i < node.arguments.length; i++) {
            let arg = c.getResolved(node.arguments[i]);
            if (ast_1.Expression.is(arg)) {
                // get callee type
                let paramType = calleeType.params[i];
                if (paramType == null) {
                    let lastType = calleeType.params[calleeType.params.length - 1];
                    if (!ast.SpreadElement.is(lastType) && i >= calleeType.params.length) {
                        console.log("calleeType: " + toCodeString_1.default(calleeType));
                        c.semanticError(`Target function only expects ${calleeType.params.length} argument${calleeType.params.length === 1 ? '' : 's'}`, arg);
                    }
                    paramType = lastType;
                }
                if (ast.SpreadElement.is(paramType)) {
                    let restType = paramType.argument;
                    paramType = (_b = (_a = restType.arguments) === null || _a === void 0 ? void 0 : _a[0]) !== null && _b !== void 0 ? _b : null;
                }
                if (paramType != null && arg.type != null) {
                    let check = c.isConsequent(toTypeExpression(arg.type), toTypeExpression(paramType));
                    if (check === false) {
                        //  we only throw an error if we KNOW that a value is invalid.
                        //  if it only might be invalid then we
                        c.semanticError(`Argument of type (${toCodeString_1.default(arg.type)}) is not valid for expected parameter type (${toCodeString_1.default(paramType)})`, arg);
                    }
                    // console.log("(" + check + ") >>>>> " + toCodeString(arg) + " type " + toCodeString(arg.type) + " ::: " + toCodeString(paramType))
                }
            }
        }
        return { type: calleeType.returnType };
    },
    MemberExpression(node, c) {
        let object = c.getResolved(node.object);
        let objectType = getType(object.type, c);
        if (ast.TypeExpression.is(objectType)) {
            let property = c.getResolved(node.property);
            let type = getMemberTypeExpression_1.default(objectType, property);
            if (type != null) {
                return { type };
            }
        }
    },
};
exports.typeProperties = new Set(["type", "returnType", "instanceType"]);
class InferContext extends createScopeMaps_1.ScopeContext {
    constructor(root, options) {
        super(root, true);
        this.resolved = new Map();
        this.options = options;
    }
    semanticError(message, node) {
        this.options.errors.push(common_1.SemanticError(message, node));
    }
    isAssignable(node) {
        if (ast.Reference.is(node)) {
            node = getDeclarator(node, this);
        }
        if (ast.Declarator.is(node)) {
            node = this.getAncestor(node, ast.Declaration.is);
        }
        if (ast.VariableDeclaration.is(node) && node.kind === "var") {
            return true;
        }
        if (ast.Parameter.is(node)) {
            return true;
        }
        if (ast.MemberExpression.is(node)) {
            //  technically, we should check the property declaration type
            //  but for now we'll just say true
            return true;
        }
        // for now assume
        return false;
    }
    setResolved(originalNode, currentNode) {
        this.resolved.set(originalNode, currentNode);
        if (originalNode !== currentNode) {
            // make sure that you can still get the correct scope for the new node
            this.scopes.set(currentNode, this.scopes.get(originalNode));
            // same for ancestors map
            this.ancestors.set(currentNode, this.ancestors.get(originalNode));
        }
    }
    getResolved(node) {
        var _a;
        return (_a = this.resolved.get(node)) !== null && _a !== void 0 ? _a : node;
    }
}
function inferTypes(root, options) {
    let sc = new InferContext(root, options);
    let ancestorsMap = sc.ancestors;
    let scopes = sc.scopes;
    let resolved = sc.resolved;
    let sorted = getSortedTypedNodes_1.default(root, scopes, ancestorsMap);
    // let newTypeDeclarations = new Map<string, ast.TypeExpression>()
    // let typeNameToIdentifierName = new Map<string,string>()
    // let preferredTypeNameToIdentifierName = new Map<string,string>()
    // function getSharedTypeReference(node: ast.TypeExpression) {
    //     let name = toCodeString(node)
    //     let absoluteName = typeNameToIdentifierName.get(name)
    //     if (absoluteName == null) {
    //         let localName = preferredTypeNameToIdentifierName.get(name) ?? "?TYPE:" +name // idGenerator.createNewIdName(name)
    //         absoluteName = getModulePath("_types", localName)
    //         typeNameToIdentifierName.set(name, absoluteName)
    //         // see if we can find sub-nodes within this thingy
    //         let declaration = new ast.TypeExpression({
    //             value: traverse(node, {
    //                 leave(child) {
    //                     if (child !== node && Node.is(child)) {
    //                         let code = toCodeString(child)
    //                         let foundSubexpression = preferredTypeNameToIdentifierName.get(code)
    //                         if (foundSubexpression) {
    //                             return new ast.Reference({ name: foundSubexpression })
    //                         }
    //                     }
    //                 }
    //             })
    //         })
    //         newTypeDeclarations.set(absoluteName, declaration)
    //     }
    //     return new ast.Reference({ location: node.location, name: absoluteName })
    // }
    function ensureResolved(originalNode, resolveDependenciesFirst = false) {
        var _a;
        if (resolved.has(originalNode)) {
            return resolved.get(originalNode);
        }
        if (resolveDependenciesFirst) {
            for (let pred of getSortedTypedNodes_1.getPredecessors(originalNode, scopes, ancestorsMap)) {
                ensureResolved(pred, resolveDependenciesFirst);
            }
        }
        // first try to simplify
        let currentNode = (_a = resolved.get(originalNode)) !== null && _a !== void 0 ? _a : originalNode;
        currentNode = evaluate_1.default(currentNode, resolved, scopes);
        sc.setResolved(originalNode, currentNode);
        // then try to infer types
        let func = exports.inferType[currentNode.constructor.name];
        let changes = func === null || func === void 0 ? void 0 : func(currentNode, sc);
        if (changes != null) {
            if (ast_1.Typed.is(changes)) {
                // we track these so they don't get properties merged later but are returned as is.
                customConvertedNodes.add(changes);
                currentNode = changes;
            }
            else {
                currentNode = currentNode.patch(changes);
            }
        }
        sc.setResolved(originalNode, currentNode);
        // if (ast.Declaration.is(currentNode) && isAbsolute(currentNode.id.name)) {
        //     let name = currentNode.id.name
        //     let code = toCodeString(currentNode.type!)
        //     if (!ast.Reference.is(currentNode.type) && name.length < code.length) {
        //         preferredTypeNameToIdentifierName.set(code, `typeof ${sanitize(name)}`)
        //     }
        // }
        return currentNode;
    }
    // in order of preference: absolute path declaration... that's it.
    // we iterate all of the typed nodes in dependency order and resolve their actual types
    let customConvertedNodes = new Set();
    for (let originalNode of sorted) {
        ensureResolved(originalNode);
    }
    // console.log(preferredTypeNameToIdentifierName)
    return traverse_1.traverse(root, {
        enter(node) {
            if (ast.Location.is(node)) {
                return traverse_1.skip;
            }
        },
        merge(node, changes, helper) {
            let result = resolved.get(node);
            if (customConvertedNodes.has(result)) {
                return result;
            }
            // if (result) {
            //     for (let name of typeProperties) {
            //         let value = result[name]
            //         if (ast.TypeExpression.is(value)) {
            //             // move to a shared location and replace with a reference.
            //             result = result!.patch({ [name]: getSharedTypeReference(value) })
            //         }
            //     }
            // }
            if (result) {
                return ast_1.Node.is(changes) ? changes : helper.patch(result, changes);
            }
        }
    });
}
exports.default = inferTypes;
