/*
  ==============================================================================

   This file is part of the JUCE library - "Jules' Utility Class Extensions"
   Copyright 2004-11 by Raw Material Software Ltd.

  ------------------------------------------------------------------------------

   JUCE can be redistributed and/or modified under the terms of the GNU General
   Public License (Version 2), as published by the Free Software Foundation.
   A copy of the license is included in the JUCE distribution, or can be found
   online at www.gnu.org/licenses.

   JUCE 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 General Public License for more details.

  ------------------------------------------------------------------------------

   To release a closed-source product which uses JUCE, commercial licenses are
   available: visit www.rawmaterialsoftware.com/juce for more information.

  ==============================================================================
*/

class OpenGLContext::CachedImage  : public CachedComponentImage,
                                    public Thread
{
public:
    CachedImage (OpenGLContext& context_,
                 Component& component_,
                 const OpenGLPixelFormat& pixelFormat,
                 void* contextToShareWith)
        : Thread ("OpenGL Rendering"),
          context (context_), component (component_),
         #if JUCE_OPENGL_ES
          shadersAvailable (true),
         #else
          shadersAvailable (false),
         #endif
          needsUpdate (true)
    {
        nativeContext = new NativeContext (component, pixelFormat, contextToShareWith);

        if (nativeContext->createdOk())
        {
            context.nativeContext = nativeContext;

           #if ! JUCE_ANDROID
            startThread (6);
           #endif
        }
        else
        {
            nativeContext = nullptr;
        }
    }

    ~CachedImage()
    {
       #if ! JUCE_ANDROID
        stopThread (10000);
       #endif
    }

    //==============================================================================
    void paint (Graphics&)
    {
        ComponentPeer* const peer = component.getPeer();

        if (peer != nullptr)
            peer->addMaskedRegion (peer->getComponent()->getLocalArea (&component, component.getLocalBounds()));
    }

    void invalidateAll()
    {
        validArea.clear();
        triggerRepaint();
    }

    void invalidate (const Rectangle<int>& area)
    {
        validArea.subtract (area);
        triggerRepaint();
    }

    void releaseResources() {}

    void triggerRepaint()
    {
        needsUpdate = true;

       #if JUCE_ANDROID
        if (nativeContext != nullptr)
            nativeContext->triggerRepaint();
       #else
        notify();
       #endif
    }

    //==============================================================================
    bool ensureFrameBufferSize (int width, int height)
    {
        const int fbW = cachedImageFrameBuffer.getWidth();
        const int fbH = cachedImageFrameBuffer.getHeight();

        if (fbW != width || fbH != height || ! cachedImageFrameBuffer.isValid())
        {
            if (! cachedImageFrameBuffer.initialise (context, width, height))
                return false;

            validArea.clear();
            JUCE_CHECK_OPENGL_ERROR
        }

        return true;
    }

    void clearRegionInFrameBuffer (const RectangleList& list)
    {
        glClearColor (0, 0, 0, 0);
        glEnable (GL_SCISSOR_TEST);

        const GLuint previousFrameBufferTarget = OpenGLFrameBuffer::getCurrentFrameBufferTarget();
        cachedImageFrameBuffer.makeCurrentRenderingTarget();

        for (RectangleList::Iterator i (list); i.next();)
        {
            const Rectangle<int>& r = *i.getRectangle();
            glScissor (r.getX(), component.getHeight() - r.getBottom(), r.getWidth(), r.getHeight());
            glClear (GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT | GL_STENCIL_BUFFER_BIT);
        }

        glDisable (GL_SCISSOR_TEST);
        context.extensions.glBindFramebuffer (GL_FRAMEBUFFER, previousFrameBufferTarget);
        JUCE_CHECK_OPENGL_ERROR
    }

    bool renderFrame()
    {
        if (! context.makeActive())
            return false;

        NativeContext::Locker locker (*nativeContext);

        JUCE_CHECK_OPENGL_ERROR
        glViewport (0, 0, component.getWidth(), component.getHeight());

        if (context.renderer != nullptr)
        {
            context.renderer->renderOpenGL();
            clearGLError();
        }

        if (context.renderComponents)
            paintComponent();

        context.swapBuffers();
        return true;
    }

    void paintComponent()
    {
        if (needsUpdate)
        {
            MessageManagerLock mm (this);
            if (! mm.lockWasGained())
                return;

            needsUpdate = false;

            // you mustn't set your own cached image object when attaching a GL context!
            jassert (get (component) == this);

            const Rectangle<int> bounds (component.getLocalBounds());
            if (! ensureFrameBufferSize (bounds.getWidth(), bounds.getHeight()))
                return;

            RectangleList invalid (bounds);
            invalid.subtract (validArea);
            validArea = bounds;

            if (! invalid.isEmpty())
            {
                clearRegionInFrameBuffer (invalid);

                {
                    ScopedPointer<LowLevelGraphicsContext> g (createOpenGLGraphicsContext (context, cachedImageFrameBuffer));
                    g->clipToRectangleList (invalid);
                    paintOwner (*g);
                    JUCE_CHECK_OPENGL_ERROR
                }

                context.makeActive();
            }

            JUCE_CHECK_OPENGL_ERROR
        }

       #if ! JUCE_ANDROID
        glEnable (GL_TEXTURE_2D);
        clearGLError();
       #endif
        context.extensions.glActiveTexture (GL_TEXTURE0);
        glBindTexture (GL_TEXTURE_2D, cachedImageFrameBuffer.getTextureID());

        const Rectangle<int> cacheBounds (cachedImageFrameBuffer.getWidth(), cachedImageFrameBuffer.getHeight());
        context.copyTexture (cacheBounds, cacheBounds, context.getWidth(), context.getHeight());
        glBindTexture (GL_TEXTURE_2D, 0);
        JUCE_CHECK_OPENGL_ERROR
    }

    void paintOwner (LowLevelGraphicsContext& context)
    {
        Graphics g (&context);

       #if JUCE_ENABLE_REPAINT_DEBUGGING
        g.saveState();
       #endif

        JUCE_TRY
        {
            component.paintEntireComponent (g, false);
        }
        JUCE_CATCH_EXCEPTION

       #if JUCE_ENABLE_REPAINT_DEBUGGING
        // enabling this code will fill all areas that get repainted with a colour overlay, to show
        // clearly when things are being repainted.
        g.restoreState();

        static Random rng;
        g.fillAll (Colour ((uint8) rng.nextInt (255),
                           (uint8) rng.nextInt (255),
                           (uint8) rng.nextInt (255),
                           (uint8) 0x50));
       #endif
    }

    //==============================================================================
    void run()
    {
        {
            // Allow the message thread to finish setting-up the context before using it..
            MessageManagerLock mml (this);
            if (! mml.lockWasGained())
                return;
        }

        nativeContext->makeActive();
        initialiseOnThread();

       #if JUCE_USE_OPENGL_SHADERS && ! JUCE_OPENGL_ES
        shadersAvailable = OpenGLShaderProgram::getLanguageVersion() > 0;
       #endif

        while (! threadShouldExit())
        {
            const uint32 frameRenderStartTime = Time::getMillisecondCounter();

            if (renderFrame())
                waitForNextFrame (frameRenderStartTime);
        }

        shutdownOnThread();
    }

    void initialiseOnThread()
    {
        associatedObjectNames.clear();
        associatedObjects.clear();

        nativeContext->initialiseOnRenderThread();
        glViewport (0, 0, component.getWidth(), component.getHeight());

        context.extensions.initialise();

        if (context.renderer != nullptr)
            context.renderer->newOpenGLContextCreated();
    }

    void shutdownOnThread()
    {
        if (context.renderer != nullptr)
            context.renderer->openGLContextClosing();

        nativeContext->shutdownOnRenderThread();

        associatedObjectNames.clear();
        associatedObjects.clear();
    }

    void waitForNextFrame (const uint32 frameRenderStartTime)
    {
        const int defaultFPS = 60;

        const int elapsed = (int) (Time::getMillisecondCounter() - frameRenderStartTime);
        wait (jmax (1, (1000 / defaultFPS) - elapsed));
    }

    //==============================================================================
    static CachedImage* get (Component& c) noexcept
    {
        return dynamic_cast<CachedImage*> (c.getCachedComponentImage());
    }

    //==============================================================================
    ScopedPointer<NativeContext> nativeContext;

    OpenGLContext& context;
    Component& component;

    OpenGLFrameBuffer cachedImageFrameBuffer;
    RectangleList validArea;

    StringArray associatedObjectNames;
    ReferenceCountedArray<ReferenceCountedObject> associatedObjects;

    WaitableEvent canPaintNowFlag, finishedPaintingFlag;
    bool volatile shadersAvailable;
    bool volatile needsUpdate;

    JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (CachedImage);
};

