"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.ScopeContext = void 0;
const traverse_1 = require("@glas/traverse");
const common_1 = require("./common");
const ast_1 = require("./ast");
const types = __importStar(require("./types"));
const isType_1 = require("./analysis/isType");
const isConsequent_1 = __importDefault(require("./analysis/isConsequent"));
class ScopeContext {
    constructor(root, isType = false) {
        this.identifiers = new Set();
        this.ancestors = new Map();
        this.isType = () => { throw new Error(`set isType in constructor`); };
        let _implements = isType ? [] : undefined;
        this.scopes = createScopeMaps(root, {
            identifiers: this.identifiers,
            ancestorsMap: this.ancestors,
            implements: _implements,
        });
        if (_implements) {
            this.isType = isType_1.createIsType(_implements);
        }
    }
    isConsequent(a, b) {
        return isConsequent_1.default(a, b, this.isType);
    }
    getParent(node) {
        return this.ancestors.get(node);
    }
    getAncestor(node, predicate) {
        return common_1.getAncestor(node, this.ancestors, predicate);
    }
    /**
     * Returns an array of self and ancestors starting at [self, parent...]
     */
    getAncestorList(node) {
        return common_1.getAncestorsAndSelfList(node, this.ancestors);
    }
    getDeclarator(node, name) {
        var _a, _b;
        if (name == null && ast_1.Reference.is(node)) {
            let result = (_a = this.getScope(node)) === null || _a === void 0 ? void 0 : _a[node.name];
            if (result == null && node.path != null) {
                // check global scope
                let global = this.scopes.get(null);
                result = global[node.path];
            }
            return result;
        }
        if (name == null) {
            throw new Error();
        }
        return (_b = this.getScope(node)) === null || _b === void 0 ? void 0 : _b[name];
    }
    getScope(node) {
        return this.scopes.get(node);
    }
}
exports.ScopeContext = ScopeContext;
/**
 * Returns a Map which will contain a scope object with variable names returning Declarations.
 * scopes.get(null) will return the global scope
 * @param root the ast
 */
function createScopeMaps(root, options = {}) {
    let { identifiers = new Set(), ancestorsMap, } = options;
    let map = new Map();
    let global = {
    // // always add "." to the global namespace so TypeExpressions don't complain
    // ".": new VariableDeclaration({ id: new Identifier({ name: "." }), kind: "const" })
    };
    let scopes = [global];
    map.set(null, global);
    function declare(node) {
        identifiers.add(node.name);
        let scope = scopes[scopes.length - 1];
        scope[node.name] = node;
        if (node.path) {
            // if path, put into global scope
            global[node.path] = node;
        }
    }
    function declarePattern(node) {
        if (ast_1.Declarator.is(node)) {
            declare(node);
        }
        else {
            traverse_1.traverse(node, {
                enter(node) {
                    if (ast_1.Declarator.is(node)) {
                        declare(node);
                        return traverse_1.skip;
                    }
                }
            });
        }
    }
    traverse_1.traverse(root, {
        enter(node, ancestors, path) {
            if (ancestorsMap) {
                let ancestor = null;
                for (let i = ancestors.length - 1; i >= 0; i--) {
                    if (ast_1.Node.is(ancestors[i])) {
                        ancestor = ancestors[i];
                        break;
                    }
                }
                ancestorsMap.set(node, ancestor);
            }
            if (options.implements && ast_1.ClassDeclaration.is(node)) {
                options.implements.push([node.id, types.Object, ...node.baseClasses]);
            }
            //  do nothing on Parameters, they're handled by their containing functions
            if (ast_1.Program.is(node)) {
                // programs declare their id into the global scope, before pushing their own scope
                declarePattern(node.id);
            }
            //  get the current scope
            let scope = scopes[scopes.length - 1];
            //  save a map from this nodes location to it's scope
            map.set(node, scope);
            // if (pathMap) {
            //     pathMap.set(node, path.slice(0))
            // }
            function pushScope() {
                var _a;
                scopes.push(scope = { __proto__: scope, __source: node.constructor.name + " => " + JSON.stringify((_a = node.location) !== null && _a !== void 0 ? _a : "NULL") });
            }
            if (ast_1.Parameter.is(node)) {
                return;
            }
            //  if this node is a scope then we push a new scope
            if (ast_1.Scope.is(node)) {
                pushScope();
                // console.log('++++')
            }
            //  declarations set themselves in scope
            if (ast_1.Pattern.is(node)) {
                declarePattern(node);
            }
            // if (ImportDefaultSpecifier.is(node)) {
            //     console.log({ import: node })
            // }
            //  functions set their parameters in scope
            if (ast_1.FunctionExpression.is(node)) {
                for (let parameter of node.params) {
                    declarePattern(parameter.id);
                }
            }
        },
        leave(node) {
            if (ast_1.Scope.is(node)) {
                // console.log('----')
                scopes.pop();
            }
        }
    });
    return map;
}
exports.default = createScopeMaps;
