""" Event loop for novelty phone.  Handles events from dialer and hook input event devices (/dev/input/event*).  Subclass and override hooks to do anything useful. """
import os
import time
import struct
import logging
import subprocess
from subprocess import PIPE, DEVNULL
from glob import glob
from select import epoll, EPOLLIN

logger = logging.getLogger(__name__)

DIR_LIB = os.path.dirname(__file__)
DIR_BIN = DIR_LIB + '/..'
DIR_MED = '/home/mitch/media'
DIR_REC = DIR_MED + '/messages'
DIR_SNG = DIR_MED + '/music'
DIR_GRE = DIR_MED + '/greetings'

class Phone(object):

    # Hooks. Override these in subclass.
    def idle(self):                pass
    def handset_up(self, event):   pass
    def handset_down(self, event): pass
    def dialed(self, char):        pass

    def __init__(self, hook_dev='/dev/input/event0', dialer_dev='/dev/input/event1', idle_cadence=0.1, debug=False):
        self.debug = debug
        self.hook = InputEventDevice(hook_dev)
        self.dialer = InputEventDevice(dialer_dev)
        self.idle_cadence = idle_cadence
        self.player = Player()
        self.light = Light(debug=debug)

    def say(self, text, voice='english-us', block=True):
        wav_file = '/tmp/{}.wav'.format(time.time())
        subprocess.run(['espeak', '-v', voice, '-w', wav_file, text], **NO_OUTPUT)
        logger.debug("Saying \"{}\".".format(text))
        p = subprocess.Popen(['aplay', wav_file], **NO_OUTPUT)
        if block:
            p.wait()
        else:
            return p

    def record(self, filename=None, rate=44100, format='S32_LE', duration=None, block=False):
        if filename is None:
            message_filenames = glob(DIR_REC + '/*')
            if not message_filenames:
                message_id = 0
            else:
                message_id = max(int(os.path.basename(f).split('.')[0]) for f in message_filenames) + 1
            filename = DIR_REC + '/{:03d}.flac'.format(message_id)
        recording = Recording(
            filename=filename,
            rate=rate,
            format=format,
            duration=duration,
            block=block,
            )
        return recording

    def play(self, filename):
        logger.debug("Playing {}.".format(filename))
        return self.player.play(filename)

    def stop(self):
        return self.player.stop()

    def run(self):
        hook = self.hook
        dialer = self.dialer
        devices = (dialer, hook)

        poller = epoll()
        device_by_fd = {}
        for device in devices:
            fd = device.fd
            poller.register(fd, EPOLLIN)
            device_by_fd[fd] = device
        while True:
            activity = poller.poll(timeout=self.idle_cadence)
            if activity:
                for fd, flags in activity:
                    if flags & EPOLLIN:
                        device = device_by_fd[fd]
                        event = device.read_event()
                        if device is hook:
                            if event.is_key:
                                logger.debug(event)
                                if event.value == 0:
                                    self.handset_up()
                                elif event.value == 1:
                                    self.handset_down()
                        elif device is dialer:
                            if event.is_keypress:
                                self.dialed(DIALER_KEY_TO_CHAR[event.code])
            else:
                self.idle()

class Player(object):
    def __init__(self):
        self.proc = subprocess.Popen(['mplayer', '-idle', '-slave'], stdin=PIPE)

    def play(self, filename):
        self.stop()
        self.proc.stdin.write("loadfile {}\n".format(filename).encode('utf-8'))
        self.proc.stdin.flush()

    def stop(self):
        self.proc.stdin.write(b"stop\n")
        self.proc.stdin.flush()

