import * as semver from 'semver';
import { SyntaxKind, ts } from 'ts-morph';
import Configuration from '../../../../configuration';
import { detectIndent } from '../../../../../utils';
import { ClassHelper } from './class-helper';
import { SignalConfigParser } from './signal-config.parser';
import { IParseDeepIdentifierResult, SymbolHelper } from './symbol-helper';

export class ComponentHelper {
    constructor(
        private classHelper: ClassHelper,
        private symbolHelper: SymbolHelper = new SymbolHelper(),
        private signalConfigParser: SignalConfigParser = new SignalConfigParser()
    ) {}

    public getComponentChangeDetection(
        props: ReadonlyArray<ts.ObjectLiteralElementLike>,
        srcFile: ts.SourceFile
    ): string {
        const result = this.symbolHelper.getSymbolDeps(props, 'changeDetection', srcFile).pop();

        if (typeof result === 'undefined') {
            const angularVersion = Configuration.mainData.angularVersion;
            const coerced = angularVersion ? semver.coerce(angularVersion) : null;
            if (coerced && semver.valid(coerced) && semver.gte(coerced.version, '22.0.0')) {
                return 'ChangeDetectionStrategy.OnPush';
            }
        }

        return result;
    }

    public getComponentEncapsulation(
        props: ReadonlyArray<ts.ObjectLiteralElementLike>,
        srcFile: ts.SourceFile
    ): Array<string> {
        return this.symbolHelper.getSymbolDeps(props, 'encapsulation', srcFile);
    }

    public getComponentPure(
        props: ReadonlyArray<ts.ObjectLiteralElementLike>,
        srcFile: ts.SourceFile
    ): string {
        return this.symbolHelper.getSymbolDeps(props, 'pure', srcFile).pop();
    }

    public getComponentName(
        props: ReadonlyArray<ts.ObjectLiteralElementLike>,
        srcFile: ts.SourceFile
    ): string {
        return this.symbolHelper.getSymbolDeps(props, 'name', srcFile).pop();
    }

    public getComponentExportAs(
        props: ReadonlyArray<ts.ObjectLiteralElementLike>,
        srcFile: ts.SourceFile
    ): string {
        return this.symbolHelper.getSymbolDeps(props, 'exportAs', srcFile).pop();
    }

    public getComponentHostDirectives(
        props: ReadonlyArray<ts.ObjectLiteralElementLike>
    ): Array<any> {
        const hostDirectiveSymbolParsed = this.symbolHelper.getSymbolDepsRaw(
            props,
            'hostDirectives'
        );
        let hostDirectiveSymbol = null;

        if (hostDirectiveSymbolParsed.length > 0) {
            hostDirectiveSymbol = hostDirectiveSymbolParsed.pop();
        }

        const result = [];

        if (
            hostDirectiveSymbol &&
            hostDirectiveSymbol.initializer &&
            hostDirectiveSymbol.initializer.elements &&
            hostDirectiveSymbol.initializer.elements.length > 0
        ) {
            hostDirectiveSymbol.initializer.elements.forEach(element => {
                if (element.kind === SyntaxKind.Identifier) {
                    result.push({
                        name: element.escapedText
                    });
                } else if (
                    element.kind === SyntaxKind.ObjectLiteralExpression &&
                    element.properties &&
                    element.properties.length > 0
                ) {
                    const parsedDirective: any = {
                        name: '',
                        inputs: [],
                        outputs: []
                    };

                    element.properties.forEach(property => {
                        if (property.name.escapedText === 'directive') {
                            parsedDirective.name = property.initializer.escapedText;
                        } else if (property.name.escapedText === 'inputs') {
                            if (
                                property.initializer &&
                                property.initializer.elements &&
                                property.initializer.elements.length > 0
                            ) {
                                property.initializer.elements.forEach(propertyElement => {
                                    parsedDirective.inputs.push(propertyElement.text);
                                });
                            }
                        } else if (property.name.escapedText === 'outputs') {
                            if (
                                property.initializer &&
                                property.initializer.elements &&
                                property.initializer.elements.length > 0
                            ) {
                                property.initializer.elements.forEach(propertyElement => {
                                    parsedDirective.outputs.push(propertyElement.text);
                                });
                            }
                        }
                    });

                    result.push(parsedDirective);
                }
            });
        }

        return result;
    }

