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

"""
Provides data access classes for accessing and managing MythTV data
"""

from MythStatic import *
from MythBase import *

import re, sys, socket, os
from time import mktime, strftime
from socket import gethostbyaddr, gethostname

def ftopen(file, type, forceremote=False):

    def _openRemote(host, filename, sgroup, type):
        if isinstance(sgroup, StorageGroup):
            sgroup = sgroup.groupname
        return FileTransfer(host, filename, sgroup, type)

    def _openLocal(host, filename, sgroup, type):
        if not isinstance(sgroup, StorageGroup):
            sgroup = self.db.getStorageGroup(groupname=sgroup, hostname=host)[0]
        return open(sgroup.dirname+filename, type)

    db = MythDBConn()
    reuri = re.compile(\
        'myth://((?P<group>.*)@)?(?P<host>[a-zA-Z0-9\.]*)(:[0-9]*)?/(?P<file>.*)')
    reip = re.compile('(?:\d{1,3}\.){3}\d{1,3}')

    if isinstance(file, Program):
        # process Program object
        file = file.filename
    elif isinstance(file, Recorded):
        # process Recorded object
        file = (file.hostname, file.basename, file.storagegroup)

    if isinstance(file, tuple) or isinstance(file, list):
        # process tuple (host, filename, storagegroup)
        if len(file) != 3:
            #log.Msg(CRITICAL, 'Incorrect FileTransfer() input size')
            raise MythError('Incorrect FileTransfer input tuple size: %d'\
                    % len(file))
        (host, filename, sgroup) = file
    elif isinstance(file, str):
        # process URI (myth://<group>@<host>[:<port>]/<path/to/file>)
        match = reuri.match(file)
        if match is None:
            #log.Msg(...)
            raise MythError('Invalid FileTransfer input string: '+file)
        host = match.group('host')
        filename = match.group('file')
        sgroup = match.group('group')
        if sgroup is None:
            sgroup = 'Default'
    else:
        #log.Msg(...)
        raise MythError('Invalid FileTransfer input type; %s' %type(file))

    # remote leading slash if exists
    filename = filename.lstrip('/')

    # get full system name
    if reip.match(host):
        host = gethostbyaddr(host)[0]

    # user forced to remote access
    if forceremote:
        return _openRemote(host, filename, sgroup, type)

    if isinstance(sgroup, StorageGroup):
        sgs = [sgroup]
    else:
        sgs = db.getStorageGroup(groupname=sgroup, hostname=host)

    if type == 'w':
        # force remote, let mythtv decide how to disk balance
        if (len(sgs) > 1):
            return _openRemote(host, filename, sgroup, type)
        # group not local, use remote access
        if not sgs[0].local:
            return _openRemote(host, filename, sgs[0], type)
        # open local file
        return _openLocal(host, filename, sgs[0], type)
    else:
        for sg in sgs:
            if sg.local:
                if os.access(sg.dirname+filename, os.F_OK):
                    # file found, open local
                    return _openLocal(host, filename, sg, type)
        # file not found, open remote
        return _openRemote(host, filename, sgroup, type)

