"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.BaseTypesVisitor = void 0;
const graphql_1 = require("graphql");
const base_visitor_js_1 = require("./base-visitor.js");
const declaration_kinds_js_1 = require("./declaration-kinds.js");
const enum_values_js_1 = require("./enum-values.js");
const mappers_js_1 = require("./mappers.js");
const scalars_js_1 = require("./scalars.js");
const utils_js_1 = require("./utils.js");
const variables_to_object_js_1 = require("./variables-to-object.js");
class BaseTypesVisitor extends base_visitor_js_1.BaseVisitor {
    constructor(_schema, rawConfig, additionalConfig, defaultScalars = scalars_js_1.DEFAULT_SCALARS) {
        super(rawConfig, {
            enumPrefix: (0, utils_js_1.getConfigValue)(rawConfig.enumPrefix, true),
            enumSuffix: (0, utils_js_1.getConfigValue)(rawConfig.enumSuffix, true),
            onlyEnums: (0, utils_js_1.getConfigValue)(rawConfig.onlyEnums, false),
            onlyOperationTypes: (0, utils_js_1.getConfigValue)(rawConfig.onlyOperationTypes, false),
            addUnderscoreToArgsType: (0, utils_js_1.getConfigValue)(rawConfig.addUnderscoreToArgsType, false),
            enumValues: (0, enum_values_js_1.parseEnumValues)({
                schema: _schema,
                mapOrStr: rawConfig.enumValues,
                ignoreEnumValuesFromSchema: rawConfig.ignoreEnumValuesFromSchema,
            }),
            declarationKind: (0, declaration_kinds_js_1.normalizeDeclarationKind)(rawConfig.declarationKind),
            scalars: (0, utils_js_1.buildScalarsFromConfig)(_schema, rawConfig, defaultScalars),
            fieldWrapperValue: (0, utils_js_1.getConfigValue)(rawConfig.fieldWrapperValue, 'T'),
            wrapFieldDefinitions: (0, utils_js_1.getConfigValue)(rawConfig.wrapFieldDefinitions, false),
            entireFieldWrapperValue: (0, utils_js_1.getConfigValue)(rawConfig.entireFieldWrapperValue, 'T'),
            wrapEntireDefinitions: (0, utils_js_1.getConfigValue)(rawConfig.wrapEntireFieldDefinitions, false),
            ignoreEnumValuesFromSchema: (0, utils_js_1.getConfigValue)(rawConfig.ignoreEnumValuesFromSchema, false),
            directiveArgumentAndInputFieldMappings: (0, mappers_js_1.transformDirectiveArgumentAndInputFieldMappings)(rawConfig.directiveArgumentAndInputFieldMappings ?? {}, rawConfig.directiveArgumentAndInputFieldMappingTypeSuffix),
            ...additionalConfig,
        });
        this._schema = _schema;
        // Note: Missing directive mappers but not a problem since always overriden by implementors
        this._argumentsTransformer = new variables_to_object_js_1.OperationVariablesToObject(this.scalars, this.convertName);
    }
    getExportPrefix() {
        return 'export ';
    }
    getFieldWrapperValue() {
        if (this.config.fieldWrapperValue) {
            return `${this.getExportPrefix()}type FieldWrapper<T> = ${this.config.fieldWrapperValue};`;
        }
        return '';
    }
    getEntireFieldWrapperValue() {
        if (this.config.entireFieldWrapperValue) {
            return `${this.getExportPrefix()}type EntireFieldWrapper<T> = ${this.config.entireFieldWrapperValue};`;
        }
        return '';
    }
    getScalarsImports() {
        return Object.keys(this.config.scalars).reduce((res, enumName) => {
            const mappedValue = this.config.scalars[enumName];
            if (mappedValue.input.isExternal) {
                res.push(this._buildTypeImport(mappedValue.input.import, mappedValue.input.source, mappedValue.input.default));
            }
            if (mappedValue.output.isExternal) {
                res.push(this._buildTypeImport(mappedValue.output.import, mappedValue.output.source, mappedValue.output.default));
            }
            return res;
        }, []);
    }
    getDirectiveArgumentAndInputFieldMappingsImports() {
        return Object.keys(this.config.directiveArgumentAndInputFieldMappings)
            .map(directive => {
            const mappedValue = this.config.directiveArgumentAndInputFieldMappings[directive];
            if (mappedValue.isExternal) {
                return this._buildTypeImport(mappedValue.import, mappedValue.source, mappedValue.default);
            }
            return null;
        })
            .filter(a => a);
    }
    get scalarsDefinition() {
        if (this.config.onlyEnums)
            return '';
        const allScalars = Object.keys(this.config.scalars).map(scalarName => {
            const inputScalarValue = this.config.scalars[scalarName].input.type;
            const outputScalarValue = this.config.scalars[scalarName].output.type;
            const scalarType = this._schema.getType(scalarName);
            const comment = scalarType?.astNode && scalarType.description ? (0, utils_js_1.transformComment)(scalarType.description, 1) : '';
            const { scalar } = this._parsedConfig.declarationKind;
            return (comment +
                (0, utils_js_1.indent)(`${scalarName}: { input: ${inputScalarValue}${this.getPunctuation(scalar)} output: ${outputScalarValue}${this.getPunctuation(scalar)} }`));
        });
        return new utils_js_1.DeclarationBlock(this._declarationBlockConfig)
            .export()
            .asKind(this._parsedConfig.declarationKind.scalar)
            .withName('Scalars')
            .withComment('All built-in and custom scalars, mapped to their actual values')
            .withBlock(allScalars.join('\n')).string;
    }
    get directiveArgumentAndInputFieldMappingsDefinition() {
        const directiveEntries = Object.entries(this.config.directiveArgumentAndInputFieldMappings);
        if (directiveEntries.length === 0) {
            return '';
        }
        const allDirectives = [];
        for (const [directiveName, parsedMapper] of directiveEntries) {
            const directiveType = this._schema.getDirective(directiveName);
            const comment = directiveType?.astNode && directiveType.description ? (0, utils_js_1.transformComment)(directiveType.description, 1) : '';
            const { directive } = this._parsedConfig.declarationKind;
            allDirectives.push(comment + (0, utils_js_1.indent)(`${directiveName}: ${parsedMapper.type}${this.getPunctuation(directive)}`));
        }
        return new utils_js_1.DeclarationBlock(this._declarationBlockConfig)
            .export()
            .asKind(this._parsedConfig.declarationKind.directive)
            .withName('DirectiveArgumentAndInputFieldMappings')
            .withComment('Type overrides using directives')
            .withBlock(allDirectives.join('\n')).string;
    }
    setDeclarationBlockConfig(config) {
        this._declarationBlockConfig = config;
    }
    setArgumentsTransformer(argumentsTransfomer) {
        this._argumentsTransformer = argumentsTransfomer;
    }
    NonNullType(node) {
        const asString = node.type;
        return asString;
    }
    getInputObjectDeclarationBlock(node) {
        return new utils_js_1.DeclarationBlock(this._declarationBlockConfig)
            .export()
            .asKind(this._parsedConfig.declarationKind.input)
            .withName(this.convertName(node))
            .withComment(node.description)
            .withBlock(node.fields.join('\n'));
    }
    getInputObjectOneOfDeclarationBlock(node) {
        // As multiple fields always result in a union, we have
        // to force a declaration kind of `type` in this case
        const declarationKind = node.fields.length === 1 ? this._parsedConfig.declarationKind.input : 'type';
        return new utils_js_1.DeclarationBlock(this._declarationBlockConfig)
            .export()
            .asKind(declarationKind)
            .withName(this.convertName(node))
            .withComment(node.description)
            .withContent(`\n` + node.fields.join('\n  |'));
    }
    InputObjectTypeDefinition(node) {
        if (this.config.onlyEnums)
            return '';
        // Why the heck is node.name a string and not { value: string } at runtime ?!
        if ((0, utils_js_1.isOneOfInputObjectType)(this._schema.getType(node.name))) {
            return this.getInputObjectOneOfDeclarationBlock(node).string;
        }
        return this.getInputObjectDeclarationBlock(node).string;
    }
    InputValueDefinition(node) {
        if (this.config.onlyEnums)
            return '';
        const comment = (0, utils_js_1.transformComment)(node.description, 1);
        const { input } = this._parsedConfig.declarationKind;
        let type = node.type;
        if (node.directives && this.config.directiveArgumentAndInputFieldMappings) {
            type = this._getDirectiveOverrideType(node.directives) || type;
        }
        return comment + (0, utils_js_1.indent)(`${node.name}: ${type}${this.getPunctuation(input)}`);
    }
    Name(node) {
        return node.value;
    }
    FieldDefinition(node) {
        if (this.config.onlyEnums)
            return '';
        const typeString = node.type;
        const { type } = this._parsedConfig.declarationKind;
        const comment = this.getNodeComment(node);
        return comment + (0, utils_js_1.indent)(`${node.name}: ${typeString}${this.getPunctuation(type)}`);
    }
    UnionTypeDefinition(node, key, parent) {
        if (this.config.onlyOperationTypes || this.config.onlyEnums)
            return '';
        const originalNode = parent[key];
        const possibleTypes = originalNode.types
            .map(t => (this.scalars[t.name.value] ? this._getScalar(t.name.value, 'output') : this.convertName(t)))
            .join(' | ');
        return new utils_js_1.DeclarationBlock(this._declarationBlockConfig)
            .export()
            .asKind('type')
            .withName(this.convertName(node))
            .withComment(node.description)
            .withContent(possibleTypes).string;
    }
    mergeInterfaces(interfaces, hasOtherFields) {
        return interfaces.join(' & ') + (interfaces.length && hasOtherFields ? ' & ' : '');
    }
    appendInterfacesAndFieldsToBlock(block, interfaces, fields) {
        block.withContent(this.mergeInterfaces(interfaces, fields.length > 0));
        block.withBlock(this.mergeAllFields(fields, interfaces.length > 0));
    }
    getObjectTypeDeclarationBlock(node, originalNode) {
        const optionalTypename = this.config.nonOptionalTypename ? '__typename' : '__typename?';
        const { type, interface: interfacesType } = this._parsedConfig.declarationKind;
        const allFields = [
            ...(this.config.addTypename
                ? [
                    (0, utils_js_1.indent)(`${this.config.immutableTypes ? 'readonly ' : ''}${optionalTypename}: '${node.name}'${this.getPunctuation(type)}`),
                ]
                : []),
            ...node.fields,
        ];
        const interfacesNames = originalNode.interfaces ? originalNode.interfaces.map(i => this.convertName(i)) : [];
        const declarationBlock = new utils_js_1.DeclarationBlock(this._declarationBlockConfig)
            .export()
            .asKind(type)
            .withName(this.convertName(node))
            .withComment(node.description);
        if (type === 'interface' || type === 'class') {
            if (interfacesNames.length > 0) {
                const keyword = interfacesType === 'interface' && type === 'class' ? 'implements' : 'extends';
                declarationBlock.withContent(`${keyword} ` + interfacesNames.join(', ') + (allFields.length > 0 ? ' ' : ' {}'));
            }
            declarationBlock.withBlock(this.mergeAllFields(allFields, false));
        }
        else {
            this.appendInterfacesAndFieldsToBlock(declarationBlock, interfacesNames, allFields);
        }
        return declarationBlock;
    }
    mergeAllFields(allFields, _hasInterfaces) {
        return allFields.join('\n');
    }
    ObjectTypeDefinition(node, key, parent) {
        if (this.config.onlyOperationTypes || this.config.onlyEnums)
            return '';
        const originalNode = parent[key];
        return [this.getObjectTypeDeclarationBlock(node, originalNode).string, this.buildArgumentsBlock(originalNode)]
            .filter(f => f)
            .join('\n\n');
    }
    getInterfaceTypeDeclarationBlock(node, _originalNode) {
        const declarationBlock = new utils_js_1.DeclarationBlock(this._declarationBlockConfig)
            .export()
            .asKind(this._parsedConfig.declarationKind.interface)
            .withName(this.convertName(node))
            .withComment(node.description);
        return declarationBlock.withBlock(node.fields.join('\n'));
    }
    InterfaceTypeDefinition(node, key, parent) {
        if (this.config.onlyOperationTypes || this.config.onlyEnums)
            return '';
        const originalNode = parent[key];
        return [this.getInterfaceTypeDeclarationBlock(node, originalNode).string, this.buildArgumentsBlock(originalNode)]
            .filter(f => f)
            .join('\n\n');
    }
    ScalarTypeDefinition(_node) {
        // We empty this because we handle scalars in a different way, see constructor.
        return '';
    }
    _buildTypeImport(identifier, source, asDefault = false) {
        const { useTypeImports } = this.config;
        if (asDefault) {
            if (useTypeImports) {
                return `import type { default as ${identifier} } from '${source}';`;
            }
            return `import ${identifier} from '${source}';`;
        }
        return `import${useTypeImports ? ' type' : ''} { ${identifier} } from '${source}';`;
    }
    handleEnumValueMapper(typeIdentifier, importIdentifier, sourceIdentifier, sourceFile) {
        if (importIdentifier !== sourceIdentifier) {
            // use namespace import to dereference nested enum
            // { enumValues: { MyEnum: './my-file#NS.NestedEnum' } }
            return [
                this._buildTypeImport(importIdentifier || sourceIdentifier, sourceFile),
                `import ${typeIdentifier} = ${sourceIdentifier};`,
            ];
        }
        if (sourceIdentifier !== typeIdentifier) {
            return [this._buildTypeImport(`${sourceIdentifier} as ${typeIdentifier}`, sourceFile)];
        }
        return [this._buildTypeImport(importIdentifier || sourceIdentifier, sourceFile)];
    }
    getEnumsImports() {
        return Object.keys(this.config.enumValues)
            .flatMap(enumName => {
            const mappedValue = this.config.enumValues[enumName];
            if (mappedValue.sourceFile) {
                if (mappedValue.isDefault) {
                    return [this._buildTypeImport(mappedValue.typeIdentifier, mappedValue.sourceFile, true)];
                }
                return this.handleEnumValueMapper(mappedValue.typeIdentifier, mappedValue.importIdentifier, mappedValue.sourceIdentifier, mappedValue.sourceFile);
            }
            return [];
        })
            .filter(Boolean);
    }
    EnumTypeDefinition(node) {
        const enumName = node.name;
        // In case of mapped external enum string
        if (this.config.enumValues[enumName]?.sourceFile) {
            return null;
        }
        return new utils_js_1.DeclarationBlock(this._declarationBlockConfig)
            .export()
            .asKind('enum')
            .withName(this.convertName(node, {
            useTypesPrefix: this.config.enumPrefix,
            useTypesSuffix: this.config.enumSuffix,
        }))
            .withComment(node.description)
            .withBlock(this.buildEnumValuesBlock(enumName, node.values)).string;
    }
    // We are using it in order to transform "description" field
    StringValue(node) {
        return node.value;
    }
    makeValidEnumIdentifier(identifier) {
        if (/^[0-9]/.exec(identifier)) {
            return (0, utils_js_1.wrapWithSingleQuotes)(identifier, true);
        }
        return identifier;
    }
    buildEnumValuesBlock(typeName, values) {
        const schemaEnumType = this._schema
            ? this._schema.getType(typeName)
            : undefined;
        return values
            .map(enumOption => {
            const optionName = this.makeValidEnumIdentifier(this.convertName(enumOption, {
                useTypesPrefix: false,
                transformUnderscore: true,
            }));
            const comment = this.getNodeComment(enumOption);
            const schemaEnumValue = schemaEnumType && !this.config.ignoreEnumValuesFromSchema
                ? schemaEnumType.getValue(enumOption.name).value
                : undefined;
            let enumValue = typeof schemaEnumValue === 'undefined' ? enumOption.name : schemaEnumValue;
            if (this.config.enumValues[typeName]?.mappedValues &&
                typeof this.config.enumValues[typeName].mappedValues[enumValue] !== 'undefined') {
                enumValue = this.config.enumValues[typeName].mappedValues[enumValue];
            }
            return (comment +
                (0, utils_js_1.indent)(`${optionName}${this._declarationBlockConfig.enumNameValueSeparator} ${(0, utils_js_1.wrapWithSingleQuotes)(enumValue, typeof schemaEnumValue !== 'undefined')}`));
        })
            .join(',\n');
    }
    DirectiveDefinition(_node) {
        return '';
    }
    getArgumentsObjectDeclarationBlock(node, name, field) {
        return new utils_js_1.DeclarationBlock(this._declarationBlockConfig)
            .export()
            .asKind(this._parsedConfig.declarationKind.arguments)
            .withName(this.convertName(name))
            .withComment(node.description)
            .withBlock(this._argumentsTransformer.transform(field.arguments));
    }
    getArgumentsObjectTypeDefinition(node, name, field) {
        if (this.config.onlyEnums)
            return '';
        return this.getArgumentsObjectDeclarationBlock(node, name, field).string;
    }
    buildArgumentsBlock(node) {
        const fieldsWithArguments = node.fields.filter(field => field.arguments && field.arguments.length > 0) || [];
        return fieldsWithArguments
            .map(field => {
            const name = node.name.value +
                (this.config.addUnderscoreToArgsType ? '_' : '') +
                this.convertName(field, {
                    useTypesPrefix: false,
                    useTypesSuffix: false,
                }) +
                'Args';
            return this.getArgumentsObjectTypeDefinition(node, name, field);
        })
            .join('\n\n');
    }
    _getScalar(name, type) {
        return `Scalars['${name}']['${type}']`;
    }
    _getDirectiveArgumentNadInputFieldMapping(name) {
        return `DirectiveArgumentAndInputFieldMappings['${name}']`;
    }
    _getDirectiveOverrideType(directives) {
        const type = directives
            .map(directive => {
            const directiveName = directive.name;
            if (this.config.directiveArgumentAndInputFieldMappings[directiveName]) {
                return this._getDirectiveArgumentNadInputFieldMapping(directiveName);
            }
            return null;
        })
            .reverse()
            .find(a => !!a);
        return type || null;
    }
    _getTypeForNode(node, isVisitingInputType) {
        const typeAsString = node.name;
        if (this.scalars[typeAsString]) {
            return this._getScalar(typeAsString, isVisitingInputType ? 'input' : 'output');
        }
        if (this.config.enumValues[typeAsString]) {
            return this.config.enumValues[typeAsString].typeIdentifier;
        }
        const schemaType = this._schema.getType(node.name);
        if (schemaType && (0, graphql_1.isEnumType)(schemaType)) {
            return this.convertName(node, {
                useTypesPrefix: this.config.enumPrefix,
                useTypesSuffix: this.config.enumSuffix,
            });
        }
        return this.convertName(node);
    }
    NamedType(node, key, parent, path, ancestors) {
        const currentVisitContext = this.getVisitorKindContextFromAncestors(ancestors);
        const isVisitingInputType = currentVisitContext.includes(graphql_1.Kind.INPUT_OBJECT_TYPE_DEFINITION);
        const typeToUse = this._getTypeForNode(node, isVisitingInputType);
        if (!isVisitingInputType && this.config.fieldWrapperValue && this.config.wrapFieldDefinitions) {
            return `FieldWrapper<${typeToUse}>`;
        }
        return typeToUse;
    }
    ListType(node, _key, _parent, _path, _ancestors) {
        const asString = node.type;
        return this.wrapWithListType(asString);
    }
    SchemaDefinition() {
        return null;
    }
    getNodeComment(node) {
        let commentText = node.description;
        const deprecationDirective = node.directives.find((v) => v.name === 'deprecated');
        if (deprecationDirective) {
            const deprecationReason = this.getDeprecationReason(deprecationDirective);
            commentText = `${commentText ? `${commentText}\n` : ''}@deprecated ${deprecationReason}`;
        }
        const comment = (0, utils_js_1.transformComment)(commentText, 1);
        return comment;
    }
    getDeprecationReason(directive) {
        if (directive.name === 'deprecated') {
            const hasArguments = directive.arguments.length > 0;
            let reason = 'Field no longer supported';
            if (hasArguments) {
                reason = directive.arguments[0].value;
            }
            return reason;
        }
    }
    wrapWithListType(str) {
        return `Array<${str}>`;
    }
}
exports.BaseTypesVisitor = BaseTypesVisitor;
