/* -*- Mode: C++; tab-width: 20; indent-tabs-mode: nil; c-basic-offset: 4 -*-
 * This Source Code Form is subject to the terms of the Mozilla Public
 * License, v. 2.0. If a copy of the MPL was not distributed with this
 * file, You can obtain one at http://mozilla.org/MPL/2.0/. */

#include "GLContextProvider.h"
#include "GLContextCGL.h"
#include "nsDebug.h"
#include "nsIWidget.h"
#include <OpenGL/gl.h>
#include "gfxFailure.h"
#include "gfxPrefs.h"
#include "prenv.h"
#include "GeckoProfiler.h"
#include "mozilla/gfx/MacIOSurface.h"
#include "mozilla/widget/CompositorWidget.h"

#include <OpenGL/OpenGL.h>
#import <IOKit/IOKitLib.h>

// When running inside a VM, creating an accelerated OpenGL context usually
// fails. Uncomment this line to emulate that behavior.
// #define EMULATE_VM

namespace mozilla {
namespace gl {

using namespace mozilla::gfx;
using namespace mozilla::widget;

class CGLLibrary
{
public:
    CGLLibrary()
        : mInitialized(false)
        , mUseDoubleBufferedWindows(true)
        , mOGLLibrary(nullptr)
    {}

    bool EnsureInitialized()
    {
        if (mInitialized) {
            return true;
        }
        if (!mOGLLibrary) {
            mOGLLibrary = PR_LoadLibrary("/System/Library/Frameworks/OpenGL.framework/OpenGL");
            if (!mOGLLibrary) {
                NS_WARNING("Couldn't load OpenGL Framework.");
                return false;
            }
        }

        const char* db = PR_GetEnv("MOZ_CGL_DB");
        if (db) {
            mUseDoubleBufferedWindows = *db != '0';
        }

        mInitialized = true;
        return true;
    }