//==============================================================================
#if JUCE_ANDROID
void OpenGLContext::NativeContext::contextCreatedCallback()
{
    isInsideGLCallback = true;

    CachedImage* const c = CachedImage::get (component);
    jassert (c != nullptr);

    if (c != nullptr)
        c->initialiseOnThread();

    isInsideGLCallback = false;
}

void OpenGLContext::NativeContext::renderCallback()
{
    isInsideGLCallback = true;

    CachedImage* const c = CachedImage::get (component);

    if (c != nullptr)
        c->renderFrame();

    isInsideGLCallback = false;
}
#endif

//==============================================================================
class OpenGLContext::Attachment  : public ComponentMovementWatcher
{
public:
    Attachment (OpenGLContext& context_, Component& comp)
       : ComponentMovementWatcher (&comp), context (context_)
    {
        if (canBeAttached (comp))
            attach();
    }

    ~Attachment()
    {
        detach();
    }

    void componentMovedOrResized (bool /*wasMoved*/, bool /*wasResized*/)
    {
        Component* const comp = getComponent();

        if (isAttached (*comp) != canBeAttached (*comp))
            componentVisibilityChanged();

        context.width  = comp->getWidth();
        context.height = comp->getHeight();

        if (comp->getWidth() > 0 && comp->getHeight() > 0
             && context.nativeContext != nullptr)
        {
            context.nativeContext->updateWindowPosition (comp->getTopLevelComponent()
                                                            ->getLocalArea (comp, comp->getLocalBounds()));
        }
    }

