import difference from 'lodash/difference.js';
import forEach from 'lodash/forEach.js';
import get from 'lodash/get.js';
import pick from 'lodash/pick.js';
import { dereferenceSchema, getRefShapeName } from "./refs.js";
import { getStorageKey } from "./schema-util.js";
import { isArraySchema, isObjectSchema } from "./types/utils.js";
import { enumerateOneOfKeys, isUnionSchema } from "./unions.js";
const typeCache = new WeakMap();
export function getType(schema) {
    let type = typeCache.get(schema);
    if (type) {
        return type;
    }
    type = schema.type;
    // Handle nullable types for example: ['object', 'null']
    if (Array.isArray(type)) {
        const filtered = type.filter((name) => name !== 'null');
        if (!filtered.length) {
            throw new Error('Invalid schema');
        }
        type = filtered[0];
    }
    typeCache.set(schema, type);
    return type;
}
function ensureArray(value) {
    return Array.isArray(value) ? value : [value];
}
export function preferKey(name, schema) {
    return getStorageKey(schema) ?? name;
}
/**
 * A key transform to select the field name
 */
export const preferName = (name) => name;
function getLocalizedKey(key, locale) {
    return locale ? `${key}:${locale}` : key;
}
function getNonDefaultLocales(projectSchema) {
    return projectSchema.locales.filter((locale) => locale !== projectSchema.defaultLocale);
}
function getLocalizedSourceKeys(keys, locales) {
    return keys.flatMap((key) => [{ key }, ...locales.map((locale) => ({ key: `${key}:${locale}`, locale }))]);
}
export function preferSourceKey(name, schema) {
    const key = getStorageKey(schema);
    return key ? [key, name] : [name];
}
export function findTransforms(pluginMap, schema, name, shape) {
    const type = getType(schema);
    if (pluginMap && (pluginMap[type] ?? pluginMap['*'])) {
        const plugins = (pluginMap[type] ?? []).concat(pluginMap['*'] ?? []);
        return plugins.find((plugin) => plugin.filter(schema, name, shape));
    }
    return undefined;
}
export function getContentTransform({ transforms, accumulators, keyTransform = preferKey, sourceKeyTransform = preferSourceKey, passThrough = true, allowNullObjects = false, projectSchema }) {
    const nonDefaultLocales = getNonDefaultLocales(projectSchema);
    const prepare = (schema, obj, name = '', parent) => {
        const shapeName = getRefShapeName(projectSchema, schema);
        schema = dereferenceSchema(projectSchema, schema);
        if (accumulators) {
            for (const accumulator of accumulators) {
                accumulator.accumulate(obj, schema, name);
            }
        }
        const shape = shapeName ? projectSchema.shapes[shapeName] : undefined;
        const transform = findTransforms(transforms, schema, name, shape);
        if (transform) {
            const next = (value, schema, name, parent) => prepare(schema, value, name, parent);
            return transform.process(obj, { ...schema, $parent: parent }, name, shape, next);
        }
        if (obj && isUnionSchema(schema)) {
            for (const { shapeId, propSchema } of enumerateOneOfKeys(projectSchema, schema.oneOf)) {
                if (obj._shapeId === shapeId) {
                    return prepare(propSchema, obj, name, schema);
                }
            }
        }
        if (obj && isObjectSchema(schema)) {
            const result = {};
            const assigned = new Set();
            forEach(schema.properties, (fieldSchema, name) => {
                const sourceKeys = ensureArray(sourceKeyTransform(name, fieldSchema, obj));
                const localizedSourceKeys = getLocalizedSourceKeys(sourceKeys, nonDefaultLocales);
                for (const { key: sourceKey, locale } of localizedSourceKeys) {
                    const value = prepare(fieldSchema, get(obj, sourceKey), name, schema);
                    if (value !== undefined) {
                        assigned.add(sourceKey);
                        const transform = findTransforms(transforms, dereferenceSchema(projectSchema, fieldSchema), name);
                        const getKey = transform?.keyTransform ? transform.keyTransform : keyTransform;
                        const key = getLocalizedKey(getKey(name, fieldSchema, sourceKey), locale);
                        result[key] = value;
                    }
                }
            });
            if (passThrough && obj) {
                const keys = difference(Object.keys(obj), [...assigned]);
                if (keys.length) {
                    Object.assign(result, pick(obj, keys));
                }
            }
            return result;
        }
        if (isArraySchema(schema)) {
            const itemSchema = schema.items;
            if (Array.isArray(obj)) {
                return obj.map((item) => prepare(itemSchema, item, name, schema));
            }
            if (obj === undefined) {
                prepare(itemSchema, obj, name, parent);
            }
        }
        if (allowNullObjects && isObjectSchema(schema) && obj === null) {
            return null;
        }
        // Allow properties that have not been transformed
        return passThrough ? obj : undefined;
    };
    return prepare;
}
function transformKey(transforms, propSchema, name, transform = preferKey) {
    const schemaTransform = findTransforms(transforms, propSchema, name);
    if (schemaTransform?.keyTransform) {
        transform = schemaTransform.keyTransform;
    }
    return transform(name, propSchema);
}
export function getPropertyInfo(context, schema, path) {
    const { projectSchema, transforms, keyTransform } = context;
    const result = [];
    const findProperty = (currentSchema, name, rest, path) => {
        if (isObjectSchema(currentSchema)) {
            const propSchema = currentSchema.properties[name];
            const isRequired = currentSchema.required?.includes(name) ?? false;
            if (!propSchema) {
                return;
            }
            const refSchema = dereferenceSchema(projectSchema, propSchema);
            const nextPath = path.concat(transformKey(transforms, refSchema, name, keyTransform));
            if (rest.length) {
                const [nextName, ...nextRest] = rest;
                findProperty(refSchema, nextName, nextRest, nextPath);
            }
            else {
                result.push({ schema: refSchema, key: nextPath.join('.'), isRequired });
            }
        }
        else if (isUnionSchema(currentSchema)) {
            for (const optionSchema of currentSchema.oneOf) {
                findProperty(dereferenceSchema(projectSchema, optionSchema), name, rest, path);
            }
        }
        else if (isArraySchema(currentSchema)) {
            findProperty(dereferenceSchema(projectSchema, currentSchema.items), name, rest, path);
        }
    };
    const [first, ...rest] = path.split('.');
    findProperty(dereferenceSchema(projectSchema, schema), first, rest, []);
    return result;
}
