"""
Jukebox audio file serving backend application
Provides a CLI and a Pyro remote API.

Written by Irmen de Jong (irmen@razorvine.net) - License: GNU LGPL 3.
"""
import cmd
import shlex
import threading
import argparse
import Pyro4
import Pyro4.socketutil
from .musicfiledb import MusicFileDatabase


BACKEND_PORT = 39776    # used when not registering with the Pyro name server


class JukeboxBackendRemoting:
    def __init__(self):
        self.mdb = MusicFileDatabase(silent=True, scan_changes=False)

    def __del__(self):
        if self.mdb:
            self.mdb.close()
            self.mdb = None

    @Pyro4.expose
    def track(self, hashcode=None, track_id=None):
        track = self.mdb.get_track(hashcode, track_id)
        return self.track2dict(track)

    @Pyro4.expose       # type: ignore
    @property
    def num_tracks(self):
        return self.mdb.num_tracks()

    @Pyro4.expose       # type: ignore
    @property
    def total_playtime(self):
        return self.mdb.total_playtime()

    @Pyro4.expose
    def query(self, title=None, artist=None, album=None, year=None, genre=None):
        max_results = 200
        return [self.track2dict(t) for t in self.mdb.query(title, artist, album, year, genre, result_limit=max_results)]

    @Pyro4.expose
    def get_file(self, track_id=None, hashcode=None):
        track = self.mdb.get_track(hashcode, track_id)
        with open(track.location, "rb") as f:
            return f.read()

    @Pyro4.expose
    def get_file_chunks(self, track_id=None, hashcode=None):
        track = self.mdb.get_track(hashcode, track_id)
        with open(track.location, "rb") as f:
            while True:
                chunk = f.read(128 * 1024)
                if not chunk:
                    break
                yield chunk

    def track2dict(self, track):
        result = vars(track)
        result["hash"] = track.hash
        return result


class JukeboxBackendCli(cmd.Cmd):
    def __init__(self, mdb, pyro_uri):
        super().__init__()
        self.mdb = mdb
        self.pyro_uri = pyro_uri
        print("Number of tracks in database:", self.mdb.num_tracks())
        print("Pyro connection uri: ", self.pyro_uri)

    def do_reload(self, args):
        """Reload the whole database."""
        if self.mdb:
            self.mdb.close()
        self.mdb = MusicFileDatabase(scan_changes=False)
        print("Number of tracks in database:", self.mdb.num_tracks())

    def do_quit(self, args):
        """Exits the program."""
        print("Bye.", args)
        return True

    def do_stats(self, args):
        """Prints some stats such as the number of tracks in the database."""
        print("Number of tracks in database: ", self.mdb.num_tracks())
        print("Total play time: ", self.mdb.total_playtime())
        print("Pyro connection uri: ", self.pyro_uri)

    def do_query(self, args):
        """Perform a query on the database. Arguments are:  field=search-value [...]"""
        if not args:
            print("Give at least one field filter.")
            return
        filters = shlex.split(args)
        try:
            filters = {f: v for f, v in (f.split('=') for f in filters)}
        except ValueError:
            print("Query arguments syntax error. Try help for this command.")
            return
        try:
            results = self.mdb.query(**filters)
        except TypeError:
            import inspect
            fields = list(inspect.signature(self.mdb.query).parameters)
            print("Invalid filter field. Valid fields are:", fields)
            return
        except Exception as x:
            print("ERROR:", x)
            return
        print("Found {:d} results. Showing max 6:".format(len(results)))
        for track in results[:6]:
            self.print_track(track, full=False)
            print()

    def print_track(self, track, full=False):
        print("Track #{:d}".format(track.id))
        print("     title:", track.title or "")
        print("    artist:", track.artist or "")
        print("     album:", track.album or "")
        print("      year:", track.year or "")
        print("     genre:", track.genre or "")
        print("  duration:", track.duration)
        if full:
            print("  modified:", track.modified)
            print("      hash:", track.hash)
            print("  location:", track.location)

    def do_path(self, path):
        """Reads the music files or iTunes library in the given path."""
        if not path:
            print("Give a path to scan for music files or iTunes library.")
            return
        self.mdb.update_path(path)

    def do_rescan(self, args):
        """Rescan the files in the database to see if there were changes."""
        self.mdb.scan_changes()

    def do_track(self, track_hash_or_id):
        """Get all information for a single track by id or hash."""
        if not track_hash_or_id:
            print("Give track id or hash.")
            return
        try:
            track = self.mdb.get_track(hashcode=track_hash_or_id)
        except LookupError:
            try:
                track = self.mdb.get_track(track_id=track_hash_or_id)
            except LookupError:
                print("Track not found.")
                return
        self.print_track(track, full=True)


class Backend:
    def __init__(self, scan=True, use_pyro_ns=False, bind_localhost=False):
        self.mdb = MusicFileDatabase(scan_changes=scan)
        host = "localhost" if bind_localhost else Pyro4.socketutil.getIpAddress(None, workaround127=True)
        self.pyro_daemon = Pyro4.Daemon(host=host, port=0 if use_pyro_ns else BACKEND_PORT)
        self.pyro_uri = self.pyro_daemon.register(JukeboxBackendRemoting, "jukebox.backend")
        if use_pyro_ns:
            with Pyro4.locateNS() as ns:
                ns.register("jukebox.backend", self.pyro_uri)
        self.cli = JukeboxBackendCli(self.mdb, self.pyro_uri)

    def run(self):
        pyro_thread = threading.Thread(target=self.pyro_daemon.requestLoop)
        pyro_thread.start()
        try:
            self.cli.cmdloop("Jukebox backend. Enter commands or 'help' for help.")
        except KeyboardInterrupt:
            print("\n<BREAK>")
        except Exception:
            print("\nAn error has occurred:")
            import traceback
            traceback.print_exc()
        print("Jukebox backend is stopping.")
        self.mdb.close()
        self.pyro_daemon.shutdown()
        pyro_thread.join()


if __name__ == "__main__":
    parser = argparse.ArgumentParser(description="Jukebox datatbase backend.")
    parser.add_argument("-noscan", required=False, action="store_true", help="don't scan disk for changes")
    parser.add_argument("-pyrons", required=False, action="store_true", help="use Pyro name server")
    parser.add_argument("-localhost", required=False, action="store_true", help="bind server only on localhost")
    args = parser.parse_args()
    backend = Backend(not args.noscan, args.pyrons, args.localhost)
    backend.run()