import threading
import time

import mpv

from blc2.workspace import Workspace
from blc2.topology import Fixture
from blc2.constants import ONESHOT

class Renderer:
    def hold(self, values):
        with self._lock:
            if isinstance(values, dict):
                values = values.items()
            for c, v in values:
                if v is None:
                    if c in self._hold:
                        del self._hold[c]
                else:
                    self._hold[c] = v
            ## Rely on the run thread to update when possible
            if not self._running:
                ## If not, just update now
                self._update()

    def clear_hold(self):
        with self._lock:
            self._hold = {}
            self.hold(())

    def _update(self):
        with self._lock:
            self._values = {c: (v if c not in self._hold else self._hold[c]) for c, v in self._last.items()}
            self.output.set_values(self._values)

    def start(self):
        with self._lock:
            if self._run_thread is not None:
                raise ValueError("Already running")
            self._run_thread = threading.Thread(target=self._run)
            self._running = True
            self._run_thread.start()

    def stop(self):
        with self._lock:
            self._running = False


    def set_functions(self, *args):
        with self._lock:
            if self._running:
                raise ValueError("Can't change while running")
            self._functions, self._data = [i[0] for i in args], [i[1] for i in args]

    @property
    def time(self):
        with self._lock:
            return self._current

    def _run(self):
        audio_cache = {}

        sleep = 1/60
        next_ap = []
        ap = {}
        running_ap = set()
        t = 0
        start = time.monotonic()
        while True:
            ## w_lock here is a formality: by assumption, we're not editing while we're 
            ## running a show
            with self._lock, self.w_lock:
                self._update()
                for a in next_ap:
                    a.pause = False
                next_ap = []
                
                if self._callback is not None:
                    self._callback(self._current, self._values)

                ## FIXME: Cleanup finished audio players?
                ## FIXME: Handle audio fades and jumps

                next_t = sleep + time.monotonic()
                self._current = next_t - start
                t = int(1000*self._current)
                _last = {c: 0 for c in self._channels}
                this_ap = set()
                for n, (f, d) in enumerate(zip(self._functions, self._data)):
                    lc, ac, self._data[n] = f.render(t, d)
                    for c, v in lc:
                        if _last[c] < v:
                            _last[c] = v

                    for guid, filename, start_t, fin, fstart, fout in ac:
                        this_ap.add(guid)
                        fstart += start_t
                        if guid in running_ap:
                            mul = 100
                            if t < fin:
                                mul = max(0, int(100*(t/fin)))
                            elif t > fstart+fout:
                                mul = -1
                            elif t > fstart:
                                mul = max(0, int(100*(1 - (t-fstart)/fout)))

                            if mul == -1:
                                ap[guid].pause = True
                            else:
                                ap[guid].volume = mul
                        else:
                            running_ap.add(guid)
                            nap = mpv.MPV()
                            nap.pause = True
                            nap.play(filename)
                            next_ap.append(nap)
                            ap[guid] = nap

                for a, p in tuple(ap.items()):
                    if a not in this_ap:
                        p.pause = True
                        del ap[a]
                        running_ap.remove(a)

                self._last = _last
                self._update()

                if not self._running:
                    ## We're done, clean up
                    for a in ap.values():
                        a.pause = True
                        del a
                    self._last = {c: 0 for c in self._channels}
                    self._current = 0
                    self._update()
                    if self._callback is not None:
                        self._callback(0, self._values)
                    self._run_thread = None
                    break

            ## END locked block
            time.sleep(max(0, next_t - time.monotonic()))

    def advance(self, *args):
        with self._lock:
            if self._run_thread is None:
                raise ValueError("Not running")
            for a in args:
                if isinstance(a, int):
                    p = None
                else: 
                    a, p = a
                f = self._functions[a]
                t = int(1000*self._current)
                if f.advance_mode == ONESHOT and self._data[a].steps and self._data[a].steps[-1].index+1 == len(f.steps):
                    continue
                try:
                    d2 = f.advance(t, self._data[a], n=p)
                except ValueError:
                    ## Done
                    pass
                else:
                    d = d2
                _, _, self._data[a] = f.render(t, d)

    def __init__(self, w: Workspace, w_lock: threading.RLock, output, callback=None):
        self.output = output
        self.w = w
        self.w_lock = w_lock 

        self._lock = threading.RLock()
        self._stop_lock = threading.Lock()
        self._hold = {}
        self._channels = frozenset().union(*((c for c in f.channels) for f in w.fixtures.values()))
        self._last = {c: 0 for c in self._channels}
        self._functions = []
        self._data = []
        self._current = 0
        self._running = False
        self._run_thread = None
        self._values = {}

        self._callback = callback