#!/usr/bin/python3
"""
PelinkWorker

Controls
- Pelink server communication
- MPV Player

Communicates to PelinkWindow through MessageBus to report progress

USAGE:
worker = PelinkWorker()
threading.Thread(target=worker.run, daemon=True).start()
"""

import asyncio
import threading
from random import randint

from pelink.PelinkClient import (
    PelinkClient,
    ConnectionError,
    ConnectionSetupError,
)

from player.Event_ts import Event_ts
from player.PelinkPlayer import PelinkPlayer


class PelinkWorker:
    def __init__(self, bus=None):
        self._event = threading.Event()
        self.lock = threading.Lock()
        self._kill_event = None
        self._kill = False
        self.bus = bus
        self.file_path = None
        self.club = None
        self.log = lambda *args, **kwargs: print("[PelinkWorker]", *args, **kwargs)

    def run(self):
        while True:
            self.log("Waiting for signal from GUI")
            self._event.wait() # Wait for GUI to tell us to launch
            self._event.clear()

            with self.lock:
                if self._kill:
                    break

            self.log("Awake")
            if self.bus:
                self.bus.progress.emit("Connecting", 5)
            try:
                asyncio.run(self.start_work())

            except ConnectionSetupError as e:
                if self.bus:
                    self.bus.error.emit("Connection failed: " + str(e))
                continue

            break

    def launch(self, state:dict):
        # This is called from the Qt thread
        self._kill_event = None
        self.file_path = state.get('file_path')
        self.club = state.get('club')
        self._event.set()

    def kill(self):
        with self.lock:
            self._kill = True
        if self._kill_event is not None and not self._kill_event.is_set():
            self._kill_event.set()
        elif self._event is not None and not self._event.is_set():
            self._event.set()

    async def start_work(self):
        try:
            async with PelinkClient(self.club) as pc:
                self._kill_event = Event_ts()
                self.pc = pc
                await self.fake_loading()
                self.loading_done()

                pplay = self.init_player()
                self.pplay = pplay

                await self.play_loop(pc, pplay)

        except ConnectionSetupError as e:
            self.log("Connection failed: " + str(e))
            raise ConnectionSetupError(str(e))

        self.log("End")

    async def fake_loading(self):
        """
        I like progress bars, let me see progress in the progress bar.
        """
        if not self.bus:
            return
        self.bus.progress.emit("Server found", 50)
        await asyncio.sleep(randint(5, 20)/100)
        self.bus.progress.emit("Client accepted", 80)
        await asyncio.sleep(randint(5, 20)/100)
        self.bus.progress.emit("Ready", 99)
        await asyncio.sleep(randint(5, 20)/100)

    def loading_done(self):
        if not self.bus:
            return
        self.bus.progress.emit("Done", 100)

    def init_player(self):
        pplay = PelinkPlayer(self.file_path)
        pplay.start_thread()
        return pplay

    async def run_command(self, cmd, data):
        self.log(f"Run command {cmd}: {data}")

        if cmd in ['peercount', 'peernew', 'peerquit']:
            if self.bus:
                self.bus.peercount.emit(int(data))

        self.pplay.run_command(cmd, data)

    async def send_command(self, cmd, data):
        self.log(f"Send command {cmd}: {data}")
        await self.pc.send_command(cmd, data)

    async def play_loop(self, pc:PelinkClient, pplay:PelinkPlayer):
        pc_wait = None
        pplay_wait = None
        suicide = None
        ask_peers = asyncio.create_task(pc.send_command("peers", "")) # Run once

        while True:
            if not pc_wait or pc_wait.done():
                pc_wait = asyncio.create_task(pc.wait_command())
            if not pplay_wait or pplay_wait.done():
                pplay_wait = asyncio.create_task(pplay.async_wait())
            if not suicide or suicide.done():
                suicide = asyncio.create_task(self._kill_event.wait())

            # Wait for one of the two tasks to finish
            tasks = [pc_wait, pplay_wait, suicide]
            if ask_peers is not None:
                tasks.append(ask_peers)

            done, pending = await asyncio.wait(
                [pc_wait, pplay_wait, suicide],
                return_when=asyncio.FIRST_COMPLETED
            )

            for task in done:
                if suicide.done():
                    pc_wait.cancel()
                    pplay_wait.cancel()
                    break

                elif pc_wait.done():
                    command, payload = pc_wait.result()
                    await self.run_command(command, payload)

                elif pplay_wait.done():
                    cmd, payload = pplay_wait.result()
                    await self.send_command(cmd, payload)

                elif ask_peers is not None and ask_peers.done():
                    ask_peers = None