    public getComponentHost(
        props: ReadonlyArray<ts.ObjectLiteralElementLike>
    ): Map<string, string> {
        return this.getSymbolDepsObject(props, 'host');
    }

    public getComponentTag(
        props: ReadonlyArray<ts.ObjectLiteralElementLike>,
        srcFile: ts.SourceFile
    ): string {
        return this.symbolHelper.getSymbolDeps(props, 'tag', srcFile).pop();
    }

    public getComponentInputsMetadata(
        props: ReadonlyArray<ts.ObjectLiteralElementLike>,
        srcFile: ts.SourceFile
    ): Array<string> {
        return this.symbolHelper.getSymbolDeps(props, 'inputs', srcFile);
    }

    public getInputOutputSignals(props) {
        const inputSignals = [];
        const outputSignals = [];
        const properties = [];

        props.forEach(prop => {
            const inputSignal = this.getInputSignal(prop);
            if (inputSignal) {
                inputSignals.push(inputSignal);
            }

            const outputSignal = this.getOutputSignal(prop);
            if (outputSignal) {
                outputSignals.push(outputSignal);
            }

            if (!inputSignal && !outputSignal) {
                properties.push(prop);
            }
        });

        return { inputSignals, outputSignals, properties };
    }

    public getInputSignal(prop) {
        const config =
            this.getSignalConfig('input', prop.defaultValue) ??
            this.getSignalConfig('model', prop.defaultValue);

        if (config) {
            return {
                ...prop,
                ...config
            };
        }

        return undefined;
    }

    public getOutputSignal(prop) {
        const config =
            this.getSignalConfig('output', prop.defaultValue) ??
            this.getSignalConfig('model', prop.defaultValue);

        if (config) {
            return {
                ...prop,
                ...config
            };
        }

        return undefined;
    }

    private getSignalConfig(type: 'input' | 'output' | 'model', defaultValue: string) {
        return this.signalConfigParser.getSignalConfig(type, defaultValue);
    }

    public parseSignalType(type: string) {
        return this.signalConfigParser.parseSignalType(type);
    }

    public getComponentStandalone(
        props: ReadonlyArray<ts.ObjectLiteralElementLike>,
        srcFile: ts.SourceFile
    ): boolean {
        let result = null;
        const parsedData = this.symbolHelper.getSymbolDeps(props, 'standalone', srcFile);
        if (parsedData.length === 1) {
            result = JSON.parse(parsedData[0]);
        }

        if (result === null) {
            const angularVersion = Configuration.mainData.angularVersion;
            const coerced = angularVersion ? semver.coerce(angularVersion) : null;
            if (coerced && semver.valid(coerced) && semver.gte(coerced.version, '19.0.0')) {
                result = true;
            }
        }

        return result;
    }

    public getComponentTemplate(
        props: ReadonlyArray<ts.ObjectLiteralElementLike>,
        srcFile: ts.SourceFile
    ): string {
        let t = this.symbolHelper.getSymbolDeps(props, 'template', srcFile, true).pop();
        if (t && typeof t === 'string') {
            t = detectIndent(t, 0);
            t = t.replace(/\n/, '');
            t = t.replace(/ +$/gm, '');
        }
        return t;
    }

    public getComponentTemplateVariables(template: string): Array<IComponentTemplateVariable> {
        return ComponentHelper.getTemplateVariablesFromTemplate(template);
    }

