// N.B. Any locking should be handled by the parent.

using namespace std;

// MythTV General headers
#include "mythcontext.h"

// MythTV videout headers
#include "util-opengl.h"
#include "util-x11.h"

#define LOC QString("OpenglCtx: ")
#define LOC_ERR QString("OpenglCtx Error: ")

#ifndef GL_TEXTURE_RECTANGLE_ARB
#define GL_TEXTURE_RECTANGLE_ARB 0x84F5 
#endif

#ifndef GL_TEXTURE_RECTANGLE_EXT
#define GL_TEXTURE_RECTANGLE_EXT 0x84F5
#endif

#ifndef GL_TEXTURE_RECTANGLE_NV
#define GL_TEXTURE_RECTANGLE_NV 0x84F5
#endif

OpenglContext::OpenglContext() :
      m_texture( GL_TEXTURE_2D ), 
      m_display(NULL), m_screen_num(0),
      m_window(0), m_visual(NULL),
      m_context(NULL), m_visible(true),
      m_supported(SUPP_NONE), m_max_tex_size(0),
      m_textures_enabled(FALSE)
{
}

OpenglContext::~OpenglContext()
{
    if (m_context && m_window)
    {
        DeletePrograms();
        DeleteTextures();
        DeleteFrameBuffers();
    }
    if (m_context)
        glXDestroyContext(m_display, m_context);
    if (m_window)
        XDestroyWindow(m_display, m_window);
    if (m_visual)
        XFree(m_visual);
}

bool OpenglContext::Create(Window window, int width, int height, bool visible)
{
    m_visible = visible;
    m_display = MythXOpenDisplay();
    if (!m_display)
    {
        VERBOSE(VB_PLAYBACK, LOC_ERR + "Failed to open display.");
        return false;
    }
    m_screen_num = DefaultScreen(m_display);
    if (!CheckGlx() ||
        !GetVisual() ||
        !CreateWindow(window, width, height) ||
        !CreateContext())
        return false;

    VERBOSE(VB_PLAYBACK, LOC + QString("Created window%1 and context.")
        .arg(m_visible ? "" : " (Offscreen)"));
    return true;
}

void OpenglContext::Init(void)
{
    MakeCurrent(true);
    m_extensions= reinterpret_cast<const char *>(glGetString( GL_EXTENSIONS ));
    glGetIntegerv( GL_MAX_TEXTURE_SIZE, &m_max_tex_size );
    MakeCurrent(false);
    if (m_max_tex_size == 0)
        m_max_tex_size = 512;
    VERBOSE(VB_PLAYBACK, LOC + 
            QString("Maximum supported texture size: %1 x %2")
            .arg(m_max_tex_size).arg(m_max_tex_size));
    m_supported += EnableFrameBufs();
    m_supported += EnableFragProgs();
    m_supported += EnableTextureRects();
}

void OpenglContext::MakeCurrent(bool current)
{
    if (current)
    {
        bool yes = glXMakeCurrent( m_display, m_window, m_context );
        if (!yes)
            VERBOSE(VB_PLAYBACK, LOC + "Could not make context current.");
    }
    else
        glXMakeCurrent( m_display, None, NULL );
}

void OpenglContext::SwapBuffers(void)
{
    if (!m_visible)
        return;
    MakeCurrent(true);
    glXSwapBuffers(m_display, m_window);
    MakeCurrent(false);
}

void OpenglContext::EnableTextures(void)
{
    if (m_textures_enabled)
        return;
    m_textures_enabled = true;
    MakeCurrent(true);
    glEnable( m_texture );
    MakeCurrent(false);
}

