# -*- coding: utf-8 -*-
# Copyright: Ankitects Pty Ltd and contributors
# License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html

# This file has been modified by lovac42
# added addon: Cacher in the rye for windows users

import aqt
import html
import re, sys, threading, time, subprocess, os, atexit
import  random
from anki.hooks import addHook, runHook
from anki.utils import  tmpdir, isWin, isMac, isLin
from anki.lang import _

# Shared utils
##########################################################################

_soundReg = r"\[sound:(.*?)\]"

audio_cached={} #wins only


def playFromText(text):
    for match in allSounds(text):
        # filename is html encoded
        match = html.unescape(match)
        play(match)

def allSounds(text):
    return re.findall(_soundReg, text)

def stripSounds(text):
    return re.sub(_soundReg, "", text)

def hasSound(text):
    return re.search(_soundReg, text) is not None

# Packaged commands
##########################################################################

# return modified command array that points to bundled command, and return
# required environment
def _packagedCmd(cmd):
    cmd = cmd[:]
    env = os.environ.copy()
    if "LD_LIBRARY_PATH" in env:
        del env['LD_LIBRARY_PATH']
    if isMac:
        dir = os.path.dirname(os.path.abspath(__file__))
        exeDir = os.path.abspath(dir + "/../../Resources/audio")
    else:
        exeDir = os.path.dirname(os.path.abspath(sys.argv[0]))
        if isWin and not cmd[0].endswith(".exe"):
            cmd[0] += ".exe"
    path = os.path.join(exeDir, cmd[0])
    if not os.path.exists(path):
        return cmd, env
    cmd[0] = path
    return cmd, env

##########################################################################

processingSrc = "rec.wav"
processingDst = "rec.mp3"
processingChain = []
recFiles = []

processingChain = [
    ["lame", processingSrc, processingDst, "--noreplaygain", "--quiet"],
    ]

# don't show box on windows
if isWin:
    si = subprocess.STARTUPINFO()
    try:
        si.dwFlags |= subprocess.STARTF_USESHOWWINDOW
    except:
        # pylint: disable=no-member
        # python2.7+
        si.dwFlags |= subprocess._subprocess.STARTF_USESHOWWINDOW
else:
    si = None

def retryWait(proc):
    # osx throws interrupted system call errors frequently
    while 1:
        try:
            return proc.wait()
        except OSError:
            continue

# MPV
##########################################################################

from anki.mpv import MPV, MPVBase

mpvPath, mpvEnv = _packagedCmd(["mpv"])

class MpvManager(MPV):

    executable = mpvPath[0]
    popenEnv = mpvEnv

    if not isLin:
        default_argv = MPVBase.default_argv + [
            "--input-media-keys=no",
        ]

    def __init__(self):
        super().__init__(window_id=None, debug=False)

    def queueFile(self, file):
        runHook("mpvWillPlay", file)

        path = os.path.join(os.getcwd(), file)
        self.command("loadfile", path, "append-play")

    def clearQueue(self):
        self.command("stop")

    def togglePause(self):
        self.set_property("pause", not self.get_property("pause"))

    def seekRelative(self, secs):
        self.command("seek", secs, "relative")

    def on_idle(self):
        runHook("mpvIdleHook")

def setMpvConfigBase(base):
    mpvConfPath = os.path.join(base, "mpv.conf")
    MpvManager.default_argv += [
        "--no-config",
        "--include="+mpvConfPath,
    ]

mpvManager = None

def setupMPV():
    global mpvManager, _player, _queueEraser
    mpvManager = MpvManager()
    _player = mpvManager.queueFile
    _queueEraser = mpvManager.clearQueue
    atexit.register(cleanupMPV)

def cleanupMPV():
    global mpvManager, _player, _queueEraser
    if mpvManager:
        mpvManager.close()
        mpvManager = None
        _player = None
        _queueEraser = None

# Mplayer in slave mode
##########################################################################