class FileTransfer( MythBEConn ):
    """
    A connection to mythbackend intended for file transfers
    """

    def __init__(self, host, filename, sgroup, type):
        if type not in ('r','w'):
            raise MythError("FileTransfer type must be read ('r') "+
                                "or write ('w')")

        self.host = host
        self.filename = filename
        self.sgroup = sgroup
        self.tsize = 2**15
        self.open = False
        # create control socket
        self.control = MythBEConn(host,'Playback')
        # continue normal Backend initialization
        MythBEConn.__init__(self,host,type)
        self.open = True

    def announce(self, type):
        # replacement announce for Backend object
        if type == 'w':
            self.w = True
        elif type == 'r':
            self.w = False
        res = self.backendCommand('ANN FileTransfer %s %d %d %s' \
                    % (socket.gethostname(), self.w, False,
                        BACKEND_SEP.join(['-1',self.filename,self.sgroup])))
        if res.split(BACKEND_SEP)[0] != 'OK':
            #log.Msg(CRITICAL, 'Unexpected answer to ANN command: %s', res)
            raise MythError('Unexpected answer to ANN command: %s' % res)
        else:
            #log.Msg(INFO, 'Successfully connected mythbackend at %s:%d',
            #            self.host, self.port)
            sp = res.split(BACKEND_SEP)
            self.sockno = int(sp[1])
            self.pos = 0
            self.size = (int(sp[2]) + (int(sp[3])<0))*2**32 + int(sp[3])

    def __del__(self):
        if not self.open:
            return
        self.control.backendCommand('QUERY_FILETRANSFER '\
                    +BACKEND_SEP.join([str(self.sockno), 'JOIN']))
        self.socket.shutdown(1)
        self.socket.close()
        self.open = False

    def tell(self):
        """
        Return the current offset from the beginning of the file
        """
        return self.pos

    def close(self):
        """
        Close the data transfer socket
        """
        self.__del__()

    def rewind(self):
        """
        Seek back to the start of the file
        """
        self.seek(0)

    def read(self, size):
        """
        Read a block of data, requests over 64KB will be buffered internally
        """
        if self.w:
            print 'Error: attempting to read from a write-only socket'
            return
        if size == 0:
            return ''
        if size > self.size - self.pos:
            size = self.size - self.pos
        csize = size
        rsize = 0
        if csize > self.tsize:
            csize = self.tsize
            rsize = size - csize

        res = self.control.backendCommand('QUERY_FILETRANSFER '+BACKEND_SEP\
                    .join([str(self.sockno),'REQUEST_BLOCK',str(csize)]))
        self.pos += int(res)
#       if int(res) == csize:
#           if csize < size:
#               self.tsize += 8192
#       else:
#           self.tsize -= 8192
#           rsize = size - int(res)
#       print 'resizing buffer to %d' % self.tsize

        return self.socket.recv(int(res)) + self.read(rsize)

    def write(self, data):
        """
        Write a block of data, requests over 64KB will be buffered internally
        """
        if not self.w:
            print 'Error: attempting to write to a read-only socket'
            return
        size = len(data)
        buff = ''
        if size == 0:
            return
        if size > self.tsize:
            size = self.tsize
            buff = data[size:]
            data = data[:size]
        self.pos += int(self.socket.send(data))
        self.control.backendCommand('QUERY_FILETRANSFER '+BACKEND_SEP\
                    .join([str(self.sockno),'WRITE_BLOCK',str(size)]))
        self.write(buff)
        return


    def seek(self, offset, whence=0):
        """
        Seek 'offset' number of bytes
           whence==0 - from start of file
                   whence==1 - from current position
        """
        if whence == 0:
            if offset < 0:
                offset = 0
            if offset > self.size:
                offset = self.size
        elif whence == 1:
            if offset + self.pos < 0:
                offset = -self.pos
            if offset + self.pos > self.size:
                offset = self.size - self.pos
        elif whence == 2:
            if offset > 0:
                offset = 0
            if offset < -self.size:
                offset = -self.size
        #else:
            #log.Msg(CRITICAL, 'Whence can only be 0, 1, or 2')

        curhigh,curlow = self.splitInt(self.pos)
        offhigh,offlow = self.splitInt(offset)

        res = self.control.backendCommand('QUERY_FILETRANSFER '+BACKEND_SEP\
                        .join([str(self.sockno),'SEEK',str(offhigh),
                                str(offlow),str(whence),str(curhigh),
                                str(curlow)])\
                    ).split(BACKEND_SEP)
        self.pos = self.joinInt(res[0],res[1])


