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

import audioop
import contextlib
import glob
import io
import os
import string
import subprocess
import syslog
import threading
import urllib2
import wave
from datetime import datetime

import linphone
from enum import Enum

VOLUME_THRESHOLD = 100


def slugify(s):
    """
    Normalizes string, converts to lowercase, removes non-alpha characters,
    and converts spaces to hyphens, wich is url/filename friendly.
    """
    valid_chars = "-_.() %s%s" % (string.ascii_letters, string.digits)
    filename = ''.join(c for c in s if c in valid_chars)
    filename = filename.replace(' ', '_')  # I don't like spaces in filenames.
    return filename


class ConversationStatus(Enum):
    READY_TO_TALK = 0
    IMTALKING = 1
    WAITFORANSWER = 2


class Conversation(object):
    def __init__(self):
        self._status = ConversationStatus.READY_TO_TALK

    @property
    def status(self):
        return self._status

    @status.setter
    def status(self, value):
        if value != self._status:
            self._status = value


current_dir = os.path.dirname(os.path.realpath(__file__))
replies_seq = glob.glob(current_dir + "/replies/sequence/*.wav")
replies_seq.sort()

replies_generic = glob.glob(current_dir + "/replies/generic/*.wav")
replies_generic.sort()

incoming_stream_file = "/tmp/lenny"

conversation = Conversation()

replies_pos = 0

THREADS_MUST_QUIT = False

KTIP_LOOKUP_URL = "https://www.ktipp.ch/service/warnlisten/detail/?warnliste_id=7&ajax=ajax-search-form&keyword={" \
                  "$number$}"

SHOULDIANSWER_LOOKUP_URL = "https://ch.shouldianswer.net/telefonnummer/{$number$}"


def is_in_blacklists(a_number):
    return is_in_local_blacklist(a_number) or is_in_ktipp_blacklist(a_number) or is_in_shiansw_blacklist(a_number)


def is_in_local_blacklist(a_number):
    black_list = current_dir + "/blacklist.txt"
    if os.path.isfile(black_list):
        return a_number in open(current_dir + "/blacklist.txt").read()


def is_in_ktipp_blacklist(a_number):
    # On peut interroger le site ktipp:
    # https://www.ktipp.ch/service/warnlisten/detail/?warnliste_id=7&ajax=ajax-search-form&keyword=0445510503
    # Si argument keyword pas trouvé, ca donne ca dans la réponse :
    # 0 Einträge

    url = KTIP_LOOKUP_URL.replace("{$number$}", a_number)
    log("KTIP lookup : " + url)
    response = ""
    try:
        response = urllib2.urlopen(url).read()
    except urllib2.HTTPError:
        pass

    return "0 Eintr" not in response


def is_in_shiansw_blacklist(a_number):
    url = SHOULDIANSWER_LOOKUP_URL.replace("{$number$}", a_number)
    log("SIANSW lookup : " + url)
    response = ""
    try:
        response = urllib2.urlopen(url).read()
    except urllib2.HTTPError:
        pass

    return '<div class="review_score negative"></div>' in response


def log(msg):
    syslog.syslog(msg)
    print(msg)


def get_wav_duration(fname):
    with contextlib.closing(wave.open(fname, 'r')) as f:
        frames = f.getnframes()
        rate = f.getframerate()
        return frames / float(rate)


def sleep(duration):
    dummy_event = threading.Event()
    dummy_event.wait(timeout=duration)


def say(core):
    global conversation
    global replies_pos
    if conversation.status is not ConversationStatus.IMTALKING:
        conversation.status = ConversationStatus.IMTALKING

        # On joue les repliques en sequence, puis quand
        # on arrive au bout, on en joue une au hasard
        # du groupe 'generic'

        voice_filename = replies_seq[replies_pos]
        replies_pos = (replies_pos + 1) % len(replies_seq)
        if replies_pos == 0:
            # On ne rejoue jamais la première réplique "allo"
            replies_pos = 1

        duration = get_wav_duration(voice_filename)
        log("Saying : " + voice_filename + "(duration : " + str(duration) + ")")

        core.play_file = voice_filename

        sleep(duration)
        core.play_file = ""

        # On laisse l'autre l'occassion de reparler
        conversation.status = ConversationStatus.WAITFORANSWER


