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

from os import getpid
import Queue
import logging
import threading
import time
import sys
import copy

import pymumble

import settings
from pymumble.constants import PYMUMBLE_CLBK_USERCREATED,\
    PYMUMBLE_CLBK_USERUPDATED, PYMUMBLE_CLBK_USERREMOVED,\
    PYMUMBLE_CLBK_TEXTMESSAGERECEIVED


class AsynchronousFileReader(threading.Thread):
    '''
    Helper class to implement asynchronous reading of a file
    in a separate thread. Pushes read lines on a queue to
    be consumed in another thread.
    '''
 
    def __init__(self, fd, queue):
        assert isinstance(queue, Queue.Queue)
        assert callable(fd.readline)
        threading.Thread.__init__(self)
        self._fd = fd
        self._queue = queue
        self.daemon = True
 
    def run(self):
        '''The body of the tread: read lines and put them on the queue.'''
        for line in iter(self._fd.readline, ''):
            self._queue.put(line)
 
    def eof(self):
        '''Check whether there is no more content to expect.'''
        return not self.is_alive() and self._queue.empty()


class AsynchronousStreamLogger(threading.Thread):
    '''
    Helper class to implement asynchronous reading of a file
    in a separate thread. Pushes read lines on a queue to
    be consumed in another thread.
    '''
 
    def __init__(self, fd, logger, level=logging.INFO):
        assert callable(fd.readline)
        threading.Thread.__init__(self)
        self._fd = fd
        self._logging = logger
        self._level = level
        self.daemon = True
 
    def run(self):
        '''The body of the tread: read lines and put them on the queue.'''
        for line in iter(self._fd.readline, ''):
            self._logging.log(self._level, line.strip())
 
    def eof(self):
        '''Check whether there is no more content to expect.'''
        return not self.is_alive() and self._queue.empty()


