#!/usr/bin/python
# -*- coding: utf-8 -*-

"""
Provides base classes for accessing MythTV
"""

from MythStatic import *
from MythBase import *
from MythData import *

import os, re, socket

class MythTV( MythBEConn ):
    """
    A connection to a MythTV backend.
    """

    locked_tuners = []

    def __del__(self):
        self.freeTuner()
        MythBEConn.__del__(self)

    def getPendingRecordings(self):
        """
        Returns a list of Program objects which are scheduled to be recorded.
        """
        programs = []
        res = self.backendCommand('QUERY_GETALLPENDING').split(BACKEND_SEP)
        has_conflict = int(res.pop(0))
        num_progs = int(res.pop(0))
        #log.Msg(DEBUG, '%s pending recordings', num_progs)
        for i in range(num_progs):
            programs.append(Program(res[i * PROGRAM_FIELDS:(i * PROGRAM_FIELDS)
                + PROGRAM_FIELDS]))
        return tuple(programs)

    def getScheduledRecordings(self):
        """
        Returns a list of Program objects which are scheduled to be recorded.
        """
        programs = []
        res = self.backendCommand('QUERY_GETALLSCHEDULED').split(BACKEND_SEP)
        num_progs = int(res.pop(0))
        #log.Msg(DEBUG, '%s scheduled recordings', num_progs)
        for i in range(num_progs):
            programs.append(Program(res[i * PROGRAM_FIELDS:(i * PROGRAM_FIELDS)
                + PROGRAM_FIELDS]))
        return tuple(programs)

    def getUpcomingRecordings(self):
        """
        Returns a list of Program objects which are scheduled to be recorded.

        Sorts the list by recording start time and only returns those with
        record status of WillRecord.
        """
        def sort_programs_by_starttime(x, y):
            if x.starttime > y.starttime:
                return 1
            elif x.starttime == y.starttime:
                return 0
            else:
                return -1
        programs = []
        res = self.getPendingRecordings()
        for p in res:
            if p.recstatus == RECSTATUS['WillRecord']:
                programs.append(p)
        programs.sort(sort_programs_by_starttime)
        return tuple(programs)

    def getRecorderList(self):
        """
        Returns a list of recorders, or an empty list if none.
        """
        recorders = []
        c = self.db.cursor()
        c.execute('SELECT cardid FROM capturecard')
        row = c.fetchone()
        while row is not None:
            recorders.append(int(row[0]))
            row = c.fetchone()
        c.close()
        return recorders

    def getFreeRecorderList(self):
        """
        Returns a list of free recorders, or an empty list if none.
        """
        res = self.backendCommand('GET_FREE_RECORDER_LIST').split(BACKEND_SEP)
        recorders = [int(d) for d in res]
        return recorders

    def lockTuner(self,id=None):
        """
        Request a tuner be locked from use, optionally specifying which tuner
        Returns a tuple of ID, video dev node, audio dev node, vbi dev node
        Returns an ID of -2 if tuner is locked
                         -1 if no tuner could be found
        """
        local = True
        cmd = 'LOCK_TUNER'
        if id is not None:
            cmd += ' %d' % id
            res = self.getRecorderDetails(id).hostname
            if res != socket.gethostname():
                local = False

        res = ''
        if local:
            res = self.backendCommand(cmd).split(BACKEND_SEP)
        else:
            myth = MythTV(res)
            res = myth.backendCommand(cmd).split(BACKEND_SEP)
            myth.close()
        res[0] = int(res[0])
        if res[0] > 0:
            self.locked_tuners.append(res[0])
        return tuple(res)


    def freeTuner(self,id=None):
        """
        Frees a requested tuner ID
        If no ID given, free all tuners listed as used by this class instance
        """
        def free(self,id):
            res = self.getRecorderDetails(id).hostname
            if res == socket.gethostname():
                self.backendCommand('FREE_TUNER %d' % id)
            else:
                myth = MythTV(res)
                myth.backendCommand('FREE_TUNER %d' % id)
                myth.close()

        if id is None:
            for i in range(len(self.locked_tuners)):
                free(self,self.locked_tuners.pop())
        else:
            try:
                self.locked_tuners.remove(id)
            except:
                pass
            free(self,id)

    def getCurrentRecording(self, recorder):
        """
        Returns a Program object for the current recorders recording.
        """
        res = self.backendCommand('QUERY_RECORDER '+
                BACKEND_SEP.join([str(recorder),'GET_CURRENT_RECORDING']))
        return Program(res.split(BACKEND_SEP))

    def isRecording(self, recorder):
        """
        Returns a boolean as to whether the given recorder is recording.
        """
        res = self.backendCommand('QUERY_RECORDER '+
                BACKEND_SEP.join([str(recorder),'IS_RECORDING']))
        if res == '1':
            return True
        else:
            return False

    def isActiveBackend(self, hostname):
        """
        Returns a boolean as to whether the given host is an active backend
        """
        res = self.backendCommand(BACKEND_SEP.join(\
                    ['QUERY_IS_ACTIVE_BACKEND',hostname]))
        if res == 'TRUE':
            return True
        else:
            return False

    def getRecording(self, chanid, starttime):
        """
        Returns a Program object matching the channel id and start time
        """
        res = self.backendCommand('QUERY_RECORDING TIMESLOT %d %d' \
                        % (chanid, starttime)).split(BACKEND_SEP)
        if res[0] == 'ERROR':
            return None
        else:
            return Program(res[1:])

    def getRecordings(self):
        """
        Returns a list of all Program objects which have already recorded
        """
        programs = []
        res = self.backendCommand('QUERY_RECORDINGS Play').split(BACKEND_SEP)
        num_progs = int(res.pop(0))
        #log.Msg(DEBUG, '%s total recordings', num_progs)
        for i in range(num_progs):
            programs.append(Program(res[i * PROGRAM_FIELDS:(i*PROGRAM_FIELDS)
                + PROGRAM_FIELDS]))
        return tuple(programs)

    def getExpiring(self):
        """
        Returns a tuple of all Program objects nearing expiration
        """
        programs = []
        res = self.backendCommand('QUERY_GETEXPIRING').split(BACKEND_SEP)
        num_progs = int(res.pop(0))
        for i in range(num_progs):
            programs.append(Program(res[i * PROGRAM_FIELDS:(i*PROGRAM_FIELDS)
                + PROGRAM_FIELDS]))
        return tuple(programs)

    def getCheckfile(self,program):
        """
        Returns location of recording in file system
        """
        res = self.backendCommand(BACKEND_SEP.join(\
                        ['QUERY_CHECKFILE','1',program.toString()]\
                    )).split(BACKEND_SEP)
        if res[0] == 0:
            return None
        else:
            return res[1]

    def deleteRecording(self,program,force=False):
        """
        Deletes recording, set 'force' true if file cannot be found
        Returns the number fewer recordings that exist afterwards
        """
        command = 'DELETE_RECORDING'
        if force:
            command = 'FORCE_DELETE_RECORDING'
        return self.backendCommand(BACKEND_SEP.join(\
                    [command,program.toString()]))

    def forgetRecording(self,program):
        """
        Forgets old recording and allows it to be re-recorded
        """
        self.backendCommand(BACKEND_SEP.join(['FORGET_RECORDING',
                    program.toString()]))

    def deleteFile(self,file,sgroup):
        """
        Deletes a file from specified storage group on the connected backend
        Takes a relative file path from the root of the storage group
        Returns 1 on success
        """
        return self.backendCommand(BACKEND_SEP.join(\
                    ['DELETE_FILE',file,sgroup]))

    def getFreeSpace(self,all=False):
        """
        Returns a tuple of FreeSpace objects
        """
        command = 'QUERY_FREE_SPACE'
        if all:
            command = 'QUERY_FREE_SPACE_LIST'
        res = self.backendCommand(command).split(BACKEND_SEP)
        dirs = []
        for i in range(0,len(res)/10):
            dirs.append(res[i*10:i*10+18])
        return tuple(dirs)

    def getFreeSpaceSummary(self):
        """
        Returns a tuple of total space (in KB) and used space (in KB)
        """
        res = self.backendCommand('QUERY_FREE_SPACE_SUMMARY')\
                    .split(BACKEND_SEP)
        return (self.joinInt(int(res[0]),int(res[1])),
                self.joinInt(int(res[2]),int(res[3])))

    def getLoad(self):
        """
        Returns a tuple of the 1, 5, and 15 minute load averages
        """
        res = self.backendCommand('QUERY_LOAD').split(BACKEND_SEP)
        return (float(res[0]),float(res[1]),float(res[2]))

    def getUptime(self):
        """
        Returns machine uptime in seconds
        """
        return self.backendCommand('QUERY_UPTIME')

    def getSGList(self,host,sg,path,filenamesonly=False):
        """
        Returns a tuple of directories, files, and filesizes.
        Two special modes
            'filenamesonly' returns only filenames, no directories or size
            empty path base directories returns base storage group paths
        """
        if filenamesonly: path = '' # force empty path
        res = self.backendCommand(BACKEND_SEP.join(\
                    ['QUERY_SG_GETFILELIST',host,sg,path,
                    str(int(filenamesonly))]\
                )).split(BACKEND_SEP)
        if res[0] == 'EMPTY LIST':
            return -1
        if res[0] == 'SLAVE UNREACHABLE: ':
            return -2
        dirs = []
        files = []
        sizes = []
        for entry in res:
            if filenamesonly:
                files.append(entry)
            elif path == '':
                type,name = entry.split('::')
                dirs.append(name)
            else:
                type,name,size = entry.split('::')
                if type == 'file':
                    files.append(name)
                    sizes.append(size)
                if type == 'dir':
                    dirs.append(name)
        if filenamesonly:
            return tuple(files)
        elif path == '':
            return tuple(dirs)
        else:
            return (tuple(dirs),tuple(files),tuple(sizes))

    def getSGFile(self,host,sg,path):
        """
        Returns a tuple of last modification time and file size
        """
        res = self.backendCommand(BACKEND_SEP.join(\
                ['QUERY_SG_FILEQUERY',host,sg,path])).split(BACKEND_SEP)
        if res[0] == 'EMPTY LIST':
            return -1
        if res[0] == 'SLAVE UNREACHABLE: ':
            return -2
        return tuple(res[1:3])

    def getLastGuideData(self):
        """
        Returns the last dat for which guide data is available
        On error, 0000-00-00 00:00 is returned
        """
        return self.backendCommand('QUERY_GUIDEDATATHROUGH')