class Recording(object):
    S16_LE = 'S16_LE'
    S32_LE = 'S32_LE'
    MIN_DURATION = 3. # Seconds.

    def __init__(self, filename, rate, format, duration, block):
        self.filename = filename
        self.rate = rate
        self.format = format
        self.duration = duration
        self.start_time = time.time()

        logger.info("Recording {}.".format(filename))
        arecord_cmd = ['arecord', '-f', format, '-r', str(rate)]
        if duration:
            arecord_cmd += ['-d', str(duration)]
        arecord = subprocess.Popen(arecord_cmd, stderr=DEVNULL, stdout=PIPE)
        logger.debug("Started {}.".format(arecord))
        ffmpeg = subprocess.Popen(['ffmpeg', '-i', '-', filename], stdin=arecord.stdout, **NO_OUTPUT)
        logger.debug("Piping to {}.".format(ffmpeg))
        self.subprocs = (arecord, ffmpeg)
        self.is_active = True

        # Only block if a duration was specified.  Otherwise we'll wait forever.
        if duration and block:
            ffmpeg.wait()
            self.is_active = False

    def stop(self, timeout=2.):
        for proc in self.subprocs:
            logger.debug("Terminating {}.".format(proc))
            proc.terminate()
            try:
                ret = proc.wait(timeout=timeout)
            except subprocess.TimeoutExpired:
                logger.warning("{} didn't terminate in {}s. Killing.".format(proc, timeout))
                proc.kill()
                ret = proc.wait()
            logger.debug("{} terminated and returned {}.".format(proc, ret))
            self.is_active = False
        duration = time.time() - self.start_time
        if duration < self.MIN_DURATION and os.path.exists(self.filename):
            logger.warning("Recording {} is less than {}s. Assuming bobbled handset. Deleting.".format(self.filename, self.MIN_DURATION))
            os.unlink(self.filename)

class Light(object):
    """ Some kind of light.  Currently implemented as IPC to a subprocess that controls a panel of 9 apa102cs. """
    def __init__(self, control_program=DIR_BIN + '/light_panel', debug=False):
        file_handles = dict(stdin=subprocess.PIPE)
        if not debug:
            file_handles.update(**NO_OUTPUT)
        self.proc = subprocess.Popen([control_program], **file_handles)
        self.set_to(255, 0, 128, 16)

    def send_command(self, command, *args):
        """ This IPC is garbage. Improve. """
        command_line = command
        if args:
            command_line += ' ' + ' '.join(str(a) for a in args)
        command_line += '\n'
        try:
            self.proc.stdin.write(command_line.encode('utf-8'))
            self.proc.stdin.flush()
        except BrokenPipeError as e:
            logger.error("BrokenPipeError talking to light.")

    def fade_to(self, r, g, b, a):
        return self.send_command('fade_to', r, g, b, a)

    def set_to(self, r, g, b, a):
        return self.send_command('set_to', r, g, b, a)

    def animate(self):
        return self.send_command('random')

class Event(object):
    EV_KEY = 0x01
    UP     = 0x00
    DOWN   = 0x01
    def __init__(self, time_sec, time_usec, type, code, value):
        self.time = time_sec + time_usec / 1000000.
        self.type = type
        self.code = code
        self.value = value

    @property
    def is_key(self):
        return self.type == self.EV_KEY

    @property
    def is_keypress(self):
        return self.is_key and self.value == self.DOWN

    def __str__(self):
        return "{}(time={}, type={}, code={}, value={})".format(self.__class__.__name__, self.time, self.type, self.code, self.value)

class InputEventDevice(object):
    structure = 'llHHI'

    def __init__(self, filename):
        self.filename = filename
        self.f = open(filename, 'rb')
        self.fd = self.f.fileno()
        self.read_size = struct.calcsize(self.structure)

    def read_event(self):
        bytestring = self.f.raw.read(self.read_size)
        raw_event = struct.unpack(self.structure, bytestring)
        return Event(*raw_event)

# /usr/include/linux/input-event-codes.h
DIALER_KEY_TO_CHAR = {
    2: '1',
    3: '2',
    4: '3',
    5: '4',
    6: '5',
    7: '6',
    8: '7',
    9: '8',
    10: '9',
    11: '0',
    55: '*',
    19: 'R',
    }

NO_OUTPUT = dict(stderr=DEVNULL, stdout=DEVNULL)
