#!/usr/bin/env python
"""
Usage

    media_player.py [options] <filename> [<filename> ...]

Plays the audio / video files listed as arguments, optionally
collecting debug info

Options
    --debug : saves sequence of internal state as a binary *.dbg
    --outfile : filename to store the debug info, defaults to filename.dbg

The raw data captured in the .dbg can be rendered as human readable
using the script report.py
"""

from __future__ import print_function

__docformat__ = 'restructuredtext'
__version__ = '$Id: $'

import os
import sys
import weakref

from pyglet.gl import *
import pyglet
from pyglet.window import key

pyglet.options['debug_media'] = False
# pyglet.options['audio'] = ('openal', 'pulse', 'silent')
from pyglet.media import buffered_logger as bl


def draw_rect(x, y, width, height, color=(1, 1, 1, 1)):
    pyglet.graphics.draw(
        4,
        GL_LINE_LOOP,
        position=('f', (x, y, 0,
                        x + width, y, 0,
                        x + width, y + height, 0,
                        x, y + height, 0,
                        )
                  ),
        colors=('f', color * 4)
    )


class Control(pyglet.event.EventDispatcher):
    x = y = 0
    width = height = 10

    def __init__(self, parent):
        super(Control, self).__init__()
        self.parent = weakref.proxy(parent)

    def hit_test(self, x, y):
        return (self.x < x < self.x + self.width and
                self.y < y < self.y + self.height)

    def capture_events(self):
        self.parent.push_handlers(self)

    def release_events(self):
        self.parent.remove_handlers(self)


class Button(Control):
    charged = False

    def draw(self):
        if self.charged:
            draw_rect(self.x, self.y, self.width, self.height)
        else:
            draw_rect(self.x, self.y, self.width, self.height, color=(1, 0, 0, 1))
        self.draw_label()

    def on_mouse_press(self, x, y, button, modifiers):
        self.capture_events()
        self.charged = True

    def on_mouse_drag(self, x, y, dx, dy, buttons, modifiers):
        self.charged = self.hit_test(x, y)

    def on_mouse_release(self, x, y, button, modifiers):
        self.release_events()
        if self.hit_test(x, y):
            self.dispatch_event('on_press')
        self.charged = False


Button.register_event_type('on_press')


class TextButton(Button):
    def __init__(self, *args, **kwargs):
        super(TextButton, self).__init__(*args, **kwargs)
        self._text = pyglet.text.Label('', anchor_x='center', anchor_y='center')

    def draw_label(self):
        self._text.x = self.x + self.width / 2
        self._text.y = self.y + self.height / 2
        self._text.draw()

    def set_text(self, text):
        self._text.text = text

    text = property(lambda self: self._text.text,
                    set_text)


class Slider(Control):
    THUMB_WIDTH = 6
    THUMB_HEIGHT = 10
    GROOVE_HEIGHT = 2
    RESPONSIVNESS = 0.3

    def __init__(self, *args, **kwargs):
        super(Slider, self).__init__(*args, **kwargs)
        self.seek_value = None

    def draw(self):
        center_y = self.y + self.height / 2
        draw_rect(self.x, center_y - self.GROOVE_HEIGHT / 2,
                  self.width, self.GROOVE_HEIGHT)
        pos = self.x + self.value * self.width / (self.max - self.min)
        draw_rect(pos - self.THUMB_WIDTH / 2, center_y - self.THUMB_HEIGHT / 2,
                  self.THUMB_WIDTH, self.THUMB_HEIGHT)

    def coordinate_to_value(self, x):
        value = float(x - self.x) / self.width * (self.max - self.min) + self.min
        return value

    def on_mouse_press(self, x, y, button, modifiers):
        value = self.coordinate_to_value(x)
        self.capture_events()
        self.dispatch_event('on_begin_scroll')
        self.dispatch_event('on_change', value)
        pyglet.clock.schedule_once(self.seek_request, self.RESPONSIVNESS)

    def on_mouse_drag(self, x, y, dx, dy, buttons, modifiers):
        # On some platforms, on_mouse_drag is triggered with a high frequency.
        # Seeking takes some time (~200ms). Asking for a seek at every 
        # on_mouse_drag event would starve the event loop. 
        # Instead we only record the last mouse position and we
        # schedule seek_request to dispatch the on_change event in the future.
        # This will allow subsequent on_mouse_drag to change the seek_value
        # without triggering yet the on_change event.
        value = min(max(self.coordinate_to_value(x), self.min), self.max)
        if self.seek_value is None:
            # We have processed the last recorded mouse position.
            # We re-schedule seek_request
            pyglet.clock.schedule_once(self.seek_request, self.RESPONSIVNESS)
        self.seek_value = value

    def on_mouse_release(self, x, y, button, modifiers):
        self.release_events()
        self.dispatch_event('on_end_scroll')
        self.seek_value = None

    def seek_request(self, dt):
        if self.seek_value is not None:
            self.dispatch_event('on_change', self.seek_value)
            self.seek_value = None


