# Copyright 2004-2022 Tom Rothamel <pytom@bishoujo.us>
#
# Permission is hereby granted, free of charge, to any person
# obtaining a copy of this software and associated documentation files
# (the "Software"), to deal in the Software without restriction,
# including without limitation the rights to use, copy, modify, merge,
# publish, distribute, sublicense, and/or sell copies of the Software,
# and to permit persons to whom the Software is furnished to do so,
# subject to the following conditions:
#
# The above copyright notice and this permission notice shall be
# included in all copies or substantial portions of the Software.
#
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
# EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
# MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
# NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
# LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
# OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
# WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.

from __future__ import division, absolute_import, with_statement, print_function, unicode_literals
from renpy.compat import PY2, basestring, bchr, bord, chr, open, pystr, range, round, str, tobytes, unicode # *


import collections

import renpy

# The movie displayable that's currently being shown on the screen.
current_movie = None

# True if the movie that is currently displaying is in fullscreen mode,
# False if it's a smaller size.
fullscreen = False

# The size of a Movie object that hasn't had an explicit size set.
default_size = (400, 300)

# The file we allocated the surface for.
surface_file = None

# The surface to display the movie on, if not fullscreen.
surface = None


def movie_stop(clear=True, only_fullscreen=False):
    """
    Stops the currently playing movie.
    """

    if (not fullscreen) and only_fullscreen:
        return

    renpy.audio.music.stop(channel='movie')


def movie_start(filename, size=None, loops=0):
    """
    This starts a movie playing.
    """

    if renpy.game.less_updates:
        return

    global default_size

    if size is not None:
        default_size = size

    filename = [ filename ]

    if loops == -1:
        loop = True
    else:
        loop = False
        filename = filename * (loops + 1)

    renpy.audio.music.play(filename, channel='movie', loop=loop)


movie_start_fullscreen = movie_start
movie_start_displayable = movie_start

# A map from a channel name to the movie texture that is being displayed
# on that channel.
texture = { }

# The set of channels that are being displayed in Movie objects.
displayable_channels = collections.defaultdict(list)

# A map from a channel to the topmost Movie being displayed on
# that channel. (Or None if no such movie exists.)
channel_movie = { }

# Is there a video being displayed fullscreen?
fullscreen = False

# Movie channels that had a hide operation since the last interaction took
# place.
reset_channels = set()


def early_interact():
    """
    Called early in the interact process, to clear out the fullscreen
    flag.
    """

    displayable_channels.clear()
    channel_movie.clear()


def interact():
    """
    This is called each time the screen is drawn, and should return True
    if the movie should display fullscreen.
    """

    global fullscreen

    for i in list(texture.keys()):
        if not renpy.audio.music.get_playing(i):
            del texture[i]

    if renpy.audio.music.get_playing("movie"):

        for i in displayable_channels.keys():
            if i[0] == "movie":
                fullscreen = False
                break
        else:
            fullscreen = True

    else:
        fullscreen = False

    return fullscreen


def get_movie_texture(channel, mask_channel=None, side_mask=False, mipmap=None):

    if not renpy.audio.music.get_playing(channel):
        return None, False

    if mipmap is None:
        mipmap = renpy.config.mipmap_movies

    c = renpy.audio.music.get_channel(channel)
    surf = c.read_video()

    if side_mask:

        if surf is not None:

            w, h = surf.get_size()
            w //= 2

            mask_surf = surf.subsurface((w, 0, w, h))
            surf = surf.subsurface((0, 0, w, h))

        else:
            mask_surf = None

    elif mask_channel:
        mc = renpy.audio.music.get_channel(mask_channel)
        mask_surf = mc.read_video()
    else:
        mask_surf = None

    if mask_surf is not None:

        # Something went wrong with the mask video.
        if surf:
            renpy.display.module.alpha_munge(mask_surf, surf, renpy.display.im.identity)
        else:
            surf = None

    if surf is not None:
        renpy.display.render.mutated_surface(surf)
        tex = renpy.display.draw.load_texture(surf, True, { "mipmap" : mipmap })
        texture[channel] = tex
        new = True
    else:
        tex = texture.get(channel, None)
        new = False

    return tex, new


def render_movie(channel, width, height):
    tex, _new = get_movie_texture(channel)

    if tex is None:
        return None

    sw, sh = tex.get_size()

    scale = min(1.0 * width / sw, 1.0 * height / sh)

    dw = scale * sw
    dh = scale * sh

    rv = renpy.display.render.Render(width, height)
    rv.forward = renpy.display.matrix.Matrix2D(1.0 / scale, 0.0, 0.0, 1.0 / scale)
    rv.reverse = renpy.display.matrix.Matrix2D(scale, 0.0, 0.0, scale)
    rv.blit(tex, (int((width - dw) / 2), int((height - dh) / 2)))

    return rv


def default_play_callback(old, new): # @UnusedVariable

    renpy.audio.music.play(new._play, channel=new.channel, loop=new.loop, synchro_start=True)

    if new.mask:
        renpy.audio.music.play(new.mask, channel=new.mask_channel, loop=new.loop, synchro_start=True)

