import { createReactive, unhookReactive } from './reactive.js';
/**
 * `LiveData` is a utility class that wraps an object and allows you to observe changes
 * to its properties. It uses a `Proxy` to intercept property access and modification,
 * and dispatches an `update` event whenever a change occurs. This makes it easy to
 * react to data updates within your application, especially in UI components.
 *
 * It also tracks the path of the last changed property using dot notation (e.g., "a.b.c").
 *
 * @template T The type of the object being wrapped.
 *
 * @example
 * ```typescript
 * // Example usage of LiveData with a simple object
 * const data = { name: 'John Doe', age: 30 }
 * const liveData = new LiveData(data)
 *
 * // Listen for updates
 * liveData.addEventListener('update', (event) => {
 *   const { path, host } = (event as CustomEvent).detail
 *   console.log(`Property at path '${path}' updated to`, host)
 * })
 *
 * // Modify the object through the proxy
 * liveData.value.name = 'Jane Doe' // Output: Property at path 'name' updated to Jane Doe
 * liveData.value.age = 31 // Output: Property at path 'age' updated to 31
 * ```
 *
 * @example
 * ```typescript
 * // Example usage of LiveData with a nested object
 * const nestedData = {
 *   user: {
 *     profile: {
 *       name: 'John Doe',
 *       age: 30,
 *     }
 *   },
 *   address: {
 *     street: "Main St",
 *   }
 * }
 * const nestedLiveData = new LiveData(nestedData)
 *
 * nestedLiveData.addEventListener('update', (event) => {
 *    const { path, host } = (event as CustomEvent).detail
 *    console.log(`Property at path '${path}' updated to`, host)
 * })
 *
 * // Output: Property at path 'user.profile.name' updated to Jane Doe
 * nestedLiveData.value.user.profile.name = 'Jane Doe'
 * // Output: Property at path 'address.street' updated to New St
 * nestedLiveData.value.address.street = "New St"
 * ```
 *
 * @example
 * ```typescript
 * // Example usage of LiveData with an array
 * const arrayData = { items: ['apple', 'banana', 'orange'] };
 * const arrayLiveData = new LiveData(arrayData);
 *
 * arrayLiveData.addEventListener('update', (event) => {
 *   const { path, host } = (event as CustomEvent).detail;
 *   console.log(`Property at path '${path}' updated to`, host);
 * });
 *
 * arrayLiveData.value.items.push('grape'); // Output: Property at path 'items.3' updated to grape
 * arrayLiveData.value.items[0] = 'apricot'; // Output: Property at path 'items.0' updated to apricot
 * ```
 *
 * @example
 * ```typescript
 * // Example of getting the path of the last changed property.
 * const exampleData = { a: 'b', c: ['d'], e: { f: 'g' } };
 * const exampleLiveData = new LiveData(exampleData);
 *
 * exampleLiveData.addEventListener('update', () => {
 *   console.log('Last changed path:', exampleLiveData.lastChangedPath);
 * });
 *
 * exampleLiveData.value.a = 'new value'; // Output: Last changed path: a
 * exampleLiveData.value.c.push('new item'); // Output: Last changed path: c.1
 * exampleLiveData.value.c[0] = "new d"; // Output: Last changed path: c.0
 * exampleLiveData.value.e.f = 'new g'; // Output: Last changed path: e.f
 * ```
 */
export class LiveData extends EventTarget {
    #proxy;
    #lastChangedPath = null;
    /**
     * Gets the wrapped object. Changes to the object must be done using the `value` property.
     *
     * @returns The wrapped object.
     */
    get value() {
        return this.#proxy;
    }
    /**
     * Returns the path to the last changed property in dot notation.
     *
     * @returns The dot notation path to the last changed property, or `null` if no changes have occurred.
     */
    get lastChangedPath() {
        return this.#lastChangedPath;
    }
    /**
     * @param obj The source object to use.
     */
    constructor(obj) {
        super();
        this.#proxy = createReactive(obj, this.#updated.bind(this));
    }
    set(obj) {
        const old = this.#proxy;
        // Before setting new object we need to clean old one from callbacks.
        unhookReactive(old);
        this.#proxy = createReactive(obj, this.#updated.bind(this));
        this.#updated({ path: '', value: this.#proxy, oldValue: old });
    }
    get(path) {
        if (!path) {
            return undefined;
        }
        const segments = path.split('.');
        let current = this.value;
        for (const segment of segments) {
            if (current === null || current === undefined || typeof current !== 'object') {
                return undefined;
            }
            if (Array.isArray(current)) {
                const index = parseInt(segment, 10);
                if (isNaN(index) || index < 0 || index >= current.length) {
                    return undefined;
                }
                current = current[index];
            }
            else {
                current = current[segment];
            }
        }
        return current;
    }
    #updated(detail) {
        this.#notify(detail);
    }
    #notify(detail) {
        this.#lastChangedPath = detail.path;
        this.dispatchEvent(new CustomEvent('update', { detail }));
    }
    /**
     * Registers a callback function to be executed when specific properties of the LiveData's value change.
     * Supports filtering by deep properties using dot notation (e.g., "prop.arr[1].value")
     * and wildcards (e.g., "rootProperty.*").
     *
     * @param properties - An array of property paths to filter updates for.
     * @param callback - The function to be called when any of the specified properties change.
     *                   The callback receives the new value, the previous value, and the full path
     *                   of the changed property.
     * @returns A function that can be called to unregister the callback.
     *
     * @example
     * ```typescript
     * const myData = new LiveData({
     *   name: 'Initial Name',
     *   user: { address: { city: 'Some City' } },
     *   items: ['a','b','c']
     * });
     *
     * // Observing a deep property
     * const unsubscribe1 = myData.observeProperties(['user.address.city'], ({ value, previousValue, path }) => {
     *   console.log(`City changed: ${previousValue} -> ${value}`);
     * });
     * myData.value.user.address.city = 'New City'; // Triggers the callback
     *
     * // Observing a property with wildcard
     * const unsubscribe2 = myData.observeProperties(['items.*'], ({ value, previousValue, path }) => {
     *   console.log(`Items changed at path: ${path} from: ${previousValue} to: ${value}`);
     * });
     *
     * myData.value.items[0] = 'd'; // Triggers the callback
     * myData.value.items.push('e'); // Triggers the callback
     *
     *
     * // Observing root property with wildcard
     * const unsubscribe3 = myData.observeProperties(['user.*'], ({ value, previousValue, path }) => {
     *   console.log(`User child changed at path: ${path} from: ${previousValue} to: ${value}`);
     * });
     * myData.value.user.address = {city: 'test'}; // Triggers the callback
     *
     * unsubscribe1(); // Stop listening for changes
     * unsubscribe2(); // Stop listening for changes
     * unsubscribe3(); // Stop listening for changes
     * ```
     */
    observeProperties(properties, callback) {
        const eventCallback = (event) => {
            const customEvent = event;
            const { path } = customEvent.detail;
            for (const propFilter of properties) {
                if (this.#matches(path, propFilter)) {
                    callback(customEvent.detail);
                    break; // Avoid calling the callback multiple times for the same update
                }
            }
        };
        this.addEventListener('update', eventCallback);
        return () => {
            this.removeEventListener('update', eventCallback);
        };
    }
    #matches(path, filter) {
        if (filter.endsWith('*')) {
            const prefix = filter.slice(0, -1);
            return path.startsWith(prefix);
        }
        else {
            return path === filter;
        }
    }
}
//# sourceMappingURL=LiveData.js.map