class Frontend(object):
    isConnected = False
    socket = None
    host = None
    port = None

    def __init__(self, host, port):
        self.host = host
        self.port = int(port)
        self.connect()
        self.disconnect()

    def __del__(self):
        if self.isConnected:
            self.disconnect()

    def __repr__(self):
        return "%s@%d" % (self.host, self.port)

    def __str__(self):
        return "%s@%d" % (self.host, self.port)

    def close(self): self.__del__()

    def connect(self):
        self.socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
        self.socket.settimeout(10)
        self.socket.connect((self.host, self.port))
        if re.search("MythFrontend Network Control.*",self.recv()) is None:
            self.socket.close()
            self.socket = None
            raise Exception('FrontendConnect',
                'Connected socket does not belong to a mythfrontend')
        self.isConnected = True

    def disconnect(self):
        self.send("exit")
        self.socket.close()
        self.socket = None
        self.isConnected = False

    def send(self,command):
        if not self.isConnected:
            self.connect()
        self.socket.send("%s\n" % command)

    def recv(self):
        curstr = ''
        prompt = re.compile('([\r\n.]*)\r\n# ')
        while not prompt.search(curstr):
            try:
                curstr += self.socket.recv(100)
            except socket.error:
                raise MythError('Frontend has closed connection')
            except KeyboardInterrupt:
                raise
        return prompt.split(curstr)[0]

    def sendJump(self,jumppoint):
        """
        Sends jumppoint to frontend
        """
        self.send("jump %s" % jumppoint)
        if self.recv() == 'OK':
            return 0
        else:
            return 1

    def getJump(self):
        """
        Returns a tuple containing available jumppoints
        """
        self.send("help jump")
        return tuple(re.findall('(\w+)[ ]+- ([\w /,]+)',self.recv()))

    def sendKey(self,key):
        """
        Sends keycode to connected frontend
        """
        self.send("key %s" % key)
        if self.recv() == 'OK':
            return 0
        else:
            return 1

    def getKey(self):
        """
        Returns a tuple containing available special keys
        """
        self.send("help key")
        return tuple(self.recv().split('\r\n')[4].split(', '))

    def sendQuery(self,query):
        """
        Returns query from connected frontend
        """
        self.send("query %s" % query)
        return self.recv()

    def getQuery(self):
        """
        Returns a tuple containing available queries
        """
        self.send("help query")
        return tuple(re.findall('query ([\w ]*\w+)[ \r\n]+- ([\w /,]+)',
                        self.recv()))

    def sendPlay(self,play):
        """
        Send playback command to connected frontend
        """
        self.send("play %s" % play)
        if self.recv() == 'OK':
            return 0
        else:
            return 1

    def getPlay(self):
        """
        Returns a tuple containing available playback commands
        """
        self.send("help play")
        return tuple(re.findall('play ([\w -:]*\w+)[ \r\n]+- ([\w /:,\(\)]+)',
                        self.recv()))


