import threading
import time


class ProgressHandler:
    def __init__(self, callback, duration=None, phase=None):
        self.callback = callback
        self.duration = duration
        self.phase = phase

    def __call__(self, *args):
        if len(args) == 2:
            # for ffmpeg -progress output
            k, v = args
            if k != "out_time_us" or not v.isnumeric() or self.duration is None:
                return
            p = float(v) / (1.0e6 * self.duration)
        elif len(args) == 1:
            (p,) = args
        else:
            raise ValueError("Invalid number of arguments")
        if self.phase is not None:
            p = {"text": self.phase, "progress": p}
        self.callback(p)


class OpProgress:
    def __init__(self, total, rate):
        self.total = total  # total number of steps
        self.rate = rate  # number of steps per second
        self.progress = 0  # number of steps completed
        self.set = None

    def __call__(self, p):
        self.progress = min(self.total, max(0, p))
        if self.set is not None:
            self.set.update()

    def finish(self):
        self.progress = self.total


class OpProgressFfmpeg(OpProgress):
    def __call__(self, k, v):
        if k != "out_time_us" or not v.isnumeric():
            return
        super().__call__(float(v) / 1e6)


class ProgressBox:
    def __init__(self, value):
        self.lock = threading.Lock()
        self.condition = threading.Condition(self.lock)
        self.value = value
        self.was_updated = False

    def set(self, value):
        with self.lock:
            if self.value is None or value is None or value > self.value:
                self.value = value
                self.was_updated = True
                self.condition.notify_all()

    def wait(self):
        with self.lock:
            while not self.was_updated:
                self.condition.wait()
            self.was_updated = False
        return self.value


class OpProgressSet:
    def __init__(self, callback, debounce=0.1):
        self._ops = []
        self._callback = callback
        self._debounce = debounce
        self._last_progress = ProgressBox(None)

        def _reporter_thread_entry(b, callback, dt):
            while True:
                next_progress = b.wait()
                t0 = time.time()
                if next_progress is None:
                    return
                try:
                    callback(100.0 * next_progress)
                except Exception as e:
                    print(f"Error in progress callback: {e}")
                remaining_wait = dt - (time.time() - t0)
                if remaining_wait > 0:
                    time.sleep(remaining_wait)

        self._reporter_thread = threading.Thread(
            target=_reporter_thread_entry,
            args=(self._last_progress, callback, debounce),
            daemon=True,
        )
        self._reporter_thread.start()

    def __del__(self):
        self._last_progress.set(None)
        self._reporter_thread.join()

    def add(self, op):
        self._ops.append(op)
        op.set = self

    def update(self):
        op_times = [op.total / op.rate for op in self._ops]
        total_time = sum(op_times)
        progress = sum(
            op.progress / op.total * t / total_time
            for op, t in zip(self._ops, op_times)
        )
        self._last_progress.set(progress)


class OpProgressMinSet(OpProgressSet):
    def __init__(self):
        super().__init__(None)
        self.set = None

    @property
    def progress(self):
        return min(op.progress for op in self._ops)

    @property
    def total(self):
        return max(op.total for op in self._ops)

    @total.setter
    def total(self, value):
        for op in self._ops:
            op.total = value

    @property
    def rate(self):
        return min(op.rate for op in self._ops)

    def finish(self):
        for op in self._ops:
            op.finish()

    def update(self):
        self.set.update()
