/** @import { Derived, Effect, Reaction, Source, Value } from '#client' */
import {
    DEV
} from 'esm-env';
import {
    component_context,
    active_reaction,
    new_deps,
    active_effect,
    untracked_writes,
    get,
    is_runes,
    schedule_effect,
    set_untracked_writes,
    set_signal_status,
    untrack,
    increment_version,
    update_effect,
    derived_sources,
    set_derived_sources,
    check_dirtiness,
    set_is_flushing_effect,
    is_flushing_effect
} from '../runtime.js';
import {
    equals,
    safe_equals
} from './equality.js';
import {
    CLEAN,
    DERIVED,
    DIRTY,
    BRANCH_EFFECT,
    INSPECT_EFFECT,
    UNOWNED,
    MAYBE_DIRTY,
    BLOCK_EFFECT
} from '../constants.js';
import * as e from '../errors.js';
import {
    legacy_mode_flag,
    tracing_mode_flag
} from '../../flags/index.js';
import {
    get_stack
} from '../dev/tracing.js';

export let inspect_effects = new Set();

/**
 * @param {Set<any>} v
 */
export function set_inspect_effects(v) {
    inspect_effects = v;
}

/**
 * @template V
 * @param {V} v
 * @param {Error | null} [stack]
 * @returns {Source<V>}
 */
export function source(v, stack) {
    /** @type {Value} */
    var signal = {
        f: 0, // TODO ideally we could skip this altogether, but it causes type errors
        v,
        reactions: null,
        equals,
        version: 0
    };

    if (DEV && tracing_mode_flag) {
        signal.created = stack ? ? get_stack('CreatedAt');
        signal.debug = null;
    }

    return signal;
}

/**
 * @template V
 * @param {V} v
 */
export function state(v) {
    return push_derived_source(source(v));
}

/**
 * @template V
 * @param {V} initial_value
 * @param {boolean} [immutable]
 * @returns {Source<V>}
 */
/*#__NO_SIDE_EFFECTS__*/
export function mutable_source(initial_value, immutable = false) {
    const s = source(initial_value);
    if (!immutable) {
        s.equals = safe_equals;
    }

    // bind the signal to the component context, in case we need to
    // track updates to trigger beforeUpdate/afterUpdate callbacks
    if (legacy_mode_flag && component_context !== null && component_context.l !== null) {
        (component_context.l.s ? ? = []).push(s);
    }

    return s;
}

/**
 * @template V
 * @param {V} v
 * @param {boolean} [immutable]
 * @returns {Source<V>}
 */
export function mutable_state(v, immutable = false) {
    return push_derived_source(mutable_source(v, immutable));
}

/**
 * @template V
 * @param {Source<V>} source
 */
/*#__NO_SIDE_EFFECTS__*/
function push_derived_source(source) {
    if (active_reaction !== null && (active_reaction.f & DERIVED) !== 0) {
        if (derived_sources === null) {
            set_derived_sources([source]);
        } else {
            derived_sources.push(source);
        }
    }

    return source;
}

/**
 * @template V
 * @param {Value<V>} source
 * @param {V} value
 */
export function mutate(source, value) {
    set(
        source,
        untrack(() => get(source))
    );
    return value;
}

/**
 * @template V
 * @param {Source<V>} source
 * @param {V} value
 * @returns {V}
 */
export function set(source, value) {
    if (
        active_reaction !== null &&
        is_runes() &&
        (active_reaction.f & (DERIVED | BLOCK_EFFECT)) !== 0 &&
        // If the source was created locally within the current derived, then
        // we allow the mutation.
        (derived_sources === null || !derived_sources.includes(source))
    ) {
        e.state_unsafe_mutation();
    }

    return internal_set(source, value);
}

/**
 * @template V
 * @param {Source<V>} source
 * @param {V} value
 * @returns {V}
 */
export function internal_set(source, value) {
    if (!source.equals(value)) {
        source.v = value;
        source.version = increment_version();

        if (DEV && tracing_mode_flag) {
            source.updated = get_stack('UpdatedAt');
        }

        mark_reactions(source, DIRTY);

        // If the current signal is running for the first time, it won't have any
        // reactions as we only allocate and assign the reactions after the signal
        // has fully executed. So in the case of ensuring it registers the reaction
        // properly for itself, we need to ensure the current effect actually gets
        // scheduled. i.e: `$effect(() => x++)`
        if (
            is_runes() &&
            active_effect !== null &&
            (active_effect.f & CLEAN) !== 0 &&
            (active_effect.f & BRANCH_EFFECT) === 0
        ) {
            if (new_deps !== null && new_deps.includes(source)) {
                set_signal_status(active_effect, DIRTY);
                schedule_effect(active_effect);
            } else {
                if (untracked_writes === null) {
                    set_untracked_writes([source]);
                } else {
                    untracked_writes.push(source);
                }
            }
        }

        if (DEV && inspect_effects.size > 0) {
            const inspects = Array.from(inspect_effects);
            var previously_flushing_effect = is_flushing_effect;
            set_is_flushing_effect(true);
            try {
                for (const effect of inspects) {
                    // Mark clean inspect-effects as maybe dirty and then check their dirtiness
                    // instead of just updating the effects - this way we avoid overfiring.
                    if ((effect.f & CLEAN) !== 0) {
                        set_signal_status(effect, MAYBE_DIRTY);
                    }
                    if (check_dirtiness(effect)) {
                        update_effect(effect);
                    }
                }
            } finally {
                set_is_flushing_effect(previously_flushing_effect);
            }
            inspect_effects.clear();
        }
    }

    return value;
}

/**
 * @param {Value} signal
 * @param {number} status should be DIRTY or MAYBE_DIRTY
 * @returns {void}
 */
function mark_reactions(signal, status) {
    var reactions = signal.reactions;
    if (reactions === null) return;

    var runes = is_runes();
    var length = reactions.length;

    for (var i = 0; i < length; i++) {
        var reaction = reactions[i];
        var flags = reaction.f;

        // Skip any effects that are already dirty
        if ((flags & DIRTY) !== 0) continue;

        // In legacy mode, skip the current effect to prevent infinite loops
        if (!runes && reaction === active_effect) continue;

        // Inspect effects need to run immediately, so that the stack trace makes sense
        if (DEV && (flags & INSPECT_EFFECT) !== 0) {
            inspect_effects.add(reaction);
            continue;
        }

        set_signal_status(reaction, status);

        // If the signal a) was previously clean or b) is an unowned derived, then mark it
        if ((flags & (CLEAN | UNOWNED)) !== 0) {
            if ((flags & DERIVED) !== 0) {
                mark_reactions( /** @type {Derived} */ (reaction), MAYBE_DIRTY);
            } else {
                schedule_effect( /** @type {Effect} */ (reaction));
            }
        }
    }
}