class MythDB( MythDBConn ):
    """
    Several canned queries for general purpose access to the database
    """
    settings = {}

    def getAllSettings(self, hostname=None):
        """
        Returns values for all settings.

        Returns None if there are no settings. If multiple rows are
        found (multiple hostnames), returns the value of the first one.
        """
        #log.Msg(DEBUG, 'Retrieving all setting for host %s', hostname)
        c = self.db.cursor()
        if hostname is None:
            hostname = 'NULL'
            c.execute("""
                    SELECT value, data
                    FROM settings
                    WHERE hostname IS NULL""")
        else:
            c.execute("""
                    SELECT value, data
                    FROM settings
                    WHERE hostname LIKE(%s)""",
                    (hostname))
        rows = c.fetchall()
        c.close()

        if rows:
            for row in rows:
                if str(hostname) not in self.settings:
                    self.settings[hostname] = {}
                if row[0] not in self.settings[str(hostname)]:
                    self.settings[hostname][row[0]] = row[1]
            return rows
        else:
            return None

    def searchRecorded(self, title=None, subtitle=None, chanid=None,
                        starttime=None, category=None, hostname=None,
                        autoexpire=None, commflagged=None, stars=None,
                        recgroup=None, playgroup=None, duplicate=None,
                        transcoded=None, watched=None, storagegroup=None,
                        cast=None):
        """
        Tries to find recordings matching the given information.
        Returns a tuple of Recorded objects
        """

        where = []
        fields = []
        tables = ['recorded']
        if title:
            where.append('recorded.title=%s')
            fields.append(title)
        if subtitle:
            where.append('recorded.subtitle=%s')
            fields.append(subtitle)
        if chanid:
            where.append('recorded.chanid=%s')
            fields.append(int(chanid))
        if starttime:
            where.append('recorded.starttime=%s')
            fields.append(starttime)
        if category:
            where.append('recorded.category=%s')
            fields.append(category)
        if hostname:
            where.append('recorded.hostname=%s')
            fields.append(host)
        if autoexpire:
            where.append('recorded.autoexpire=%s')
            fields.append(autoexpire)
        if commflagged:
            where.append('recorded.commflagged=%s')
            fields.append(commflagged)
        if stars:
            where.append('recorded.stars=%s')
            fields.append(stars)
        if recgroup:
            where.append('recorded.recgroup=%s')
            fields.append(recgroup)
        if playgroup:
            where.append('recorded.playgroup=%s')
            fields.append(playgroup)
        if duplicate:
            where.append('recorded.duplicate=%s')
            fields.append(duplicate)
        if transcoded:
            where.append('recorded.transcoded=%s')
            fields.append(transcoded)
        if watched:
            where.append('recorded.watched=%s')
            fields.append(watched)
        if storagegroup:
            where.append('recorded.storagegroup=%s')
            fields.append(storagegroup)
        if cast:
            tables.append('recordedcredits')
            where.append('recordedcredits.chanid=recorded.chanid')
            where.append('recordedcredits.starttime=recorded.starttime')
            fields.append(cast)
            if isinstance(cast,int):
                where.append('videometadatacast.person=%s')
            else:
                tables.append('people')
                where.append('recordedcredits.person=people.person')
                where.append('people.name=%s')

        if len(where) == 0:
            return None
        where  = ' AND '.join(where)
        tables = ','.join(tables)

        c = self.cursor()
        c.execute("""SELECT recorded.* FROM %s
                    WHERE %s""" % (tables,where), fields)
        rows = c.fetchall()
        c.close()

        if len(rows) == 0:
            return None
        records = []
        for row in rows:
            records.append(Recorded(data=row,db=self))
        if len(records):
            return tuple(records)
        else:
            return None

    def getFrontends(self):
        """
        Returns a list of Frontend objects for accessible frontends
        """
        cursor = self.db.cursor()
        cursor.execute("""  SELECT DISTINCT hostname
                            FROM settings
                            WHERE hostname IS NOT NULL
                            AND value='NetworkControlEnabled'
                            AND data=1""")
        frontends = []
        for fehost in cursor.fetchall():
            try:
                frontend = self.getFrontend(fehost[0])
                frontends.append(frontend)
            except:
                print "%s is not a valid frontend" % fehost[0]
        cursor.close()
        return frontends

    def getFrontend(self,host):
        """
        Returns a Frontend object for the specified host
        """
        port = self.db.setting[host].NetworkControlPort
        return Frontend(host,port)

    def getRecorded(self, title=None, subtitle=None, chanid=None,
                        starttime=None):
        """
        Tries to find a recording matching the given information.
        Returns a Recorded object.
        """
        records = self.searchRecorded(title,subtitle,season,\
                                episode,host,file,exactfile)
        if records:
            return records[0]
        else:
            return None

    def getChannels(self):
        """
        Returns a tuple of channel object defined in the database
        """
        channels = []
        c = self.db.cursor()
        c.execute("""SELECT * FROM channel""")
        for row in c.fetchall():
            channels.append(Channel(row,self))
        c.close()
        return tuple(channels)

    def getChannel(self,chanid):
        """
        Returns a single channel object for the given chanid
        """
        c = self.db.cursor()
        if c.execute("""SELECT * FROM channel
                WHERE chanid=%s""", (chanid,)):
            return Channel(c.fetchone(),self)
        else:
            return None

    def getGuideData(self, chanid, date):
        """
        Returns tuple of guide data for one channel on one date.
        Date is in the format YYYYMMDD.
        """
        guide = []
        c = self.db.cursor()
        c.execute("""SELECT * FROM program
                WHERE chanid=%s AND DATE(starttime)=%s""",
                (chanid,date))
        for show in c.fetchall():
            guide.append(Guide(show,self))
        c.close()
        return tuple(guide)