Slider.register_event_type('on_begin_scroll')
Slider.register_event_type('on_end_scroll')
Slider.register_event_type('on_change')


class PlayerWindow(pyglet.window.Window):
    GUI_WIDTH = 400
    GUI_HEIGHT = 40
    GUI_PADDING = 4
    GUI_BUTTON_HEIGHT = 16

    def __init__(self, player):
        super(PlayerWindow, self).__init__(caption='Media Player',
                                           visible=False,
                                           resizable=True)
        # We only keep a weakref to player as we are about to push ourself
        # as a handler which would then create a circular reference between
        # player and window.
        self.player = weakref.proxy(player)
        self._player_playing = False
        self.player.push_handlers(self)

        self.slider = Slider(self)
        self.slider.push_handlers(self)
        self.slider.x = self.GUI_PADDING
        self.slider.y = self.GUI_PADDING * 2 + self.GUI_BUTTON_HEIGHT

        self.play_pause_button = TextButton(self)
        self.play_pause_button.x = self.GUI_PADDING
        self.play_pause_button.y = self.GUI_PADDING
        self.play_pause_button.height = self.GUI_BUTTON_HEIGHT
        self.play_pause_button.width = 45
        self.play_pause_button.on_press = self.on_play_pause

        self.window_button = TextButton(self)
        self.window_button.x = self.play_pause_button.x + \
                               self.play_pause_button.width + self.GUI_PADDING
        self.window_button.y = self.GUI_PADDING
        self.window_button.height = self.GUI_BUTTON_HEIGHT
        self.window_button.width = 90
        self.window_button.text = 'Windowed'
        self.window_button.on_press = lambda: self.set_fullscreen(False)

        self.controls = [
            self.slider,
            self.play_pause_button,
            self.window_button,
        ]

        x = self.window_button.x + self.window_button.width + self.GUI_PADDING
        i = 0
        for screen in self.display.get_screens():
            screen_button = TextButton(self)
            screen_button.x = x
            screen_button.y = self.GUI_PADDING
            screen_button.height = self.GUI_BUTTON_HEIGHT
            screen_button.width = 80
            screen_button.text = f'Screen {i + 1}'
            screen_button.on_press = lambda screen=screen: self.set_fullscreen(True, screen)
            self.controls.append(screen_button)
            i += 1
            x += screen_button.width + self.GUI_PADDING

        glEnable(GL_BLEND)
        glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA)

    def on_player_next_source(self):
        self.gui_update_state()
        self.gui_update_source()
        self.set_default_video_size()
        return True

    def on_player_eos(self):
        self.gui_update_state()
        pyglet.clock.schedule_once(self.auto_close, 0.1)
        return True

    def gui_update_source(self):
        if self.player.source:
            source = self.player.source
            self.slider.min = 0.
            self.slider.max = source.duration
        self.gui_update_state()

    def gui_update_state(self):
        if self.player.playing:
            self.play_pause_button.text = 'Pause'
        else:
            self.play_pause_button.text = 'Play'

    def get_video_size(self):
        if not self.player.source or not self.player.source.video_format:
            return 0, 0
        video_format = self.player.source.video_format
        width = video_format.width
        height = video_format.height
        if video_format.sample_aspect > 1:
            width *= video_format.sample_aspect
        elif video_format.sample_aspect < 1:
            height /= video_format.sample_aspect
        return width, height

    def set_default_video_size(self):
        """Make the window size just big enough to show the current
        video and the GUI."""
        width = self.GUI_WIDTH
        height = self.GUI_HEIGHT
        video_width, video_height = self.get_video_size()
        width = max(width, video_width)
        height += video_height
        self.set_size(int(width), int(height))

    def on_resize(self, width, height):
        """Position and size video image."""
        super(PlayerWindow, self).on_resize(width, height)
        self.slider.width = width - self.GUI_PADDING * 2

        height -= self.GUI_HEIGHT
        if height <= 0:
            return

        video_width, video_height = self.get_video_size()
        if video_width == 0 or video_height == 0:
            return
        display_aspect = width / float(height)
        video_aspect = video_width / float(video_height)
        if video_aspect > display_aspect:
            self.video_width = width
            self.video_height = width / video_aspect
        else:
            self.video_height = height
            self.video_width = height * video_aspect
        self.video_x = (width - self.video_width) / 2
        self.video_y = (height - self.video_height) / 2 + self.GUI_HEIGHT

    def on_mouse_press(self, x, y, button, modifiers):
        for control in self.controls:
            if control.hit_test(x, y):
                control.on_mouse_press(x, y, button, modifiers)

    def on_key_press(self, symbol, modifiers):
        if symbol == key.SPACE:
            self.on_play_pause()
        elif symbol == key.ESCAPE:
            self.dispatch_event('on_close')
        elif symbol == key.LEFT:
            self.player.seek(0)
        elif symbol == key.RIGHT:
            self.player.next_source()

    def on_close(self):
        self.player.pause()
        self.close()

    def auto_close(self, dt):
        self.close()

    def on_play_pause(self):
        if self.player.playing:
            self.player.pause()
        else:
            if self.player.time >= self.player.source.duration:
                self.player.seek(0)
            self.player.play()
        self.gui_update_state()

    def on_draw(self):
        self.clear()

        # Video
        if self.player.source and self.player.source.video_format:
            video_texture = self.player.texture
            video_texture.blit(self.video_x,
                               self.video_y,
                               width=self.video_width,
                               height=self.video_height)

        # GUI
        self.slider.value = self.player.time
        for control in self.controls:
            control.draw()

    def on_begin_scroll(self):
        self._player_playing = self.player.playing
        self.player.pause()

    def on_change(self, value):
        self.player.seek(value)

    def on_end_scroll(self):
        if self._player_playing:
            self.player.play()


