/*****************************************************************************
 * VLCOpenGLES2VideoView.m: iOS OpenGL ES provider through CAEAGLLayer
 *****************************************************************************
 * Copyright (C) 2001-2017 VLC authors and VideoLAN
 * Copyright (C) 2021 Videolabs
 *
 * Authors: Pierre d'Herbemont <pdherbemont at videolan dot org>
 *          Felix Paul Kühne <fkuehne at videolan dot org>
 *          David Fuhrmann <david dot fuhrmann at googlemail dot com>
 *          Rémi Denis-Courmont
 *          Laurent Aimar <fenrir _AT_ videolan _DOT_ org>
 *          Eric Petit <titer@m0k.org>
 *          Alexandre Janniaux <ajanni@videolabs.io>
 *
 * This program 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 program 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 program; if not, write to the Free Software Foundation,
 * Inc., 51 Franklin Street, Fifth Floor, Boston MA 02110-1301, USA.
 *****************************************************************************/

/*****************************************************************************
 * Preamble
 *****************************************************************************/

#import <UIKit/UIKit.h>
#import <OpenGLES/EAGL.h>
#import <OpenGLES/ES2/gl.h>
#import <OpenGLES/ES2/glext.h>
#import <QuartzCore/QuartzCore.h>
#import <dlfcn.h>

#ifdef HAVE_CONFIG_H
# import "config.h"
#endif

#import <vlc_common.h>
#import <vlc_threads.h>
#import <vlc_plugin.h>
#import <vlc_vout_display.h>
#import <vlc_opengl.h>
#import <vlc_dialog.h>
#import "../opengl/vout_helper.h"
#import "../opengl/gl_api.h"

@interface VLCOpenGLES2VideoView : UIView {
    vlc_gl_t *_gl;

    EAGLContext *_eaglContext;
    EAGLContext *_previousEaglContext;
    CAEAGLLayer *_layer;

    vlc_mutex_t _mutex;
    vlc_cond_t  _gl_attached_wait;
    BOOL        _gl_attached;

    BOOL _bufferNeedReset;
    BOOL _appActive;
    BOOL _eaglEnabled;

    GLuint _renderBuffer;
    GLuint _frameBuffer;

    struct vlc_gl_api _api;
}

- (id)initWithFrame:(CGRect)frame gl:(vlc_gl_t*)gl;
- (BOOL)makeCurrent;
- (void)releaseCurrent;
- (void)presentRenderbuffer;
- (void)didMoveToWindow;
- (void)detachFromWindow;
@end

static void vlc_dispatch_sync(void (^block_function)())
{
    CFRunLoopRef runloop = CFRunLoopGetMain();

    __block vlc_sem_t performed;
    vlc_sem_init(&performed, 0);

    CFStringRef modes_cfstrings[] = {
        kCFRunLoopCommonModes,
        CFSTR("org.videolan.vlccore.window"),
    };

    CFArrayRef modes = CFArrayCreate(NULL, (const void **)modes_cfstrings,
            ARRAY_SIZE(modes_cfstrings),
            &kCFTypeArrayCallBacks);

    /* NOTE: we're using CFRunLoopPerformBlock with a custom mode tag
     * to avoid deadlocks between the window module (main thread) and the
     * display module, which would happen when using dispatch_sycn here. */
    CFRunLoopPerformBlock(runloop, modes, ^{
        (block_function)();
        vlc_sem_post(&performed);
    });
    CFRunLoopWakeUp(runloop);

    vlc_sem_wait(&performed);
    CFRelease(modes);
}

/*****************************************************************************
 * vlc_gl_t callbacks
 *****************************************************************************/
static void *GetSymbol(vlc_gl_t *gl, const char *name)
{
    VLC_UNUSED(gl);
    return dlsym(RTLD_DEFAULT, name);
}

static int MakeCurrent(vlc_gl_t *gl)
{
    VLCOpenGLES2VideoView *view = (__bridge VLCOpenGLES2VideoView *)gl->sys;

    if (![view makeCurrent])
        return VLC_EGENERIC;
    return VLC_SUCCESS;
}

static void ReleaseCurrent(vlc_gl_t *gl)
{
    VLCOpenGLES2VideoView *view = (__bridge VLCOpenGLES2VideoView *)gl->sys;
    [view releaseCurrent];
}

