#!/usr/bin/python

import sys
import os
import time
import getopt
import signal

import mpdclient2
import lastfm
import lastfm.client
import lastfm.config
import lastfm.marshaller

DAEMON_NAME = 'lastmp'
USAGE = 'usage: %s [--debug] [--no-daemon] [--help]' % DAEMON_NAME

# If the system load is high, we may not wake again until slightly
# longer than we slept. This should really be like 0.1, but
# unfortunately MPD only reports times with a resolution of 1 second.

FUZZ = 1

class NoHostError(Exception): pass
class MPDAuthError(Exception): pass

class Song:
    def __init__(self, sobj):
        self.artist = getattr(sobj, 'artist', '')
        self.title = getattr(sobj, 'title', '')
        self.album = getattr(sobj, 'album', '')
        self.length = int(getattr(sobj, 'time', 0))
        self.file = getattr(sobj, 'file', '')

    def __eq__(self, other):
        if other == None:
            return False
        else:
            return self.file == other.file or \
                (self.artist == other.artist and self.title == other.title and
                self.album == other.album and self.length == other.length)

    # O RLY?
    def __ne__(self, other):
        # YA RLY!
        return not self == other

    def __str__(self):
        try:
            return lastfm.repr(self.dict())
        except ValueError:
            return self.file

    def dict(self):
        d = {'time': time.gmtime(),
            'artist': lastfm.marshaller.guess_enc(self.artist, 'utf-8'),
            'title': lastfm.marshaller.guess_enc(self.title, 'utf-8'),
            'length': self.length}
        if self.album:
            d['album'] = lastfm.marshaller.guess_enc(self.album, 'utf-8')
        for reqd in ('artist', 'title', 'length'):
            if not d[reqd]:
                raise ValueError('%s is missing %s' % (self.file, reqd))
        else:
            return d

class MPDMonitor:
    def __init__(self, cli):
        self.cli = cli
        self.sleep = cli.conf.sleep_time
        self.mpd_args = {'host': cli.conf.host,
            'port': cli.conf.port,
            'password': cli.conf.password}
        self.mpd = None

    def wake(self):
        status = self.mpd.do.status()
        song = Song(self.mpd.do.currentsong())

        if not hasattr(status, 'state'):
            raise MPDAuthError

        if not self.prevstatus or status.state != self.prevstatus.state:
            self.cli.log.debug('Changed state: %s' % status.state)

        if status.state in ('play', 'pause'):
            pos, length = map(float, status.time.split(':'))
            if length == 0: length = lastfm.MAX_LEN

            if status.state == 'play':
                if song != self.prevsong or \
                        self.prevstatus.state == 'stop':
                    self.cli.log.info(u'New song: %s' % song)
                    if (self.prevsong and pos > self.sleep +
                            FUZZ + int(status.xfade)) or \
                            (self.prevsong is None and
                            pos/length > lastfm.SUB_PERCENT or
                            pos > lastfm.SUB_SECONDS):
                        self.cli.log.warning('Started at %d, will not submit'
                            % pos)
                        self.skipped = True
                    else:
                        self.skipped = False
                        self.submitted = False
                        self.played_enough = False
                else:
                    if self.prevpos and pos < self.sleep + FUZZ and \
                            (self.prevpos/length >= lastfm.SUB_PERCENT or \
                            self.prevpos >= lastfm.SUB_SECONDS):
                        self.cli.log.info('Restarted song')
                        self.skipped = False
                        self.submitted = False
                        self.played_enough = False
                    if self.prevpos and \
                            pos > self.prevpos + self.sleep + FUZZ:
                        self.cli.log.warning('Skipped from %d to %d' %
                            (self.prevpos, pos))
                        self.skipped = True
                    if not self.skipped and not self.played_enough and \
                            length >= lastfm.MIN_LEN and \
                            length <= lastfm.MAX_LEN and \
                            (pos >= lastfm.SUB_SECONDS or \
                            pos/length >= lastfm.SUB_PERCENT):
                        self.cli.log.debug('OK, %d/%d played' % (pos, length))
                        self.played_enough = True
                    if not self.submitted and not self.skipped and \
                            self.played_enough:
                        self.submit(song)
                        self.submitted = True
        else:
            pos = None

        self.prevsong = song
        self.prevstatus = status
        self.prevpos = pos

    def observe(self):
        """Loop forever, periodically checking MPD's state and submitting
        songs when necessary."""
        failed = False
        while True:
            try:
                if not self.mpd:
                    self.mpd = mpdclient2.connect(**self.mpd_args)
                    self.cli.log.info('Connected to MPD')
                    self.prevstatus = None
                    self.prevsong = None
                    self.prevpos = None
                    self.skipped = False
                    self.submitted = False
                    self.played_enough = False
                if self.mpd:
                    self.wake()
            except (EOFError, mpdclient2.socket.error):
                if not failed:
                    self.cli.log.error("Can't connect or lost connection to MPD")
                self.mpd = None
                failed = True
            except MPDAuthError:
                if not failed:
                    self.cli.log.error("Can't read info from MPD (bad password?)")
                failed = True

            time.sleep(self.sleep)

    def submit(self, song):
        try:
            self.cli.submit(song.dict())
            self.cli.log.info('Sent to daemon')
        except ValueError, e:
            self.cli.log.error('Missing tags: %s' % e)
        except IOError:
            self.cli.log.error("Can't write sub: %s" % e)

class LmpConfig(lastfm.config.Config):
    def __init__(self):
        lastfm.config.Config.__init__(self, search=DAEMON_NAME)
        self.host = self.cp.get('mpd', 'host', None)
        self.port = int(self.cp.get('mpd', 'port', 6600))
        self.password = self.cp.get('mpd', 'password', None)
        if not self.host:
            raise NoHostError

def daemon(cli):
    mo = MPDMonitor(cli)

    def shutdown(signum, frame):
        cli.cleanup()
        sys.exit(0)

    signal.signal(signal.SIGTERM, shutdown)
    signal.signal(signal.SIGINT, shutdown)

    # Even if we couldn't connect, start the observe loop anyway. Once MPD is
    # up, we will connect.
    mo.observe()

if __name__ == '__main__':
    shortopts = 'dnh'
    longopts = ['debug', 'no-daemon', 'help']

    try:
        opts, args = getopt.getopt(sys.argv[1:], shortopts, longopts)
    except getopt.GetoptError, e:
        print >>sys.stderr, '%s: %s' % (DAEMON_NAME, e)
        print >>sys.stderr, USAGE
        sys.exit(1)

    debug = False
    fork = True
    stderr = False

    for opt, arg in opts:
        if opt in ('--debug', '-d'):
            debug = True
        elif opt in ('--no-daemon', '-n'):
            fork = False
            stderr = True
        elif opt in ('--help', '-h'):
            print USAGE
            sys.exit(0)

    try:
        conf = LmpConfig()
    except NoHostError:
        print >>sys.stderr, '%s: no MPD host specified' % DAEMON_NAME
        sys.exit(1)

    if conf.debug:
        debug = True
    cli = lastfm.client.Daemon(DAEMON_NAME, conf)
    cli.daemonize(fork)
    cli.open_log(debug, stderr)

    try:
        cli.log.info('Starting')
        daemon(cli)
    except SystemExit, e:
        cli.log.info('Exiting')
        sys.exit(e.args[0])
    except:
        import traceback
        einfo = traceback.format_exception(*sys.exc_info())
        cli.log.error('Aborting: %s' % ''.join(einfo))
        sys.exit(1)