class MumbleAudioBot:
    """
    stay connected on a mumble server.  When USER_COUNT are connected, start to record.  Stop when the
    number of users goes below.
    Create webvtt files with chapters, timestamps and speakers captions
    understand a number of commands sent through the mumble chat system
    """
    def __init__(self):

        pid = str(getpid())
        file(settings.PIDFILE,'w').write("%s\n" % pid)  # store the process id

        self.recording = False  # recording ongoing"
        self.audio_file = None  # output audio_file
        
        self.cursor_time = None  # time for which the audio is treated
        
        self.force_start = False
        self.force_stop = True
        self.force_newfile = False
        self.exit = False
        
        self.cmd_log = logging.getLogger("cmd")
        self.cmd_log.setLevel(logging.INFO)
        fmt = logging.Formatter("CMD %(message)s")
        sh = logging.StreamHandler(sys.stdout)
        sh.setFormatter(fmt)
        self.cmd_log.addHandler(sh)

        self.users = dict()  # store some local informations about the users session

        self.stdin_q = Queue.Queue()
        self.stdin_reader = AsynchronousFileReader(sys.stdin, self.stdin_q)
        self.stdin_reader.start()

        self.cmd_log.info("RUNNING")       

        # default values
        self.settings = {
            "PORT": settings.PORT,
            "HOST": settings.HOST,
            "USER": settings.USER,
            "PASSWORD": settings.PASSWORD,
            "CHANNEL": settings.CHANNEL,
            "OUTPUT": {
                "FORMAT": settings.OUTPUT_FORMAT,
                "QUALITY": settings.OUTPUT_QUALITY,
                "SERVER": settings.OUTPUT_SERVER,
                "PORT": settings.OUTPUT_PORT,
                "USERNAME": settings.OUTPUT_USERNAME,
                "NAME": settings.OUTPUT_NAME,
                "PUBLIC": settings.OUTPUT_PUBLIC,
                "PASSWORD": settings.OUTPUT_PASSWORD,
                "MOUNT": settings.OUTPUT_MOUNT,
            }
        }

        # Process commands from stdin prior to startup
        # Continue with start after "CONNECT"
        while True:
            line = self.stdin_q.get()
            line_s = line.split(None, 2)
            if line_s[0] == "CONNECT":
                break
            else:
                self.cmd(line)
            if self.stdin_q.empty():
                time.sleep(0.1)

        # Create the mumble instance and assign callbacks
        self.mumble = pymumble.Mumble(self.settings["HOST"], int(self.settings["PORT"]), self.settings["USER"], self.settings["PASSWORD"], debug=settings.DEBUG)
        self.mumble.daemon = True
        self.mumble.callbacks.set_callback(PYMUMBLE_CLBK_USERCREATED, self.user_created)
        self.mumble.callbacks.set_callback(PYMUMBLE_CLBK_USERUPDATED, self.user_modified)
        self.mumble.callbacks.set_callback(PYMUMBLE_CLBK_USERREMOVED, self.user_removed)
        self.mumble.callbacks.set_callback(PYMUMBLE_CLBK_TEXTMESSAGERECEIVED, self.message_received)
    
        self.mumble.start()  # start the mumble thread
        self.mumble.is_ready()  # wait for the end of the connection process
        if not self.mumble.is_alive() or self.mumble.connected > 2: # some error occured
            self.cmd_log.error("ERROR "+" ".join([str(i) for i in self.mumble.connection_error]))
            return

        self.cmd_log.info("CONNECTED")
        try:
            self.mumble.channels.find_by_name(self.settings["CHANNEL"]).move_in()  # move to the configured channel
            self.cmd_log.info("JOINED %s" % self.settings["CHANNEL"])
        except pymumble.errors.UnknownChannelError:
            self.cmd_log.error("ERROR 2001 Could not change channel, channel does not exist")

        self.mumble.users.myself.mute()  # mute the user (just to make clear he don't speak)
        self.loop()

    def set_user_stereo(self, session, channel=None):
        if channel is not None and self.mumble.channels[channel]["name"] in settings.STEREO_CHANNELS:
            self.users[session]["stereo"] = settings.STEREO_CHANNELS[self.mumble.channels[channel]["name"]]["stereo"]
            self.users[session]["region"] = settings.STEREO_CHANNELS[self.mumble.channels[channel]["name"]]["region"]
        else:
            self.users[session]["stereo"] = (1, 1)
            self.users[session]["region"] = None

    ## Callbacks

    def user_created(self, user):
        """A user is connected on the server.  Create the specific structure with the local informations"""
        if user["session"] not in self.users:
            self.users[user["session"]] = dict()
            self.set_user_stereo(user["session"])
        
        if "name" in user:
            self.users[user["session"]]["name"] = user["name"]
            
        if "channel_id" in user:
            self.users[user["session"]]["channel_id"] = user["channel_id"]
            self.set_user_stereo(user["session"], user["channel_id"])

    def user_modified(self, user, actions):
        """A modification was sent by the server about a connected user"""
        if "channel_id" in actions:
            if "channel_id" not in self.users[user["session"]] or self.users[user["session"]]["channel_id"] != user["channel_id"]:
                self.set_user_stereo(user["session"], user["channel_id"])
                self.users[user["session"]]["channel_id"] = user["channel_id"]
            
        if "self_mute" in actions:
            if "self_mute" not in self.users[user["session"]] or self.users[user["session"]]["self_mute"] != user["self_mute"]:
                self.users[user["session"]]["self_mute"] = user["self_mute"]
            
    def user_removed(self, user, *args):
        """a user has disconnected"""
        if "name" in self.users[user["session"]]:
            del self.users[user["session"]]
    
    def message_received(self, message):
        """receive a text message from the server"""

        # return

        self.cmd(message[1:])

        #TODO: check if message is sent only to me

    def cmd(self, msg):
        cmd = msg.split()[0].upper()
        if hasattr(self, "cmd_%s" % cmd):
            getattr(self, "cmd_%s" % cmd)(msg)
        else:
            logging.error("Unknown command '%s'" % cmd)
    
    def cmd_STARTBROADCAST(self, msg):
        self.force_start = True
        self.force_stop = False   
        self.mumble.users.myself.comment("Recording forced." + settings.COMMENT_SUFFIX)

    def cmd_STOPBROADCAST(self, msg):
        self.force_start = False
        self.force_stop = True
        self.mumble.users.myself.comment("Recording blocked." + settings.COMMENT_SUFFIX)

    def cmd_EXIT(self, msg):
        self.exit = True

    def cmd_SET(self, msg):
        line = msg.split(None, 2)
        val = ''
        if len(line) > 2:
            val = line[2].strip()
        if line[1].startswith("OUTPUT"):
            line[1] = line[1].split(":",1)[1]
            self.settings["OUTPUT"][line[1]] = val
        else:
            self.settings[line[1]] = val


   
    def loop(self):
        """Master loop""" 
        import os.path
        import audioop
        
        silent = "\x00" * settings.STEREO_CHUNK_SIZE
        
        self.mumble.users.myself.texture(self.load_bitmap(settings.STOP_BITMAP))
        
        while self.mumble.is_alive() and not self.exit:
            while not self.stdin_q.empty():
                line = self.stdin_q.get()
                self.cmd(line)
  
            if  ( ( self.recording and not self.force_stop ) or self.force_start ) and not self.force_newfile: 
                if not self.audio_file:
                    # Start recording
                    # ask the pymumble library to handle incoming audio
                    self.mumble.set_receive_sound(True)
                    # signal the others I'm recording (to be fair)
                    self.mumble.users.myself.recording()
                    # Change the recorder avatar
                    self.mumble.users.myself.texture(self.load_bitmap(settings.START_BITMAP))
                    # time of the start of the recording
                    self.cursor_time = time.time() - settings.BUFFER
                    # Assemble file name
                    audio_file_name = os.path.join(settings.SAVEDIR, "mumble-%s" % time.strftime("%Y%m%d-%H%M%S"))
                    print "Starting new Recording"
                    self.audio_file = AudioFile(audio_file_name, self.settings["OUTPUT"])
                    
                    self.cmd_log.info("RECORDING START")

                if self.cursor_time < time.time() - settings.BUFFER:  # it's time to check audio
                    base_sound = copy.copy(silent)
                    
                    for user in self.mumble.users.values():  # check the audio queue of each users
                        session = user["session"]
                        while ( user.sound.is_sound() and
                                user.sound.first_sound().time < self.cursor_time):
                            user.sound.get_sound(settings.FLOAT_RESOLUTION)  # forget about too old sounds
                        
                        if user.sound.is_sound():
                            if ( user.sound.first_sound().time >= self.cursor_time and
                               user.sound.first_sound().time < self.cursor_time + settings.FLOAT_RESOLUTION ):
                                # available sound is to be treated now and not later
                                sound = user.sound.get_sound(settings.FLOAT_RESOLUTION)

                                if sound.target == 0:  # take care of the stereo feature
                                    stereo_pcm = audioop.tostereo(sound.pcm, 2, *self.users[session]["stereo"])
                                else:
                                    stereo_pcm = audioop.tostereo(sound.pcm, 2, 1, 1)
                                    
                                base_sound = self.add_sound(base_sound, stereo_pcm)

                    if not self.audio_file.write(base_sound):
                        self.cmd_log.error("ERROR 2002 ENCODER closed while write")
                        self.force_stop = True
                        self.force_start = False
                        
                    self.cursor_time += settings.FLOAT_RESOLUTION
                else:
                    time.sleep(settings.FLOAT_RESOLUTION)
                                
            else:
                if self.audio_file:
                    # finish recording
                    self.mumble.users.myself.unrecording()
                    self.mumble.users.myself.texture(self.load_bitmap(settings.STOP_BITMAP))
                    self.mumble.set_receive_sound(False)
                    self.cursor_time = None
                    self.audio_file.close() 
                    self.audio_file = None

                    self.cmd_log.info("RECORDING STOP")

         
                self.force_newfile = False   
                time.sleep(0.5)

    def add_sound(self, s1, s2):
        """add 2 stereo PCM audio streams"""
        from sound_add import sound_add
        
        if len(s1) != len(s2):
            raise Exception("both sound must have same length")
        
        return sound_add(s1, s2)
        
    def load_bitmap(self, filen):
        """read the bitmap info to update the avatar"""
        bitmap = open(filen, "rb")
        
        result = ""
        
        rec = bitmap.read(4096)
        while rec != "":
            result += rec
            rec = bitmap.read(4096)
        
        bitmap.close()
        return result
    

