"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;
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.write = exports.makeDirectories = exports.exists = exports.getInputFilesRecursive = exports.getPathFromFilename = exports.read = exports.findPackage = exports.difference = exports.intersection = exports.union = exports.getLast = exports.mapValues = exports.toMap = exports.SemanticError = exports.freeze = exports.clone = exports.getOriginalDeclaration = exports.getAncestor = exports.getAncestorsAndSelfList = exports.getDeclarator = exports.getOriginalDeclarator = exports.hasDeclarator = exports.isValidId = exports.memoize = exports.memoizeIntern = exports.getNodesOfType = exports.runtimeModuleName = void 0;
const fs = __importStar(require("fs"));
const np = __importStar(require("path"));
const traverse_1 = require("@glas/traverse");
const ast_1 = require("./ast");
const pathFunctions_1 = require("./pathFunctions");
exports.runtimeModuleName = "ionscript";
function getNodesOfType(root, predicate) {
    let nodes = new Array();
    traverse_1.traverse(root, {
        enter(node) {
            if (predicate(node)) {
                nodes.push(node);
            }
        }
    });
    return nodes;
}
exports.getNodesOfType = getNodesOfType;
function memoizeIntern(fn) {
    const cache = new Map();
    return memoize(fn, false, cache);
}
exports.memoizeIntern = memoizeIntern;
function memoize(fn, cacheResultAsKey = false, cache = new WeakMap()) {
    return function (arg) {
        let result = cache.get(arg);
        if (result === undefined) {
            cache.set(arg, result = fn.apply(this, arguments));
            if (cacheResultAsKey) {
                cache.set(result, result);
            }
        }
        return result;
    };
}
exports.memoize = memoize;
const validIdRegex = /^[a-z_][a-z0-9_]*$/i;
function isValidId(name) {
    return validIdRegex.test(name);
}
exports.isValidId = isValidId;
////////////////////////////////////////////////////////////////////////////////
//  Miscelaneous Functions
////////////////////////////////////////////////////////////////////////////////
//  quick check for a declarator without creating a scope map
function hasDeclarator(statements, name) {
    // console.log(statements)
    for (let s of statements) {
        if (ast_1.ImportDeclaration.is(s)) {
            for (let specifier of s.specifiers) {
                if (specifier.local.name === name) {
                    return true;
                }
            }
        }
        if (ast_1.VariableDeclaration.is(s)) {
            if (ast_1.Identifier.is(s.id)) {
                if (s.id.name === name) {
                    return true;
                }
            }
            //  TODO: Make this work for ObjectPatterns.
            //  They aren't currently using Declarator's as they should.
        }
    }
    return false;
}
exports.hasDeclarator = hasDeclarator;
function getOriginalDeclarator(declarator, scopes, ancestors, throwErrors = true) {
    let parent = ancestors.get(declarator);
    if (ast_1.ModuleSpecifier.is(parent)) {
        let containingModule = getAncestor(parent, ancestors, ast_1.Program.is);
        let importDeclaration = ancestors.get(parent);
        let absoluteSource = pathFunctions_1.getRelative(containingModule.id.name, importDeclaration.source.value);
        //  we cannot just use importDeclaration.source.value  // that may be relative...
        //  must at least make it absolute first.
        let sourceModuleDeclaration = scopes.get(declarator)[absoluteSource];
        if (sourceModuleDeclaration) {
            if (ast_1.ImportNamespaceSpecifier.is(parent)) {
                return sourceModuleDeclaration;
            }
            let name;
            if (ast_1.ImportSpecifier.is(parent)) {
                name = parent.imported.name;
            }
            else if (ast_1.ImportDefaultSpecifier.is(parent)) {
                name = "default";
            }
            else {
                throw new Error();
            }
            let sourceProgramScope = scopes.get(sourceModuleDeclaration);
            let sourceDeclarator = sourceProgramScope[name];
            if (sourceDeclarator == null) {
                if (!throwErrors) {
                    return null;
                }
                throw SemanticError(`${importDeclaration.source.value} does not have export ${name}`, parent);
            }
            let sourceDeclaratorParent = ancestors.get(sourceDeclarator);
            if (!ast_1.Exportable.is(sourceDeclaratorParent) || !sourceDeclaratorParent.export) {
                if (!throwErrors) {
                    return null;
                }
                throw SemanticError(`${importDeclaration.source.value} ${name} is not exported`, parent);
            }
            // recurse in case this exported value is also a reference or re-export
            // return sourceDeclarator
            return getOriginalDeclarator(sourceDeclarator, scopes, ancestors);
        }
    }
    // traverse variables that are just constant re-declarations
    if (ast_1.VariableDeclaration.is(parent) && parent.kind !== "var" && ast_1.Reference.is(parent.value)) {
        // keep following references to the original
        return getDeclarator(parent.value, scopes, ancestors, true, false);
    }
    return declarator;
}
exports.getOriginalDeclarator = getOriginalDeclarator;
function getDeclarator(ref, scopes, ancestors, traverseReferences = false, throwError = true) {
    let scope = scopes.get(ref);
    let declarator = scope[ref.name];
    if (declarator == null) {
        if (throwError) {
            throw SemanticError(`${ref.name} declarator not found`, ref);
        }
        else {
            return null;
        }
    }
    return traverseReferences ? getOriginalDeclarator(declarator, scopes, ancestors) : declarator;
}
exports.getDeclarator = getDeclarator;
/**
 * Returns an array of self and ancestors where 0 == self and length - 1 = root
 */
