from __future__ import print_function
from __future__ import division
from builtins import object
# ----------------------------------------------------------------------------
# pyglet
# Copyright (c) 2006-2008 Alex Holkner
# All rights reserved.
#
# Redistribution and use in source and binary forms, with or without
# modification, are permitted provided that the following conditions
# are met:
#
#  * Redistributions of source code must retain the above copyright
#    notice, this list of conditions and the following disclaimer.
#  * Redistributions in binary form must reproduce the above copyright
#    notice, this list of conditions and the following disclaimer in
#    the documentation and/or other materials provided with the
#    distribution.
#  * Neither the name of pyglet nor the names of its
#    contributors may be used to endorse or promote products
#    derived from this software without specific prior written
#    permission.
#
# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS
# FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE
# COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT,
# INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING,
# BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
# LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
# CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
# LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN
# ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
# POSSIBILITY OF SUCH DAMAGE.
# ----------------------------------------------------------------------------

import ctypes
from collections import deque

import pyglet
from pyglet.compat import bytes_type, BytesIO
from pyglet.media.exceptions import MediaException, CannotSeekException

_debug = pyglet.options['debug_media']


class AudioFormat(object):
    """Audio details.

    An instance of this class is provided by sources with audio tracks.  You
    should not modify the fields, as they are used internally to describe the
    format of data provided by the source.

    Args:
        channels (int): The number of channels: 1 for mono or 2 for stereo
            (pyglet does not yet support surround-sound sources).
        sample_size (int): Bits per sample; only 8 or 16 are supported.
        sample_rate (int): Samples per second (in Hertz).
    """

    def __init__(self, channels, sample_size, sample_rate):
        self.channels = channels
        self.sample_size = sample_size
        self.sample_rate = sample_rate

        # Convenience
        self.bytes_per_sample = (sample_size >> 3) * channels
        self.bytes_per_second = self.bytes_per_sample * sample_rate

    def __eq__(self, other):
        if other is None:
            return False
        return (self.channels == other.channels and
                self.sample_size == other.sample_size and
                self.sample_rate == other.sample_rate)

    def __ne__(self, other):
        return not self.__eq__(other)

    def __repr__(self):
        return '%s(channels=%d, sample_size=%d, sample_rate=%d)' % (
            self.__class__.__name__, self.channels, self.sample_size,
            self.sample_rate)


class VideoFormat(object):
    """Video details.

    An instance of this class is provided by sources with a video stream. You
    should not modify the fields.

    Note that the sample aspect has no relation to the aspect ratio of the
    video image.  For example, a video image of 640x480 with sample aspect 2.0
    should be displayed at 1280x480.  It is the responsibility of the
    application to perform this scaling.

    Args:
        width (int): Width of video image, in pixels.
        height (int): Height of video image, in pixels.
        sample_aspect (float): Aspect ratio (width over height) of a single
            video pixel.
        frame_rate (float): Frame rate (frames per second) of the video.

            .. versionadded:: 1.2
    """

    def __init__(self, width, height, sample_aspect=1.0):
        self.width = width
        self.height = height
        self.sample_aspect = sample_aspect
        self.frame_rate = None

    def __eq__(self, other):
        if isinstance(other, VideoFormat):
            return (self.width == other.width and
                    self.height == other.height and
                    self.sample_aspect == other.sample_aspect and
                    self.frame_rate == other.frame_rate)
        return False