static void Swap(vlc_gl_t *gl)
{
    VLCOpenGLES2VideoView *view = (__bridge VLCOpenGLES2VideoView *)gl->sys;
    [view presentRenderbuffer];
}

static void Resize(vlc_gl_t *gl, unsigned width, unsigned height)
{
    VLC_UNUSED(gl); VLC_UNUSED(width); VLC_UNUSED(height);
    /* Use the parent frame size for now, resize is smoother and called
     * automatically from the main thread queue. */
}

static void Close(vlc_gl_t *gl)
{
    /* Transfer ownership back from VLC to ARC so that it can be released. */
    VLCOpenGLES2VideoView *view = (__bridge_transfer VLCOpenGLES2VideoView*)gl->sys;

    /* We need to detach because the superview has a reference to our view. */
    [view detachFromWindow];
}

/*****************************************************************************
 * Our UIView object
 *****************************************************************************/
@implementation VLCOpenGLES2VideoView

+ (Class)layerClass
{
    return [CAEAGLLayer class];
}

- (id)initWithFrame:(CGRect)frame gl:(vlc_gl_t*)gl
{
    _gl = gl;

    _appActive = ([UIApplication sharedApplication].applicationState != UIApplicationStateBackground);
    if (unlikely(!_appActive))
        return nil;

    self = [super initWithFrame:frame];
    if (!self)
        return nil;

    _eaglEnabled = YES;
    _bufferNeedReset = YES;

    vlc_mutex_init(&_mutex);
    vlc_cond_init(&_gl_attached_wait);

    /* The following creates a new OpenGL ES context with the API version we
     * need. If there is already an active context created by another OpenGL
     * provider we cache it and restore analog to the
     * makeCurrent/releaseCurrent pattern used through-out the class */
    _eaglContext = [[EAGLContext alloc] initWithAPI:kEAGLRenderingAPIOpenGLES2];
    _previousEaglContext = nil;

    _layer = (CAEAGLLayer *)self.layer;
    _layer.drawableProperties = [NSDictionary dictionaryWithObject:kEAGLColorFormatRGBA8 forKey: kEAGLDrawablePropertyColorFormat];
    _layer.opaque = YES;

    /* Resize is done accordingly to the parent frame directly. */
    self.autoresizingMask = UIViewAutoresizingFlexibleWidth
                          | UIViewAutoresizingFlexibleHeight;
    self.contentMode = UIViewContentModeScaleToFill;

    /* Connect to the parent UIView which will contain this surface.
     * Using a parent UIView makes it easier to handle window resize and
     * still have full control over the display object, since layers don't
     * need to draw anything to exist. */
    if (![self attachToWindow: _gl->surface])
        return nil;

    /* Listen application state change because we cannot use OpenGL in the
     * background. This should probably move to the vout_window reports in
     * the future, which could even signal that we need to disable the whole
     * display and potentially adapt playback for that. */

    [[NSNotificationCenter defaultCenter] addObserver:self
                                             selector:@selector(applicationStateChanged:)
                                                 name:UIApplicationWillEnterForegroundNotification
                                               object:nil];
    [[NSNotificationCenter defaultCenter] addObserver:self
                                             selector:@selector(applicationStateChanged:)
                                                 name:UIApplicationDidEnterBackgroundNotification
                                               object:nil];
    [[NSNotificationCenter defaultCenter] addObserver:self
                                             selector:@selector(applicationStateChanged:)
                                                 name:UIApplicationWillResignActiveNotification
                                               object:nil];
    [[NSNotificationCenter defaultCenter] addObserver:self
                                             selector:@selector(applicationStateChanged:)
                                                 name:UIApplicationDidBecomeActiveNotification
                                               object:nil];

    /* Setup the usual vlc_gl_t callbacks before loading the API since we need
     * the get_proc_address symbol and a current context. */
    static const struct vlc_gl_operations gl_ops =
    {
        .make_current = MakeCurrent,
        .release_current = ReleaseCurrent,
        .resize = Resize,
        .swap = Swap,
        .get_proc_address = GetSymbol,
        .close = Close,
    };
    gl->ops = &gl_ops;

    return self;
}