    bool UseDoubleBufferedWindows() const {
        MOZ_ASSERT(mInitialized);
        return mUseDoubleBufferedWindows;
    }

private:
    bool mInitialized;
    bool mUseDoubleBufferedWindows;
    PRLibrary* mOGLLibrary;
};

CGLLibrary sCGLLibrary;

GLContextCGL::GLContextCGL(CreateContextFlags flags, const SurfaceCaps& caps,
                           NSOpenGLContext* context, bool isOffscreen,
                           ContextProfile profile)
    : GLContext(flags, caps, nullptr, isOffscreen)
    , mContext(context)
{
    SetProfileVersion(profile, 210);
}

GLContextCGL::~GLContextCGL()
{
    MarkDestroyed();
    [mContext release];
}

bool
GLContextCGL::Init()
{
    return InitWithPrefix("gl", true);
}

CGLContextObj
GLContextCGL::GetCGLContext() const
{
    return static_cast<CGLContextObj>([mContext CGLContextObj]);
}

bool
GLContextCGL::MakeCurrentImpl(bool aForce)
{
    if (IsDestroyed()) {
        [NSOpenGLContext clearCurrentContext];
        return false;
    }

    if (!aForce && [NSOpenGLContext currentContext] == mContext) {
        return true;
    }

    if (mContext) {
        [mContext makeCurrentContext];
        MOZ_ASSERT(IsCurrent());
        // Use non-blocking swap in "ASAP mode".
        // ASAP mode means that rendering is iterated as fast as possible.
        // ASAP mode is entered when layout.frame_rate=0 (requires restart).
        // If swapInt is 1, then glSwapBuffers will block and wait for a vblank signal.
        // When we're iterating as fast as possible, however, we want a non-blocking
        // glSwapBuffers, which will happen when swapInt==0.
        GLint swapInt = gfxPrefs::LayoutFrameRate() == 0 ? 0 : 1;
        [mContext setValues:&swapInt forParameter:NSOpenGLCPSwapInterval];
    }
    return true;
}

bool
GLContextCGL::IsCurrent() {
    return [NSOpenGLContext currentContext] == mContext;
}

GLenum
GLContextCGL::GetPreferredARGB32Format() const
{
    return LOCAL_GL_BGRA;
}

bool
GLContextCGL::SetupLookupFunction()
{
    return false;
}

bool
GLContextCGL::IsDoubleBuffered() const
{
  return sCGLLibrary.UseDoubleBufferedWindows();
}

bool
GLContextCGL::SwapBuffers()
{
  PROFILER_LABEL("GLContextCGL", "SwapBuffers",
    js::ProfileEntry::Category::GRAPHICS);

  [mContext flushBuffer];
  return true;
}

void
GLContextCGL::GetWSIInfo(nsCString* const out) const
{
    out->AppendLiteral("CGL");
}

already_AddRefed<GLContext>
GLContextProviderCGL::CreateWrappingExisting(void*, void*)
{
    return nullptr;
}

static const NSOpenGLPixelFormatAttribute kAttribs_singleBuffered[] = {
    NSOpenGLPFAAllowOfflineRenderers,
    0
};

static const NSOpenGLPixelFormatAttribute kAttribs_singleBuffered_accel[] = {
    NSOpenGLPFAAccelerated,
    NSOpenGLPFAAllowOfflineRenderers,
    0
};

static const NSOpenGLPixelFormatAttribute kAttribs_doubleBuffered[] = {
    NSOpenGLPFAAllowOfflineRenderers,
    NSOpenGLPFADoubleBuffer,
    0
};

static const NSOpenGLPixelFormatAttribute kAttribs_doubleBuffered_accel[] = {
    NSOpenGLPFAAccelerated,
    NSOpenGLPFAAllowOfflineRenderers,
    NSOpenGLPFADoubleBuffer,
    0
};

static const NSOpenGLPixelFormatAttribute kAttribs_offscreen[] = {
    0
};

static const NSOpenGLPixelFormatAttribute kAttribs_offscreen_allow_offline[] = {
    NSOpenGLPFAAllowOfflineRenderers,
    0
};

static const NSOpenGLPixelFormatAttribute kAttribs_offscreen_accel[] = {
    NSOpenGLPFAAccelerated,
    0
};

static const NSOpenGLPixelFormatAttribute kAttribs_offscreen_coreProfile[] = {
    NSOpenGLPFAAccelerated,
#if defined(MAC_OS_X_VERSION_10_7) && (MAC_OS_X_VERSION_MAX_ALLOWED >= MAC_OS_X_VERSION_10_7)
    NSOpenGLPFAOpenGLProfile, NSOpenGLProfileVersion3_2Core,
#endif
    0
};

static NSOpenGLContext*
CreateWithFormat(const NSOpenGLPixelFormatAttribute* attribs)
{
    NSOpenGLPixelFormat* format = [[NSOpenGLPixelFormat alloc]
                                   initWithAttributes:attribs];
    if (!format) {
        NS_WARNING("Failed to create NSOpenGLPixelFormat.");
        return nullptr;
    }

    NSOpenGLContext* context = [[NSOpenGLContext alloc] initWithFormat:format
                                shareContext:nullptr];

    [format release];

    return context;
}

already_AddRefed<GLContext>
GLContextProviderCGL::CreateForCompositorWidget(CompositorWidget* aCompositorWidget, bool aForceAccelerated)
{
    return CreateForWindow(aCompositorWidget->RealWidget(), aForceAccelerated);
}

already_AddRefed<GLContext>
GLContextProviderCGL::CreateForWindow(nsIWidget* aWidget, bool aForceAccelerated)
{
    if (!sCGLLibrary.EnsureInitialized()) {
        return nullptr;
    }

#ifdef EMULATE_VM
    if (aForceAccelerated) {
        return nullptr;
    }
#endif

    NSOpenGLContext* context = nullptr;
    bool forceSoftware = false;

#if !defined(MAC_OS_X_VERSION_10_6) || (MAC_OS_X_VERSION_MAX_ALLOWED < MAC_OS_X_VERSION_10_6)
    // These old drivers don't support framebuffer objects, we cannot use them.
    // However, we still need some sort of OpenGL for the basic compositor,
    // so check for these and force the software renderer.
    CFMutableDictionaryRef matchingDict = IOServiceMatching("IOAccelerator");
    io_iterator_t iterator;
    if (IOServiceGetMatchingServices(kIOMasterPortDefault, matchingDict, &iterator) == kIOReturnSuccess) {
        io_registry_entry_t registryEntry;
        while ((registryEntry = IOIteratorNext(iterator))) {
            CFTypeRef bundleName = IORegistryEntryCreateCFProperty(registryEntry,
                                                                   CFSTR("IOGLBundleName"),
                                                                   kCFAllocatorDefault,
                                                                   0);
            if (bundleName) {
                if (CFGetTypeID(bundleName) == CFStringGetTypeID()) {
                    NSString* str = (NSString*)bundleName;
                    if ([str isEqualToString:@"GeForce2MXGLDriver"] ||
                        [str isEqualToString:@"ATIRadeon8500GLDriver"] ||
                        [str isEqualToString:@"GeForce3GLDriver"]) {
                        forceSoftware = true;
                    }
                }
                CFRelease(bundleName);
            }
            IOObjectRelease(registryEntry);
            if (forceSoftware) break;
        }
        IOObjectRelease(iterator);
    }

    if (forceSoftware) {
        NSOpenGLPixelFormatAttribute attribs[] = {
            NSOpenGLPFARendererID, 0x00020200, // kCGLRendererGenericFloatID
            NSOpenGLPFAAllowOfflineRenderers,
            NSOpenGLPFADoubleBuffer,
            0
        };
        NSOpenGLPixelFormat* pixelFormat = [[NSOpenGLPixelFormat alloc] initWithAttributes:attribs];
        context = [[NSOpenGLContext alloc] initWithFormat:pixelFormat shareContext:nil];
        [pixelFormat release];
    }
#endif

    if (!forceSoftware) {
        const NSOpenGLPixelFormatAttribute* attribs;
        if (sCGLLibrary.UseDoubleBufferedWindows()) {
            attribs = aForceAccelerated ? kAttribs_doubleBuffered_accel : kAttribs_doubleBuffered;
        } else {
            attribs = aForceAccelerated ? kAttribs_singleBuffered_accel : kAttribs_singleBuffered;
        }
        context = CreateWithFormat(attribs);
    }

    if (!context) {
        return nullptr;
    }

    // make the context transparent
    GLint opaque = 0;
    [context setValues:&opaque forParameter:NSOpenGLCPSurfaceOpacity];

    SurfaceCaps caps = SurfaceCaps::ForRGBA();
    ContextProfile profile = ContextProfile::OpenGLCompatibility;
    RefPtr<GLContextCGL> glContext = new GLContextCGL(CreateContextFlags::NONE, caps,
                                                      context, false, profile);

    if (!glContext->Init()) {
        glContext = nullptr;
        [context release];
        return nullptr;
    }

    return glContext.forget();
}

static already_AddRefed<GLContextCGL>
CreateOffscreenFBOContext(CreateContextFlags flags)
{
    if (!sCGLLibrary.EnsureInitialized()) {
        return nullptr;
    }

    ContextProfile profile;
    NSOpenGLContext* context = nullptr;

    if (!(flags & CreateContextFlags::REQUIRE_COMPAT_PROFILE)) {
        profile = ContextProfile::OpenGLCore;
        context = CreateWithFormat(kAttribs_offscreen_coreProfile);
    }
    if (!context) {
        profile = ContextProfile::OpenGLCompatibility;

        if (flags & CreateContextFlags::ALLOW_OFFLINE_RENDERER) {
          if (gfxPrefs::RequireHardwareGL())
              context = CreateWithFormat(kAttribs_singleBuffered);
          else
              context = CreateWithFormat(kAttribs_offscreen_allow_offline);

        } else {
          if (gfxPrefs::RequireHardwareGL())
              context = CreateWithFormat(kAttribs_offscreen_accel);
          else
              context = CreateWithFormat(kAttribs_offscreen);
        }
    }
    if (!context) {
        NS_WARNING("Failed to create NSOpenGLContext.");
        return nullptr;
    }

    SurfaceCaps dummyCaps = SurfaceCaps::Any();
    RefPtr<GLContextCGL> glContext = new GLContextCGL(flags, dummyCaps, context, true,
                                                      profile);

    if (gfxPrefs::GLMultithreaded()) {
        CGLEnable(glContext->GetCGLContext(), kCGLCEMPEngine);
    }
    return glContext.forget();
}

already_AddRefed<GLContext>
GLContextProviderCGL::CreateHeadless(CreateContextFlags flags,
                                     nsACString* const out_failureId)
{
    RefPtr<GLContextCGL> gl;
    gl = CreateOffscreenFBOContext(flags);
    if (!gl) {
        *out_failureId = NS_LITERAL_CSTRING("FEATURE_FAILURE_CGL_FBO");
        return nullptr;
    }

    if (!gl->Init()) {
        *out_failureId = NS_LITERAL_CSTRING("FEATURE_FAILURE_CGL_INIT");
        NS_WARNING("Failed during Init.");
        return nullptr;
    }

    return gl.forget();
}

already_AddRefed<GLContext>
GLContextProviderCGL::CreateOffscreen(const IntSize& size,
                                      const SurfaceCaps& minCaps,
                                      CreateContextFlags flags,
                                      nsACString* const out_failureId)
{
    RefPtr<GLContext> gl = CreateHeadless(flags, out_failureId);
    if (!gl) {
        return nullptr;
    }

    if (!gl->InitOffscreen(size, minCaps)) {
        *out_failureId = NS_LITERAL_CSTRING("FEATURE_FAILURE_CGL_INIT");
        return nullptr;
    }

    return gl.forget();
}

static RefPtr<GLContext> gGlobalContext;

GLContext*
GLContextProviderCGL::GetGlobalContext()
{
    static bool triedToCreateContext = false;
    if (!triedToCreateContext) {
        triedToCreateContext = true;

        MOZ_RELEASE_ASSERT(!gGlobalContext);
        nsCString discardFailureId;
        RefPtr<GLContext> temp = CreateHeadless(CreateContextFlags::NONE,
                                                &discardFailureId);
        gGlobalContext = temp;

        if (!gGlobalContext) {
            NS_WARNING("Couldn't init gGlobalContext.");
        }
    }

    return gGlobalContext;
}

void
GLContextProviderCGL::Shutdown()
{
    gGlobalContext = nullptr;
}

} /* namespace gl */
} /* namespace mozilla */
