import numpy as np
from concurrent.futures import ThreadPoolExecutor

def extract_windows(signal, window_size, overlap):
    """
    Extract sliding windows from the signal.

    :param signal: 1D array-like input signal.
    :param window_size: Number of samples per window.
    :param overlap: Number of samples to overlap between windows.
    :return: A list of windows (each window is a 1D array).
    """
    step_size = window_size - overlap
    num_windows = (len(signal) - window_size) // step_size + 1
    return [signal[i * step_size : i * step_size + window_size] for i in range(num_windows)]

def apply_sliding_window(func, windows=None, signal=None, window_size=None, overlap=None, **kwargs):
    """
    Apply a function to each window in the signal. If precomputed windows are provided,
    they are used directly; otherwise, the windows are computed from the signal.

    Per-window computation is parallelized using threads.

    :param func: The function to apply to each window.
    :param windows: Optional; precomputed list of windows.
    :param signal: The input signal (used only if windows is None).
    :param window_size: The window size (used only if windows is None).
    :param overlap: The number of samples to overlap between windows (used only if windows is None).
    :param kwargs: Additional keyword arguments passed to func.
    :return: NumPy array of results computed for each window.
    """
    if windows is None:
        if signal is None or window_size is None or overlap is None:
            raise ValueError("Either precomputed windows or signal with window_size and overlap must be provided.")
        windows = extract_windows(signal, window_size, overlap)

    # Parallelize the application of func over windows.
    with ThreadPoolExecutor() as executor:
        results = list(executor.map(lambda win: func(win, **kwargs), windows))
    return np.array(results)