    public static getTemplateVariablesFromTemplate(
        template: string
    ): Array<IComponentTemplateVariable> {
        if (!template || typeof template !== 'string') {
            return [];
        }

        const variables: Array<IComponentTemplateVariable> = [];
        const regex = /@let\s+([A-Za-z_$][\w$]*)\s*=\s*([^;]+);/g;
        let match: RegExpExecArray | null;

        while ((match = regex.exec(template))) {
            variables.push({
                name: match[1],
                defaultValue: match[2].trim()
            });
        }

        return variables;
    }

    public getComponentStyleUrls(
        props: ReadonlyArray<ts.ObjectLiteralElementLike>,
        srcFile: ts.SourceFile
    ): string[] {
        const plural = this.symbolHelper.getSymbolDeps(props, 'styleUrls', srcFile);
        const singular = this.symbolHelper.getSymbolDeps(props, 'styleUrl', srcFile);
        return [...new Set([...plural, ...singular])];
    }

    public getComponentStyleUrl(
        props: ReadonlyArray<ts.ObjectLiteralElementLike>,
        srcFile: ts.SourceFile
    ): string {
        return this.symbolHelper.getSymbolDeps(props, 'styleUrl', srcFile).pop();
    }

    public getComponentShadow(
        props: ReadonlyArray<ts.ObjectLiteralElementLike>,
        srcFile: ts.SourceFile
    ): string {
        return this.symbolHelper.getSymbolDeps(props, 'shadow', srcFile).pop();
    }

    public getComponentScoped(
        props: ReadonlyArray<ts.ObjectLiteralElementLike>,
        srcFile: ts.SourceFile
    ): string {
        return this.symbolHelper.getSymbolDeps(props, 'scoped', srcFile).pop();
    }

    public getComponentAssetsDir(
        props: ReadonlyArray<ts.ObjectLiteralElementLike>,
        srcFile: ts.SourceFile
    ): string {
        return this.symbolHelper.getSymbolDeps(props, 'assetsDir', srcFile).pop();
    }

    public getComponentAssetsDirs(
        props: ReadonlyArray<ts.ObjectLiteralElementLike>,
        srcFile: ts.SourceFile
    ): string[] {
        return this.sanitizeUrls(this.symbolHelper.getSymbolDeps(props, 'assetsDir', srcFile));
    }

    public getComponentStyles(
        props: ReadonlyArray<ts.ObjectLiteralElementLike>,
        srcFile: ts.SourceFile
    ): string[] {
        return this.symbolHelper.getSymbolDeps(props, 'styles', srcFile);
    }

    public getComponentModuleId(
        props: ReadonlyArray<ts.ObjectLiteralElementLike>,
        srcFile: ts.SourceFile
    ): string {
        return this.symbolHelper.getSymbolDeps(props, 'moduleId', srcFile).pop();
    }

    public getComponentOutputs(
        props: ReadonlyArray<ts.ObjectLiteralElementLike>,
        srcFile: ts.SourceFile
    ): string[] {
        return this.symbolHelper.getSymbolDeps(props, 'outputs', srcFile);
    }

    public getComponentProviders(
        props: ReadonlyArray<ts.ObjectLiteralElementLike>,
        srcFile: ts.SourceFile
    ): Array<IParseDeepIdentifierResult> {
        return this.symbolHelper
            .getSymbolDeps(props, 'providers', srcFile)
            .map(name => this.symbolHelper.parseDeepIndentifier(name));
    }

    public getComponentImports(
        props: ReadonlyArray<ts.ObjectLiteralElementLike>,
        srcFile: ts.SourceFile
    ): Array<IParseDeepIdentifierResult> {
        return this.symbolHelper
            .getSymbolDeps(props, 'imports', srcFile)
            .map(name => this.symbolHelper.parseDeepIndentifier(name));
    }