def call_state_changed(core, call, state, message):
    global conversation
    global replies_pos
    log("state changed : " + message)

    if state == linphone.CallState.Released:
        # Let's convert wav to mp3
        log("Converting output from wav to mp3")
        if call.current_params.record_file is not None and os.path.isfile(call.current_params.record_file):
            subprocess.call('lame --quiet --preset insane %s' % call.current_params.record_file, shell=True)
            os.remove(call.current_params.record_file)

    if state == linphone.CallState.IncomingReceived:
        log("Incoming call : {}".format(call.remote_address.username))

        replies_pos = 0

        if is_in_blacklists(call.remote_address.username):
            log("telemarketer calling : " + call.remote_address.username)

            call_params = core.create_call_params(call)
            a_file = current_dir + "/out/call_from_" + slugify(call.remote_address.username) + \
                     "_" + datetime.now().strftime(
                '%Y-%m-%d_%Hh%Mmn%Ss') + ".wav"

            log(a_file)

            call_params.record_file = a_file

            # Let ring some time
            sleep(5)

            core.accept_call_with_params(call, call_params)
            call.start_recording()

            sleep(2)

            t = threading.Thread(target=incoming_stream_worker, args=[core, call])
            t.start()

            say(core)


def incoming_stream_worker(core, call):
    global conversation
    log("Worker is starting")

    f = open(incoming_stream_file, "rb")
    f.seek(0, io.SEEK_END)
    p = f.tell()
    buf = ''

    previous_status = conversation.status

    while call.state is not linphone.CallState.End and not THREADS_MUST_QUIT:
        if conversation.status is ConversationStatus.IMTALKING:
            f.seek(0, io.SEEK_END)
            p = f.tell()
        else:

            if previous_status != conversation.status:
                f.seek(0, io.SEEK_END)
                p = f.tell()

            f.seek(p)
            buf += f.read(4096)
            p = f.tell()

            if len(buf) >= 20000:
                volume = audioop.rms(buf, 2)
                print("Detected volume : " + str(volume))
                # print("State : " + str(conversation.status))
                buf = ''
                if volume < VOLUME_THRESHOLD:
                    if conversation.status is ConversationStatus.READY_TO_TALK:
                        threading.Thread(target=say, args=[core]).start()
                else:
                    conversation.status = ConversationStatus.READY_TO_TALK

        # We must sleep a bit to avoid cpu hog
        sleep(0.01)
        previous_status = conversation.status

    log("Worker is quitting")


def main():
    log("lenny is starting ...")
    callbacks = {
        'call_state_changed': call_state_changed
    }

    username = "621"
    password = "toto"
    port = "5060"
    domain = "192.168.1.1"

    core = linphone.Core.new(callbacks, None, None)

    # On fait le setup pour la capture et analyse du stream entrant
    os.system("rm -rf " + incoming_stream_file)
    os.system("touch " + incoming_stream_file)
    core.use_files = True
    core.record_file = incoming_stream_file

    proxy_cfg = core.create_proxy_config()
    proxy_cfg.identity_address = core.create_address('sip:' + username + '@' + domain + ':' + port)
    proxy_cfg.server_addr = 'sip:' + domain + ':' + port
    proxy_cfg.register_enabled = True
    core.add_proxy_config(proxy_cfg)
    auth_info = core.create_auth_info(username, None, password, None, None, domain)
    core.add_auth_info(auth_info)

    while True:
        sleep(0.03)
        core.iterate()


if __name__ == "__main__":
    try:
        main()
    except KeyboardInterrupt:
        THREADS_MUST_QUIT = True
        print "Bye"

# TODO : créer un fichier de config
# TODO : pouvoir s'enregistrer sur plusieurs comptes SIP
# TODO : Que le stream entrant utilise un fichier temporaire
# TODO : De pouvoir utiliser des mp3 pour gagner de la place, qu'on convertit au vol en wav, puis qu'on efface.
# TODO : Ecrire le readme