class FreeSpace(DictData):
    """
    Represents a Storage Group entry
    """
    field_order = [ 'host',         'path',     'islocal',
                    'disknumber',   'sgroupid', 'blocksize',
                    'ts_high',      'ts_low',   'us_high',
                    'us_low']
    field_type = [3, 3, 2, 0, 0, 0, 0, 0, 0, 0]
    def __str__(self):
        return "Storage Group %d (%s@%s)"\
                    % (self.sgroupid, self.path, self.host)

    def __repr__(self):
        return "Storage Group %d (%s@%s)"\
                    % (self.sgroupid, self.path, self.host)

    def __init__(self, data):
        DictData.__init__(self, data)
        self.totalspace = self.joinInt(self.ts_high, self.ts_low)
        self.usedspace = self.joinInt(self.us_high, self.us_low)
        self.freespace = self.totalspace - self.usedspace


class Program(DictData):
    """
    Represents a program with all the detail known.
    """

    # field names and type in order
    #   0: means integer
    #   1: means float
    #   2: means bool
    #   3: means string
    #   4: means date
    field_order = [ 'title',        'subtitle',     'description',
                    'category',     'chanid',       'channum',
                    'callsign',     'channame',     'filename',
                    'fs_high',      'fs_low',       'starttime',
                    'endtime',      'duplicate',    'shareable',
                    'findid',       'hostname',     'sourceid',
                    'cardid',       'inputid',      'recpriority',
                    'recstatus',    'recordid',     'rectype',
                    'dupin',        'dupmethod',    'recstartts',
                    'recendts',     'repeat',       'programflags',
                    'recgroup',     'commfree',     'outputfilters',
                    'seriesid',     'programid',    'lastmodified',
                    'stars',        'airdate',      'hasairdate',
                    'playgroup',    'recpriority2', 'parentid',
                    'storagegroup', 'audio_props',  'video_props',
                    'subtitle_type','year']
    field_type = [  3,      3,      3,
                    3,      0,      3,
                    3,      3,      3,
                    0,      0,      4,
                    4,      0,      0,
                    0,      3,      0,
                    0,      0,      0,
                    0,      0,      3,
                    0,      0,      4,
                    4,      0,      3,
                    3,      0,      3,
                    3,      3,      3,
                    1,      3,      0,
                    3,      0,      3,
                    3,      0,      0,
                    0,      0]
    def __str__(self):
        return u"%s (%s)" % (self.title,
                    self.starttime.strftime('%Y-%m-%d %H:%M:%S'))

    def __repr__(self):
        return u"%s (%s)".encode('utf-8') % (self.title,
                    self.starttime.strftime('%Y-%m-%d %H:%M:%S'))

    def __init__(self, data):
        DictData.__init__(self, data)
        self.filesize = self.joinInt(self.fs_high,self.fs_low)

    def toString(self):
        data = []
        for i in range(0,PROGRAM_FIELDS):
            if self.data[self.field_order[i]] == None:
                datum = ''
            elif self.field_type[i] in (0,1,3):
                datum = str(self.data[self.field_order[i]])
            elif self.field_type[i] == 2:
                datum = str(int(self.data[self.field_order[i]]))
            elif self.field_type[i] == 4:
                datum = str(int(mktime(\
                        self.data[self.field_order[i]].timetuple())))
            data.append(datum)
        return BACKEND_SEP.join(data)

    def getRecorded(self):
        """
        Return a Recorded object for manipulation of recording
        """
        return Recorded((self.chanid,self.recstartts))

    def open(self, type='r'):
        if type != 'r':
            MythError('Program only supports read-only access to files')
        return ftopen(self, 'r')