class AudioData(object):
    """A single packet of audio data.

    This class is used internally by pyglet.

    Args:
        data (str or ctypes array or pointer): Sample data.
        length (int): Size of sample data, in bytes.
        timestamp (float): Time of the first sample, in seconds.
        duration (float): Total data duration, in seconds.
        events (List[:class:`pyglet.media.events.MediaEvent`]): List of events
            contained within this packet. Events are timestamped relative to
            this audio packet.
    """

    def __init__(self, data, length, timestamp, duration, events):
        self.data = data
        self.length = length
        self.timestamp = timestamp
        self.duration = duration
        self.events = events

    def __eq__(self, other):
        if isinstance(other, AudioData):
            return (self.data == other.data and
                    self.length == other.length and
                    self.timestamp == other.timestamp and
                    self.duration == other.duration and
                    self.events == other.events)
        return False

    def consume(self, bytes, audio_format):
        """Remove some data from the beginning of the packet.

        All events are cleared.

        Args:
            bytes (int): The number of bytes to consume from the packet.
            audio_format (:class:`.AudioFormat`): The packet audio format.
        """
        self.events = ()
        if bytes >= self.length:
            self.data = None
            self.length = 0
            self.timestamp += self.duration
            self.duration = 0.
            return
        elif bytes == 0:
            return

        if not isinstance(self.data, str):
            # XXX Create a string buffer for the whole packet then
            #     chop it up.  Could do some pointer arith here and
            #     save a bit of data pushing, but my guess is this is
            #     faster than fudging aruond with ctypes (and easier).
            data = ctypes.create_string_buffer(self.length)
            ctypes.memmove(data, self.data, self.length)
            self.data = data
        self.data = self.data[bytes:]
        self.length -= bytes
        self.duration -= bytes / float(audio_format.bytes_per_second)
        self.timestamp += bytes / float(audio_format.bytes_per_second)

    def get_string_data(self):
        """Return data as a bytestring.

        Returns:
            bytes or str: Data as a (byte)string. For Python 3 it's a
            bytestring while for Python 2 it's a string.
        """
        if self.data is None:
            return b''

        if isinstance(self.data, bytes_type):
            return self.data

        buf = ctypes.create_string_buffer(self.length)
        ctypes.memmove(buf, self.data, self.length)
        return buf.raw


class SourceInfo(object):
    """Source metadata information.

    Fields are the empty string or zero if the information is not available.

    Args:
        title (str): Title
        author (str): Author
        copyright (str): Copyright statement
        comment (str): Comment
        album (str): Album name
        year (int): Year
        track (int): Track number
        genre (str): Genre

    .. versionadded:: 1.2
    """

    title = ''
    author = ''
    copyright = ''
    comment = ''
    album = ''
    year = 0
    track = 0
    genre = ''


class Source(object):
    """An audio and/or video source.

    Args:
        audio_format (:class:`.AudioFormat`): Format of the audio in this
            source, or ``None`` if the source is silent.
        video_format (:class:`.VideoFormat`): Format of the video in this
            source, or ``None`` if there is no video.
        info (:class:`.SourceInfo`): Source metadata such as title, artist,
            etc; or ``None`` if the` information is not available.

            .. versionadded:: 1.2
    """

    _duration = None

    audio_format = None
    video_format = None
    info = None

    @property
    def duration(self):
        """float: The length of the source, in seconds.

        Not all source durations can be determined; in this case the value
        is ``None``.

        Read-only.
        """
        return self._duration

    def play(self):
        """Play the source.

        This is a convenience method which creates a Player for
        this source and plays it immediately.

        Returns:
            :class:`.Player`
        """
        from pyglet.media.player import Player  # XXX Nasty circular dependency
        player = Player()
        player.queue(self)
        player.play()
        return player

    def get_animation(self):
        """
        Import all video frames into memory.

        An empty animation will be returned if the source has no video.
        Otherwise, the animation will contain all unplayed video frames (the
        entire source, if it has not been queued on a player). After creating
        the animation, the source will be at EOS (end of stream).

        This method is unsuitable for videos running longer than a
        few seconds.

        .. versionadded:: 1.1

        Returns:
            :class:`pyglet.image.Animation`
        """
        from pyglet.image import Animation, AnimationFrame
        if not self.video_format:
            # XXX: This causes an assertion in the constructor of Animation
            return Animation([])
        else:
            frames = []
            last_ts = 0
            next_ts = self.get_next_video_timestamp()
            while next_ts is not None:
                image = self.get_next_video_frame()
                if image is not None:
                    delay = next_ts - last_ts
                    frames.append(AnimationFrame(image, delay))
                    last_ts = next_ts
                next_ts = self.get_next_video_timestamp()
            return Animation(frames)

    def get_next_video_timestamp(self):
        """Get the timestamp of the next video frame.

        .. versionadded:: 1.1

        Returns:
            float: The next timestamp, or ``None`` if there are no more video
            frames.
        """
        pass

    def get_next_video_frame(self):
        """Get the next video frame.

        .. versionadded:: 1.1

        Returns:
            :class:`pyglet.image.AbstractImage`: The next video frame image,
            or ``None`` if the video frame could not be decoded or there are
            no more video frames.
        """
        pass

    # Internal methods that PlayList calls on the source:

    def seek(self, timestamp):
        """Seek to given timestamp.

        Args:
            timestamp (float): Time where to seek in the source. The
                ``timestamp`` will be clamped to the duration of the source.
        """
        raise CannotSeekException()

    def _get_queue_source(self):
        """Return the ``Source`` to be used as the queue source for a player.

        Default implementation returns self.
        """
        return self

    def get_audio_data(self, bytes, compensation_time=0.0):
        """Get next packet of audio data.

        Args:
            bytes (int): Maximum number of bytes of data to return.
            compensation_time (float): Time in sec to compensate due to a
                difference between the master clock and the audio clock.

        Returns:
            :class:`.AudioData`: Next packet of audio data, or ``None`` if
            there is no (more) data.
        """
        return None