- (BOOL)attachToWindow:(vlc_window_t*)wnd
{
    @try {
        UIView *viewContainer = (__bridge UIView*)wnd->handle.nsobject;
        /* get the object we will draw into */
        if (unlikely(viewContainer == nil)) {
            msg_Err(_gl, "provided view container is nil");
            return NO;
        }

        if (unlikely(![viewContainer respondsToSelector:@selector(isKindOfClass:)])) {
            msg_Err(_gl, "void pointer not an ObjC object");
            return NO;
        }

        if (unlikely(![viewContainer isKindOfClass:[UIView class]])) {
            msg_Err(_gl, "passed ObjC object not of class UIView");
            return NO;
        }

        /* Initial size setup */
        self.frame = viewContainer.bounds;

        [viewContainer addSubview:self];

        return YES;
    } @catch (NSException *exception) {
        msg_Err(_gl, "Handling the view container failed due to an Obj-C exception (%s, %s", [exception.name UTF8String], [exception.reason UTF8String]);
        return NO;
    }
}

- (void)detachFromWindow
{
    /* Flush the OpenGL pipeline before leaving. */
    vlc_mutex_lock(&_mutex);
    if (_eaglEnabled)
        [self flushEAGLLocked];
    _eaglEnabled = NO;
    vlc_mutex_unlock(&_mutex);

    /* We can drop the context as fast as possible since we don't use it
     * anymore. It avoids crashes with the context modifying destroyed
     * resources when it is being deallocated in the main thread. */
    _eaglContext = nil;

    /* This cannot be a synchronous dispatch because player is usually running
     * in the main thread and block the main thread unless we accept our fate
     * and exit here. */
    dispatch_async(dispatch_get_main_queue(), ^{
         /* Remove the external references to the view so that
          * dealloc can be called by ARC. */

         [[NSNotificationCenter defaultCenter] removeObserver:self
                                                         name:UIApplicationWillResignActiveNotification
                                                       object:nil];

         [[NSNotificationCenter defaultCenter] removeObserver:self
                                                         name:UIApplicationDidBecomeActiveNotification
                                                       object:nil];

         [[NSNotificationCenter defaultCenter] removeObserver:self
                                                         name:UIApplicationWillEnterForegroundNotification
                                                       object:nil];

         [[NSNotificationCenter defaultCenter] removeObserver:self
                                                         name:UIApplicationDidEnterBackgroundNotification
                                                       object:nil];
        assert(!_gl_attached);
        [self removeFromSuperview];
    });
}

- (void)didMoveToWindow
{
    self.contentScaleFactor = self.window.screen.scale;

    vlc_mutex_lock(&_mutex);
    _bufferNeedReset = YES;
    vlc_mutex_unlock(&_mutex);
}

- (BOOL)doResetBuffers:(vlc_gl_t *)gl
{
    if (_frameBuffer != 0)
    {
        /* clear frame buffer */
        glDeleteFramebuffers(1, &_frameBuffer);
        _frameBuffer = 0;
    }

    if (_renderBuffer != 0)
    {
        /* clear render buffer */
        glDeleteRenderbuffers(1, &_renderBuffer);
        _renderBuffer = 0;
    }

    glGenFramebuffers(1, &_frameBuffer);
    glBindFramebuffer(GL_FRAMEBUFFER, _frameBuffer);

    glGenRenderbuffers(1, &_renderBuffer);
    glBindRenderbuffer(GL_RENDERBUFFER, _renderBuffer);

    [_eaglContext renderbufferStorage:GL_RENDERBUFFER fromDrawable:_layer];

    glFramebufferRenderbuffer(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_RENDERBUFFER, _renderBuffer);
    if (glCheckFramebufferStatus(GL_FRAMEBUFFER) != GL_FRAMEBUFFER_COMPLETE)
    {
        msg_Err(_gl, "Failed to make complete framebuffer object %x", glCheckFramebufferStatus(GL_FRAMEBUFFER));
        return NO;
    }
    return YES;
}

- (BOOL)makeCurrent
{
    vlc_mutex_lock(&_mutex);
    assert(!_gl_attached);

    if (unlikely(!_appActive))
    {
        vlc_mutex_unlock(&_mutex);
        return NO;
    }

    assert(_eaglEnabled);
    _previousEaglContext = [EAGLContext currentContext];

    assert(_eaglContext);
    if (![EAGLContext setCurrentContext:_eaglContext])
    {
        vlc_mutex_unlock(&_mutex);
        return NO;
    }

    BOOL resetBuffers = NO;


    if (unlikely(_bufferNeedReset))
    {
        _bufferNeedReset = NO;
        resetBuffers = YES;
    }

    _gl_attached = YES;

    vlc_mutex_unlock(&_mutex);

    if (resetBuffers && ![self doResetBuffers:_gl])
    {
        [self releaseCurrent];
        return NO;
    }
    return YES;
}