# A serial number that's used to generated movie channels.
movie_channel_serial = 0

class Movie(renpy.display.core.Displayable):
    """
    :doc: movie

    This is a displayable that shows the current movie.

    `fps`
        The framerate that the movie should be shown at. (This is currently
        ignored, but the parameter is kept for backwards compatibility.
        The framerate is auto-detected.)

    `size`
        This should be specified as either a tuple giving the width and
        height of the movie, or None to automatically adjust to the size
        of the playing movie. (If None, the displayable will be (0, 0)
        when the movie is not playing.)

    `channel`
        The audio channel associated with this movie. When a movie file
        is played on that channel, it will be displayed in this Movie
        displayable. If this is left at the default of "movie", and `play`
        is provided, a channel name is automatically selected, using
        :var:`config.single_movie_channel` and :var:`config.auto_movie_channel`.

    `play`
        If given, this should be the path to a movie file, or a list
        of paths to movie files. These movie
        files will be automatically played on `channel` when the Movie is
        shown, and automatically stopped when the movie is hidden.

    `side_mask`
        If true, this tells Ren'Py to use the side-by-side mask mode for
        the Movie. In this case, the movie is divided in half. The left
        half is used for color information, while the right half is used
        for alpha information. The width of the displayable is half the
        width of the movie file.

        Where possible, `side_mask` should be used over `mask` as it has
        no chance of frames going out of sync.

    `mask`
        If given, this should be the path to a movie file, or a list of paths
        to movie files, that are used as
        the alpha channel of this displayable. The movie file will be
        automatically played on `movie_channel` when the Movie is shown,
        and automatically stopped when the movie is hidden.

    `mask_channel`
        The channel the alpha mask video is played on. If not given,
        defaults to `channel`\\_mask. (For example, if `channel` is "sprite",
        `mask_channel` defaults to "sprite_mask".)

    `start_image`
        An image that is displayed when playback has started, but the
        first frame has not yet been decoded.

    `image`
        An image that is displayed when `play` has been given, but the
        file it refers to does not exist. (For example, this can be used
        to create a slimmed-down mobile version that does not use movie
        sprites.) Users can also choose to fall back to this image as a
        preference if video is too taxing for their system. The image will
        also be used if the video plays, and then the movie ends.

    `play_callback`
        If not None, a function that's used to start the movies playing.
        (This may do things like queue a transition between sprites, if
        desired.) It's called with the following arguments:

        `old`
            The old Movie object, or None if the movie is not playing.
        `new`
            The new Movie object.

        A movie object has the `play` parameter available as ``_play``,
        while the ``channel``, ``loop``, ``mask``, and ``mask_channel`` fields
        correspond to the given parameters.

        Generally, this will want to use :func:`renpy.music.play` to start
        the movie playing on the given channel, with synchro_start=True.
        A minimal implementation is::

            def play_callback(old, new):

                renpy.music.play(new._play, channel=new.channel, loop=new.loop, synchro_start=True)

                if new.mask:
                    renpy.music.play(new.mask, channel=new.mask_channel, loop=new.loop, synchro_start=True)

    `loop`
        If False, the movie will not loop. If `image` is defined, the image
        will be displayed when the movie ends. Otherwise, the displayable will
        become transparent.
    """

    fullscreen = False
    channel = "movie"
    _play = None
    _original_play = None

    mask = None
    mask_channel = None
    side_mask = False

    image = None
    start_image = None

    play_callback = None

    loop = True


    def any_loadable(self, name):
        """
        If `name` is a string, checks if that filename is loadable.
        If `name` is a list of strings, checks if any filenames is loadable.
        """

        if isinstance(name, basestring):
            return renpy.loader.loadable(name)
        else:
            return any(renpy.loader.loadable(i) for i in name)

    def after_setstate(self):
        play = self._original_play or self._play
        if (play is not None) and self.any_loadable(play):
            self._original_play = self._play = play
        else:
            self._play = None
            self._original_play = play

        global movie_channel_serial

        if (self.channel is not None) and ((" " in self.channel) or ("/" in self.channel)):
            self.channel = "_movie_{}".format(movie_channel_serial)
            movie_channel_serial += 1

            if self.mask_channel is not None:
                self.mask_channel = self.channel + "_mask"


    def ensure_channel(self, name):

        if name is None:
            return

        if renpy.audio.music.channel_defined(name):
            return

        if self.mask:
            framedrop = True
        else:
            framedrop = False

        renpy.audio.music.register_channel(name, renpy.config.movie_mixer, loop=True, stop_on_mute=False, movie=True, framedrop=framedrop, force=True)

    def __init__(self, fps=24, size=None, channel="movie", play=None, mask=None, mask_channel=None, image=None, play_callback=None, side_mask=False, loop=True, start_image=None, **properties):

        global movie_channel_serial

        super(Movie, self).__init__(**properties)

        if channel == "movie" and play and renpy.config.single_movie_channel:
            channel = renpy.config.single_movie_channel
        elif channel == "movie" and play and renpy.config.auto_movie_channel:
            channel = "_movie_{}".format(movie_channel_serial)
            movie_channel_serial += 1

        self.size = size
        self.channel = channel
        self.loop = loop

        self._original_play = play
        if (play is not None) and self.any_loadable(play):
            self._play = play

        if side_mask:
            mask = None

        self.mask = mask

        if mask is None:
            self.mask_channel = None
        elif mask_channel is None:
            self.mask_channel = channel + "_mask"
        else:
            self.mask_channel = mask_channel

        self.side_mask = side_mask

        self.ensure_channel(self.channel)
        self.ensure_channel(self.mask_channel)

        self.image = renpy.easy.displayable_or_none(image)
        self.start_image = renpy.easy.displayable_or_none(start_image)

        self.play_callback = play_callback

        if (self.channel == "movie") and (renpy.config.hw_video) and renpy.mobile:
            raise Exception("Movie(channel='movie') doesn't work on mobile when config.hw_video is true. (Use a different channel argument.)")

    def render(self, width, height, st, at):

        if self._play and not (renpy.game.preferences.video_image_fallback is True):
            channel_movie[self.channel] = self

            if st == 0:
                reset_channels.add(self.channel)

        playing = renpy.audio.music.get_playing(self.channel)

        not_playing = not playing

        if self.channel in reset_channels:
            not_playing = False

        if (self.image is not None) and not_playing:
            surf = renpy.display.render.render(self.image, width, height, st, at)
            w, h = surf.get_size()
            rv = renpy.display.render.Render(w, h)
            rv.blit(surf, (0, 0))

            return rv

        if self.size is None:

            tex, _ = get_movie_texture(self.channel, self.mask_channel, self.side_mask, self.style.mipmap)

            if (not not_playing) and (tex is not None):
                width, height = tex.get_size()

                rv = renpy.display.render.Render(width, height)
                rv.blit(tex, (0, 0))

            elif (not not_playing) and (self.start_image is not None):
                surf = renpy.display.render.render(self.start_image, width, height, st, at)
                w, h = surf.get_size()
                rv = renpy.display.render.Render(w, h)
                rv.blit(surf, (0, 0))

            else:
                rv = renpy.display.render.Render(0, 0)

        else:

            w, h = self.size

            if not playing:
                rv = None
            else:
                rv = render_movie(self.channel, w, h)

            if rv is None:
                rv = renpy.display.render.Render(w, h)

        # Usually we get redrawn when the frame is ready - but we want
        # the movie to disappear if it's ended, or if it hasn't started
        # yet.
        renpy.display.render.redraw(self, 0.1)

        return rv

    def play(self, old):
        if old is None:
            old_play = None
        else:
            old_play = old._play

        if (self._play != old_play) or renpy.config.replay_movie_sprites:
            if self._play:

                if self.play_callback is not None:
                    self.play_callback(old, self)
                else:
                    default_play_callback(old, self)

            else:
                renpy.audio.music.stop(channel=self.channel)

                if self.mask:
                    renpy.audio.music.stop(channel=self.mask_channel) # type: ignore

    def stop(self):
        if self._play:
            renpy.audio.music.stop(channel=self.channel)

            if self.mask:
                renpy.audio.music.stop(channel=self.mask_channel) # type: ignore

    def per_interact(self):
        displayable_channels[(self.channel, self.mask_channel)].append(self)
        renpy.display.render.redraw(self, 0)

    def visit(self):
        return [ self.image, self.start_image ]