def main(target, dbg_file, debug):
    set_logging_parameters(target, dbg_file, debug)

    player = pyglet.media.Player()
    window = PlayerWindow(player)

    player.queue(pyglet.media.load(filename) for filename in sys.argv[1:])

    window.gui_update_source()
    window.set_visible(True)
    window.set_default_video_size()

    # this is an async call
    player.play()
    window.gui_update_state()

    pyglet.app.run()


def set_logging_parameters(target_file, dbg_file, debug):
    if not debug:
        bl.logger = None
        return
    if dbg_file is None:
        dbg_file = target_file + ".dbg"
    else:
        dbg_dir = os.path.dirname(dbg_file)
        if dbg_dir and not os.path.isdir(dbg_dir):
            os.mkdir(dbg_dir)
    bl.logger = bl.BufferedLogger(dbg_file)
    from pyglet.media.instrumentation import mp_events
    # allow to detect crashes by prewriting a crash file, if no crash
    # it will be overwrited by the captured data
    sample = os.path.basename(target_file)
    bl.logger.log("version", mp_events["version"])
    bl.logger.log("crash", sample)
    bl.logger.save_log_entries_as_pickle()
    bl.logger.clear()
    # start the real capture data
    bl.logger.log("version", mp_events["version"])
    bl.logger.log("mp.im", sample)


def usage():
    print(__doc__)
    sys.exit(1)


def sysargs_to_mainargs():
    """builds main args from sys.argv"""
    if len(sys.argv) < 2:
        usage()
    debug = False
    dbg_file = None
    for i in range(2):
        if sys.argv[1].startswith("--"):
            a = sys.argv.pop(1)
            if a.startswith("--debug"):
                debug = True
            elif a.startswith("--outfile="):
                dbg_file = a[len("--outfile="):]
            else:
                print("Error unknown option:", a)
                usage()
    target_file = sys.argv[1]
    return target_file, dbg_file, debug


if __name__ == '__main__':
    target_file, dbg_file, debug = sysargs_to_mainargs()
    main(target_file, dbg_file, debug)