    void componentPeerChanged()
    {
        detach();
        componentVisibilityChanged();
    }

    void componentVisibilityChanged()
    {
        Component* const comp = getComponent();

        if (canBeAttached (*comp))
        {
            if (! isAttached (*comp))
                attach();
        }
        else
        {
            detach();
        }
    }

   #if JUCE_DEBUG || JUCE_LOG_ASSERTIONS
    void componentBeingDeleted (Component& component)
    {
        /* You must call detach() or delete your OpenGLContext to remove it
           from a component BEFORE deleting the component that it is using!
        */
        jassertfalse;

        ComponentMovementWatcher::componentBeingDeleted (component);
    }
   #endif

private:
    OpenGLContext& context;

    static bool canBeAttached (const Component& comp) noexcept
    {
        return comp.getWidth() > 0 && comp.getHeight() > 0 && comp.isShowing();
    }

    static bool isAttached (const Component& comp) noexcept
    {
        return comp.getCachedComponentImage() != nullptr;
    }

    void attach()
    {
        Component* const comp = getComponent();
        comp->setCachedComponentImage (new CachedImage (context, *comp,
                                                        context.pixelFormat,
                                                        context.contextToShareWith));
    }

    void detach()
    {
        getComponent()->setCachedComponentImage (nullptr);
        context.nativeContext = nullptr;
    }
};