def playing():
    if renpy.audio.music.get_playing("movie"):
        return True

    for i in displayable_channels:
        channel, _mask_channel = i

        if renpy.audio.music.get_playing(channel):
            return True

    return


def update_playing():
    """
    Calls play/stop on Movie displayables.
    """

    old_channel_movie = renpy.game.context().movie

    for c, m in channel_movie.items():

        old = old_channel_movie.get(c, None)

        if (c in reset_channels) and renpy.config.replay_movie_sprites:
            m.play(old)
        elif old is not m:
            m.play(old)

    for c, m in old_channel_movie.items():
        if c not in channel_movie:
            m.stop()

    renpy.game.context().movie = dict(channel_movie)
    reset_channels.clear()


def frequent():
    """
    Called to update the video playback. Returns true if a video refresh is
    needed, false otherwise.
    """

    update_playing()

    renpy.audio.audio.advance_time()

    if displayable_channels:

        update = True

        for i in displayable_channels:
            channel, mask_channel = i

            c = renpy.audio.audio.get_channel(channel)
            if not c.video_ready():
                update = False
                break

            if mask_channel:
                c = renpy.audio.audio.get_channel(mask_channel)
                if not c.video_ready():
                    update = False
                    break

        if update:
            for v in displayable_channels.values():
                for j in v:
                    renpy.display.render.redraw(j, 0.0)

        return False

    elif fullscreen and not ((renpy.android or renpy.ios) and renpy.config.hw_video):

        c = renpy.audio.audio.get_channel("movie")

        if c.video_ready():
            return True
        else:
            return False

    return False