    public getComponentEntryComponents(
        props: ReadonlyArray<ts.ObjectLiteralElementLike>,
        srcFile: ts.SourceFile
    ): Array<IParseDeepIdentifierResult> {
        return this.symbolHelper
            .getSymbolDeps(props, 'entryComponents', srcFile)
            .map(name => this.symbolHelper.parseDeepIndentifier(name));
    }

    public getComponentViewProviders(
        props: ReadonlyArray<ts.ObjectLiteralElementLike>,
        srcFile: ts.SourceFile
    ): Array<IParseDeepIdentifierResult> {
        return this.symbolHelper
            .getSymbolDeps(props, 'viewProviders', srcFile)
            .map(name => this.symbolHelper.parseDeepIndentifier(name));
    }

    public getComponentTemplateUrl(
        props: ReadonlyArray<ts.ObjectLiteralElementLike>,
        srcFile: ts.SourceFile
    ): Array<string> {
        return this.symbolHelper.getSymbolDeps(props, 'templateUrl', srcFile);
    }

    public getComponentExampleUrls(text: string): Array<string> | undefined {
        let exampleUrlsMatches = text.match(/<example-url>(.*?)<\/example-url>/g);
        let exampleUrls = undefined;
        if (exampleUrlsMatches && exampleUrlsMatches.length) {
            exampleUrls = exampleUrlsMatches.map(function (val) {
                return val.replace(/<\/?example-url>/g, '');
            });
        }
        return exampleUrls;
    }

    public getComponentPreserveWhitespaces(
        props: ReadonlyArray<ts.ObjectLiteralElementLike>,
        srcFile: ts.SourceFile
    ): string {
        return this.symbolHelper.getSymbolDeps(props, 'preserveWhitespaces', srcFile).pop();
    }

    public getComponentSelector(
        props: ReadonlyArray<ts.ObjectLiteralElementLike>,
        srcFile: ts.SourceFile
    ): string {
        return this.symbolHelper.getSymbolDeps(props, 'selector', srcFile).pop();
    }

    private parseProperties(node: ts.ObjectLiteralElementLike): Map<string, string> {
        let obj = new Map<string, string>();
        const element = node as any;
        let properties = element.initializer?.properties || [];
        properties.forEach((prop: any) => {
            obj.set(prop.name?.text, prop.initializer?.text);
        });
        return obj;
    }

    public getSymbolDepsObject(
        props: ReadonlyArray<ts.ObjectLiteralElementLike>,
        type: string,
        multiLine?: boolean
    ): Map<string, string> {
        let i = 0,
            len = props.length,
            filteredProps = [];

        for (i; i < len; i++) {
            if (props[i].name && (props[i].name as any).text === type) {
                filteredProps.push(props[i]);
            }
        }
        return filteredProps.map(x => this.parseProperties(x)).pop();
    }

    public getComponentIO(
        filename: string,
        sourceFile: ts.SourceFile,
        node: ts.Node,
        fileBody,
        astFile: ts.SourceFile
    ): any {
        /**
         * Copyright https://github.com/ng-bootstrap/ng-bootstrap
         */
        let reducedSource = fileBody ? fileBody.statements : sourceFile.statements;
        let res = reducedSource.reduce((directive, statement) => {
            if (ts.isClassDeclaration(statement)) {
                if (statement.pos === node.pos && statement.end === node.end) {
                    return directive.concat(
                        this.classHelper.visitClassDeclaration(
                            filename,
                            statement,
                            sourceFile,
                            astFile
                        )
                    );
                }
            }

            return directive;
        }, []);

        return res[0] || {};
    }

    private sanitizeUrls(urls: Array<string>): Array<string> {
        return urls.map(url => url.replace('./', ''));
    }
}

export interface IComponentTemplateVariable {
    name: string;
    defaultValue: string;
}

export class ComponentCache {
    private cache: Map<string, any> = new Map();

    public get(key: string): any {
        return this.cache.get(key);
    }

    public set(key: string, value: any): void {
        this.cache.set(key, value);
    }
}