//==============================================================================
OpenGLContext::OpenGLContext()
    : nativeContext (nullptr), renderer (nullptr), contextToShareWith (nullptr),
      width (0), height (0), renderComponents (true)
{
}

OpenGLContext::~OpenGLContext()
{
    detach();
}

void OpenGLContext::setRenderer (OpenGLRenderer* rendererToUse) noexcept
{
    // This method must not be called when the context has already been attached!
    // Call it before attaching your context, or use detach() first, before calling this!
    jassert (nativeContext == nullptr);

    renderer = rendererToUse;
}

void OpenGLContext::setComponentPaintingEnabled (bool shouldPaintComponent) noexcept
{
    // This method must not be called when the context has already been attached!
    // Call it before attaching your context, or use detach() first, before calling this!
    jassert (nativeContext == nullptr);

    renderComponents = shouldPaintComponent;
}

void OpenGLContext::setPixelFormat (const OpenGLPixelFormat& preferredPixelFormat) noexcept
{
    // This method must not be called when the context has already been attached!
    // Call it before attaching your context, or use detach() first, before calling this!
    jassert (nativeContext == nullptr);

    pixelFormat = preferredPixelFormat;
}

void OpenGLContext::setNativeSharedContext (void* nativeContextToShareWith) noexcept
{
    // This method must not be called when the context has already been attached!
    // Call it before attaching your context, or use detach() first, before calling this!
    jassert (nativeContext == nullptr);

    contextToShareWith = nativeContextToShareWith;
}

void OpenGLContext::attachTo (Component& component)
{
    component.repaint();

    if (getTargetComponent() != &component)
    {
        detach();

        width  = component.getWidth();
        height = component.getHeight();

        attachment = new Attachment (*this, component);
    }
}

void OpenGLContext::detach()
{
    attachment = nullptr;
    nativeContext = nullptr;
    width = height = 0;
}

bool OpenGLContext::isAttached() const noexcept
{
    return nativeContext != nullptr;
}

Component* OpenGLContext::getTargetComponent() const noexcept
{
    return attachment != nullptr ? attachment->getComponent() : nullptr;
}

OpenGLContext* OpenGLContext::getCurrentContext()
{
   #if JUCE_ANDROID
    NativeContext* const nc = NativeContext::getActiveContext();
    if (nc == nullptr)
        return nullptr;

    CachedImage* currentContext = CachedImage::get (nc->component);
   #else
    CachedImage* currentContext = dynamic_cast <CachedImage*> (Thread::getCurrentThread());
   #endif

    return currentContext != nullptr ? &currentContext->context : nullptr;
}

bool OpenGLContext::makeActive() const noexcept     { return nativeContext != nullptr && nativeContext->makeActive(); }
bool OpenGLContext::isActive() const noexcept       { return nativeContext != nullptr && nativeContext->isActive(); }
void OpenGLContext::deactivateCurrentContext()      { NativeContext::deactivateCurrentContext(); }

void OpenGLContext::triggerRepaint()
{
    CachedImage* const currentContext
            = dynamic_cast <CachedImage*> (Thread::getCurrentThread());

    if (currentContext != nullptr)
    {
        currentContext->triggerRepaint();
        currentContext->component.repaint();
    }
}

void OpenGLContext::swapBuffers()
{
    if (nativeContext != nullptr)
        nativeContext->swapBuffers();
}

unsigned int OpenGLContext::getFrameBufferID() const noexcept
{
    return nativeContext != nullptr ? nativeContext->getFrameBufferID() : 0;
}

bool OpenGLContext::setSwapInterval (int numFramesPerSwap)
{
    return nativeContext != nullptr && nativeContext->setSwapInterval (numFramesPerSwap);
}

int OpenGLContext::getSwapInterval() const
{
    return nativeContext != nullptr ? nativeContext->getSwapInterval() : 0;
}

void* OpenGLContext::getRawContext() const noexcept
{
    return nativeContext != nullptr ? nativeContext->getRawContext() : nullptr;
}