int OpenglContext::EnableTextureRects(void)
{
    if (m_extensions.contains("GL_NV_texture_rectangle"))
    {
        VERBOSE(VB_PLAYBACK, LOC + "Using NV NPOT texture extension.");
        m_texture = GL_TEXTURE_RECTANGLE_NV;
    }
    else if (m_extensions.contains("GL_ARB_texture_rectangle"))
    {
        VERBOSE(VB_PLAYBACK, LOC + "Using ARB NPOT texture extension.");
        m_texture = GL_TEXTURE_RECTANGLE_ARB;
    }
    else if (m_extensions.contains("GL_EXT_texture_rectangle"))
    {
        VERBOSE(VB_PLAYBACK, LOC + "Using EXT NPOT texture extension.");
        m_texture = GL_TEXTURE_RECTANGLE_EXT;
    }
    else
    {
        VERBOSE(VB_PLAYBACK, LOC + "NPOT textures not available.");
        return 0;
    }

    return SUPP_RECT;
}

int OpenglContext::EnableFragProgs(void)
{
    if (!m_extensions.contains("GL_ARB_fragment_program"))
    {
        VERBOSE(VB_PLAYBACK, LOC + "GL_ARB_fragment_program unavailable.");
        return 0;
    }
    m_glGenProgramsARB = (PFNGLGENPROGRAMSARBPROC)
                glXGetProcAddress("glGenProgramsARB");
    m_glBindProgramARB = (PFNGLBINDPROGRAMARBPROC)
                glXGetProcAddress("glBindProgramARB");
    m_glProgramStringARB = (PFNGLPROGRAMSTRINGARBPROC)
                glXGetProcAddress("glProgramStringARB");
    m_glProgramEnvParameter4fARB = (PFNGLPROGRAMENVPARAMETER4FARBPROC)
                glXGetProcAddress("glProgramEnvParameter4fARB");
    m_glDeleteProgramsARB = (PFNGLDELETEPROGRAMSARBPROC)
                glXGetProcAddress("glDeleteProgramsARB");
    m_glGetProgramivARB = (PFNGLGETPROGRAMIVARBPROC)
                glXGetProcAddress("glGetProgramivARB");

    if (!m_glGenProgramsARB    ||
        !m_glBindProgramARB    ||
        !m_glProgramStringARB  ||
        !m_glDeleteProgramsARB ||
        !m_glGetProgramivARB   ||
        !m_glProgramEnvParameter4fARB)
    {
        VERBOSE(VB_PLAYBACK, LOC + "Couldn't get proc address");
        return 0;
    }
    VERBOSE(VB_PLAYBACK, LOC + "Fragment programs supported.");
    return SUPP_PROG;
}

int OpenglContext::EnableFrameBufs(void)
{
    if (!m_extensions.contains("GL_EXT_framebuffer_object"))
    {
        VERBOSE(VB_PLAYBACK, LOC + "GL_EXT_framebuffer_object unavailable.");
        return 0;
    }
    m_glGenFramebuffersEXT = (PFNGLGENFRAMEBUFFERSEXTPROC)
                glXGetProcAddress("glGenFramebuffersEXT");
    m_glBindFramebufferEXT = (PFNGLBINDFRAMEBUFFEREXTPROC)
                glXGetProcAddress("glBindFramebufferEXT");
    m_glFramebufferTexture2DEXT = (PFNGLFRAMEBUFFERTEXTURE2DEXTPROC)
                glXGetProcAddress("glFramebufferTexture2DEXT");
    m_glCheckFramebufferStatusEXT = (PFNGLCHECKFRAMEBUFFERSTATUSEXTPROC)
                glXGetProcAddress("glCheckFramebufferStatusEXT");
    m_glDeleteFramebuffersEXT = (PFNGLDELETEFRAMEBUFFERSEXTPROC)
                glXGetProcAddress("glDeleteFramebuffersEXT");

    if (!m_glGenFramebuffersEXT   ||
        !m_glBindFramebufferEXT   ||
        !m_glFramebufferTexture2DEXT ||
        !m_glDeleteFramebuffersEXT ||
        !m_glCheckFramebufferStatusEXT)
    {
        VERBOSE(VB_PLAYBACK, LOC + "Couldn't get proc address");
        return 0;
    }
    VERBOSE(VB_PLAYBACK, LOC + "Frame Buffer Objects supported.");
    return SUPP_FBUF;
}