class StreamingSource(Source):
    """A source that is decoded as it is being played.

    The source can only be queued once.
    """

    _is_queued = False

    @property
    def is_queued(self):
        """
        bool: Determine if this source has been queued.

        Check on a :py:class:`~pyglet.media.player.Player` if this source
        has been queued.

        Read-only.
        """
        return self._is_queued

    def _get_queue_source(self):
        """Return the ``Source`` to be used as the queue source for a player.

        Default implementation returns self.

        Returns:
            :class:`.Source`
        """
        if self._is_queued:
            raise MediaException('This source is already queued on a player.')
        self._is_queued = True
        return self

    def delete(self):
        """Release the resources held by this StreamingSource."""
        pass


class StaticSource(Source):
    """A source that has been completely decoded in memory.

    This source can be queued onto multiple players any number of times.

    Construct a :py:class:`~pyglet.media.StaticSource` for the data in
    ``source``.

    Args:
        source (Source):  The source to read and decode audio and video data
            from.
    """

    def __init__(self, source):
        source = source._get_queue_source()
        if source.video_format:
            raise NotImplementedError(
                'Static sources not supported for video yet.')

        self.audio_format = source.audio_format
        if not self.audio_format:
            self._data = None
            self._duration = 0.
            return

        # Arbitrary: number of bytes to request at a time.
        buffer_size = 1 << 20  # 1 MB

        # Naive implementation.  Driver-specific implementations may override
        # to load static audio data into device (or at least driver) memory.
        data = BytesIO()
        while True:
            audio_data = source.get_audio_data(buffer_size)
            if not audio_data:
                break
            data.write(audio_data.get_string_data())
        self._data = data.getvalue()

        self._duration = (len(self._data) /
                          float(self.audio_format.bytes_per_second))

    def _get_queue_source(self):
        if self._data is not None:
            return StaticMemorySource(self._data, self.audio_format)

    def get_audio_data(self, bytes, compensation_time=0.0):
        """The StaticSource does not provide audio data.

        When the StaticSource is queued on a
        :class:`~pyglet.media.player.Player`, it creates a
        :class:`.StaticMemorySource` containing its internal audio data and
        audio format.

        Raises:
            RuntimeError
        """
        raise RuntimeError('StaticSource cannot be queued.')