OpenGLContext::CachedImage* OpenGLContext::getCachedImage() const noexcept
{
    Component* const comp = getTargetComponent();
    return comp != nullptr ? CachedImage::get (*comp) : nullptr;
}

bool OpenGLContext::areShadersAvailable() const
{
    CachedImage* const c = getCachedImage();
    return c != nullptr && c->shadersAvailable;
}

ReferenceCountedObject* OpenGLContext::getAssociatedObject (const char* name) const
{
    jassert (name != nullptr);

    CachedImage* const c = getCachedImage();

    // This method must only be called from an openGL rendering callback.
    jassert (c != nullptr && nativeContext != nullptr);
    jassert (getCurrentContext() != nullptr);

    const int index = c->associatedObjectNames.indexOf (name);
    return index >= 0 ? c->associatedObjects.getUnchecked (index) : nullptr;
}

void OpenGLContext::setAssociatedObject (const char* name, ReferenceCountedObject* newObject)
{
    jassert (name != nullptr);

    CachedImage* const c = getCachedImage();

    // This method must only be called from an openGL rendering callback.
    jassert (c != nullptr && nativeContext != nullptr);
    jassert (getCurrentContext() != nullptr);

    const int index = c->associatedObjectNames.indexOf (name);

    if (index >= 0)
    {
        c->associatedObjects.set (index, newObject);
    }
    else
    {
        c->associatedObjectNames.add (name);
        c->associatedObjects.add (newObject);
    }
}