void OpenglContext::CreateTexture(GLuint *tex)
{
    MakeCurrent(true);
    glGenTextures( 1, tex );
    SetupTextureFilters(tex, GL_LINEAR);
    textures.push_back(*tex);
    MakeCurrent(false);
}

bool OpenglContext::SetupTexture(QSize size, GLuint *tex, int filt)
{
    scratchspace = new unsigned char[(size.width()
                                        * size.height() * 4) + 4];
    memset(scratchspace, 0 , size.width() * size.height() * 4);
 
    GLint check;
    MakeCurrent(true);

    SetupTextureFilters(tex, filt);
    glTexImage2D( m_texture, 0, GL_RGBA8,
                size.width(), size.height(),
                0, GL_RGB , GL_UNSIGNED_BYTE, scratchspace );
    glGetTexLevelParameteriv(m_texture, 0, GL_TEXTURE_WIDTH, &check);
    MakeCurrent(false);
    delete scratchspace;
    if (check !=size.width())
        return false;
    return true;
}

void OpenglContext::SetupTextureFilters(GLuint *tex, int filt)
{
    glBindTexture( m_texture, *tex );
    glTexParameteri (m_texture, GL_TEXTURE_MIN_FILTER, filt);
    glTexParameteri (m_texture, GL_TEXTURE_MAG_FILTER, filt);
    glTexParameteri (m_texture, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE);
    glTexParameteri (m_texture, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE);
}

void OpenglContext::DeleteTexture(GLuint *tex)
{
    MakeCurrent(true);
    vector<GLuint>::iterator it;
    for (it = textures.begin(); it !=textures.end(); it++)
    {
        if (*(it) == *tex)
        {
            glDeleteTextures( 1, tex);
            textures.erase(it);
            break;
        }
    }
    MakeCurrent(false);
}

void OpenglContext::DeleteTextures(void)
{
    MakeCurrent(true);
    vector<GLuint>::iterator it;
    for (it = textures.begin(); it !=textures.end(); it++)
        glDeleteTextures( 1, &(*(it)));
    textures.clear();
    MakeCurrent(false);
}

bool OpenglContext::CreateProgram(QString *program, GLuint *prog)
{
    bool success = true;
    GLint error;
    MakeCurrent(true);
    m_glGenProgramsARB   ( 1, prog );
    m_glBindProgramARB   ( GL_FRAGMENT_PROGRAM_ARB, *prog );
    m_glProgramStringARB ( GL_FRAGMENT_PROGRAM_ARB,
                           GL_PROGRAM_FORMAT_ASCII_ARB,
                           program->length(), program->latin1() );

    glGetIntegerv ( GL_PROGRAM_ERROR_POSITION_ARB, &error );
    if (error != -1)
    {
        VERBOSE(VB_PLAYBACK, LOC_ERR +
                QString("Fragment Program compile error: position %1:'%2'")
                .arg(error).arg(program->mid(error)));
        success = false;
    }
    m_glGetProgramivARB( GL_FRAGMENT_PROGRAM_ARB,
                         GL_PROGRAM_UNDER_NATIVE_LIMITS_ARB, &error );
    if (error != 1)
    {
        VERBOSE(VB_PLAYBACK, LOC_ERR + 
                QString("Fragment program exceeds hardware capabilities."));
        success = false;
    }

    if (success)
        programs.push_back(*prog);
    else
        m_glDeleteProgramsARB( 1, prog );
    MakeCurrent(false);
    return success;
}

void OpenglContext::DeleteProgram(GLuint *prog)
{
    MakeCurrent(true);
    vector<GLuint>::iterator it;
    for (it = programs.begin(); it !=programs.end(); it++)
    {
        if (*(it) == *prog)
        {
            m_glDeleteProgramsARB( 1, prog);
            programs.erase(it);
            break;
        }
    }
    MakeCurrent(false);
}


