#!/usr/bin/env python3
#
# Copyright (c) 2024 Stormux
#
# This library is free software; you can redistribute it and/or
# modify it under the terms of the GNU Lesser General Public
# License as published by the Free Software Foundation; either
# version 2.1 of the License, or (at your option) any later version.
#
# This library is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
# Lesser General Public License for more details.
#
# You should have received a copy of the GNU Lesser General Public
# License along with this library; if not, write to the
# Free Software Foundation, Inc., Franklin Street, Fifth Floor,
# Boston MA  02110-1301 USA.
#
# Cthulhu project: https://git.stormux.org/storm/cthulhu

"""GStreamer-based audio player for Piper TTS synthesis output."""

__id__ = "$Id:$"
__version__ = "$Revision:$"
__date__ = "$Date:$"
__copyright__ = "Copyright (c) 2024 Stormux"
__license__ = "LGPL"

import threading

import gi
from gi.repository import GLib
from . import debug
from . import gstreamer_support
from . import sound_sink

try:
    gi.require_version('Gst', '1.0')
    from gi.repository import Gst
except Exception:
    _gstreamerAvailable = False
else:
    _gstreamerAvailable = gstreamer_support.gst_init_check_available(Gst)


class PiperAudioPlayer:
    """GStreamer-based audio player for Piper TTS output.

    Handles raw PCM audio data from Piper synthesis and plays it through
    a GStreamer pipeline with volume control.
    """

    def __init__(self, sampleRate=22050):
        """Initialize the audio player.

        Arguments:
        - sampleRate: Audio sample rate in Hz (default 22050, common for Piper)
        """
        self._sampleRate = sampleRate
        self._pipeline = None
        self._appsrc = None
        self._volume = None
        self._initialized = False
        self._playing = False
        self._lock = threading.Lock()
        self._completionCallback = None

        if not _gstreamerAvailable:
            msg = 'PIPER AUDIO: GStreamer is not available'
            debug.printMessage(debug.LEVEL_WARNING, msg, True)
            return

        self._init()

    def _init(self):
        """Initialize the GStreamer pipeline."""
        if self._initialized:
            return True

        if not _gstreamerAvailable:
            return False

        self._resetPipeline()

        try:
            self._pipeline = Gst.Pipeline.new("piper-audio")
            configuredSink = sound_sink.get_configured_sound_sink()

            self._appsrc = Gst.ElementFactory.make("appsrc", "source")
            if self._appsrc is None:
                msg = 'PIPER AUDIO: Failed to create appsrc element'
                debug.printMessage(debug.LEVEL_WARNING, msg, True)
                return False

            self._appsrc.set_property("format", Gst.Format.TIME)
            self._appsrc.set_property("is-live", False)
            self._appsrc.set_property("block", False)

            caps = Gst.Caps.from_string(
                f"audio/x-raw,format=S16LE,channels=1,"
                f"rate={self._sampleRate},layout=interleaved"
            )
            self._appsrc.set_property("caps", caps)

            convert = Gst.ElementFactory.make("audioconvert", "convert")
            resample = Gst.ElementFactory.make("audioresample", "resample")

            self._volume = Gst.ElementFactory.make("volume", "volume")
            if self._volume is None:
                msg = 'PIPER AUDIO: Failed to create volume element'
                debug.printMessage(debug.LEVEL_WARNING, msg, True)
                return False

            sink, sinkName, sinkError = sound_sink.create_audio_sink("sink", configuredSink)
            if sink is None:
                msg = f'PIPER AUDIO: {sinkError or "Failed to create audio sink"}'
                debug.printMessage(debug.LEVEL_WARNING, msg, True)
                return False

            for element in [self._appsrc, convert, resample, self._volume, sink]:
                self._pipeline.add(element)

            if not self._appsrc.link(convert):
                msg = 'PIPER AUDIO: Failed to link appsrc to convert'
                debug.printMessage(debug.LEVEL_WARNING, msg, True)
                return False
            if not convert.link(resample):
                msg = 'PIPER AUDIO: Failed to link convert to resample'
                debug.printMessage(debug.LEVEL_WARNING, msg, True)
                return False
            if not resample.link(self._volume):
                msg = 'PIPER AUDIO: Failed to link resample to volume'
                debug.printMessage(debug.LEVEL_WARNING, msg, True)
                return False
            if not self._volume.link(sink):
                msg = 'PIPER AUDIO: Failed to link volume to sink'
                debug.printMessage(debug.LEVEL_WARNING, msg, True)
                return False

            bus = self._pipeline.get_bus()
            bus.add_signal_watch()
            bus.connect("message", self._onMessage)

            self._initialized = True
            msg = (
                f'PIPER AUDIO: Pipeline initialized successfully using '
                f'{sinkName} (soundSink={configuredSink})'
            )
            debug.printMessage(debug.LEVEL_INFO, msg, True)
            return True

        except Exception as e:
            msg = f'PIPER AUDIO: Failed to initialize pipeline: {e}'
            debug.printMessage(debug.LEVEL_WARNING, msg, True)
            return False

    def _resetPipeline(self):
        """Reset the GStreamer pipeline and related elements."""
        if self._pipeline is not None:
            self._pipeline.set_state(Gst.State.NULL)
            self._pipeline = None
        self._appsrc = None
        self._volume = None
        self._initialized = False

    def _onMessage(self, bus, message):
        """Handle GStreamer bus messages."""
        if message.type == Gst.MessageType.EOS:
            self._pipeline.set_state(Gst.State.NULL)
            with self._lock:
                self._playing = False
            if self._completionCallback:
                GLib.idle_add(self._completionCallback)
                self._completionCallback = None
        elif message.type == Gst.MessageType.ERROR:
            self._pipeline.set_state(Gst.State.NULL)
            with self._lock:
                self._playing = False
            error, info = message.parse_error()
            msg = f'PIPER AUDIO ERROR: {error}'
            debug.printMessage(debug.LEVEL_WARNING, msg, True)

    def setSampleRate(self, sampleRate):
        """Update the sample rate for the audio stream.

        Arguments:
        - sampleRate: New sample rate in Hz
        """
        if sampleRate != self._sampleRate:
            self._sampleRate = sampleRate
            self.stop()
            self._resetPipeline()
            self._init()

    def setVolume(self, volumeLevel):
        """Set the playback volume.

        Arguments:
        - volumeLevel: Volume level from 0.0 to 1.0
        """
        if self._volume is not None:
            volume = max(0.0, min(1.0, volumeLevel))
            self._volume.set_property("volume", volume)

    def play(self, audioData, interrupt=True, completionCallback=None):
        """Play raw PCM audio data.

        Arguments:
        - audioData: Raw PCM audio data as bytes (16-bit signed, little-endian)
        - interrupt: If True, stop any current playback first
        - completionCallback: Optional callback to invoke when playback completes
        """
        if not self._initialized:
            if not self._init():
                msg = 'PIPER AUDIO: Cannot play - not initialized'
                debug.printMessage(debug.LEVEL_WARNING, msg, True)
                return False

        if interrupt:
            self.stop()

        with self._lock:
            self._playing = True

        self._completionCallback = completionCallback

        self._pipeline.set_state(Gst.State.PLAYING)

        buf = Gst.Buffer.new_wrapped(audioData)
        result = self._appsrc.emit("push-buffer", buf)
        if result != Gst.FlowReturn.OK:
            msg = f'PIPER AUDIO: Failed to push buffer: {result}'
            debug.printMessage(debug.LEVEL_WARNING, msg, True)

        self._appsrc.emit("end-of-stream")
        return True

    def playStream(self, audioGenerator, interrupt=True, completionCallback=None):
        """Play audio from a streaming generator.

        Arguments:
        - audioGenerator: Iterator/generator yielding audio chunks as bytes
        - interrupt: If True, stop any current playback first
        - completionCallback: Optional callback when playback completes
        """
        if not self._initialized:
            if not self._init():
                msg = 'PIPER AUDIO: Cannot play stream - not initialized'
                debug.printMessage(debug.LEVEL_WARNING, msg, True)
                return False

        if interrupt:
            self.stop()

        with self._lock:
            self._playing = True

        self._completionCallback = completionCallback

        self._pipeline.set_state(Gst.State.PLAYING)

        def feedThread():
            try:
                for audioChunk in audioGenerator:
                    with self._lock:
                        if not self._playing:
                            break

                    buf = Gst.Buffer.new_wrapped(bytes(audioChunk))
                    result = self._appsrc.emit("push-buffer", buf)
                    if result != Gst.FlowReturn.OK:
                        msg = f'PIPER AUDIO: Stream push failed: {result}'
                        debug.printMessage(debug.LEVEL_WARNING, msg, True)
                        break
            except Exception as e:
                msg = f'PIPER AUDIO: Stream feed error: {e}'
                debug.printMessage(debug.LEVEL_WARNING, msg, True)
            finally:
                self._appsrc.emit("end-of-stream")

        thread = threading.Thread(target=feedThread, daemon=True)
        thread.start()
        return True

    def isPlaying(self):
        """Check if audio is currently playing.

        Returns True if playback is in progress.
        """
        with self._lock:
            return self._playing

    def stop(self):
        """Stop any current playback."""
        with self._lock:
            self._playing = False

        if self._pipeline is not None:
            self._pipeline.set_state(Gst.State.NULL)

        self._completionCallback = None

    def shutdown(self):
        """Shut down the audio player and release resources."""
        self.stop()
        self._resetPipeline()
