import * as path from "path";

import {
    Project,
    ts,
    PropertyDeclaration,
    SyntaxKind,
    VariableDeclaration,
} from "ts-morph";
import FileEngine from "../app/engines/file.engine";

const ast = new Project();

export class ImportsUtil {
    private static instance: ImportsUtil;
    private constructor() {}
    public static getInstance() {
        if (!ImportsUtil.instance) {
            ImportsUtil.instance = new ImportsUtil();
        }
        return ImportsUtil.instance;
    }
    /**
     * Find for a sourceFile a variable value in a local enum
     * @param srcFile
     * @param variableName
     * @param variableValue
     */
    private findInEnums(srcFile, variableName: string, variableValue: string) {
        let res = "";
        srcFile.getEnum((e) => {
            if (e.getName() === variableName) {
                e.getMember((m) => {
                    if (m.getName() === variableValue) {
                        res = m.getValue();
                    }
                });
            }
        });
        return res;
    }

    /**
     * Find for a sourceFile a variable value in a local static class
     * @param srcFile
     * @param variableName
     * @param variableValue
     */
    private findInClasses(
        srcFile,
        variableName: string,
        variableValue: string,
    ) {
        let res = "";
        srcFile.getClass((c) => {
            let staticProperty: PropertyDeclaration =
                c.getStaticProperty(variableValue);
            if (staticProperty) {
                if (staticProperty.getInitializer()) {
                    res = staticProperty.getInitializer().getText();
                }
            }
        });
        return res;
    }

    /**
     * Find a value in a local variable declaration like an object
     * @param variableDeclaration
     * @param variablesAttributes
     */
    private findInObjectVariableDeclaration(
        variableDeclaration,
        variablesAttributes,
    ) {
        let variableKind = variableDeclaration.getKind();
        if (variableKind && variableKind === SyntaxKind.VariableDeclaration) {
            let initializer = variableDeclaration.getInitializer();
            if (initializer) {
                let initializerKind = initializer.getKind();
                if (
                    initializerKind &&
                    initializerKind === SyntaxKind.ObjectLiteralExpression
                ) {
                    let compilerNode =
                            initializer.compilerNode as ts.ObjectLiteralExpression,
                        finalValue = "";
                    // Find thestring from AVAR.BVAR.thestring inside properties
                    let depth = 0;
                    let loopProperties = (properties) => {
                        properties.forEach((prop) => {
                            if (prop.name) {
                                if (variablesAttributes[depth + 1]) {
                                    if (
                                        prop.name.getText() ===
                                        variablesAttributes[depth + 1]
                                    ) {
                                        if (prop.initializer) {
                                            if (prop.initializer.properties) {
                                                depth += 1;
                                                loopProperties(
                                                    prop.initializer.properties,
                                                );
                                            } else {
                                                finalValue =
                                                    prop.initializer.text;
                                            }
                                        } else {
                                            finalValue = prop.initializer.text;
                                        }
                                    }
                                }
                            }
                        });
                    };
                    loopProperties(compilerNode.properties);
                    return finalValue;
                }
            }
        }
    }

