/**
 * For the ORM capabilities,
 * we have to merge the document prototype
 * with the ORM functions and the data
 * We do this iterating over the properties and
 * adding them to a new object.
 * In the future we should do this by chaining the __proto__ objects
 */

import type {
    RxCollection,
    RxDocument,
    RxDocumentData
} from './types/index.d.ts';
import {
    createRxDocumentConstructor,
    basePrototype,
    createWithConstructor as createRxDocumentWithConstructor
} from './rx-document.ts';
import {
    runPluginHooks
} from './hooks.ts';
import { overwritable } from './overwritable.ts';
import { getFromMapOrCreate } from './plugins/utils/index.ts';

const constructorForCollection = new WeakMap();

export function getDocumentPrototype(
    rxCollection: RxCollection
): any {
    const schemaProto = rxCollection.schema.getDocumentPrototype();
    const ormProto = getDocumentOrmPrototype(rxCollection);
    const baseProto = basePrototype;
    const proto = {};
    [
        schemaProto,
        ormProto,
        baseProto
    ].forEach(obj => {
        const props = Object.getOwnPropertyNames(obj);
        props.forEach(key => {
            const desc: any = Object.getOwnPropertyDescriptor(obj, key);
            /**
             * When enumerable is true, it will show on console dir(instance)
             * To not pollute the output, only getters and methods are enumerable
             */
            let enumerable = true;
            if (
                key.startsWith('_') ||
                key.endsWith('_') ||
                key.startsWith('$') ||
                key.endsWith('$')
            ) enumerable = false;

            if (typeof desc.value === 'function') {
                // when getting a function, we automatically do a .bind(this)
                Object.defineProperty(proto, key, {
                    get() {
                        return desc.value.bind(this);
                    },
                    enumerable,
                    configurable: false
                });

            } else {
                desc.enumerable = enumerable;
                desc.configurable = false;
                if (desc.writable)
                    desc.writable = false;
                Object.defineProperty(proto, key, desc);
            }
        });
    });
    return proto;
}

export function getRxDocumentConstructor<RxDocType, ORM>(
    rxCollection: RxCollection<RxDocType, ORM>
) {
    return getFromMapOrCreate(
        constructorForCollection,
        rxCollection,
        () => createRxDocumentConstructor(
            getDocumentPrototype(rxCollection as any)
        )
    );
}

/**
 * Create a RxDocument-instance from the jsonData
 * and the prototype merge.
 * You should never call this method directly,
 * instead you should get the document from collection._docCache.getCachedRxDocument().
 */
export function createNewRxDocument<RxDocType, ORM, Reactivity>(
    rxCollection: RxCollection<RxDocType, ORM, {}, {}, Reactivity>,
    documentConstructor: any,
    docData: RxDocumentData<RxDocType>
): RxDocument<RxDocType, ORM, Reactivity> {
    const doc = createRxDocumentWithConstructor(
        documentConstructor,
        rxCollection as any,
        overwritable.deepFreezeWhenDevMode(docData as any)
    );
    rxCollection._runHooksSync('post', 'create', docData, doc);
    runPluginHooks('postCreateRxDocument', doc);
    return doc as any;
}


/**
 * returns the prototype-object
 * that contains the orm-methods,
 * used in the proto-merge
 */
export function getDocumentOrmPrototype(rxCollection: RxCollection): any {
    const proto: any = {};
    Object
        .entries(rxCollection.methods)
        .forEach(([k, v]) => {
            proto[k] = v;
        });
    return proto;
}