# if anki crashes, an old mplayer instance may be left lying around,
# which prevents renaming or deleting the profile
def cleanupOldMplayerProcesses():
    # pylint: disable=import-error
    import psutil

    exeDir = os.path.dirname(os.path.abspath(sys.argv[0]))

    for proc in psutil.process_iter():
        try:
            info = proc.as_dict(attrs=['pid', 'name', 'exe'])
            if not info['exe'] or info['name'] != 'mplayer.exe':
                continue

            # not anki's bundled mplayer
            if os.path.dirname(info['exe']) != exeDir:
                continue

            print("terminating old mplayer process...")
            proc.kill()
        except:
            print("error iterating mplayer processes")

mplayerCmd = ["mplayer", "-really-quiet", "-noautosub"]
if isWin:
    mplayerCmd += ["-ao", "win32"]

    cleanupOldMplayerProcesses()

mplayerQueue = []
mplayerManager = None
mplayerReader = None
mplayerEvt = threading.Event()
mplayerClear = False

class MplayerMonitor(threading.Thread):

    def run(self):
        global mplayerClear
        self.mplayer = None
        self.deadPlayers = []
        while 1:
            mplayerEvt.wait()
            mplayerEvt.clear()
            # clearing queue?
            if mplayerClear and self.mplayer:
                try:
                    self.mplayer.stdin.write(b"stop\n")
                    self.mplayer.stdin.flush()
                except:
                    # mplayer quit by user (likely video)
                    self.deadPlayers.append(self.mplayer)
                    self.mplayer = None
            # loop through files to play
            while mplayerQueue:
                # ensure started
                if not self.mplayer:
                    self.startProcess()
                # pop a file
                try:
                    item = mplayerQueue.pop(0)
                except IndexError:
                    # queue was cleared by main thread
                    continue
                if mplayerClear:
                    mplayerClear = False
                    extra = b""
                else:
                    extra = b" 1"
                cmd = b'loadfile "%s"%s\n' % (item.encode("utf8"), extra)
                try:
                    self.mplayer.stdin.write(cmd)
                    self.mplayer.stdin.flush()
                except:
                    # mplayer has quit and needs restarting
                    self.deadPlayers.append(self.mplayer)
                    self.mplayer = None
                    self.startProcess()
                    self.mplayer.stdin.write(cmd)
                    self.mplayer.stdin.flush()
                # if we feed mplayer too fast it loses files
                time.sleep(1)
            # wait() on finished processes. we don't want to block on the
            # wait, so we keep trying each time we're reactivated
            def clean(pl):
                if pl.poll() is not None:
                    pl.wait()
                    return False
                else:
                    return True
            self.deadPlayers = [pl for pl in self.deadPlayers if clean(pl)]

    def kill(self):
        if not self.mplayer:
            return
        try:
            self.mplayer.stdin.write(b"quit\n")
            self.mplayer.stdin.flush()
            self.deadPlayers.append(self.mplayer)
        except:
            pass
        self.mplayer = None

    def startProcess(self):
        try:
            cmd = mplayerCmd + ["-slave", "-idle"]
            cmd, env = _packagedCmd(cmd)
            self.mplayer = subprocess.Popen(
                cmd, startupinfo=si, stdin=subprocess.PIPE,
                stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL,
                env=env)
        except OSError:
            mplayerEvt.clear()
            raise Exception("Did you install mplayer?")

def queueMplayer(path):
    ensureMplayerThreads()
    if isWin and os.path.exists(path):
        # mplayer on windows doesn't like the encoding, so we create a
        # temporary file instead. oddly, foreign characters in the dirname
        # don't seem to matter.
        if aqt.mw.pm.profile.get("mpv.directAccess",True): #TODO: testNoCache
            path=path.replace("\\", "/")
        elif path in audio_cached:
            path=audio_cached[path]
        else:
            key=path
            dir = tmpdir()
            name = os.path.join(dir, "audio%s%s" % (
                random.randrange(0, 1000000), os.path.splitext(path)[1]))
            f = open(name, "wb")
            f.write(open(path, "rb").read())
            f.close()
            # it wants unix paths, too!
            path = name.replace("\\", "/")
            audio_cached[key]=path
    mplayerQueue.append(path)
    mplayerEvt.set()