    /**
     * Find in imports something like myvar
     * @param  {string} inputVariableName              like myvar
     * @return {[type]}                                myvar value
     */
    public findValueInImportOrLocalVariables(
        inputVariableName: string,
        sourceFile: ts.SourceFile,
        decoratorType?: string,
    ) {
        let metadataVariableName = inputVariableName,
            searchedImport,
            aliasOriginalName = "",
            foundWithNamedImport = false,
            foundWithDefaultImport = false,
            foundWithAlias = false;

        const file =
            typeof ast.getSourceFile(sourceFile.fileName) !== "undefined"
                ? ast.getSourceFile(sourceFile.fileName)
                : ast.addSourceFileAtPathIfExists(sourceFile.fileName); // tslint:disable-line
        const imports = file.getImportDeclarations();

        /**
         * Loop through all imports, and find one matching inputVariableName
         */
        imports.forEach((i) => {
            let namedImports = i.getNamedImports(),
                namedImportsLength = namedImports.length,
                j = 0;

            if (namedImportsLength > 0) {
                for (j; j < namedImportsLength; j++) {
                    let importName = namedImports[j]
                            .getNameNode()
                            .getText() as string,
                        importAlias;

                    if (namedImports[j].getAliasNode()) {
                        importAlias = namedImports[j].getAliasNode().getText();
                    }
                    if (importName === metadataVariableName) {
                        foundWithNamedImport = true;
                        searchedImport = i;
                        break;
                    }
                    if (importAlias === metadataVariableName) {
                        foundWithNamedImport = true;
                        foundWithAlias = true;
                        aliasOriginalName = importName;
                        searchedImport = i;
                        break;
                    }
                }
            }
            const namespaceImport = i.getNamespaceImport();
            if (namespaceImport) {
                const namespaceImportLocalName = namespaceImport.getText();
                if (namespaceImportLocalName === metadataVariableName) {
                    searchedImport = i;
                }
            }

            if (!foundWithNamedImport) {
                const defaultImport = i.getDefaultImport();
                if (defaultImport) {
                    const defaultImportText = defaultImport.getText();
                    if (defaultImportText === metadataVariableName) {
                        foundWithDefaultImport = true;
                        searchedImport = i;
                    }
                }
            }
        });

        function hasFoundValues(variableDeclaration) {
            let variableKind = variableDeclaration.getKind();

            if (
                variableKind &&
                variableKind === SyntaxKind.VariableDeclaration
            ) {
                let initializer = variableDeclaration.getInitializer();
                if (initializer) {
                    let initializerKind = initializer.getKind();
                    if (
                        initializerKind &&
                        initializerKind === SyntaxKind.ObjectLiteralExpression
                    ) {
                        let compilerNode =
                            initializer.compilerNode as ts.ObjectLiteralExpression;
                        return compilerNode.properties;
                    } else if (
                        initializerKind &&
                        initializerKind === SyntaxKind.ArrayLiteralExpression
                    ) {
                        return variableDeclaration.compilerNode;
                    }
                }
            }
        }

        if (typeof searchedImport !== "undefined") {
            let importPathReference =
                searchedImport.getModuleSpecifierSourceFile();
            let importPath;
            if (typeof importPathReference !== "undefined") {
                importPath = importPathReference.compilerNode.fileName;

                const sourceFileImport =
                    typeof ast.getSourceFile(importPath) !== "undefined"
                        ? ast.getSourceFile(importPath)
                        : ast.addSourceFileAtPathIfExists(importPath); // tslint:disable-line

                if (sourceFileImport) {
                    let variableName = foundWithAlias
                        ? aliasOriginalName
                        : metadataVariableName;
                    let variableDeclaration =
                        sourceFileImport.getVariableDeclaration(variableName);

                    if (variableDeclaration) {
                        return hasFoundValues(variableDeclaration);
                    } else {
                        // Try with exports
                        const exportDeclarations =
                            sourceFileImport.getExportedDeclarations();

                        if (exportDeclarations && exportDeclarations.size > 0) {
                            for (const [
                                exportDeclarationKey,
                                exportDeclarationValues,
                            ] of exportDeclarations) {
                                exportDeclarationValues.forEach(
                                    (exportDeclarationValue) => {
                                        if (
                                            exportDeclarationValue instanceof
                                                VariableDeclaration &&
                                            exportDeclarationValue.getName() ===
                                                variableName
                                        ) {
                                            return hasFoundValues(
                                                exportDeclarationValue,
                                            );
                                        }
                                    },
                                );
                            }
                        }
                    }
                }
            }
            if (
                !importPathReference &&
                decoratorType === "template" &&
                searchedImport.getModuleSpecifierValue().indexOf(".html") !== -1
            ) {
                // @ts-ignore
                const originalSourceFilePath = sourceFile.path;
                const originalSourceFilePathFolder =
                    originalSourceFilePath.substring(
                        0,
                        originalSourceFilePath.lastIndexOf("/"),
                    );
                const finalImportedPath =
                    originalSourceFilePathFolder +
                    "/" +
                    searchedImport.getModuleSpecifierValue();
                const finalImportedPathData =
                    FileEngine.getSync(finalImportedPath);
                return finalImportedPathData;
            }
        } else {
            // Find in local variables of the file
            const variableDeclaration =
                file.getVariableDeclaration(metadataVariableName);
            if (variableDeclaration) {
                let variableKind = variableDeclaration.getKind();

                if (
                    variableKind &&
                    variableKind === SyntaxKind.VariableDeclaration
                ) {
                    let initializer = variableDeclaration.getInitializer();
                    if (initializer) {
                        let initializerKind = initializer.getKind();
                        if (
                            initializerKind &&
                            initializerKind ===
                                SyntaxKind.ObjectLiteralExpression
                        ) {
                            let compilerNode =
                                initializer.compilerNode as ts.ObjectLiteralExpression;
                            return compilerNode.properties;
                        } else if (
                            initializerKind &&
                            (initializerKind === SyntaxKind.StringLiteral ||
                                initializerKind ===
                                    SyntaxKind.NoSubstitutionTemplateLiteral)
                        ) {
                            if (decoratorType === "template") {
                                return initializer.getText();
                            } else {
                                return variableDeclaration.compilerNode;
                            }
                        } else if (initializerKind) {
                            return variableDeclaration.compilerNode;
                        }
                    }
                }
            }
        }

        return [];
    }

