/** @import { StoreReferencesContainer } from '#client' */
/** @import { Store } from '#shared' */
import {
    subscribe_to_store
} from '../../../store/utils.js';
import {
    noop
} from '../../shared/utils.js';
import {
    get
} from '../runtime.js';
import {
    teardown
} from './effects.js';
import {
    mutable_source,
    set
} from './sources.js';

/**
 * Whether or not the prop currently being read is a store binding, as in
 * `<Child bind:x={$y} />`. If it is, we treat the prop as mutable even in
 * runes mode, and skip `binding_property_non_reactive` validation
 */
let is_store_binding = false;

/**
 * Gets the current value of a store. If the store isn't subscribed to yet, it will create a proxy
 * signal that will be updated when the store is. The store references container is needed to
 * track reassignments to stores and to track the correct component context.
 * @template V
 * @param {Store<V> | null | undefined} store
 * @param {string} store_name
 * @param {StoreReferencesContainer} stores
 * @returns {V}
 */
export function store_get(store, store_name, stores) {
    const entry = (stores[store_name] ? ? = {
        store: null,
        source: mutable_source(undefined),
        unsubscribe: noop
    });

    if (entry.store !== store) {
        entry.unsubscribe();
        entry.store = store ? ? null;

        if (store == null) {
            entry.source.v = undefined; // see synchronous callback comment below
            entry.unsubscribe = noop;
        } else {
            var is_synchronous_callback = true;

            entry.unsubscribe = subscribe_to_store(store, (v) => {
                if (is_synchronous_callback) {
                    // If the first updates to the store value (possibly multiple of them) are synchronously
                    // inside a derived, we will hit the `state_unsafe_mutation` error if we `set` the value
                    entry.source.v = v;
                } else {
                    set(entry.source, v);
                }
            });

            is_synchronous_callback = false;
        }
    }

    return get(entry.source);
}

/**
 * Unsubscribe from a store if it's not the same as the one in the store references container.
 * We need this in addition to `store_get` because someone could unsubscribe from a store but
 * then never subscribe to the new one (if any), causing the subscription to stay open wrongfully.
 * @param {Store<any> | null | undefined} store
 * @param {string} store_name
 * @param {StoreReferencesContainer} stores
 */
export function store_unsub(store, store_name, stores) {
    /** @type {StoreReferencesContainer[''] | undefined} */
    let entry = stores[store_name];

    if (entry && entry.store !== store) {
        // Don't reset store yet, so that store_get above can resubscribe to new store if necessary
        entry.unsubscribe();
        entry.unsubscribe = noop;
    }

    return store;
}

/**
 * Sets the new value of a store and returns that value.
 * @template V
 * @param {Store<V>} store
 * @param {V} value
 * @returns {V}
 */
export function store_set(store, value) {
    store.set(value);
    return value;
}

/**
 * @param {StoreReferencesContainer} stores
 * @param {string} store_name
 */
export function invalidate_store(stores, store_name) {
    var entry = stores[store_name];
    if (entry.store !== null) {
        store_set(entry.store, entry.source.v);
    }
}

/**
 * Unsubscribes from all auto-subscribed stores on destroy
 * @returns {StoreReferencesContainer}
 */
export function setup_stores() {
    /** @type {StoreReferencesContainer} */
    const stores = {};

    teardown(() => {
        for (var store_name in stores) {
            const ref = stores[store_name];
            ref.unsubscribe();
        }
    });

    return stores;
}

/**
 * Updates a store with a new value.
 * @param {Store<V>} store  the store to update
 * @param {any} expression  the expression that mutates the store
 * @param {V} new_value  the new store value
 * @template V
 */
export function store_mutate(store, expression, new_value) {
    store.set(new_value);
    return expression;
}

/**
 * @param {Store<number>} store
 * @param {number} store_value
 * @param {1 | -1} [d]
 * @returns {number}
 */
export function update_store(store, store_value, d = 1) {
    store.set(store_value + d);
    return store_value;
}

/**
 * @param {Store<number>} store
 * @param {number} store_value
 * @param {1 | -1} [d]
 * @returns {number}
 */
export function update_pre_store(store, store_value, d = 1) {
    const value = store_value + d;
    store.set(value);
    return value;
}

/**
 * Called inside prop getters to communicate that the prop is a store binding
 */
export function mark_store_binding() {
    is_store_binding = true;
}

/**
 * Returns a tuple that indicates whether `fn()` reads a prop that is a store binding.
 * Used to prevent `binding_property_non_reactive` validation false positives and
 * ensure that these props are treated as mutable even in runes mode
 * @template T
 * @param {() => T} fn
 * @returns {[T, boolean]}
 */
export function capture_store_binding(fn) {
    var previous_is_store_binding = is_store_binding;

    try {
        is_store_binding = false;
        return [fn(), is_store_binding];
    } finally {
        is_store_binding = previous_is_store_binding;
    }
}