- (void)releaseCurrent
{
    vlc_mutex_lock(&_mutex);
    assert(_gl_attached);
    _gl_attached = NO;
    [EAGLContext setCurrentContext:_previousEaglContext];
    _previousEaglContext = nil;
    vlc_cond_signal(&_gl_attached_wait);
    vlc_mutex_unlock(&_mutex);
}

- (void)presentRenderbuffer
{
    [_eaglContext presentRenderbuffer:GL_RENDERBUFFER];
}

- (void)layoutSubviews
{
    vlc_mutex_lock(&_mutex);
    _bufferNeedReset = YES;
    vlc_mutex_unlock(&_mutex);
}

- (void)flushEAGLLocked
{
    assert(_eaglEnabled);

    /* Ensure that all previously submitted commands are drained from the
     * command buffer and are executed by OpenGL ES before moving to the
     * background.*/
    EAGLContext *previousEaglContext = [EAGLContext currentContext];
    if ([EAGLContext setCurrentContext:_eaglContext])
        glFinish();
    [EAGLContext setCurrentContext:previousEaglContext];
}

- (void)applicationStateChanged:(NSNotification *)notification
{
    vlc_mutex_lock(&_mutex);

    if ([[notification name] isEqualToString:UIApplicationWillResignActiveNotification])
        _appActive = NO;
    else if ([[notification name] isEqualToString:UIApplicationDidEnterBackgroundNotification])
    {
        _appActive = NO;

        /* Wait for the vout to unlock the eagl context before releasing
         * it. */
        while (_gl_attached && _eaglEnabled)
            vlc_cond_wait(&_gl_attached_wait, &_mutex);

        /* _eaglEnabled can change during the vlc_cond_wait
         * as the mutex is unlocked during that, so this check
         * has to be done after the vlc_cond_wait! */
        if (_eaglEnabled) {
            [self flushEAGLLocked];
            _eaglEnabled = NO;
        }
    }
    else if ([[notification name] isEqualToString:UIApplicationWillEnterForegroundNotification])
        _eaglEnabled = YES;
    else
    {
        assert([[notification name] isEqualToString:UIApplicationDidBecomeActiveNotification]);
        _appActive = YES;
    }

    vlc_mutex_unlock(&_mutex);
}

- (void)updateConstraints
{
    [super updateConstraints];
}

- (BOOL)isOpaque
{
    return YES;
}

- (UIView *)hitTest:(CGPoint)point withEvent:(UIEvent *)event
{
    /* Disable events for this view, as the vout_window view will be the one
     * handling them. */
    return nil;
}
@end



static int Open(vlc_gl_t *gl, unsigned width, unsigned height,
                const struct vlc_gl_cfg *gl_cfg)
{
    vlc_window_t *wnd = gl->surface;

    if (gl_cfg->need_alpha)
    {
        msg_Err(gl, "Cannot support alpha yet");
        return VLC_ENOTSUP;
    }

    /* We only support UIView container window. */
    if (wnd->type != VLC_WINDOW_TYPE_NSOBJECT)
        return VLC_EGENERIC;

    @autoreleasepool {
        /* NOTE: we're using CFRunLoopPerformBlock with the "vlc_runloop" tag
         * to avoid deadlocks between the window module (main thread) and the
         * display module, which would happen when using dispatch_sycn here. */
        vlc_dispatch_sync(^{
            gl->sys = (__bridge_retained void*)[[VLCOpenGLES2VideoView alloc]
               initWithFrame:CGRectMake(0.,0.,width,height) gl:gl];
        });

        if (gl->sys == NULL)
        {
            msg_Err(gl, "Creating OpenGL ES 2 view failed");
            return VLC_EGENERIC;
        }
    }

    return VLC_SUCCESS;
}

vlc_module_begin ()
    set_shortname (N_("CAEAGL"))
    set_description (N_("CAEAGL provider for OpenGL"))
    set_subcategory (SUBCAT_VIDEO_VOUT)
    set_callback_opengl_es2 (Open, 50)
    add_shortcut ("caeagl")
vlc_module_end ()
