import threading

def start_daemon(**kwargs):
    import threading
    thread = threading.Thread(**kwargs)
    thread.daemon = True
    thread.start()
    return thread


import os


def batch_threading(targets, max_threads=None, show_errors=True, flatten=False):
    import concurrent.futures
    import traceback
    from itertools import chain

    def execute_target(target):
        try:
            return target[0](*target[1])
        except Exception:
            if show_errors:
                print(traceback.format_exc())
            return None

    if not max_threads:
        max_threads = min((os.cpu_count() or 1) * (2**13), 2**19)

    # executor.map preserves the order of results (matching your original list index logic)
    # and lazily submits tasks so it doesn't flood memory with Future objects.
    with concurrent.futures.ThreadPoolExecutor(max_workers=max_threads) as executor:
        # map returns an iterator, converting to list triggers the execution
        results = list(executor.map(execute_target, targets))

    if flatten:
        results = list(chain.from_iterable(results))
    return results


def batch_threading_gen(targets, max_threads=None, show_errors=True):
    import concurrent.futures
    import traceback

    def execute_target(target):
        try:
            return target[0](*target[1])
        except Exception:
            if show_errors:
                print(traceback.format_exc())
            return None

    if not max_threads:
        max_threads = min((os.cpu_count() or 1) * (2**13), 2**19)

    with concurrent.futures.ThreadPoolExecutor(max_workers=max_threads) as executor:
        # Create an iterator so we don't load all targets into memory if it's a generator
        targets_iter = iter(targets)
        futures = set()

        # Initial fill: submit only up to max_threads tasks
        for _ in range(max_threads):
            try:
                task = next(targets_iter)
                futures.add(executor.submit(execute_target, task))
            except StopIteration:
                break

        # Sliding window: As one finishes, yield it and submit a new one
        while futures:
            # Wait for the first available thread to finish
            done, futures = concurrent.futures.wait(
                futures, return_when=concurrent.futures.FIRST_COMPLETED
            )

            # Yield results from finished threads
            for f in done:
                try:
                    yield f.result()
                except Exception:
                    yield None

                # Attempt to submit the next task
                try:
                    new_task = next(targets_iter)
                    futures.add(executor.submit(execute_target, new_task))
                except StopIteration:
                    # No more tasks to submit, just finish processing existing futures
                    pass


def batch_multiprocess_threaded(targets_and_args, disable_warning=False, killable=False, daemon=False):
    import kthread
    import threading
    if killable:
        thread_method = kthread.KThread
    else:
        thread_method = threading.Thread
    t = thread_method(target=batch_multiprocess, args=(targets_and_args, disable_warning))
    if daemon:
        t.daemon = True
    t.start()
    return t


def multiprocess_threaded(target, args=(), disable_warning=False, killable=False, daemon=False):
    return batch_multiprocess_threaded([(target, args)], disable_warning=False, killable=False, daemon=False)


zstd_comp = None
zstd_decomp = None

def _get_std_objects():
    global zstd_comp, zstd_decomp
    if zstd_comp is None or zstd_decomp is None:
        import zstandard as zstd
        zstd_comp = zstd.ZstdCompressor(level=4)
        zstd_decomp = zstd.ZstdDecompressor()
    return zstd_comp, zstd_decomp


class thread_join_return(threading.Thread):
    def __init__(self, group=None, target=None, name=None, args=(), kwargs={}):
        super().__init__(group, target, name, args, kwargs)
        self._return = None

    def run(self):
        if self._target is not None:
            self._return = self._target(*self._args, **self._kwargs)

    # A custom 'join' that also returns the value
    def join(self, *args):
        super().join(*args)
        return self._return