    public getFileNameOfImport(
        variableName: string,
        sourceFile: ts.SourceFile,
    ) {
        const file =
            typeof ast.getSourceFile(sourceFile.fileName) !== "undefined"
                ? ast.getSourceFile(sourceFile.fileName)
                : ast.addSourceFileAtPath(sourceFile.fileName); // tslint:disable-line
        const imports = file.getImportDeclarations();
        let searchedImport,
            aliasOriginalName = "",
            finalPath = "",
            foundWithAlias = false;
        imports.forEach((i) => {
            let namedImports = i.getNamedImports(),
                namedImportsLength = namedImports.length,
                j = 0;

            if (namedImportsLength > 0) {
                for (j; j < namedImportsLength; j++) {
                    let importName = namedImports[j]
                            .getNameNode()
                            .getText() as string,
                        importAlias;

                    if (namedImports[j].getAliasNode()) {
                        importAlias = namedImports[j].getAliasNode().getText();
                    }
                    if (importName === variableName) {
                        searchedImport = i;
                        break;
                    }
                    if (importAlias === variableName) {
                        foundWithAlias = true;
                        aliasOriginalName = importName;
                        searchedImport = i;
                        break;
                    }
                }
            }
        });
        if (typeof searchedImport !== "undefined") {
            let importPath = path.resolve(
                path.dirname(sourceFile.fileName) +
                    "/" +
                    searchedImport.getModuleSpecifierValue() +
                    ".ts",
            );
            let cleaner = (process.cwd() + path.sep).replace(/\\/g, "/");
            finalPath = importPath.replace(cleaner, "");
        }
        return finalPath;
    }

    /**
     * Find the file path of imported variable
     * @param  {string} inputVariableName  like thestring
     * @return {[type]}                    thestring destination path
     */
    public findFilePathOfImportedVariable(
        inputVariableName,
        sourceFilePath: string,
    ) {
        let searchedImport,
            finalPath = "",
            aliasOriginalName = "",
            foundWithAlias = false;
        const file =
            typeof ast.getSourceFile(sourceFilePath) !== "undefined"
                ? ast.getSourceFile(sourceFilePath)
                : ast.addSourceFileAtPath(sourceFilePath); // tslint:disable-line
        const imports = file.getImportDeclarations();

        /**
         * Loop through all imports, and find one matching inputVariableName
         */
        imports.forEach((i) => {
            let namedImports = i.getNamedImports(),
                namedImportsLength = namedImports.length,
                j = 0;

            if (namedImportsLength > 0) {
                for (j; j < namedImportsLength; j++) {
                    let importName = namedImports[j]
                            .getNameNode()
                            .getText() as string,
                        importAlias;

                    if (namedImports[j].getAliasNode()) {
                        importAlias = namedImports[j].getAliasNode().getText();
                    }
                    if (importName === inputVariableName) {
                        searchedImport = i;
                        break;
                    }
                    if (importAlias === inputVariableName) {
                        foundWithAlias = true;
                        aliasOriginalName = importName;
                        searchedImport = i;
                        break;
                    }
                }
            }
        });
        if (typeof searchedImport !== "undefined") {
            finalPath = path.resolve(
                path.dirname(sourceFilePath) +
                    "/" +
                    searchedImport.getModuleSpecifierValue() +
                    ".ts",
            );
        }
        return finalPath;
    }

