"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 createScopeMaps_1 = __importDefault(require("../createScopeMaps"));
const traverse_1 = require("@glas/traverse");
const ast_1 = require("../ast");
const common_1 = require("../common");
const pathFunctions_1 = require("../pathFunctions");
const types = __importStar(require("../types"));
const reservedWords_1 = __importDefault(require("../reservedWords"));
function checkReferences(root, options) {
    let ancestorsMap = new Map();
    let scopes = createScopeMaps_1.default(root, { ancestorsMap });
    let getName = common_1.memoizeIntern((d) => {
        d = common_1.getOriginalDeclarator(d, scopes, ancestorsMap, false);
        if (d == null) {
            return null;
        }
        let parent = ancestorsMap.get(d);
        let { name } = d;
        let location;
        if (ast_1.Program.is(parent)) {
            name = "";
        }
        else if (ast_1.Exportable.is(parent) && parent.export !== 0) {
            if (parent.export === 2) {
                name = "default";
            }
        }
        else {
            location = d.location;
        }
        return pathFunctions_1.getModulePath(d.location.filename, name, location);
    });
    return traverse_1.traverse(root, {
        leave(node, ancestors) {
            // we also set variable id onto function expressions if they aren't named already
            if (ast_1.VariableDeclaration.is(node)) {
                if (ast_1.Identifier.is(node.id) && ast_1.FunctionExpression.is(node.value) && node.value.id == null) {
                    // but NOT if they are reserved
                    let name = node.kind === "type" ? `is${node.id.name}` : node.id.name;
                    if (!reservedWords_1.default.has(node.id.name)) {
                        return node.patch({
                            value: node.value.patch({
                                id: new ast_1.Declarator({
                                    location: node.location,
                                    name,
                                    path: node.id.path
                                })
                            })
                        });
                    }
                }
            }
            if (ast_1.Declarator.is(node)) {
                return node.patch({ path: getName(node) });
            }
            if (ast_1.Reference.is(node)) {
                let scope = scopes.get(node);
                if (scope == null) {
                    //  we might have been mutated by a changed template sub-reference
                    //  so we will use our unmutated ancestors scope if needed
                    scope = scopes.get(ancestors[ancestors.length - 1]);
                    if (scope == null) {
                        console.log("scope not found for: ", node);
                    }
                }
                let declarator = scope[node.name];
                if (declarator == null) {
                    // if we cannot find a declarator then this must be a global reference
                    return pathFunctions_1.getGlobalReference(node);
                }
                else {
                    let declaration = common_1.getAncestor(declarator, ancestorsMap, ast_1.Declaration.is);
                    if (ast_1.VariableDeclaration.is(declaration)) {
                        let cls = common_1.getAncestor(declaration, ancestorsMap, ast_1.ClassDeclaration.is);
                        if (declaration.instance && cls.instance.declarations.find(d => { var _a; return ((_a = d.id) === null || _a === void 0 ? void 0 : _a.name) === node.name; })) {
                            // check that this isn't contained within a -> function nested within the local function
                            //  that would cause 'this' values to be not bound
                            let func = common_1.getAncestor(node, ancestorsMap, ast_1.FunctionExpression.is);
                            if (func) {
                                if (func !== declaration.value && !func.bind) {
                                    options.errors.push(common_1.SemanticError(`Function implicitly references this.${node.name}, use a bound function "=>"`, node));
                                }
                            }
                            // add implied this. to instance property references
                            return new ast_1.MemberExpression({
                                object: new ast_1.ThisExpression({}),
                                property: new ast_1.Identifier(node)
                            });
                        }
                        else if (declaration.static && cls.static.find(d => { var _a; return ((_a = d.id) === null || _a === void 0 ? void 0 : _a.name) === node.name; })) {
                            //  add implied Class. to static property references
                            let classDeclaration = common_1.getAncestor(node, ancestorsMap, ast_1.ClassDeclaration.is);
                            // declaratorAncestors[declaratorAncestors.length - 3] as ClassDeclaration
                            return new ast_1.MemberExpression({
                                object: new ast_1.Reference(classDeclaration.id),
                                property: new ast_1.Identifier(node)
                            });
                        }
                    }
                    return node.patch({ path: getName(declarator) });
                }
            }
            // semantic checks.
            if (ast_1.FunctionExpression.is(node)) {
                // check that only a single RestElement max and is final arg
                for (let i = 0; i < node.params.length; i++) {
                    let param = node.params[i];
                    if (ast_1.RestElement.is(param.id)) {
                        // must be last parameter
                        if (i + 1 < node.params.length) {
                            throw common_1.SemanticError(`Rest element must be final parameter`, param);
                        }
                        let { type } = param;
                        if (type) {
                            if (!ast_1.Reference.is(type) || type.path !== types.Array.path) {
                                throw common_1.SemanticError(`Rest element type must be an Array`, type);
                            }
                        }
                    }
                }
            }
        }
    });
}
exports.default = checkReferences;
