#!/usr/bin/env python
# vim: ts=4:sw=4:bg=dark:expandtab

import sys
import socket
from pprint import pprint

class Channel(object):
    def __init__(self, Num, Name, Frequency, Parameters, Source,
                       Symbol_Rate, VPID, APID, TPID,
                       Conditional_Access, SID, NID, TID, RID):
        self.Num = Num
        if ';' in Name:
            self.Name, self.Sat = Name.split(';')
        else:
            self.Name = Name
            self.Sat = ''
        self.Frequency = Frequency
        self.Parameters = Parameters
        self.Source = Source
        self.Symbol_Rate = Symbol_Rate
        self.VPID = VPID
        self.APID = APID
        self.TPID = TPID
        self.Conditional_Access = Conditional_Access
        self.SID = SID
        self.NID = NID
        self.TID = TID
        self.RID = RID

    def channel_id(self):
        return '-'.join((self.Source, self.NID, self.TID, self.SID, self.RID))


class SVDRP(object):
    def __init__(self, host='localhost', port=2001):
        self._addr = (host, port)
    
    def readline(self, sock):
        line = ''
        while 1:
            c = sock.recv(1)
            line += c
            if c == '' or c == '\n':
                break
        return line
    
    def connect(self):
        sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
        sock.connect(self._addr)
        welcome = self.readline(sock)
        code, hostname, proto, app = welcome.split(' ')[:4]
        if not (code == '220' and proto == 'SVDRP' and app == 'VideoDiskRecorder'):
            print "Not talking to VDR!"
            sock.close()
            raise ValueError
        return sock

    def disconnect(self, sock):
        sock.sendall('QUIT\n')
        quit = self.readline(sock)
        sock.close()

    def execute(self, command):
        sock = self.connect()
        if not command.endswith('\n'):
            command += '\n'
        sock.sendall(command)
        buffer = ''
        while 1:
            line = self.readline(sock)
            buffer += line
            if line == '' or line[3] == ' ':
                break
        self.disconnect(sock)
        return buffer

    def sendepg(self, body):
        sock = self.connect()
        sock.sendall('PUTE\n')
        ack = self.readline(sock)
        if body[-1] != '\n':
            body += '\n'
        sock.sendall(body+'.\n')
        result = self.readline(sock)
        self.disconnect(sock)
        return result

    def getepg(self, channel_id, at=None):
        command = 'LSTE '+channel_id
        if at:
            command += ' at '+at
        buffer = self.execute(command)
        if buffer[:3] != '215':
            print "bad LSTE result:", buffer.strip()
            return []
        lines = [ l[4:] for l in buffer.splitlines()]
        return lines

    def getchannel(self, name):
        buffer = self.execute('LSTC '+name)
        if buffer[:3] != '250':
            print buffer.strip()
            return None
        line = buffer.splitlines()[0].strip()
        num, data = line[4:].split(' ',1)
        return Channel( num, *data.split(':') )

    def getchannels(self):
        buffer = self.execute('LSTC')
        if buffer[:3] != '250':
            print "Couldn't fetch the list of channels. Got:"
            print buffer
            return None
        channels = {}
        for line in buffer.splitlines():
            line = line[4:]
            num, data = line.split(' ',1)
            chan = Channel(*data.split(':'))
            if channels.has_key(chan.Name):
                continue 
            channels[chan.Name] = chan
        return channels