void OpenglContext::DeletePrograms(void)
{
    MakeCurrent(true);
    vector<GLuint>::iterator it;
    for (it = programs.begin(); it !=programs.end(); it++)
        m_glDeleteProgramsARB( 1, &(*(it)));
    MakeCurrent(false);
    programs.clear();
}

bool OpenglContext::CreateFrameBuffer(GLuint *buf, GLuint *tex,
                                      int width, int height)
{
    SetupTextureFilters(tex, GL_LINEAR);
    MakeCurrent(true);
    glPushAttrib (GL_VIEWPORT_BIT);
    glViewport( 0, 0, width, height);
    m_glGenFramebuffersEXT( 1, buf );
    m_glBindFramebufferEXT( GL_FRAMEBUFFER_EXT, *buf );
    glBindTexture( m_texture, *tex );
    glTexImage2D( m_texture, 0, GL_RGBA8,
                  (GLint)width,
                  (GLint)height, 0,
                  GL_RGB, GL_UNSIGNED_BYTE, NULL);
    m_glFramebufferTexture2DEXT( GL_FRAMEBUFFER_EXT,
        GL_COLOR_ATTACHMENT0_EXT, m_texture, *tex, 0 );
    GLenum status;
    status = m_glCheckFramebufferStatusEXT( GL_FRAMEBUFFER_EXT );
    m_glBindFramebufferEXT( GL_FRAMEBUFFER_EXT, 0 );
    glPopAttrib ();
    bool success = false;
    switch(status)
    {
        case GL_FRAMEBUFFER_COMPLETE_EXT:
            VERBOSE(VB_PLAYBACK, LOC + 
                QString("Created frame buffer object (%1x%2).")
                .arg(width).arg(height));
            success = true;
            break;
        case GL_FRAMEBUFFER_INCOMPLETE_ATTACHMENT_EXT:
            VERBOSE(VB_PLAYBACK, LOC + "Frame buffer incomplete_ATTACHMENT");
            break;
        case GL_FRAMEBUFFER_INCOMPLETE_MISSING_ATTACHMENT_EXT:
            VERBOSE(VB_PLAYBACK, LOC +
                "Frame buffer incomplete_MISSING_ATTACHMENT");
            break;
        case GL_FRAMEBUFFER_INCOMPLETE_DUPLICATE_ATTACHMENT_EXT:
            VERBOSE(VB_PLAYBACK, LOC +
                "Frame buffer incomplete_DUPLICATE_ATTACHMENT"); 
            break;
        case GL_FRAMEBUFFER_INCOMPLETE_DIMENSIONS_EXT:
            VERBOSE(VB_PLAYBACK, LOC +
                "Frame buffer incomplete_DIMENSIONS_EXT");
            break;
        case GL_FRAMEBUFFER_INCOMPLETE_FORMATS_EXT:
            VERBOSE(VB_PLAYBACK, LOC +
                "Frame buffer incomplete_FORMATS_EXT");
            break;
        case GL_FRAMEBUFFER_INCOMPLETE_DRAW_BUFFER_EXT:
            VERBOSE(VB_PLAYBACK, LOC +
                "Frame buffer incomplete_DRAW_BUFFER_EXT");
            break;
        case GL_FRAMEBUFFER_INCOMPLETE_READ_BUFFER_EXT:
            VERBOSE(VB_PLAYBACK, LOC +
                "Frame buffer incomplete_READ_BUFFER_EXT");
            break;
        case GL_FRAMEBUFFER_UNSUPPORTED_EXT:
            VERBOSE(VB_PLAYBACK, LOC + "Frame buffer unsupported.");
            break;
        default:
            VERBOSE(VB_PLAYBACK, LOC +
                QString("Unknown frame buffer error %1.").arg(status));
    }
    if (success)
        framebuffers.push_back(*buf);
    else
        m_glDeleteFramebuffersEXT( 1, buf );
    MakeCurrent(false);
    return success;
}

