import { Observable } from '@babylonjs/core/Misc/observable';
import { AddTypeDiscriminant } from './Types';

/**
 * Represents the event data for a property change event.
 * @template T - The type of the sender object.
 */

export interface PropertyChangeEventDataBase<T> {
    /** object instance {@link property} changed on.
     * @remarks
     * To get changed value use
     * ```typescript
     * const newValue = sender[property];
     * ```
     */
    sender: T;
    /**
     * property that changed on object of type `T`.
     * @remarks
     * To get new value use
     * ```typescript
     * const newValue = sender[property];
     * ```
     * */
    property: keyof T;
    /**
     * previous value of {@link property}
     * @remarks
     * To get new value use
     * ```typescript
     * const newValue = sender[property];
     * ```
     * */
    oldVal: any;
}

/**
 * Represents the event data for a property change.
 * @template T - The type of the property value.
 */
export type PropertyChangeEventData<T> =
    | AddTypeDiscriminant<PropertyChangeEventDataBase<T>, 'set'>
    | AddTypeDiscriminant<PropertyChangeEventDataBase<T>, 'delete'>
    | AddTypeDiscriminant<Exclude<PropertyChangeEventDataBase<T>, 'oldVal'>, 'define'>;

/**
 * Represents an object that can observe property changes.
 *
 * @template T - The type of the object being observed.
 */
export type PropertyChangeObservable<T> = T & {
    onPropertyChanged: Observable<PropertyChangeEventData<T>>;
};

/**
 * Creates a property change observable object from a existing object.
 * The observable object has a additional property `onPropertyChanged` which is an observable that can be subscribed to.
 * Any changes to properties on the object can be observed by subscribing to the `onPropertyChanged` observable.
 *
 * @template T - The type of the object being observed.
 * @param target - The object to observe.
 * @returns The property change observable object.
 */
export function makePropertyChangeObservable<T extends object>(target: T): PropertyChangeObservable<T> {
    const onPropertyChanged = new Observable<PropertyChangeEventData<T>>();
    return new Proxy<T>(target, {
        get: (target: T, key: string | symbol, receiver: any) => {
            if ((key as keyof PropertyChangeObservable<T>) === 'onPropertyChanged') return onPropertyChanged;
            return Reflect.get(target, key, receiver);
        },
        set: (target: T, key: string | symbol, newValue: any, receiver: any) => {
            const currentVal = Reflect.get(target, key, receiver);
            const wasSet = Reflect.set(target, key, newValue, receiver);
            if (currentVal !== newValue)
                onPropertyChanged.notifyObservers({
                    type: 'set',
                    sender: receiver,
                    property: key as any,
                    oldVal: currentVal
                });
            return wasSet;
        },
        deleteProperty: (target: T, p: string | symbol) => {
            const oldVal = Reflect.get(target, p);
            const wasDeleted = Reflect.deleteProperty(target, p);
            if (wasDeleted) {
                onPropertyChanged.notifyObservers({ type: 'delete', sender: target, property: p as keyof T, oldVal });
            }
            return wasDeleted;
        },
        defineProperty: (target: T, p: string | symbol, attributes: PropertyDescriptor) => {
            const wasDefined = Reflect.defineProperty(target, p, attributes);
            if (wasDefined) {
                onPropertyChanged.notifyObservers({
                    type: 'define',
                    sender: target,
                    property: p as keyof T,
                    oldVal: undefined
                });
            }
            return wasDefined;
        }
    }) as PropertyChangeObservable<T>;
}