class StaticMemorySource(StaticSource):
    """
    Helper class for default implementation of :class:`.StaticSource`.

    Do not use directly. This class is used internally by pyglet.

    Args:
        data (AudioData): The audio data.
        audio_format (AudioFormat): The audio format.
    """

    def __init__(self, data, audio_format):
        """Construct a memory source over the given data buffer."""
        self._file = BytesIO(data)
        self._max_offset = len(data)
        self.audio_format = audio_format
        self._duration = len(data) / float(audio_format.bytes_per_second)

    def seek(self, timestamp):
        """Seek to given timestamp.

        Args:
            timestamp (float): Time where to seek in the source.
        """
        offset = int(timestamp * self.audio_format.bytes_per_second)

        # Align to sample
        if self.audio_format.bytes_per_sample == 2:
            offset &= 0xfffffffe
        elif self.audio_format.bytes_per_sample == 4:
            offset &= 0xfffffffc

        self._file.seek(offset)

    def get_audio_data(self, bytes, compensation_time=0.0):
        """Get next packet of audio data.

        Args:
            bytes (int): Maximum number of bytes of data to return.
            compensation_time (float): Not used in this class.

        Returns:
            :class:`.AudioData`: Next packet of audio data, or ``None`` if
            there is no (more) data.
        """
        offset = self._file.tell()
        timestamp = float(offset) / self.audio_format.bytes_per_second

        # Align to sample size
        if self.audio_format.bytes_per_sample == 2:
            bytes &= 0xfffffffe
        elif self.audio_format.bytes_per_sample == 4:
            bytes &= 0xfffffffc

        data = self._file.read(bytes)
        if not len(data):
            return None

        duration = float(len(data)) / self.audio_format.bytes_per_second
        return AudioData(data, len(data), timestamp, duration, [])


class PlayList(object):
    """Represents a queue of sources.

    This class is used internally by pyglet.

    .. versionadded:: 1.4
    """

    # TODO can sources list go empty?  what behaviour (ignore or error)?

    def __init__(self):
        self.audio_format = None
        self.video_format = None
        self.duration = 0.0
        self._sources = deque()

    def seek(self, time):
        """Seek the first source to given timestamp.

        Args:
            timestamp (float): Time where to seek in the first source. The
                ``timestamp`` will be clamped to the duration of the source.
        """
        if self._sources:
            self._sources[0].seek(time)

    def queue(self, source):
        """Add the source to the queue.

        :Parameters:
            `source`: :class:`~pyglet.media.Source`
                The source to queue.
        """
        source = source._get_queue_source()
        if not self._sources:
            # The first queued source will determine the initial playlist
            # audio and video format.
            # These are updated each time `next_source` is called.
            self.audio_format = source.audio_format
            self.video_format = source.video_format
        self._sources.append(source)
        self.duration += source.duration

    def has_next(self):
        """Check if there is a source behind the current one.

        Returns:
            bool
        """
        return len(self._sources) > 1

    def next_source(self):
        """Discard the current source.

        This places the next source on top of the playlist.
        """
        if self._sources:
            old_source = self._sources.popleft()
            self.duration -= old_source.duration
        if self._sources:
            source = self._sources[0]
            self.audio_format = source.audio_format
            self.video_format = source.video_format

    #: :deprecated: Use `next_source` instead.
    next = next_source  # old API, worked badly with 2to3

    def get_current_source(self):
        """Get the current source on the playlist.

        Returns:
            :class:`.Source`
        """
        if self._sources:
            return self._sources[0]

    def get_audio_data(self, bytes, compensation_time=0.0):
        """Get next audio packet.

        Args:
            bytes (int): Hint for preferred size of audio packet; may be
                ignored.
            compensation_time (float): Time in sec to compensate due to a
                difference between the master clock and the audio clock.

        Returns:
            :class:`.AudioData`: The audio data or ``None`` if there is no
            more data.
        """
        if not self._sources:
            return None
        data = self._sources[0].get_audio_data(bytes, compensation_time)
        return data

    def get_next_video_timestamp(self):
        """Get the timestamp of the next video frame for the current source.

        :rtype: float
        :return: The next timestamp, or `None` if there are no more video
            frames.
        """
        if not self._sources:
            return None
        timestamp = self._sources[0].get_next_video_timestamp()
        return timestamp

    def get_next_video_frame(self):
        """Get the next video frame.

        :rtype: :class:`pyglet.image.AbstractImage`
        :return: The next video frame image.
        """
        if self._sources:
            return self._sources[0].get_next_video_frame()