class MythVideo( MythDBConn ):
    """
    Provides convenient methods to access the MythTV MythVideo database.
    """
    def __init__(self,db=None):
        """
        Initialise the MythDB connection.
        """
        MythDBConn.__init__(self,db)

        # check schema version
        sver = self._check_schema('mythvideo.DBSchemaVer',MVSCHEMA_VERSION)
        if sver:
            #log.Msg(CRITICAL,
            #        'DB speaks schema version %d, but we speak version %d',
            #        sver, MVSCHEMA_VERSION)
            self.close()
            raise MythError('Mismatched schema version')

    def pruneMetadata(self):
        """
        Removes metadata from the database for files that no longer exist.
        Respects 'Videos' storage groups and relative paths.
        """
        c = self.cursor()
        c.execute("""SELECT DISTINCT host FROM videometadata""")
        hosts = c.fetchall()
        c.close()

        for h in hosts:
            host = h[0]
            myth = None
            if host is None:
                if self.getVideoSG(local=True) is None:
                    continue    # no local video directories to process
            else:
                try:
                    myth = MythTV(host)
                except MythError:
                    continue    # cannot connect to remote backend for scan

            videos = self.searchVideos(host=host)
            bases = self.getVideoSG(host)
            paths = {}
            for vid in videos:
                if host is None:    # check for local videos
                    if not os.path.exists(vid.filename):
                        vid.delete()
                    continue

                # scan available storage group paths
                vpath = '/'+vid.filename
                vfile = vpath[vpath.rfind('/')+1:]
                vpath = vpath[:vpath.rfind('/')]

                if vpath not in paths:
                    files = []
                    for b in bases:
                        d,f,s = myth.getSGList(host,'Videos',b+vpath)
                        files.extend(f)
                    paths[vpath] = files
                if vfile not in paths[vpath]:
                    print u'deleting '+str(vid)
                    vid.delete()


    def searchVideos(self, title=None, subtitle=None, season=None,
                        episode=None, host=None, file=None, exactfile=False,
                        cast=None, genre=None, country=None, category=None):
        """
        Tries to find videos matching the given information.
        Returns a tuple of Video objects
        """
        if host:
            if self.getVideoSG(host):
                host = host.lower()
            else:
                host = None
        where = []
        fields = []
        tables = ['videometadata']
        if title:
            where.append('videometadata.title=%s')
            fields.append(title)
        if subtitle:
            where.append('videometadata.subtitle=%s')
            fields.append(subtitle)
        if season:
            where.append('videometadata.season=%s')
            fields.append(int(season))
        if episode:
            where.append('videometadata.episode=%s')
            fields.append(int(episode))
        if host:
            where.append('videometadata.host=%s')
            fields.append(host)
        if file:
            if exactfile:
                where.append('videometadata.filename=%s')
                fields.append(file)
            else:
                where.append('videometadata.filename LIKE %s')
                fields.append('%'+file)
        if cast:
            tables.append('videometadatacast')
            where.append('videometadata.intid=videometadatacast.idvideo')
            fields.append(cast)
            if isinstance(cast,int):
                where.append('videometadatacast.idcast=%s')
            else:
                tables.append('videocast')
                where.append('videometadatacast.idcast=videocast.intid')
                where.append('videocast.cast=%s')
        if genre:
            tables.append('videometadatagenre')
            where.append('videometadata.intid=videometadatagenre.idvideo')
            fields.append(genre)
            if isinstance(genre,int):
                where.append('videometadatagenre.idgenre=%s')
            else:
                tables.append('videogenre')
                where.append('videometadatagenre.idgenre=videogenre.intid')
                where.append('videogenre.genre=%s')
        if country:
            tables.append('videometadatacountry')
            where.append('videometadata.intid=videometadatacountry.idvideo')
            fields.append(country)
            if isinstance(country,int):
                where.append('videometadatacountry.idcountry=%s')
            else:
                tables.append('videocontry')
                where.append('videometadatacountry.idcountry=videocountry.intid')
                where.append('videocast.country=%s')
        if category:
            fields.append(category)
            if isinstance(category,int):
                where.append('vidoemetadata.category=%s')
            else:
                tables.append('videocategory')
                where.append('videometadata.category=videocategory.intid')
                where.append('videocategory.category=%s')

        if len(where) == 0:
            return None
        where  = ' AND '.join(where)
        tables = ','.join(tables)

        c = self.cursor()
        c.execute("""SELECT videometadata.* FROM %s
                    WHERE %s""" % (tables,where), fields)
        rows = c.fetchall()
        c.close()

        if len(rows) == 0:
            return None
        videos = []
        for row in rows:
            videos.append(Video(data=row,db=self))
        if len(videos):
            return tuple(videos)
        else:
            return None

    def getVideo(self, title=None, subtitle=None, season=None,
                        episode=None, host=None, file=None, exactfile=False):
        """
        Tries to find a video matching the given information.
        Returns a Video object.
        """
        videos = self.searchVideos(title,subtitle,season,\
                                episode,host,file,exactfile)
        if videos:
            return videos[0]
        else:
            return None

