import { reflector, Class } from '@ditsmod/core';
import { REQUIRED } from '#constants';
import { isProperty } from './type-guards.js';
export class Content {
    content = {};
    scanInProgress = new WeakSet();
    standartTypes = [Boolean, Number, String, Array, Object];
    standartTypesStrings = ['boolean', 'number', 'string', 'array', 'object'];
    /**
     * Sets media type.
     */
    set(contentOptions) {
        const { mediaType, mediaTypeParams, model } = contentOptions;
        let schema = {};
        if (mediaType.includes('text/')) {
            schema = { type: 'string' };
        }
        else {
            if (model) {
                schema = this.getSchema(model);
            }
        }
        const params = mediaTypeParams ? `;${mediaTypeParams}` : '';
        this.content[`${mediaType}${params}`] = { schema, encoding: contentOptions.encoding };
        return this;
    }
    get(contentOptions) {
        if (contentOptions) {
            this.set(contentOptions);
        }
        return { ...this.content };
    }
    getSchema(model) {
        const schema = this.getSchemaStubForModel(model);
        const modelMeta = reflector.getMetadata(model);
        for (const property in modelMeta) {
            const propertyMeta = modelMeta[property].decorators.find(isProperty);
            if (propertyMeta && (!propertyMeta.value.schema?.type || propertyMeta.value.schema.type == 'array')) {
                const propertyType = modelMeta[property].type;
                this.checkTypeDefinitionConflict(model.name, property, propertyType, schema.type, propertyMeta.value.customType);
                if (!schema.properties) {
                    schema.properties = {};
                }
                this.setRequiredProperties(schema, property, propertyMeta.value.schema);
                schema.properties[property] = this.fillPropertySchema(model, propertyType, propertyMeta.value.schema, propertyMeta.value.customType);
            }
        }
        this.scanInProgress.delete(model);
        return schema;
    }
    /**
     * Sets minimum properties for the model's schema:
     * - `type` if it's standart model (Boolean, Number, String, Array, Object);
     * - init empty `items` and `properties` if it's suitable.
     */
    getSchemaStubForModel(model) {
        const schema = {};
        if (this.standartTypes.includes(model)) {
            schema.type = (model.name?.toLowerCase() || 'null');
            if (schema.type == 'array' && !schema.items) {
                schema.items = {};
            }
        }
        else if (model instanceof Class) {
            schema.type = 'object';
            schema.properties = {};
        }
        return schema;
    }
    checkTypeDefinitionConflict(modelName, property, propertyType, type, customType) {
        if ((type == 'array' || customType?.array) && propertyType.name != 'Array') {
            const msg = `Wrong definition for ${modelName}: property '${property}' is an array or not array?`;
            throw new TypeError(msg);
        }
        if ((propertyType.name == 'Array' || customType?.array) && customType?.enum) {
            const msg = `Wrong definition for ${modelName}: property '${property}' is an enum or an array?`;
            throw new TypeError(msg);
        }
    }
    setRequiredProperties(parentSchema, propertyName, schema) {
        if (schema?.[REQUIRED]) {
            if (!parentSchema.required) {
                parentSchema.required = [propertyName];
            }
            else {
                parentSchema.required.push(propertyName);
            }
        }
    }
    fillPropertySchema(model, propertyType, originPropertySchema, customType) {
        const propertySchema = { ...originPropertySchema };
        if (customType?.enum) {
            this.fillEnum(propertySchema, customType.enum);
        }
        else if (this.standartTypes.includes(propertyType)) {
            propertySchema.type = (propertyType.name?.toLowerCase() || 'null');
        }
        else if (propertyType instanceof Class) {
            if (this.scanInProgress.has(model)) {
                propertySchema.type = 'object';
                propertySchema.description = `[Circular references to ${model.name}]`;
                propertySchema.properties = {};
            }
            else {
                this.scanInProgress.add(model);
                Object.assign(propertySchema, this.getSchema(propertyType));
            }
        }
        else {
            propertySchema.type = 'null';
        }
        if (propertySchema.type == 'array' && !propertySchema.items) {
            this.fillItems(model, propertySchema, customType);
        }
        return propertySchema;
    }
    /**
     * @todo Refactor this.
     */
    fillItems(model, propertySchema, customType) {
        if (customType?.array) {
            if (Array.isArray(customType.array) && customType.array.every((item) => Array.isArray(item))) {
                customType.array.forEach((item) => {
                    const modelNames = item.map((customItem) => customItem.name.toLowerCase());
                    if (modelNames.every((type) => this.standartTypesStrings.includes(type))) {
                        let type;
                        if (modelNames.length == 1) {
                            type = modelNames[0];
                        }
                        else {
                            type = modelNames;
                        }
                        propertySchema.items = { type: 'array', items: { type } };
                    }
                    else {
                        propertySchema.items = { type: 'array', items: [] };
                        item.forEach((customItem) => {
                            this.checkCircularRefAndFillItems(model.name, propertySchema, customItem, 'object');
                        });
                    }
                });
            }
            else if (Array.isArray(customType.array)) {
                const modelNames = customType.array.map((customItem) => customItem.name.toLowerCase());
                if (modelNames.every((type) => this.standartTypesStrings.includes(type))) {
                    let type;
                    if (modelNames.length == 1) {
                        type = modelNames[0];
                    }
                    else {
                        type = modelNames;
                    }
                    propertySchema.items = { type };
                }
                else {
                    propertySchema.items = [];
                    customType.array.forEach((customItem) => {
                        this.checkCircularRefAndFillItems(model.name, propertySchema, customItem, 'object');
                    });
                }
            }
            else {
                this.checkCircularRefAndFillItems(model.name, propertySchema, customType.array, 'array');
            }
        }
        else {
            propertySchema.items = [];
        }
    }
    /**
     * @todo Refactor this.
     */
    checkCircularRefAndFillItems(modelName, propertySchema, customItem, whatIs) {
        let description = '';
        let schema;
        if (this.scanInProgress.has(customItem)) {
            description = `[Circular references to ${modelName}]`;
        }
        else {
            this.scanInProgress.add(customItem);
            schema = this.getSchema(customItem);
        }
        if (whatIs == 'object') {
            const items = Array.isArray(propertySchema.items)
                ? propertySchema.items
                : propertySchema.items?.items;
            if (description) {
                items.push({
                    type: 'object',
                    properties: {},
                    description,
                });
            }
            else {
                items.push(schema);
            }
        }
        else {
            if (description) {
                propertySchema.description = description;
                propertySchema.items = {};
            }
            else {
                propertySchema.items = schema;
            }
        }
    }
    fillEnum(schema, enumModel) {
        const enums = Array.isArray(enumModel) ? enumModel : [enumModel];
        const typesSet = new Set();
        const value = [];
        enums.forEach((enm) => {
            Object.keys(enm).forEach((key) => {
                // An enum member cannot have a numeric name
                if (isNaN(+key)) {
                    value.push(enm[key]);
                    typesSet.add(typeof enm[key]);
                }
            });
        });
        let type;
        if (typesSet.size == 1) {
            type = [...typesSet][0];
        }
        else if (typesSet.size > 1) {
            type = [...typesSet];
        }
        else {
            type = 'string';
        }
        schema.type = type;
        schema.enum = value;
    }
}
export function getContent(contentOptions) {
    return new Content().get(contentOptions);
}
//# sourceMappingURL=content.js.map