def clearMplayerQueue():
    global mplayerClear, mplayerQueue, audio_cached
    mplayerQueue = []
    mplayerClear = True
    mplayerEvt.set()
    if isWin and len(audio_cached)>32:
        audio_cached={}

def ensureMplayerThreads():
    global mplayerManager
    if not mplayerManager:
        mplayerManager = MplayerMonitor()
        mplayerManager.daemon = True
        mplayerManager.start()
        # ensure the tmpdir() exit handler is registered first so it runs
        # after the mplayer exit
        if not aqt.mw.pm.profile.get("mpv.directAccess",True):
            tmpdir() #TODO: rm if testNoCache passes
        # clean up mplayer on exit
        atexit.register(stopMplayer)

def stopMplayer(*args):
    if not mplayerManager:
        return
    mplayerManager.kill()
    if isWin:
        cleanupOldMplayerProcesses()

addHook("unloadProfile", stopMplayer)

# PyAudio recording
##########################################################################

try:
    import pyaudio
    import wave

    PYAU_FORMAT = pyaudio.paInt16
    PYAU_CHANNELS = 1 #aqt.mw.pm.profile.get("PYAU_CHANNELS", 1)
    PYAU_INPUT_INDEX = None #aqt.mw.pm.profile.get("PYAU_INPUT_INDEX", None)
except:
    pyaudio = None

class _Recorder:

    def postprocess(self, encode=True):
        self.encode = encode
        for c in processingChain:
            #print c
            if not self.encode and c[0] == 'lame':
                continue
            try:
                cmd, env = _packagedCmd(c)
                ret = retryWait(subprocess.Popen(cmd, startupinfo=si, env=env))
            except:
                ret = True
            finally:
                self.cleanup()
            if ret:
                raise Exception(_(
                    "Error running %s") %
                                " ".join(cmd))

    def cleanup(self):
        if os.path.exists(processingSrc):
            os.unlink(processingSrc)

class PyAudioThreadedRecorder(threading.Thread):

    def __init__(self, startupDelay):
        threading.Thread.__init__(self)
        self.startupDelay = startupDelay
        self.finish = False

    def run(self):
        chunk = 1024
        format=pyaudio.paInt16 #TODO: set gui for 16,24,32 bits, refresh related gui items.
        channels=aqt.mw.pm.profile.get("PYAU_CHANNELS", 1)
        input_device_index=aqt.mw.pm.profile.get("PYAU_INPUT_INDEX", None)
        rate=aqt.mw.pm.profile.get("PYAU_INPUT_RATE", 44100)
        wait = int(rate * self.startupDelay)

        p = pyaudio.PyAudio()
        stream = p.open(format=format,
                        channels=channels,
                        rate=rate,
                        input=True,
                        input_device_index=input_device_index,
                        frames_per_buffer=chunk)
        stream.read(wait)

        data = b""
        while not self.finish:
            data += stream.read(chunk, exception_on_overflow=False)
        stream.close()
        p.terminate()
        wf = wave.open(processingSrc, 'wb')
        wf.setnchannels(channels)
        wf.setsampwidth(p.get_sample_size(format))
        wf.setframerate(rate)
        wf.writeframes(data)
        wf.close()

class PyAudioRecorder(_Recorder):

    # discard first 250ms which may have pops/cracks
    startupDelay = 0.25

    def __init__(self):
        for t in recFiles + [processingSrc, processingDst]:
            try:
                os.unlink(t)
            except OSError:
                pass
        self.encode = False

    def start(self):
        self.thread = PyAudioThreadedRecorder(startupDelay=self.startupDelay)
        self.thread.start()

    def stop(self):
        self.thread.finish = True
        self.thread.join()

    def file(self):
        if self.encode:
            tgt = "rec%d.mp3" % time.time()
            os.rename(processingDst, tgt)
            return tgt
        else:
            return processingSrc

if not pyaudio:
    PyAudioRecorder = None

# Audio interface
##########################################################################

_player = queueMplayer
_queueEraser = clearMplayerQueue

def play(path):
    _player(path)

def clearAudioQueue():
    _queueEraser()

Recorder = PyAudioRecorder
