from __future__ import print_function
import logging
import os
import subprocess
import tempfile
import threading
from .util import safe_text

LOG = logging.getLogger(__name__)


class AudioRenderer(object):
    def __init__(self, config, runtime_dir):
        self.config = config
        self.runtime_dir = runtime_dir
        self.lock = threading.RLock()
        if not os.path.isdir(runtime_dir):
            os.makedirs(runtime_dir)

    def render(self, text, name="announcement", prefix_files=None, suffix_files=None):
        text = safe_text(text, 6000)
        if not text:
            raise RuntimeError("Refusing to render empty announcement")
        if not self.config.get("enabled", True):
            path = os.path.join(self.runtime_dir, name + ".txt")
            with open(path, "w") as handle:
                handle.write(text + "\n")
            return os.path.splitext(path)[0]
        with self.lock:
            return self._render_locked(text, name, prefix_files, suffix_files)

    def _render_locked(self, text, name, prefix_files, suffix_files):
        work = tempfile.mkdtemp(prefix="brs-audio-", dir=self.runtime_dir)
        try:
            speech = os.path.join(work, "speech.wav")
            self._tts(text, speech)
            segments = []
            delay_ms = max(0, int(self.config.get("delay_ms", 0) or 0))
            if delay_ms:
                silence = os.path.join(work, "silence.wav")
                self._silence(delay_ms, silence)
                segments.append(silence)
            index = 0
            for source in self._list(prefix_files):
                index += 1
                normalized = os.path.join(work, "prefix-%03d.wav" % index)
                self._normalize(source, normalized)
                segments.append(normalized)
            normalized_speech = os.path.join(work, "speech-normalized.wav")
            self._normalize(speech, normalized_speech)
            segments.append(normalized_speech)
            for source in self._list(suffix_files):
                index += 1
                normalized = os.path.join(work, "suffix-%03d.wav" % index)
                self._normalize(source, normalized)
                segments.append(normalized)
            combined = os.path.join(work, "combined.wav")
            if len(segments) == 1:
                combined = segments[0]
            else:
                self._concat(segments, combined, work)
            ulaw = os.path.join(self.runtime_dir, name + ".ulaw")
            self._convert(combined, ulaw)
            return os.path.splitext(ulaw)[0]
        finally:
            if not self.config.get("keep_generated", False):
                self._remove_tree(work)

    @staticmethod
    def _list(value):
        if not value:
            return []
        if isinstance(value, (list, tuple)):
            return [str(item) for item in value if item]
        return [str(value)]

    def _tts(self, text, wav):
        engine = str(self.config.get("engine", "espeak")).lower()
        if engine == "piper":
            model = self.config.get("piper_model")
            if not model:
                raise RuntimeError("audio.piper_model is required for Piper TTS")
            command = ["piper", "--model", model, "--output_file", wav]
            proc = subprocess.Popen(command, stdin=subprocess.PIPE, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
            out, err = proc.communicate(text.encode("utf-8"))
            if proc.returncode:
                raise RuntimeError("Piper failed: %s" % err.decode("utf-8", "replace"))
            return
        if engine == "custom":
            command = []
            for token in self.config.get("custom_command") or []:
                command.append(str(token).format(text=text, wav=wav))
            if not command:
                raise RuntimeError("audio.custom_command is empty")
            self._run(command)
            return
        binary = "espeak-ng"
        if not self._exists(binary):
            binary = "espeak"
        command = [binary, "-v", str(self.config.get("voice", "en-us")), "-s", str(self.config.get("rate", 155)), "-w", wav, text]
        self._run(command)

    def _silence(self, milliseconds, target):
        seconds = max(0.001, float(milliseconds) / 1000.0)
        self._run([
            self.config.get("ffmpeg_binary", "/usr/bin/ffmpeg"), "-hide_banner", "-loglevel", "error", "-y",
            "-f", "lavfi", "-i", "anullsrc=r=8000:cl=mono", "-t", "%.3f" % seconds,
            "-ar", "8000", "-ac", "1", "-c:a", "pcm_s16le", target,
        ])

    def _normalize(self, source, target):
        if not os.path.exists(source):
            raise RuntimeError("Audio file does not exist: %s" % source)
        command = [self.config.get("ffmpeg_binary", "/usr/bin/ffmpeg"), "-hide_banner", "-loglevel", "error", "-y"]
        lower = source.lower()
        if lower.endswith(".ulaw") or lower.endswith(".mulaw"):
            command += ["-f", "mulaw", "-ar", "8000", "-ac", "1"]
        elif lower.endswith(".sln") or lower.endswith(".sln16"):
            rate = "16000" if lower.endswith(".sln16") else "8000"
            command += ["-f", "s16le", "-ar", rate, "-ac", "1"]
        command += ["-i", source, "-ar", "8000", "-ac", "1", "-c:a", "pcm_s16le", target]
        self._run(command)

    def _concat(self, segments, target, work):
        list_path = os.path.join(work, "concat.txt")
        with open(list_path, "w") as handle:
            for path in segments:
                escaped = path.replace("'", "'\\''")
                handle.write("file '%s'\n" % escaped)
        self._run([
            self.config.get("ffmpeg_binary", "/usr/bin/ffmpeg"), "-hide_banner", "-loglevel", "error", "-y",
            "-f", "concat", "-safe", "0", "-i", list_path,
            "-ar", "8000", "-ac", "1", "-c:a", "pcm_s16le", target,
        ])

    def _convert(self, wav, ulaw):
        command = [
            self.config.get("ffmpeg_binary", "/usr/bin/ffmpeg"), "-hide_banner", "-loglevel", "error", "-y",
            "-i", wav, "-ar", "8000", "-ac", "1", "-f", "mulaw", ulaw,
        ]
        self._run(command)

    @staticmethod
    def _exists(binary):
        if os.path.isabs(binary):
            return os.path.exists(binary)
        for directory in os.environ.get("PATH", "").split(os.pathsep):
            if os.path.isfile(os.path.join(directory, binary)):
                return True
        return False

    @staticmethod
    def _run(command):
        try:
            subprocess.check_output(command, stderr=subprocess.STDOUT)
        except OSError as exc:
            raise RuntimeError("Command is not installed: %s (%s)" % (command[0], exc))
        except subprocess.CalledProcessError as exc:
            raise RuntimeError("Command failed: %s" % exc.output.decode("utf-8", "replace"))

    @staticmethod
    def _remove_tree(path):
        import shutil
        try:
            shutil.rmtree(path)
        except OSError:
            pass