void OpenGLContext::copyTexture (const Rectangle<int>& targetClipArea,
                                 const Rectangle<int>& anchorPosAndTextureSize,
                                 const int contextWidth, const int contextHeight)
{
    if (contextWidth <= 0 || contextHeight <= 0)
        return;

    JUCE_CHECK_OPENGL_ERROR
    glBlendFunc (GL_ONE, GL_ONE_MINUS_SRC_ALPHA);
    glEnable (GL_BLEND);

   #if JUCE_USE_OPENGL_SHADERS
    if (areShadersAvailable())
    {
        struct OverlayShaderProgram  : public ReferenceCountedObject
        {
            OverlayShaderProgram (OpenGLContext& context)
                : program (context), builder (program), params (program)
            {}

            static const OverlayShaderProgram& select (OpenGLContext& context)
            {
                static const char programValueID[] = "juceGLComponentOverlayShader";
                OverlayShaderProgram* program = static_cast <OverlayShaderProgram*> (context.getAssociatedObject (programValueID));

                if (program == nullptr)
                {
                    program = new OverlayShaderProgram (context);
                    context.setAssociatedObject (programValueID, program);
                }

                program->program.use();
                return *program;
            }

            struct ProgramBuilder
            {
                ProgramBuilder (OpenGLShaderProgram& program)
                {
                    program.addShader ("attribute " JUCE_HIGHP " vec2 position;"
                                       "uniform " JUCE_HIGHP " vec2 screenSize;"
                                       "varying " JUCE_HIGHP " vec2 pixelPos;"
                                       "void main()"
                                       "{"
                                        "pixelPos = position;"
                                        JUCE_HIGHP " vec2 scaled = position / (0.5 * screenSize.xy);"
                                        "gl_Position = vec4 (scaled.x - 1.0, 1.0 - scaled.y, 0, 1.0);"
                                       "}",
                                       GL_VERTEX_SHADER);

                    program.addShader ("uniform sampler2D imageTexture;"
                                       "uniform " JUCE_HIGHP " float textureBounds[4];"
                                       "varying " JUCE_HIGHP " vec2 pixelPos;"
                                       "void main()"
                                       "{"
                                        JUCE_HIGHP " vec2 texturePos = (pixelPos - vec2 (textureBounds[0], textureBounds[1]))"
                                                                         "/ vec2 (textureBounds[2], textureBounds[3]);"
                                        "gl_FragColor = texture2D (imageTexture, vec2 (texturePos.x, 1.0 - texturePos.y));"
                                       "}",
                                       GL_FRAGMENT_SHADER);
                    program.link();
                }
            };

            struct Params
            {
                Params (OpenGLShaderProgram& program)
                    : positionAttribute (program, "position"),
                      screenSize (program, "screenSize"),
                      imageTexture (program, "imageTexture"),
                      textureBounds (program, "textureBounds")
                {}

                void set (const float targetWidth, const float targetHeight, const Rectangle<float>& bounds) const
                {
                    const GLfloat m[] = { bounds.getX(), bounds.getY(), bounds.getWidth(), bounds.getHeight() };
                    textureBounds.set (m, 4);
                    imageTexture.set (0);
                    screenSize.set (targetWidth, targetHeight);
                }

                OpenGLShaderProgram::Attribute positionAttribute;
                OpenGLShaderProgram::Uniform screenSize, imageTexture, textureBounds;
            };

            OpenGLShaderProgram program;
            ProgramBuilder builder;
            Params params;
        };

        const GLshort left   = (GLshort) targetClipArea.getX();
        const GLshort top    = (GLshort) targetClipArea.getY();
        const GLshort right  = (GLshort) targetClipArea.getRight();
        const GLshort bottom = (GLshort) targetClipArea.getBottom();
        const GLshort vertices[] = { left, bottom, right, bottom, left, top, right, top };

        const OverlayShaderProgram& program = OverlayShaderProgram::select (*this);
        program.params.set ((float) contextWidth, (float) contextHeight, anchorPosAndTextureSize.toFloat());

        extensions.glVertexAttribPointer (program.params.positionAttribute.attributeID, 2, GL_SHORT, GL_FALSE, 4, vertices);
        extensions.glEnableVertexAttribArray (program.params.positionAttribute.attributeID);
        JUCE_CHECK_OPENGL_ERROR

        glDrawArrays (GL_TRIANGLE_STRIP, 0, 4);

        extensions.glUseProgram (0);
        extensions.glDisableVertexAttribArray (program.params.positionAttribute.attributeID);
    }
    #if JUCE_USE_OPENGL_FIXED_FUNCTION
    else
    #endif
   #endif

   #if JUCE_USE_OPENGL_FIXED_FUNCTION
    {
        glEnable (GL_SCISSOR_TEST);
        glScissor (targetClipArea.getX(), contextHeight - targetClipArea.getBottom(),
                   targetClipArea.getWidth(), targetClipArea.getHeight());

        JUCE_CHECK_OPENGL_ERROR
        glColor4f (1.0f, 1.0f, 1.0f, 1.0f);
        glDisableClientState (GL_COLOR_ARRAY);
        glDisableClientState (GL_NORMAL_ARRAY);
        glEnableClientState (GL_VERTEX_ARRAY);
        glEnableClientState (GL_TEXTURE_COORD_ARRAY);
        OpenGLHelpers::prepareFor2D (contextWidth, contextHeight);
        JUCE_CHECK_OPENGL_ERROR

        const GLfloat textureCoords[] = { 0, 0, 1.0f, 0, 0, 1.0f, 1.0f, 1.0f };
        glTexCoordPointer (2, GL_FLOAT, 0, textureCoords);

        const GLshort left   = (GLshort) anchorPosAndTextureSize.getX();
        const GLshort right  = (GLshort) anchorPosAndTextureSize.getRight();
        const GLshort top    = (GLshort) (contextHeight - anchorPosAndTextureSize.getY());
        const GLshort bottom = (GLshort) (contextHeight - anchorPosAndTextureSize.getBottom());
        const GLshort vertices[] = { left, bottom, right, bottom, left, top, right, top };
        glVertexPointer (2, GL_SHORT, 0, vertices);

        glDrawArrays (GL_TRIANGLE_STRIP, 0, 4);
        glDisable (GL_SCISSOR_TEST);
    }
   #endif

    JUCE_CHECK_OPENGL_ERROR
}