def main(otv_filename, conversions_filename):
    first = True
    svdrp = SVDRP()
    # Extract conversion channels
    print "Reading conversion file:", conversions_filename
    conv_file = file(conversions_filename)
    conversions = {}
    for line in conv_file:
        line = line.strip()
        if ':' in line:
            key, value = line.split(':')
            value = value.split(';')[0]
            conversions[key] = value
    conv_file.close()
    print "Reading OTV file:", otv_filename
    # Parse OTV epg file
    missing_channels = {}
    otv = {}
    for line in file(otv_filename):
        line = line.strip()
        if line[0] == 'C':
            chan_name = ' '.join(line.split(' ')[2:])
            print "Looking for channel:", chan_name
            channel = svdrp.getchannel(chan_name)
            if channel is None and chan_name != chan_name.upper():
                channel = svdrp.getchannel(chan_name.upper())
            if channel is None:
                try:
                    channel = svdrp.getchannel(conversions[chan_name])
                except KeyError:
                    missing_channels[chan_name] = True
        elif line[0] == 'E':
            entry = {'table': '0'}
            if line[1:].strip() != '':
                print "Oops! Found a E line with unexpected content:"
                print line
        elif line[0] == 'X':
            x, ts, z, h = line.split(' ')
            entry['ts'] = ts
            entry['event_id'] = int(ts)/60 & 0xFFFF
        elif line[0] == 'T':
            entry['title'] = line[2:]
        elif line[0] == 'D':
            entry['description'] = line[2:]
        elif line[0] == 'L':
            entry['length'] = str(60*int(line[2:]))
        elif line[0] == 'S':
            entry['category'] = line[2:]
        elif line[0] == 'e':
            if channel is not None:
                otv.setdefault(channel,[]).append(entry)
            del entry
        elif line[0] == 'c':
            channel = None
    # Cleanup OTV based on what VDR knowns allready and inject the new stuff
    epg_buffer = ''
    for channel, entries in otv.iteritems():
        print "Getting VDR's EPG data for channel:", channel.Name
        epg_data = svdrp.getepg(channel.Num)
        epg_data = epg_data[:-1]
        epgs = []
        event_ids = []
        for line in epg_data:
            line = line.strip()
            if line[0] == 'C':
                pass
            elif line[0] == 'E':
                entry = {}
                e, i, ts, length, table, version = line.split(' ')
                entry['event_id'] = int(i)
                event_ids.append(i)
                entry['ts'] = ts
                entry['length'] = length
                entry['table'] = table
            elif line[0] == 'T':
                entry['title'] = line[2:]
            elif line[0] == 'D':
                entry['description'] = line[2:]
            elif line[0] == 'S':
                entry['category'] = line[2:]
            elif line[0] == 'X':
                entry.setdefault('streams', []).append(line[2:])
            elif line[0] == 'e':
                epgs.append(entry)
                del entry
            elif line[0] == 'c':
                pass
            else:
                print "Unexpected kind of line:", line
        # Remove duplicate
        print "Removing duplicate"
        for entry in epgs:
            for i, otv_entry in enumerate(entries):
                # remove the event_id before comparing the entries
                s_entry = dict(entry)
                s_otv_entry = dict(otv_entry)
                del s_entry['event_id']
                del s_otv_entry['event_id']
                if s_entry == s_otv_entry:
                    del entries[i]
        print "Generating the update command with", len(entries), "entries"
        epg_buffer += 'C '+channel.channel_id()+' '+channel.Name+'\n'
        for entry in entries:
            # Shift event_id in case of duplicate
            initial_id = entry['event_id']
            while entry['event_id'] in event_ids:
                entry['event_id'] = (entry['event_id'] + 1) & 0xFFFF
                if entry['event_id'] == initial_id:
                    print "ERROR: Couldn't find a free event id !!!"
                    print "channel:"
                    pprint(channel)
                    print "entry:"
                    pprint(entry)
                    raise ValueError()
            if entry['event_id'] != initial_id:
                print "Shifted the event_id from", initial_id, "to", entry['event_id']
            epg_buffer += 'E '+str(entry['event_id'])+' '+entry['ts']+' '+entry['length']+' '+entry['table']+'\n'
            epg_buffer += 'T '+entry['title']+'\n'
            if entry.has_key('category'):
                epg_buffer += 'S '+entry['category']+'\n'
            if entry.has_key('description'):
                epg_buffer += 'D '+entry['description']+'\n'
            if entry.has_key('streams'):
                for stream in entry['streams']:
                    buffer += 'X '+stream+'\n'
            epg_buffer += 'e\n'
        epg_buffer += 'c\n'
    print "Executing the update query"
    answer = svdrp.sendepg(epg_buffer)
    if answer[:3] != '250':
        print "An error occured when uploading:"
        print buffer
        print "PUTE answer:", answer.strip()
    # Save rebuilt conversion file
    conv_file = file(conversions_filename, 'w')
    for key, value in conversions.iteritems():
        print >> conv_file, key+':'+value
    for chan in missing_channels.iterkeys():
        print >> conv_file, chan
    conv_file.close()

if __name__ == '__main__':
    if len(sys.argv) != 3:
        print "Usage: %s otv.epg conversions_channels.txt" % sys.argv[0]
        raise SystemExit
    main(sys.argv[1], sys.argv[2])
