import LabelView from "../../../src/view/common/LabelView.js";
import EmptyView from "../../../src/view/elements/EmptyView.js";
import { createFieldEditor } from "./createFieldEditor.js";
import { FieldDescriptor } from "./FieldDescriptor.js";
import { FieldValueAdapter } from "./FieldValueAdapter.js";
import { findNearestRegisteredType } from "./findNearestRegisteredType.js";

/**
 * @template T
 * @param {T|undefined} value
 */
function objectToClass(value) {
    if (value === null || value === undefined) {
        return;
    }

    const type = typeof value;

    switch (type) {
        case "object":
            const proto = Object.getPrototypeOf(value);

            if (proto === null) {
                return;
            }

            return proto.constructor;
        case "boolean":
            return Boolean;
        case "function":
            return Function;
        case "string":
            return String;
        case "number":
            return Number;
        default:
            throw new Error(`Unsupported type '${type}'`);
    }
}

/**
 *
 * @param {Object} object
 * @param {string} field_name
 * @returns {boolean}
 */
function isPublicField(object, field_name) {
    if (field_name.startsWith('_')) {
        // private
        return false;
    }

    return true;
}

/**
 *
 * @param {Object} object
 * @param {string} field_name
 * @returns {boolean}
 */
function isReservedField(object, field_name) {
    if (field_name.startsWith('@')) {
        return true;
    }

    return false;
}

/**
 *
 * @type {string[]}
 */
const reserved_prototype_names = [
    "constructor"
];

const reserved_prototypes = [
    Object,
    Object.prototype
];

/**
 *
 * @param {Object} object
 * @param {FieldDescriptor[]} result
 * @param {*} object_proto
 */
function extract_object_fields_reflection(object, result, object_proto) {
    const keys = Object.keys(object);

    for (let i = 0; i < keys.length; i++) {
        const key = keys[i];

        if (!isPublicField(object, key)) {
            continue;
        }

        if (isReservedField(object, key)) {
            continue;
        }

        const field_type = objectToClass(object[key]);

        if (field_type === undefined) {
            continue;
        }

        const descriptor = new FieldDescriptor();
        descriptor.name = key;
        descriptor.type = field_type;
        descriptor.adapter = new FieldValueAdapter();

        result.push(descriptor);
    }

    // TODO exclude value fields that belong to the prototype, such as .isTransform

    if (!reserved_prototypes.includes(object_proto)) {

        const descriptors = Object.getOwnPropertyDescriptors(object_proto);

        for (let d_name in descriptors) {

            if (reserved_prototype_names.includes(d_name)) {
                // reserved name, skip
                continue;
            }

            const d = descriptors[d_name];

            const adapter = new FieldValueAdapter();

            if (d.get === undefined && d.value === undefined) {
                adapter.readable = false;
            }

            if (d.get !== undefined && d.set === undefined) {
                adapter.writable = false;
            }

            if (!adapter.readable) {
                continue;
            }


            let field_value;

            try {
                field_value = adapter.read(object, d_name);
            } catch (e) {
                console.warn(e);
                continue;
            }

            const field_type = objectToClass(field_value);

            if (field_type === undefined) {
                continue;
            }

            const descriptor = new FieldDescriptor();

            descriptor.name = d_name;
            descriptor.type = field_type;
            descriptor.adapter = adapter;

            result.push(descriptor);
        }

    }
}

/**
 *
 * @param {Object} object
 * @param {Map<*, TypeEditor>} registry
 * @returns {FieldDescriptor[]}
 */
function extractObjectFields(object, registry) {

    const result = [];

    const object_proto = Object.getPrototypeOf(object);
    const Klass = object_proto.constructor;

    const editor = registry.get(Klass);

    if (!(editor?.schema?.additionalProperties === false)) {

        extract_object_fields_reflection(object, result, object_proto);

    }


    if (editor !== undefined) {

        const schema = editor.schema;

        if (schema !== undefined) {
            // has schema, apply
            const prop_keys = Object.keys(schema.properties);

            for (let i = 0; i < prop_keys.length; i++) {
                const key = prop_keys[i];

                const property = schema.properties[key];

                const field_index = result.findIndex(a => a.name === key);

                let field;

                if (field_index === -1) {
                    // not in schema yet, build
                    field = new FieldDescriptor();
                    field.name = key;
                    field.adapter = new FieldValueAdapter();

                    result.push(field);
                } else {
                    field = result[field_index];
                }

                let actual_type = field.type;

                if (actual_type === undefined || actual_type === null) {
                    let actual_value;

                    try {
                        actual_value = field.adapter.read(object, key);
                    } catch (e) {
                        // silent failure
                    }

                    if (actual_value !== undefined && actual_value !== null) {
                        actual_type = objectToClass(actual_value);
                    }
                }

                if (property.type) {
                    // validate type
                    if (actual_type !== undefined && actual_type !== null) {
                        if (actual_type !== property.type) {
                            console.error(`Schema violation at field '${key}', schema type is different from actual type`, schema, actual_type);
                        }
                    }

                    field.type = property.type;
                } else if (actual_type !== undefined && field.type !== actual_type) {
                    field.type = actual_type;
                }


                field.schema = property;

                if (property.transient === true) {
                    // no point in editing transient
                    result.splice(field_index, 1);
                }
            }
        }
    }

    return result;
}

const build_path = [];
let build_path_offset = 0;


/**
 * @param {Object} object
 * @param {Map<*, TypeEditor>} registry
 * @param {FieldDescriptor} [field_descriptor]
 */
export function buildObjectEditorFromRegistry(object, registry, field_descriptor) {

    const editor = findNearestRegisteredType(registry, object);

    let fd;

    if (field_descriptor === undefined) {

        fd = new FieldDescriptor();
        fd.adapter = new FieldValueAdapter();

        fd.adapter.read = () => object;
        fd.adapter.writable = false;

        if (editor.schema) {
            fd.schema = editor.schema;
        }

    } else {
        fd = field_descriptor;
    }

    return editor.build(null, fd, registry);
}

const MAX_DEPTH = 7;

/**
 * @param {Object} object
 * @param {Map<*, TypeEditor>} registry
 */
export function createObjectEditor(object, registry) {

    if (build_path_offset > MAX_DEPTH) {
        // too deep
        return undefined;
    }

    const seen_index = build_path.indexOf(object);
    if (seen_index >= 0 && seen_index < build_path_offset) {
        // recursion
        return undefined;
    }


    build_path[build_path_offset++] = object;


    const vResult = new EmptyView({
        classList: ['auto-object-editor'],
        css: {}
    });


    const fields = extractObjectFields(object, registry);

    const field_views = [];

    for (let i = 0; i < fields.length; i++) {
        const field = fields[i];

        let vField;

        try {
            vField = createFieldEditor(object, field, registry);
        } catch (e) {
            vField = new LabelView('ERROR');
            console.warn(e);
        }

        if (vField === null) {
            continue;
        }

        field_views.push(vField);
    }

    build_path_offset--;

    if (field_views.length === 0) {
        // skip containers with no data
        return null;
    }

    vResult.addChildren(field_views);

    return vResult;
}