    /**
     * Find in imports something like VAR.AVAR.BVAR.thestring
     * @param  {string} inputVariableName                   like VAR.AVAR.BVAR.thestring
     * @return {[type]}                                thestring value
     */
    public findPropertyValueInImportOrLocalVariables(
        inputVariableName,
        sourceFile: ts.SourceFile,
    ) {
        let variablesAttributes = inputVariableName.split("."),
            metadataVariableName = variablesAttributes[0],
            searchedImport,
            aliasOriginalName = "",
            foundWithAlias = false;

        const file =
            typeof ast.getSourceFile(sourceFile.fileName) !== "undefined"
                ? ast.getSourceFile(sourceFile.fileName)
                : ast.addSourceFileAtPath(sourceFile.fileName); // tslint:disable-line
        const imports = file.getImportDeclarations();

        /**
         * Loop through all imports, and find one matching inputVariableName
         */
        imports.forEach((i) => {
            let namedImports = i.getNamedImports(),
                namedImportsLength = namedImports.length,
                j = 0;

            if (namedImportsLength > 0) {
                for (j; j < namedImportsLength; j++) {
                    let importName = namedImports[j]
                            .getNameNode()
                            .getText() as string,
                        importAlias;

                    if (namedImports[j].getAliasNode()) {
                        importAlias = namedImports[j].getAliasNode().getText();
                    }
                    if (importName === metadataVariableName) {
                        searchedImport = i;
                        break;
                    }
                    if (importAlias === metadataVariableName) {
                        foundWithAlias = true;
                        aliasOriginalName = importName;
                        searchedImport = i;
                        break;
                    }
                }
            }
        });

        let fileToSearchIn, variableDeclaration;
        if (typeof searchedImport !== "undefined") {
            let importPath = path.resolve(
                path.dirname(sourceFile.fileName) +
                    "/" +
                    searchedImport.getModuleSpecifierValue() +
                    ".ts",
            );
            const sourceFileImport =
                typeof ast.getSourceFile(importPath) !== "undefined"
                    ? ast.getSourceFile(importPath)
                    : ast.addSourceFileAtPath(importPath); // tslint:disable-line
            if (sourceFileImport) {
                fileToSearchIn = sourceFileImport;
                let variableName = foundWithAlias
                    ? aliasOriginalName
                    : metadataVariableName;
                variableDeclaration =
                    fileToSearchIn.getVariableDeclaration(variableName);
            }
        } else {
            fileToSearchIn = file;
            // Find in local variables of the file
            variableDeclaration =
                fileToSearchIn.getVariableDeclaration(metadataVariableName);
        }

        if (variableDeclaration) {
            return this.findInObjectVariableDeclaration(
                variableDeclaration,
                variablesAttributes,
            );
        }
        // Try find it in enums
        if (variablesAttributes.length > 0) {
            if (typeof fileToSearchIn !== "undefined") {
                let val = this.findInEnums(
                    fileToSearchIn,
                    metadataVariableName,
                    variablesAttributes[1],
                );
                if (val !== "") {
                    return val;
                }
                val = this.findInClasses(
                    fileToSearchIn,
                    metadataVariableName,
                    variablesAttributes[1],
                );
                if (val !== "") {
                    return val;
                }
            }
        }
    }
}

export default ImportsUtil.getInstance();