function getAncestorsAndSelfList(node, ancestorsMap) {
    let ancestorsList = new Array();
    while (node != null) {
        ancestorsList.push(node);
        node = ancestorsMap.get(node);
    }
    return ancestorsList;
}
exports.getAncestorsAndSelfList = getAncestorsAndSelfList;
function getAncestor(node, ancestors, predicate) {
    while (node != null) {
        let ancestor = ancestors.get(node);
        if (predicate(ancestor)) {
            return ancestor;
        }
        node = ancestor;
    }
    return null;
}
exports.getAncestor = getAncestor;
function getOriginalDeclaration(ref, scopes, ancestors, predicate = ast_1.Declaration.is) {
    let declarator = getDeclarator(ref, scopes, ancestors, true, false);
    return declarator != null ? getAncestor(declarator, ancestors, predicate) : null;
}
exports.getOriginalDeclaration = getOriginalDeclaration;
function clone(value) {
    if (value == null || typeof value !== "object") {
        return value;
    }
    if (value.clone) {
        return value.clone();
    }
    if (value instanceof Set) {
        return new Set(Array.from(value.values()).map(clone));
    }
    if (value instanceof Map) {
        return new Map(Array.from(value.entries()).map(clone));
    }
    if (Array.isArray(value)) {
        return value.map(clone);
    }
    if (ast_1.Node.is(value)) {
        return new value.constructor(value);
    }
    let newValues = {};
    for (let name in value) {
        newValues[name] = clone(value[name]);
    }
    let copy = new value.constructor(newValues);
    return copy;
}
exports.clone = clone;
function freeze(object, deep = true) {
    if (object != null && typeof object === 'object') {
        Object.freeze(object);
        if (deep) {
            for (let name in object) {
                freeze(object[name]);
            }
        }
    }
}
exports.freeze = freeze;
function SemanticError(message, location) {
    let error = new Error(message);
    error.location = location.location || location;
    return error;
}
exports.SemanticError = SemanticError;
function toMap(object) {
    let result = new Map();
    for (let name in object) {
        result.set(name, object[name]);
    }
    return result;
}
exports.toMap = toMap;
function mapValues(object, fn) {
    let result = new Map();
    for (let key of object.keys()) {
        let value = object.get(key);
        result.set(key, fn(value, key));
    }
    return result;
}
exports.mapValues = mapValues;
function getLast(array, predicate) {
    for (let i = array.length - 1; i >= 0; i--) {
        let item = array[i];
        if (predicate(item)) {
            return item;
        }
    }
    return null;
}
exports.getLast = getLast;
////////////////////////////////////////////////////////////////////////////////
//  Set Functions
////////////////////////////////////////////////////////////////////////////////
function union(a, b) {
    let result = new Set();
    for (let e of a)
        result.add(e);
    for (let e of b)
        result.add(e);
    return result;
}
exports.union = union;
function intersection(a, b) {
    let result = new Set();
    for (let e of a) {
        if (b.has(e))
            result.add(e);
    }
    return result;
}
exports.intersection = intersection;
function difference(a, b) {
    let result = new Set();
    for (let e of a) {
        if (!b.has(e))
            result.add(e);
    }
    return result;
}
exports.difference = difference;
////////////////////////////////////////////////////////////////////////////////
//  File operations
////////////////////////////////////////////////////////////////////////////////
const ionExt = '.is';
function findPackage(dir = process.cwd()) {
    let checkFilename = np.join(dir, "package.json");
    // console.log("check", checkFilename)
    if (fs.existsSync(checkFilename)) {
        return require(checkFilename);
    }
    let newDir = np.dirname(dir);
    if (newDir != dir) {
        return findPackage(newDir);
    }
    return null;
}
exports.findPackage = findPackage;
function read(file) {
    return fs.readFileSync(file, 'utf8');
}
exports.read = read;
function getPathFromFilename(namespace, filename) {
    let path = filename.substring(0, filename.length - ionExt.length).split(/[\/\\]+/g);
    return namespace.replace(/[\/\\]/g, "/") + "/" + path.join("/");
}
exports.getPathFromFilename = getPathFromFilename;
function getInputFilesRecursive(directory, namespace, rootDirectory = null, allFiles = {}) {
    if (Array.isArray(directory)) {
        for (let dir of directory) {
            getInputFilesRecursive(dir, namespace, dir, allFiles);
        }
    }
    else {
        if (rootDirectory == null)
            rootDirectory = directory;
        for (let name of fs.readdirSync(directory)) {
            let filename = np.join(directory, name);
            let fileInfo = fs.statSync(filename);
            if (fileInfo.isFile()) {
                if (name.endsWith(ionExt)) {
                    let path = getPathFromFilename(namespace, filename.substring(rootDirectory.length + 1));
                    allFiles[path] = read(filename);
                }
            }
            else {
                getInputFilesRecursive(filename, namespace, rootDirectory, allFiles);
            }
        }
    }
    return allFiles;
}
exports.getInputFilesRecursive = getInputFilesRecursive;
function exists(file) {
    return fs.existsSync(file);
}
exports.exists = exists;
function makeDirectories(dir) {
    if (!exists(dir)) {
        // make parent first
        makeDirectories(np.dirname(dir));
        // make self
        fs.mkdirSync(dir);
    }
}
exports.makeDirectories = makeDirectories;
function write(file, content, encoding) {
    makeDirectories(np.dirname(file));
    if (content != null) {
        if (encoding === undefined && typeof content === 'string')
            encoding = 'utf8';
        fs.writeFileSync(file, content, { encoding });
    }
    else if (exists(file)) {
        fs.unlinkSync(file);
    }
}
exports.write = write;
