import * as _ from 'lodash';

import { ts, SyntaxKind } from 'ts-morph';

import { TsPrinterUtil } from '../../../../../utils/ts-printer.util';

import ImportsUtil from '../../../../../utils/imports.util';

export class SymbolHelper {
    private readonly unknown = '???';

    public parseDeepIndentifier(name: string, srcFile?: ts.SourceFile): IParseDeepIdentifierResult {
        let result = {
            name: '',
            type: ''
        };

        if (typeof name === 'undefined') {
            return result;
        }
        let nsModule = name.split('.');
        let type = this.getType(name);

        if (nsModule.length > 1) {
            result.ns = nsModule[0];
            result.name = name;
            result.type = type;
            return result;
        }
        if (typeof srcFile !== 'undefined') {
            result.file = ImportsUtil.getFileNameOfImport(name, srcFile);
        }
        result.name = name;
        result.type = type;
        return result;
    }

    public getType(name: string): string {
        let type;
        if (name.toLowerCase().indexOf('component') !== -1) {
            type = 'component';
        } else if (name.toLowerCase().indexOf('pipe') !== -1) {
            type = 'pipe';
        } else if (name.toLowerCase().indexOf('controller') !== -1) {
            type = 'controller';
        } else if (name.toLowerCase().indexOf('module') !== -1) {
            type = 'module';
        } else if (name.toLowerCase().indexOf('directive') !== -1) {
            type = 'directive';
        } else if (
            name.toLowerCase().indexOf('injectable') !== -1 ||
            name.toLowerCase().indexOf('service') !== -1
        ) {
            type = 'injectable';
        }
        return type;
    }

    /**
     * Output
     * RouterModule.forRoot 179
     */
    public buildIdentifierName(
        node: ts.Identifier | ts.PropertyAccessExpression | ts.SpreadElement,
        name
    ) {
        if (ts.isIdentifier(node) && !ts.isPropertyAccessExpression(node)) {
            return `${node.text}.${name}`;
        }

        name = name ? `.${name}` : '';

        let nodeName = this.unknown;
        if (node.name) {
            nodeName = node.name.text;
        } else if (node.text) {
            nodeName = node.text;
        } else if (node.expression) {
            if (node.expression.text) {
                nodeName = node.expression.text;
            } else if (node.expression.elements) {
                if (ts.isArrayLiteralExpression(node.expression)) {
                    nodeName = node.expression.elements.map(el => el.text).join(', ');
                    nodeName = `[${nodeName}]`;
                }
            }
        }

        if (ts.isSpreadElement(node)) {
            return `...${nodeName}`;
        }
        return `${this.buildIdentifierName(node.expression, nodeName)}${name}`;
    }

    /**
     * parse expressions such as:
     * { provide: APP_BASE_HREF, useValue: '/' }
     * { provide: 'Date', useFactory: (d1, d2) => new Date(), deps: ['d1', 'd2'] }
     */
    public parseProviderConfiguration(node: ts.ObjectLiteralExpression): string {
        if (node.kind && node.kind === SyntaxKind.ObjectLiteralExpression) {
            // Search for provide: HTTP_INTERCEPTORS
            // and if true, return type: 'interceptor' + name
            let interceptorName, hasInterceptor;
            if (node.properties) {
                if (node.properties.length > 0) {
                    _.forEach(node.properties, property => {
                        if (property.kind && property.kind === SyntaxKind.PropertyAssignment) {
                            if (property.name.text === 'provide') {
                                if (property.initializer.text === 'HTTP_INTERCEPTORS') {
                                    hasInterceptor = true;
                                }
                            }
                            if (
                                property.name.text === 'useClass' ||
                                property.name.text === 'useExisting'
                            ) {
                                interceptorName = property.initializer.text;
                            }
                        }
                    });
                }
            }
            if (hasInterceptor) {
                return interceptorName;
            } else {
                return new TsPrinterUtil().print(node);
            }
        } else {
            return new TsPrinterUtil().print(node);
        }
    }