class Recorded( DBDataWrite ):
    """
    Class for managing recordings
    """
    table = 'recorded'
    where = 'chanid=%s AND starttime=%s'

    class _Cast( DBDataCRef ):
        table = 'people'
        ref = 'recordedcredits'
        t_field = 'person'
        t_add = ['name']
        r_field = 'person'
        r_add = ['role']
        w_field = ['chanid','starttime']
        def __repr__(self):
            if len(self.data) == 0:
                return 'No cast'
            cast = []
            for cref in self.field_order:
                cast.append(self.data[cref].name)
            return ', '.join(cast)

    def __str__(self):
        return u"%s (%s)" % (self.title,
                    self.starttime.strftime('%Y-%m-%d %H:%M:%S'))

    def __repr__(self):
        return u"%s (%s)".encode('utf-8') % (self.title,
                    self.starttime.strftime('%Y-%m-%d %H:%M:%S'))

    def __init__(self, data=None, db=None):
        """
        Creates a new recording object.
        Should be supplied with database if possible.
        Can be initialized in three manners:
            data = (int(chanid),int(starttime)):
                    initialize object to existing entry in recorded
            data = list:
                   use data from a row returned from 'select * from recorded'
            data = dictionary:
                    initialize object to new recording, using dictionary
        """
        self._setDefs()
        self.db = MythDBConn(db)
        self.field_order = self.db.tablefields[self.table]

        if isinstance(data,tuple) or isinstance(data,list):
            if len(data) == 2:
                self.wheredat = tuple(data)
                self._pull()
            elif len(data) == len(self.field_order):
                self._import(data)
                self.wheredat = (self.chanid,self.starttime)
            else:
                raise MythError('Incorrect list input length to Recorded()')
        elif isinstance(data,dict):
            self.create(data)
            self.wheredat = (data.chanid,data.starttime)
            self._pull()
        else:
            raise MythError('Invalid input type to Recorded(): '+str(type(data)))
        if self.data is None:
            raise MythError('Failed to read Recorded() data:'+str(self.wheredat))
        self.cast = self._Cast(self.wheredat,self.db)

    def delete(self, rerecord=False):
        if rerecord:
            c = self.db.cursor()
            c.execute("""UPDATE %s SET duplicate=0 WHERE %s"""\
                % ('oldrecorded',self.where), self.wheredat)
            c.close()
        # call for deletion over backend socket

    def open(self, type='r'):
        return ftopen(self, type)


class Job( DBDataWrite ):
    """
    Class for managing tasks within mythtv's jobqueue
    """
    # types
    NONE         = 0x0000
    SYSTEMJOB    = 0x00ff
    TRANSCODE    = 0x0001
    COMMFLAG     = 0x0002
    USERJOB      = 0xff00
    USERJOB1     = 0x0100
    USERJOB2     = 0x0200
    USERJOB3     = 0x0300
    USERJOB4     = 0x0400
    # cmds
    RUN          = 0x0000
    PAUSE        = 0x0001
    RESUME       = 0x0002
    STOP         = 0x0004
    RESTART      = 0x0008
    # flags
    NO_FLAGS     = 0x0000
    USE_CUTLIST  = 0x0001
    LIVE_REC     = 0x0002
    EXTERNAL     = 0x0004
    # statuses
    UNKNOWN      = 0x0000
    QUEUED       = 0x0001
    PENDING      = 0x0002
    STARTING     = 0x0003
    RUNNING      = 0x0004
    STOPPING     = 0x0005
    PAUSED       = 0x0006
    RETRY        = 0x0007
    ERRORING     = 0x0008
    ABORTING     = 0x0008
    DONE         = 0x0100
    FINISHED     = 0x0110
    ABORTED      = 0x0120
    ERRORED      = 0x0130
    CANCELLED    = 0x0140

    table = 'jobqueue'
    def __init__(self, data=None, db=None):
        """
        Creates a new job object.
        Should be supplied with database if possible.
        Can be initialized in three manners:
            data = int(jobid) ---or--- data = (int(chanid),int(starttime)):
                    initialize object to existing entry in jobqueue
            data = dictionary:
                    initialize object to new job, using data from dictionary
        """
        self._setDefs()
        self.db = MythDBConn(db)
        self.field_order = self.db.tablefields[self.table]

        if isinstance(data,int):
            self.where = 'id=%s'
            self.wheredat = (data,)
        elif isinstance(data,tuple) or isinstance(data,list):
            if len(data) != 2:
                raise MythError('Incorrect list input length to Job()')
            self.where = 'chanid=%s AND starttime=%s'
            self.wheredat = tuple(data)
        elif isinstance(data,dict):
            self.where = 'id=%s'
            self.wheredat = (self.create(data),)
        else:
            raise MythError('Invalid input type to Job(): '+str(type(data)))
        self._getData()
        if self.data is None:
            raise MythError('Failed to read Job() data: ' + str(self.wheredat))

    def setComment(self,comment):
        self.comment = comment
        self.update()

    def setStatus(self,status):
        self.status = status
        self.update()