void OpenglContext::DeleteFrameBuffer(GLuint *buf)
{
    MakeCurrent(true);
    vector<GLuint>::iterator it;
    for (it = framebuffers.begin(); it !=framebuffers.end(); it++)
    {
        if (*(it) == *buf)
        {
            m_glDeleteFramebuffersEXT( 1, buf);
            framebuffers.erase(it);
            break;
        }
    }
    MakeCurrent(false);
}

void OpenglContext::DeleteFrameBuffers(void)
{
    MakeCurrent(true);
    vector<GLuint>::iterator it;
    for (it = framebuffers.begin(); it !=framebuffers.end(); it++)
        m_glDeleteFramebuffersEXT( 1, &(*(it)));
    framebuffers.clear();
    MakeCurrent(false);
}

bool OpenglContext::CheckGlx(void)
{
    int ndummy;
    if (!glXQueryExtension(m_display, &ndummy, &ndummy))
    {
        VERBOSE(VB_PLAYBACK, LOC_ERR + "OpenGL extension not present.");
        return false;
    }
    return true;
}

bool OpenglContext::GetVisual(void)
{
    int attribList[] = {GLX_RGBA, GLX_DEPTH_SIZE, 0, GLX_DOUBLEBUFFER, 1,
                        GLX_RED_SIZE, 1, GLX_GREEN_SIZE, 1,
                        GLX_BLUE_SIZE, 1, None};
    m_visual = glXChooseVisual(m_display, m_screen_num, attribList);
    if (m_visual == NULL) 
    {
        // see if we can can get a visual WITH depth buffer
        int withdepth[] = {GLX_RGBA,
                        GLX_DOUBLEBUFFER, 1, GLX_RED_SIZE, 1,
                        GLX_GREEN_SIZE, 1, GLX_BLUE_SIZE, 1, None};
        m_visual = glXChooseVisual(m_display, m_screen_num, withdepth);
        if (m_visual == NULL) 
        {
            VERBOSE(VB_PLAYBACK, LOC_ERR + "No appropriate visual found");
            return false;
        }
    }
    return true;
}

bool OpenglContext::CreateWindow( Window winid, int width, int height)
{
    XSetWindowAttributes swa;
    swa.colormap = XCreateColormap(m_display,
                                   RootWindow(m_display, m_visual->screen),
                                   m_visual->visual, AllocNone);
    if (swa.colormap == 0)
    {
        VERBOSE(VB_PLAYBACK, LOC_ERR + "Failed to create colormap");
        return false;
    }
    m_window = XCreateWindow(m_display, winid, 0, 0,
                           m_visible ? width : 1, m_visible ? height : 1,
                           0, m_visual->depth,
                           InputOutput, m_visual->visual, CWColormap, &swa);
    if (m_window == 0)
    {
        VERBOSE(VB_PLAYBACK, LOC_ERR + "Failed to create window");
        return false;
    }
    if (m_visible)
        XMapWindow(m_display, m_window);
    return true;
}

bool OpenglContext::CreateContext(void)
{
    m_context = glXCreateContext(m_display, m_visual, None, GL_TRUE);
    if (m_context == NULL)
    {
        VERBOSE(VB_PLAYBACK, LOC_ERR + "Failed to create Glx context");
        return false;
    }

    if (!glXMakeCurrent(m_display, m_window, m_context))
    {
        VERBOSE(VB_PLAYBACK, LOC_ERR + "Failed to make Glx context current.");
        return false;
    }
    glXMakeCurrent(m_display, None, NULL);
    return true;
}

__GLXextFuncPtr OpenglContext::glXGetProcAddress(const char * const procName)
{
    __GLXextFuncPtr ret = glXGetProcAddressARB((const GLubyte *) procName);
    if (!ret)
    {
        VERBOSE(VB_PLAYBACK,
                QString("Error: glXGetProcAddressARB unable to find %1")
                .arg(procName));
    }
    return ret;
}