    /**
     * Kind
     *  181 CallExpression => "RouterModule.forRoot(args)"
     *   71 Identifier     => "RouterModule" "TodoStore"
     *    9 StringLiteral  => "./app.component.css" "./tab.scss"
     */
    public parseSymbolElements(
        node:
            | ts.CallExpression
            | ts.Identifier
            | ts.StringLiteral
            | ts.PropertyAccessExpression
            | ts.SpreadElement
    ): string {
        // parse expressions such as: AngularFireModule.initializeApp(firebaseConfig)
        // if (ts.isCallExpression(node) && ts.isPropertyAccessExpression(node.expression)) {
        if (
            (ts.isCallExpression(node) && ts.isPropertyAccessExpression(node.expression)) ||
            (ts.isNewExpression(node) && ts.isElementAccessExpression(node.expression))
        ) {
            let className = this.buildIdentifierName(node.expression);

            // function arguments could be really complex. There are so
            // many use cases that we can't handle. Just print "args" to indicate
            // that we have arguments.

            let functionArgs = node.arguments.length > 0 ? 'args' : '';
            let text = `${className}(${functionArgs})`;
            return text;
        } else if (ts.isPropertyAccessExpression(node)) {
            // parse expressions such as: Shared.Module
            return this.buildIdentifierName(node);
        } else if (ts.isIdentifier(node)) {
            // parse expressions such as: MyComponent
            if (node.text) {
                return node.text;
            }
            if (node.escapedText) {
                return node.escapedText;
            }
        } else if (ts.isSpreadElement(node)) {
            // parse expressions such as: ...MYARRAY
            // Resolve MYARRAY in imports or local file variables after full scan, just return the name of the variable
            if (node.expression && node.expression.text) {
                return node.expression.text;
            }
        }

        return node.text ? node.text : this.parseProviderConfiguration(node);
    }

    /**
     * Kind
     *  177 ArrayLiteralExpression
     *  122 BooleanKeyword
     *    9 StringLiteral
     */
    private parseSymbols(
        node: ts.ObjectLiteralElement,
        srcFile: ts.SourceFile,
        decoratorType: string
    ): Array<string | boolean> {
        let localNode = node;

        if (ts.isShorthandPropertyAssignment(localNode) && decoratorType !== 'template') {
            localNode = ImportsUtil.findValueInImportOrLocalVariables(
                node.name.text,
                srcFile,
                decoratorType
            );
        }
        if (ts.isShorthandPropertyAssignment(localNode) && decoratorType === 'template') {
            const data = ImportsUtil.findValueInImportOrLocalVariables(
                node.name.text,
                srcFile,
                decoratorType
            );
            return [data];
        }

        if (localNode.initializer && ts.isArrayLiteralExpression(localNode.initializer)) {
            return localNode.initializer.elements.map(x => this.parseSymbolElements(x));
        } else if (
            (localNode.initializer && ts.isStringLiteral(localNode.initializer)) ||
            (localNode.initializer && ts.isTemplateLiteral(localNode.initializer)) ||
            (localNode.initializer &&
                ts.isPropertyAssignment(localNode) &&
                localNode.initializer.text)
        ) {
            return [localNode.initializer.text];
        } else if (
            localNode.initializer &&
            localNode.initializer.kind &&
            (localNode.initializer.kind === SyntaxKind.TrueKeyword ||
                localNode.initializer.kind === SyntaxKind.FalseKeyword)
        ) {
            return [localNode.initializer.kind === SyntaxKind.TrueKeyword ? true : false];
        } else if (localNode.initializer && ts.isPropertyAccessExpression(localNode.initializer)) {
            let identifier = this.parseSymbolElements(localNode.initializer);
            return [identifier];
        } else if (
            localNode.initializer &&
            localNode.initializer.elements &&
            localNode.initializer.elements.length > 0
        ) {
            // Node replaced by ts-simple-ast & kind = 265
            return localNode.initializer.elements.map(x => this.parseSymbolElements(x));
        }
    }

    public getSymbolDeps(
        props: ReadonlyArray<ts.ObjectLiteralElementLike>,
        decoratorType: string,
        srcFile: ts.SourceFile,
        multiLine?: boolean
    ): Array<string> {
        if (props.length === 0) {
            return [];
        }

        let i = 0,
            len = props.length,
            filteredProps = [];

        for (i; i < len; i++) {
            if (props[i].name && props[i].name.text === decoratorType) {
                filteredProps.push(props[i]);
            }
        }

        return filteredProps.map(x => this.parseSymbols(x, srcFile, decoratorType)).pop() || [];
    }

    public getSymbolDepsRaw(
        props: ReadonlyArray<ts.ObjectLiteralElementLike>,
        type: string,
        multiLine?: boolean
    ): Array<ts.ObjectLiteralElementLike> {
        return props.filter(node => node.name.text === type);
    }
}

export interface IParseDeepIdentifierResult {
    ns?: any;
    name: string;
    file?: string;
    type: string | undefined;
}