class Channel(DBDataWrite):
    """
    Represents a single channel from the channel table
    """
    table = 'channel'
    where = 'chanid=%s'
    
    def __str__(self):
        return u"%s (%s)" % (self.chanid, self.name)

    def __repr__(self):
        return u"%s (%s)".encode('utf-8') % (self.chanid, self.name)

    def __init__(self,data,db=None):
        """
        Creates a new channel object.
        Should be supplied with database if possible.
        Can be initialized in three manners:
            data = chanid:
                    initialize object to existing entry in recorded
            data = list/tuple:
                   use data from a row returned from 'select * from channel'
            data = dictionary:
                    initialize object to new recording, using dictionary
        """
        self._setDefs()
        self.db = MythDBConn(db)
        self.field_order = self.db.tablefields[self.table]

        if isinstance(data, int) or isinstance(data, str):
            self.wheredat = (int(data))
            self._pull()
        elif isinstance(data,tuple) or isinstance(data,list):
            if len(data) == len(self.field_order):
                self._import(data)
                self.wheredat = (self.chanid)
            else:
                raise MythError('Incorrect list input length to Recorded()')
        elif isinstance(data,dict):
            self.create(data)
            self.wheredat = (data.chanid)
            self._pull()
        else:
            raise MythError('Invalid input type to Channel(): '+str(type(data)))
        if self.data is None:
            raise MythError('Failed to read Channel() data:'+str(self.wheredat))
    
#    def openIcon(self, mode='r'):
    

class Guide(DBData):
    """
    Represents a single program from the program guide
    """
    table = 'program'
    where = 'chanid=%s AND starttime=%s'
    
    def __str__(self):
        return u"%s (%s)" % (self.title,
                self.starttime.strftime('%Y-%m-%d %H:%M:%S'))

    def __repr__(self):
        return u"%s (%s)".encode('utf-8') % (self.title,
                self.starttime.strftime('%Y-%m-%d %H:%M:%S'))

    def __init__(self,data,db=None):
        DBData.__init__(self,data,db)
        self.wheredat = (self.chanid,self.starttime)