class AudioFile():
    """
    Manage the audio saving, through a pipe or in a WAV file
    """
    def __init__(self, name, config):
        from subprocess import Popen, PIPE
        
        self.name = name
        self.type = None
        self.file_obj = None

        stdout_logger = logging.getLogger("encoder")

        mysettings = dict([(k,v) for k, v in config.iteritems()])
       
        pipeline = []
        pipeline += settings.PIPELINE_BASE
        pipeline += settings.PIPELINE_ENCODER[mysettings["FORMAT"].lower()]
        pipeline += settings.PIPELINE_ICECAST

        mysettings["QUALITY"] = settings.QUALITY_FACTOR[mysettings["FORMAT"].lower()](mysettings["QUALITY"])

        pipeline = " ! ".join(pipeline)
        pipeline = pipeline.format(**mysettings)

        pipeline = pipeline.replace("mix. ! mix.", "mix. mix.").replace("name=mix !","name=mix")
  
        stdout_logger.debug("PIPELINE %s" % pipeline)
        stdout_logger.debug("PIPELINE %s" % mysettings)
 
        cmdline = settings.ENCODER.split()
        if settings.USE_PIPELINE:
            cmdline += pipeline.split(" ") 
        else:
            cmdline = settings.ENCODER.format(**mysettings).split(" ")

        print "ENCODER", " ".join(cmdline)

        proc = Popen(cmdline, shell=False, bufsize=-1, stdin=PIPE, stdout=PIPE, stderr=PIPE)
        self.file_obj = proc.stdin
        self.type = "pipe"

        self.stdout_logging = AsynchronousStreamLogger(proc.stdout, stdout_logger)
        self.stdout_logging.start()

        self.stderr_logging = AsynchronousStreamLogger(proc.stderr, stdout_logger, logging.ERROR)
        self.stderr_logging.start()
        
    def write(self, data):
        if self.type == "pipe":
            if not self.file_obj.closed:
                try:
                    self.file_obj.write(data)
                except IOError:
                    return False
            else:
                return False
        else:
            self.file_obj.writeframes(data)
        return True
    
    def close(self):
        self.file_obj.close()

def printHex(string):
    mystr=''
    for i in range(len(string)):
        mystr += "%02x" % ord(string[i])

    return mystr


if __name__ == "__main__":

    stdout_logger = logging.getLogger("encoder")
    stdout_logger.setLevel(logging.INFO)
    fmt = logging.Formatter("ENCODER %(message)s")
    sh = logging.StreamHandler(sys.stdout)
    sh.setFormatter(fmt)
    stdout_logger.addHandler(sh)

    audio_bot = MumbleAudioBot()
    audio_bot.cmd_log.info("EXIT")
