import { GraphQLBoolean, GraphQLFloat, GraphQLInt, GraphQLList, GraphQLNonNull, GraphQLObjectType, GraphQLString, GraphQLEnumType, GraphQLInputObjectType, GraphQLScalarType } from 'graphql';
// import GraphQLUpload from 'graphql-upload/GraphQLUpload.js';
import GraphQLUpload from 'graphql-upload/GraphQLUpload.mjs';
import { capitalizeProtoName } from './utils.js';
import { authSubjectType } from './types.js';
import { FieldDescriptorProto_Label, FieldDescriptorProto_Type } from 'ts-proto-descriptors';
export const registeredTypings = new Map();
export const scalarTypes = ['Boolean', 'Int', 'Float', 'String', 'ID', 'Upload'];
const typeNameAndNameSpaceMapping = new Map();
const registeredEnumTypes = [];
const MapScalar = new GraphQLScalarType({
    name: 'MapScalar',
});
const GoogleProtobufAnyValue = new GraphQLScalarType({
    name: 'GoogleProtobufAnyValue'
});
const protobufAnyFields = {
    typeUrl: {
        type: GraphQLString
    },
    value: {
        type: GoogleProtobufAnyValue
    }
};
const GoogleProtobufAny = new GraphQLObjectType({
    name: 'GoogleProtobufAny',
    fields: protobufAnyFields
});
export const IGoogleProtobufAny = new GraphQLInputObjectType({
    name: 'IGoogleProtobufAny',
    fields: protobufAnyFields
});
const DateTime = new GraphQLScalarType({
    name: 'DateTime',
    description: `A date-time string at UTC, such as 2007-12-03T10:15:30Z,
                compliant with the date-time format outlined in section 5.6 of
                the RFC 3339 profile of the ISO 8601 standard for representation
                of dates and times using the Gregorian calendar.`,
});
const IDateTime = new GraphQLScalarType({
    name: 'IDateTime',
    description: `A date-time string at UTC, such as 2007-12-03T10:15:30Z,
                compliant with the date-time format outlined in section 5.6 of
                the RFC 3339 profile of the ISO 8601 standard for representation
                of dates and times using the Gregorian calendar.`,
});
const googleProtobufAnyName = '.google.protobuf.Any';
const googleProtobufTimestampName = '.google.protobuf.Timestamp';
const Mutate = ['Create', 'Update', 'Upsert'];
const CRUD_TRAVERSAL_OP_NAMES = ['Cretae', 'Update', 'Upsert', 'Delete', 'Read', 'Traversal'];
const TodoScalar = new GraphQLScalarType({
    name: 'TodoScalar',
    serialize: () => {
        throw Error('Not Implemented!');
    },
    parseValue: () => {
        throw Error('Not Implemented!');
    }
});
export const clearRegistry = () => {
    registeredTypings.clear();
};
export const getRegisteredEnumTypings = () => {
    return registeredEnumTypes;
};
export const getNameSpaceTypeName = (typeName) => {
    return typeNameAndNameSpaceMapping.get(typeName);
};
export const getTyping = (type) => {
    return registeredTypings.get(type);
};
// Iterate through the object and collect list of all enum types with their keys / paths
export const recursiveEnumCheck = (typeName, enumMap, prevFieldName, traversedFields) => {
    if (!scalarTypes.includes(typeName)) {
        if (typeName && typeName.startsWith('[') && typeName.endsWith('!]')) {
            typeName = typeName.substring(1, typeName.length - 2);
        }
        const objectNameSpace = getNameSpaceTypeName(typeName);
        if (objectNameSpace) {
            const objectType = getTyping(objectNameSpace);
            if (objectType?.input && (registeredEnumTypes.indexOf(objectType.input.toString()) > -1)) {
                enumMap.set(objectType?.input.toString(), prevFieldName);
                prevFieldName = '';
            }
            else if (objectType?.input) {
                // get nested fields from this object and check recursively
                const gqlFields = objectType.input.getFields();
                if (gqlFields) {
                    const fieldNames = Object.keys(gqlFields);
                    for (let fieldName of fieldNames) {
                        let fieldType = gqlFields[fieldName].type.toString();
                        // if fieldType is not basic type, get the object and make recursive check till no more objects are found
                        if (scalarTypes.indexOf(fieldType) <= -1) {
                            // check if fieldName already exists in the traversedFields (to avoid circular reference for infinite loop)
                            if (traversedFields.indexOf(fieldName) <= -1) {
                                traversedFields.push(fieldName);
                            }
                            else if (traversedFields.indexOf(fieldName) > -1) {
                                // skip loop as this GQL type is already traversed
                                continue;
                            }
                            recursiveEnumCheck(fieldType, enumMap, fieldName, traversedFields);
                        }
                    }
                }
            }
        }
    }
    return enumMap;
};
const resolveMeta = (key, field, rootObjType, objName, input) => {
    let result;
    switch (field.type) {
        case FieldDescriptorProto_Type.TYPE_BOOL:
            result = GraphQLBoolean;
            break;
        case FieldDescriptorProto_Type.TYPE_STRING:
            result = GraphQLString;
            break;
        case FieldDescriptorProto_Type.TYPE_ENUM:
        case FieldDescriptorProto_Type.TYPE_MESSAGE:
            const objType = field.typeName;
            if (objType === googleProtobufAnyName) {
                if (input) {
                    result = IGoogleProtobufAny;
                    break;
                }
                result = GoogleProtobufAny;
                break;
            }
            if (objType === googleProtobufTimestampName) {
                if (input) {
                    result = IDateTime;
                    break;
                }
                result = DateTime;
                break;
            }
            if (!registeredTypings.has(objType)) {
                throw new Error('Typing \'' + objType + '\' not registered for key \'' + key + '\' in object: ' + objName);
            }
            let typingData = registeredTypings.get(objType);
            let mapEntry = false;
            if (typingData.meta.options) {
                mapEntry = !!typingData.meta.options?.mapEntry;
            }
            // TODO Actually unroll maps into entries
            if (mapEntry) {
                return MapScalar;
            }
            if (!input) {
                result = typingData.output;
                break;
            }
            if (objType === authSubjectType) {
                return null;
            }
            result = typingData.input;
            break;
        case FieldDescriptorProto_Type.TYPE_BYTES:
            if (input) {
                // TODO Why can't it be nullable?
                result = GraphQLUpload;
                break;
            }
            // TODO Output Buffer
            result = TodoScalar;
            break;
        case FieldDescriptorProto_Type.TYPE_INT32:
        case FieldDescriptorProto_Type.TYPE_UINT32:
        case FieldDescriptorProto_Type.TYPE_INT64:
        case FieldDescriptorProto_Type.TYPE_UINT64:
        case FieldDescriptorProto_Type.TYPE_SINT32:
        case FieldDescriptorProto_Type.TYPE_SINT64:
            result = GraphQLInt;
            break;
        case FieldDescriptorProto_Type.TYPE_DOUBLE:
        case FieldDescriptorProto_Type.TYPE_FLOAT:
        case FieldDescriptorProto_Type.TYPE_FIXED64:
        case FieldDescriptorProto_Type.TYPE_FIXED32:
        case FieldDescriptorProto_Type.TYPE_SFIXED32:
        case FieldDescriptorProto_Type.TYPE_SFIXED64:
            result = GraphQLFloat;
            break;
        default:
            throw new Error('unknown typing type \'' + field.type + '\' for key \'' + key + '\' in: ' + objName);
    }
    if (field.label === FieldDescriptorProto_Label.LABEL_REPEATED) {
        result = new GraphQLList(new GraphQLNonNull(result));
    }
    if (field.label === FieldDescriptorProto_Label.LABEL_REQUIRED) {
        result = new GraphQLNonNull(result);
    }
    return result;
};
const ModeType = new GraphQLEnumType({
    name: 'ModeType',
    values: {
        CREATE: {
            value: 'CREATE'
        },
        UPDATE: {
            value: 'UPDATE'
        },
        UPSERT: {
            value: 'UPSERT'
        },
    }
});
export const registerTyping = (protoPackage, message, methodDef, opts, inputOpts, messageOptions) => {
    let insertMode = false;
    let crudOperation = false;
    const type = (protoPackage.startsWith('.') ? '' : '.') + protoPackage + '.' + message.name;
    if (methodDef && methodDef.length > 0) {
        for (let method of methodDef) {
            // if method def is Create, Read / Upsert and input types of method and message type are equal
            // then update input type to include `mode` parameter
            if ((Mutate.indexOf(method.name) > -1) && type === method.inputType) {
                insertMode = true;
            }
            // add scope
            if ((CRUD_TRAVERSAL_OP_NAMES.indexOf(method.name) > -1) && type === method.inputType) {
                crudOperation = true;
            }
        }
    }
    if (type === googleProtobufAnyName) {
        // Do not register any type
        typeNameAndNameSpaceMapping.set(GoogleProtobufAny.name, type);
        registeredTypings.set(type, {
            output: GoogleProtobufAny,
            input: IGoogleProtobufAny,
            meta: message
        });
    }
    if (type === googleProtobufTimestampName) {
        typeNameAndNameSpaceMapping.set(DateTime.name, type);
        registeredTypings.set(type, {
            output: DateTime,
            input: IDateTime,
            meta: message
        });
    }
    if (registeredTypings.has(type)) {
        // TODO Log debug "Typings for object are already registered"
        return;
    }
    const name = opts?.name || capitalizeProtoName(type);
    const inputName = inputOpts?.name || 'I' + name;
    const fields = () => {
        const result = {};
        message.field?.forEach(field => {
            // TODO Union types
            const resolvedMeta = resolveMeta(field.jsonName, field, type, name, false);
            if (resolvedMeta !== null) {
                result[field.jsonName] = {
                    type: resolvedMeta
                };
            }
            if (messageOptions && messageOptions.fields) {
                if (field.name in messageOptions.fields) {
                    const data = messageOptions.fields[field.name].resolver;
                    const resolved = resolveMeta(data.fieldName, {
                        type: FieldDescriptorProto_Type.TYPE_MESSAGE,
                        label: field.label,
                        typeName: data.targetType
                    }, type, name, false);
                    if (resolvedMeta !== null) {
                        result[data.fieldName] = {
                            type: resolved
                        };
                    }
                }
            }
        });
        return result;
    };
    const inputFields = () => {
        const result = {};
        message.field?.forEach(field => {
            // TODO Union types
            const resolvedMeta = resolveMeta(field.jsonName, field, type, name, true);
            if (resolvedMeta !== null) {
                result[field.jsonName] = {
                    type: resolvedMeta
                };
            }
        });
        if (insertMode) {
            result['mode'] = {
                type: ModeType
            };
        }
        if (crudOperation) {
            // add scope to all mutations / queries
            if (!result.scope) {
                result['scope'] = {
                    description: 'target scope',
                    type: GraphQLString
                };
            }
        }
        return result;
    };
    const resultObj = new GraphQLObjectType({
        ...(opts || { name }),
        fields,
    });
    const resultInputObj = new GraphQLInputObjectType({
        ...(inputOpts || { name: inputName }),
        fields: inputFields,
    });
    typeNameAndNameSpaceMapping.set(resultInputObj.name, type);
    registeredTypings.set(type, {
        output: resultObj,
        input: resultInputObj,
        meta: message
    });
};
export const registerEnumTyping = (protoPackage, message, opts) => {
    const type = (protoPackage.startsWith('.') ? '' : '.') + protoPackage + '.' + message.name;
    if (registeredTypings.has(type)) {
        // TODO Log debug "Typings for enum are already registered"
        return;
    }
    const values = {};
    message.value?.forEach(entry => {
        values[entry.name] = {
            value: entry.number
        };
    });
    const name = opts?.name || capitalizeProtoName(type);
    const result = new GraphQLEnumType({
        ...(opts || { name }),
        values
    });
    registeredEnumTypes.push(name);
    typeNameAndNameSpaceMapping.set(name, type);
    registeredTypings.set(type, {
        output: result,
        input: result,
        meta: message
    });
};
const registerMessageTypesRecursive = (packageName, methodDef, options, ...types) => {
    types.forEach(m => {
        registerTyping(packageName, m, methodDef, undefined, undefined, options && options.messages && options.messages[m.name]);
        if (m.enumType) {
            m.enumType.forEach((enumType) => {
                registerEnumTyping(packageName + '.' + m.name, enumType);
            });
        }
        if (m.nestedType) {
            registerMessageTypesRecursive(packageName + '.' + m.name, methodDef, options, ...m.nestedType);
        }
    });
};
export const registerPackagesRecursive = (...protoMetadata) => {
    protoMetadata.forEach(meta => {
        meta.dependencies && registerPackagesRecursive(...meta.dependencies);
        meta.fileDescriptor.messageType && registerMessageTypesRecursive(meta.fileDescriptor.package, meta.fileDescriptor.service[0]?.method, meta.options, ...meta.fileDescriptor.messageType);
        meta.fileDescriptor.enumType?.forEach((m) => {
            registerEnumTyping(meta.fileDescriptor.package, m);
        });
        Object.keys(meta.references).forEach(key => {
            registeredTypings.get(key).processor = meta.references[key];
        });
    });
};
//# sourceMappingURL=registry.js.map