#!/usr/bin/python3

import sys
import os
import time
import threading
import ctypes
import ctypes.util

class MikModError(RuntimeError):
    pass


class MikMod:
    def __init__(self):
        libpath = ctypes.util.find_library("mikmod")
        if libpath is None:
            raise MikModError("Could not locate libmikmod. Install libmikmod.")

        try:
            self._lib = ctypes.CDLL(libpath)
        except Exception as e:
            raise MikModError(f"Failed to load library '{libpath}': {e}")

        # Try to bind expected functions; if not present, set to None and warn.
        def _bind(name, argtypes=None, restype=ctypes.c_int):
            fn = getattr(self._lib, name, None)
            if not fn:
                return None
            #try:
            if argtypes is not None:
                    fn.argtypes = argtypes
            fn.restype = restype
            #except Exception:
            #    # some bindings may not match expected signatures on some versions
            #    pass
            return fn

        # core functions commonly available in libmikmod
        self.MikMod_Init = _bind("MikMod_Init", [ctypes.c_char_p], ctypes.c_int)
        self.MikMod_Exit = _bind("MikMod_Exit", None, None)
        self.MikMod_RegisterAllDrivers = _bind("MikMod_RegisterAllDrivers", None, None)
        self.MikMod_RegisterAllLoaders = _bind("MikMod_RegisterAllLoaders", None, None)

        # player functions
        # Player_Load(const char *name, int maxchannels) -> Module *
        self.Player_Load = _bind("Player_Load", [ctypes.c_char_p, ctypes.c_int], ctypes.c_void_p)
        # Player_Start(Module *) -> int
        self.Player_Start = _bind("Player_Start", [ctypes.c_void_p], ctypes.c_int)
        # Player_Stop(void)
        self.Player_Stop = _bind("Player_Stop", None, None)
        # Player_Free(Module *)
        self.Player_Free = _bind("Player_Free", [ctypes.c_void_p], None)
        # Player_Active(void) -> int  (optional: used to poll playback)
        self.Player_Active = _bind("Player_Active", None, ctypes.c_int)
        # MikMod_Update(void) _> void
        self.MikMod_Update = _bind("MikMod_Update", None, None)

        # Keep track of loaded module pointer
        self._module = None
        self._initialized = False

    def init(self):
        if self._initialized:
            return
        # register drivers/loaders
        self.MikMod_RegisterAllDrivers()
        self.MikMod_RegisterAllLoaders()
        ret = self.MikMod_Init(b"")
        if ret != 0:
            raise MikModError(f"MikMod_Init returned error code {ret}")
        self._initialized = True

    def load(self, filename, maxchannels = 64):
        if not self._initialized:
            raise MikModError("Library not initialized. Call init() first.")
        if self._module is not None:
            self.free()

        self._module = self.Player_Load(filename.encode("utf-8"), maxchannels)
        if not self._module:
            raise MikModError(f"Player_Load failed for '{filename}' (returned NULL).")
        return self._module

    def start(self):
        res = self.Player_Start(self._module)
        if res != 0:
            raise MikModError(f"Player_Start returned error code {res}")
        return res

    def stop(self):
        self.Player_Stop()

    def free(self):
        if self._module:
            # stop first for safety
            try:
                self.Player_Stop()
            except Exception:
                pass
            self.Player_Free(self._module)
            self._module = None

    def play_thread(self):
        thread_id = None
        def player_thread():
            while thread_id==self.thread:
                self.MikMod_Update()
                time.sleep(0.001)
        self.start()
        self.thread = threading.Thread(target=player_thread, daemon=True)
        thread_id = self.thread
        self.thread.start()

    def stop_thread(self):
        self.stop()
        self.thread = None

    def exit(self):
        try:
            self.free()
        except Exception:
            pass
        self.MikMod_Exit()
        self._initialized = False


def putc(char):
    try:
        sys.stdout.buffer.write(char)
        sys.stdout.flush()
    except BrokenPipeError:
        # sys.exit(0) raises another exception, just quit now
        os._exit(0)


def fix_path(files, prefix="/usr/share/bb"):
    for fn in files:
        pfn = os.path.join(prefix, fn)
        if os.path.exists(pfn):
            yield pfn
        elif os.path.exists(fn):
            yield fn
        else:
            print(f"Missing file: {fn}", file=sys.stderr)
            sys.exit(1)


if __name__ == "__main__":
    #print(f"ARGS: {sys.argv}", file=sys.stderr)

    mm = MikMod()
    files = list(fix_path(sys.argv[4:]))
    try:
        mm.init()
    except MikModError as e:
        print("Error:", e, file=sys.stderr)

    putc(b"O")

    while True:
        char = sys.stdin.buffer.read(1)
        #if char:
        #    print(f"ROW: {char}", file=sys.stderr)
        if char==b"!":
            break
        elif char==b"S":
            mm.start()
        elif char==b"T":
            mm.stop()
        elif char in [b"0", b"1", b"2"]:
            mm.load(files[int(char)])
            mm.play_thread()
            mm.stop()

    mm.exit()
    mm.stop_thread()
    putc(b"!")