class Video( DBDataWrite ):
    """
    Class for managing and creating metadata for a single video file
    """
    field_type = 'Pass'
    table = 'videometadata'
    where = 'intid=%s'
    defaults = {'subtitle':u'',             'director':u'Unknown',
                'rating':u'NR',             'inetref':u'00000000',
                'year':1895,                'userrating':0.0,
                'length':0,                 'showlevel':1,
                'coverfile':u'No Cover',    'host':u'',
                'intid':None}

    def __init__(self, id=None, data=None, db=None):
        """
        Creates a new video object.
        Should be supplied with existing database object if possible.
        Can be initialized in three manners, or left uninitialized.
            id = intid, data = None:
                    initialize object to an existing video entry
            id = intid, data = dictionary:
                    initialize object to an existing video entry
                    and update with info in dictionary
            id = none, data = list:
                    initialize object to an existing video entry
                    given by a row returned from 'select * from videometadata'
            id = none, data = dictionary:
                    initialize object to a new video entry
                    using info from dictionary
            id = none, data = none:
                leave object empty
        """
        self._setDefs()
        self.db = MythDBConn(db)
        self.field_order = self.db.tablefields[self.table]
        
        if id:
            self.wheredat = (id,)
            self._pull()
            if len(self.data) == 0:
                raise MythError(\
                        'Invalid intid used during initialization of Video()')
            if isinstance(data,dict):
                self._import(data)
                self.update()
        elif isinstance(data,list) or isinstance(data,tuple):
            self._import(data)
            self.wheredat = (self.intid,)
        elif isinstance(data,dict):
            self.wheredat = (self.create(data),)
            self._pull()
        self.cast = self._Cast((self.intid,), self.db)
        self.genre = self._Genre((self.intid,), self.db)
        self.country = self._Country((self.intid,), self.db)

    def __repr__(self):
        return self.__str__().encode('utf-8')

    def __str__(self):
        res = self.title
        if self.season and self.episode:
            res += u' - %dx%02d' % (self.season, self.episode)
        if self.subtitle:
            res += u' - '+self.subtitle
        return res

    class _Cast( DBDataCRef ):
        table = 'videocast'
        ref = 'videometadatacast'
        t_field = 'intid'
        t_add = ['cast']
        r_field = 'idcast'
        r_add = []
        w_field = ['idvideo']
        def __repr__(self):
            if len(self.data) == 0:
                return ''
            cast = []
            for cref in self.field_order:
                cast.append(self.data[cref].cast)
            return ', '.join(cast)

    class _Genre( DBDataCRef ):
        table = 'videogenre'
        ref = 'videometadatagenre'
        t_field = 'intid'
        t_add = ['genre']
        r_field = 'idgenre'
        r_add = []
        w_field = ['idvideo']
        def __repr__(self):
            if len(self.data) == 0:
                return ''
            cast = []
            for cref in self.field_order:
                cast.append(self.data[cref].genre)
            return ', '.join(cast)

    class _Country( DBDataCRef ):
        table = 'videocountry'
        ref = 'videometadatacountry'
        t_field = 'intid'
        t_add = ['country']
        r_field = 'idcountry'
        r_add = []
        w_field = ['idvideo']
        def __repr__(self):
            if len(self.data) == 0:
                return ''
            cast = []
            for cref in self.field_order:
                cast.append(self.data[cref].country)
            return ', '.join(cast)

    def _open(self, type, mode='r'):
        """
        Open data socket
        """
        sgroup = {  'filename':'Videos',        'banner':'Banners',
                    'coverfile':'Coverart',     'fanart':'Fanart',
                    'screenshot':'Screenshots', 'trailer':'Trailers'}
        if self.data is None:
            return None
        if type not in sgroup:
            raise MythError('Video._open() - '+\
                            'Trying to open invalid file type: '+str(type))
        SG = self.db.getStorageGroup(sgroup[type], self.host)
        if len(SG) == 0:
            SG = self.db.getStorageGroup('Videos', self.host)
            if len(SG) == 0:
                raise MythError('Could not find storagegroup')
        return ftopen((self.host,self.data[type],SG[0].groupname),mode)

    def delete(self):
        """
        Delete video entry from database
        """
        if self.data is None:
            return
        self.cleanCast()
        self.cleanGenre()
        self.cleanCountry()
        DBDataWrite.delete(self)

    def open(self,mode='r'):
        """Open file pointer to video"""
        return self._open('filename',mode)

    def openBanner(self,mode='r'):
        """Open file pointer to banner image"""
        return self._open('banner',mode)

    def openCoverart(self,mode='r'):
        """Open file pointer to coverart image"""
        return self._open('coverfile',mode)

    def openFanart(self,mode='r'):
        """Open file pointer to fanart image"""
        return self._open('fanart',mode)

    def openScreenshot(self,mode='r'):
        """Open file pointer to screenshot image"""
        return self._open('screenshot',mode)

    def openTrailer(self,mode='r'):
        """Open file pointer to trailer"""
        return self._open('trailer',mode)


