/*******************************************************************************
* Copyright 2003-2020 Intel Corporation.
*
* This software and the related documents are Intel copyrighted  materials,  and
* your use of  them is  governed by the  express license  under which  they were
* provided to you (License).  Unless the License provides otherwise, you may not
* use, modify, copy, publish, distribute,  disclose or transmit this software or
* the related documents without Intel's prior written permission.
*
* This software and the related documents  are provided as  is,  with no express
* or implied  warranties,  other  than those  that are  expressly stated  in the
* License.
*******************************************************************************/

#if !defined(__TARGET_ARCH_MIC)

#include "base_renderer.h"

#ifdef ENABLE_RENDERING

static unsigned int ImageColorToGl(ColorFormat format)
{
    switch(format)
    {
    case CF_GRAY: return GL_LUMINANCE;
    case CF_BGR:  return GL_BGR_EXT;
    case CF_BGRA: return GL_BGRA_EXT;
    case CF_RGB:  return GL_RGB;
    case CF_RGBA: return GL_RGBA;
    default:   return 0;
    }
}

Textures::Textures()
{
    m_pTextures = NULL;
    m_texNum = 0;
}
Textures::~Textures()
{
    Release();
}
void Textures::Release()
{
    if(m_pTextures)
    {
        glDeleteTextures(m_texNum, m_pTextures);
        delete[] m_pTextures;
        m_pTextures = NULL;
        m_texNum = 0;
    }
}

void Textures::Generate(GLsizei num)
{
    Release();

    m_texNum = num;
    m_pTextures = new GLuint[num];
    glGenTextures(m_texNum, m_pTextures);
}

RendererOpenGL::RendererOpenGL(void)
{
    m_bInitialized   = false;

#if defined _WIN32
    m_hWnd      = 0;
    m_wdc       = 0;
    m_glContext = 0;
#else
    m_glContext = 0;
    m_display   = NULL;
    m_window    = 0;
    m_pvisinfo  = NULL;
    m_iScreen   = 0;
#endif

    m_bKeepAspect    = true;
    m_rectPrev.x     = m_rectPrev.y = 0;
    m_rectPrev.width = m_rectPrev.height = 0;

    m_texTile.width  = 128;
    m_texTile.height = 128;
}

RendererOpenGL::~RendererOpenGL(void)
{
    Close();
}

Status RendererOpenGL::Init(RendererContext *pContext)
{
    if(!pContext)
        return STS_ERR_NULL_PTR;

    Close();

    m_bKeepAspect = true;

#if defined _WIN32
    m_hWnd = pContext->m_hWnd;

    PIXELFORMATDESCRIPTOR pfd = {
        sizeof(PIXELFORMATDESCRIPTOR),  1, PFD_DRAW_TO_WINDOW | PFD_SUPPORT_OPENGL | PFD_DOUBLEBUFFER,
        PFD_TYPE_RGBA, 24, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 8, 0, 0, PFD_MAIN_PLANE, 0, 0, 0, 0
    };

    m_wdc = GetDC(m_hWnd);
    if(NULL == m_wdc)
        return STS_ERR_FAILED;

    if(!SetPixelFormat(m_wdc, ChoosePixelFormat(m_wdc, &pfd), &pfd))
        return STS_ERR_FAILED;
#else
    m_display        = pContext->m_pDisplay;
    m_window         = pContext->m_window;
    m_iScreen        = pContext->m_iScreen;
    m_pvisinfo       = pContext->m_pVisualInfo;
#endif

    if(InitForThread() != STS_OK)
        return STS_ERR_FAILED;

    m_bInitialized = true;

    return STS_OK;
}

Status RendererOpenGL::Close(void)
{
#if defined _WIN32
    if(m_glContext)
    {
        wglDeleteContext(m_glContext);
        m_glContext = 0;
    }
    if(m_wdc && m_hWnd)
    {
        ReleaseDC(m_hWnd, m_wdc);
        m_wdc = 0;
    }
#else
    if(m_glContext)
    {
        glXDestroyContext(m_display, m_glContext);
        m_glContext = 0;
    }
#endif

    m_imageTiles.Release();

    return STS_OK;
}

Status RendererOpenGL::InitForThread()
{
#if defined _WIN32
    m_glContext = wglCreateContext(m_wdc); // create rendering context
    if(NULL == m_glContext)
        return STS_ERR_FAILED;

    if(!wglMakeCurrent(m_wdc, m_glContext)) // set it as current
        return STS_ERR_FAILED;
#else
    if (NULL == (m_glContext = glXCreateContext( m_display, m_pvisinfo, NULL, true )))
        return STS_ERR_FAILED;

    if (!glXMakeCurrent(m_display, m_window, m_glContext))
        return STS_ERR_FAILED;
#endif

    // OpenGL context already tied to output window
    // to disable all slow GL components
    // it is not mandatory to disable all if we have accelerated card
    glClearColor(.0f, .0f, .0f, .0f);
    glClearDepth(1.0);
    glDepthFunc(GL_NEVER);

    // disable slow GL extensions
    glDisable(GL_DEPTH_TEST); /*glDisable(GL_ALPHA_TEST);   glDisable(GL_BLEND);*/
    glDisable(GL_DITHER);     glDisable(GL_FOG);          glDisable(GL_STENCIL_TEST);
    glDisable(GL_LIGHTING);   glDisable(GL_LOGIC_OP);     glDisable(GL_TEXTURE_1D);

    glPixelTransferi(GL_MAP_COLOR, GL_FALSE);
    glPixelTransferi(GL_RED_SCALE,   1);  glPixelTransferi(GL_RED_BIAS,   0);
    glPixelTransferi(GL_GREEN_SCALE, 1);  glPixelTransferi(GL_GREEN_BIAS, 0);
    glPixelTransferi(GL_BLUE_SCALE,  1);  glPixelTransferi(GL_BLUE_BIAS,  0);
    glPixelTransferi(GL_ALPHA_SCALE, 1);  glPixelTransferi(GL_ALPHA_BIAS, 0);

    glEnable(GL_BLEND);
    glEnable(GL_ALPHA_TEST);
    glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA);

    glEnable(GL_TEXTURE_2D);

    glMatrixMode(GL_PROJECTION);
    glLoadIdentity();
    glOrtho(0, 1, 0, 1, 0, 1);
    glMatrixMode(GL_MODELVIEW);
    glLoadIdentity();

    glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);

#if defined _WIN32
    ::SwapBuffers(m_wdc);
#else
    glXSwapBuffers(m_display, m_window);
#endif

    return STS_OK;
}

Rect RendererOpenGL::ScaleViewRect(Rect rect)
{
#if defined _WIN32
    ::RECT wndRect;
    GetClientRect(m_hWnd, &wndRect);
    int iWndWidth  = wndRect.right;
    int iWndHeight = wndRect.bottom;
#else
    XWindowAttributes wattc = {0};
    XGetWindowAttributes(m_display, m_window, &wattc);
    int iWndWidth  = wattc.width;
    int iWndHeight = wattc.height;
#endif

    if(m_bKeepAspect)
    {
        float fScale = MIN((float)iWndWidth/rect.width, (float)iWndHeight/rect.height);
        rect.width  = (unsigned int)(fScale*rect.width);
        rect.height = (unsigned int)(fScale*rect.height);
    }

    rect.x = (int)((rect.x >= 0)?(rect.x):((iWndWidth - rect.width)/2));
    rect.y = (int)((rect.y >= 0)?(rect.y):((iWndHeight - rect.height)/2));

    return rect;
}

void RendererOpenGL::SetViewport(Rect rect)
{
    if(m_rectPrev.x != rect.x || m_rectPrev.y != rect.y || m_rectPrev.width != rect.width || m_rectPrev.height != rect.height)
    {
        m_rectPrev = rect;
        glViewport((int)m_rectPrev.x, (int)m_rectPrev.y, (int)m_rectPrev.width, (int)m_rectPrev.height);
    }
}

#define TILING_EXPERIMENTAL 0

Status RendererOpenGL::DrawImage(Image *pFrame, bool bForce, Rect rect)
{
    Status status = STS_OK;
    GLenum glSts;

    (void)rect;

    if(!pFrame)
    {
        if(m_template.ptr())
            pFrame = &m_template;
        else
            return STS_ERR_NULL_PTR;
    }

    int imageFormat = ImageColorToGl(pFrame->m_color);

    int borderA = 0;
    int borderB = 0;

#if TILING_EXPERIMENTAL == 1
    int texNumW = (int)((pFrame->m_size.width  + m_texTile.width - borderA - 1)/(m_texTile.width - borderA));
    int texNumH = (int)((pFrame->m_size.height + m_texTile.height - borderA - 1)/(m_texTile.height - borderA));
#else
    int texNumW = 1;
    int texNumH = 1;
#endif

    // Check if we have same frame
    if(m_template.ptr() != pFrame->ptr() || bForce)
    {
        // Create new textures
        if(texNumW*texNumH > m_imageTiles.m_texNum)
            m_imageTiles.Generate(texNumW*texNumH);

#if TILING_EXPERIMENTAL == 0
        m_texTile = Size(1, 1);
        do
        {
            if (m_texTile.width < pFrame->m_size.width)
                m_texTile.width <<= 1;
            if (m_texTile.height < pFrame->m_size.height)
                m_texTile.height <<= 1;
        } while((m_texTile.width < pFrame->m_size.width) || (m_texTile.height < pFrame->m_size.height));
#endif

        // Update textures data
        for(int h = 0; h < texNumH; h++)
        {
            for(int w = 0; w < texNumW; w++)
            {
                glBindTexture(GL_TEXTURE_2D, m_imageTiles.m_pTextures[texNumW*h + w]);
                glSts = glGetError();
                if(glSts != GL_NO_ERROR)
                    return STS_ERR_FAILED;

                glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR);
                glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR);
                glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP);
                glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP);
                glTexEnvi(GL_TEXTURE_ENV, GL_TEXTURE_ENV_MODE, GL_DECAL);

                glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA, (int)m_texTile.width, (int)m_texTile.height, 0, imageFormat, GL_UNSIGNED_BYTE, NULL);
                glSts = glGetError();
                if(glSts != GL_NO_ERROR)
                    return STS_ERR_FAILED;

                int borderShiftX = borderA;
                int borderShiftY = borderA;
                Size tileSize;
                tileSize.width  = pFrame->m_size.width - (m_texTile.width - borderA)*w;
                tileSize.height = pFrame->m_size.height - (m_texTile.height - borderA)*h;
                if(tileSize.width > m_texTile.width)
                    tileSize.width = m_texTile.width;
                if(tileSize.height > m_texTile.height)
                    tileSize.height = m_texTile.height;
                if(h == 0)
                    borderShiftY = 0;
                if(w == 0)
                    borderShiftX = 0;

                for(int row = 0; row < (int)tileSize.height; row++)
                {
                    glTexSubImage2D(GL_TEXTURE_2D, 0, 0, row, (int)tileSize.width, 1, imageFormat, GL_UNSIGNED_BYTE,
                        pFrame->ptr(h*(m_texTile.height - borderShiftY) + row, w*(m_texTile.width - borderShiftX)));
                    glSts = glGetError();
                    if(glSts != GL_NO_ERROR)
                        return STS_ERR_FAILED;
                }
            }
        }

        m_template = *pFrame;
    }

    // Render textures
    if(m_rectPrev.width != 0 && m_rectPrev.height != 0)
    {
  //      glPolygonMode( GL_FRONT_AND_BACK, GL_LINE );
        glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);
        glColor4ub(255, 20, 147, 255);

        float xStep = ((float)1/(pFrame->m_size.width + texNumW*borderA))*m_texTile.width;
        float yStep = ((float)1/(pFrame->m_size.height + texNumH*borderA))*m_texTile.height;
        float xAnch;
        float yAnch;

        for(int h = 0; h < texNumH; h++)
        {
            for(int w = 0; w < texNumW; w++)
            {
                glBindTexture(GL_TEXTURE_2D, m_imageTiles.m_pTextures[texNumW*h + w]);
                glSts = glGetError();
                if(glSts != GL_NO_ERROR)
                    return STS_ERR_FAILED;

                float xBorderPx = ((float)1/(MAX(pFrame->m_size.width, pFrame->m_size.height)))*borderB;
                float yBorderPx = ((float)1/(MAX(pFrame->m_size.width, pFrame->m_size.height)))*borderB;
               /* if(h == 0)
                    yBorderPx = 0;
                if(w == 0)
                    xBorderPx = 0;*/

                xAnch = xStep*w;
                yAnch = 1 - yStep*h;

                glBegin(GL_QUADS);
                glTexCoord2f(xBorderPx,  yBorderPx);                glVertex2f(xAnch,          yAnch);            // LeftTop
                glTexCoord2f(1-xBorderPx,  yBorderPx);            glVertex2f(xAnch + xStep,  yAnch);            // RightTop
                glTexCoord2f(1-xBorderPx,  1-yBorderPx);        glVertex2f(xAnch + xStep,  yAnch - yStep);    // RightBottom
                glTexCoord2f(xBorderPx,  1-yBorderPx);        glVertex2f(xAnch,          yAnch - yStep);    // LeftBottom
                glEnd();

                glSts = glGetError();
                if(glSts != GL_NO_ERROR)
                    return STS_ERR_FAILED;
            }
        }
    }

    return status;
}

Status RendererOpenGL::SwapBuffers()
{
    GLenum glSts;

    glFlush();
    glSts = glGetError();
    if(glSts != GL_NO_ERROR)
        return STS_ERR_FAILED;

#if defined _WIN32
    if(!::SwapBuffers(m_wdc))   // to draw on physical screen
        return STS_ERR_FAILED;
#else
    glXSwapBuffers(m_display, m_window);
#endif

    return STS_OK;
}

#endif

/*
// Window helpers
*/
void keyCall(int iKeyCode, int iCount, unsigned int iFlags, void *pParams)
{
    WindowDraw *pWD = (WindowDraw*)pParams;
    iCount = iCount;

    if(!pWD)
        return;

    if((iFlags & KF_MESSAGE_UP) || !(iFlags & KF_STATE_CHANGE))
        return;

    if(iKeyCode == KK_ESCAPE)
    {
        pWD->m_pWndClass->m_feedback.bDestroyMessage = true;
    }
    else if(iKeyCode == KK_F)
    {
        pWD->m_pWndClass->WindowSwitchFullscreen();
        pWD->m_pWndClass->Invalidate();
    }
/*    else if(iKeyCode == KK_SPACE)
    {
        //pWD->m_pWndClass->Invalidate();
    }*/

    return;
}

unsigned int VM_THREAD_CALLCONVENTION wndProcThread(void *pParams)
{
    WindowDraw *pWD = (WindowDraw*)pParams;

    if(!pWD->m_pWndClass->WindowCreate(pWD->m_wndName, pWD->m_wndStyle))
    {
        vm_event_set(&pWD->m_wndInited, 1);
        return 1;
    }

    pWD->m_pWndClass->WindowResize(200, 200);
    pWD->m_pWndClass->WindowShow();

    pWD->m_pWndClass->AddKeyboardCallback(keyCall, pWD);

    vm_event_set(&pWD->m_wndInited, 1);

    while(!pWD->m_bKillWndThread)
    {
        pWD->m_pWndClass->CheckMessages();
        vm_time_sleep(10);
    }

    return 0;
}

WindowDraw::WindowDraw()
{
    reset();
}

WindowDraw::WindowDraw(const char* name, unsigned int flags)
{
    reset();
    Create(name, flags);
}

void WindowDraw::reset()
{
    m_pWndClass = NULL;
    m_pRenClass = NULL;

    m_wndStyle = 0;
    m_flags    = 0;

    m_bFirstFrame = true;

    vm_thread_construct(&m_wndProcThread);
    vm_event_construct(&m_wndInited);

    m_bKillWndThread = false;
    m_bInitialized = false;
}

WindowDraw::~WindowDraw()
{
    Close();
}

bool WindowDraw::Create(const char* sName, unsigned int iFlags)
{
#ifdef ENABLE_RENDERING
    if(!sName)
        return false;

    Close();

    m_flags      = iFlags;
    int iStyle   = WSTYLE_NORMAL;

#if defined _WIN32
    m_pWndClass = new WindowWin();
#elif defined __unix__
    m_pWndClass = new WindowX();
#endif
    if(!m_pWndClass)
    {
        Close();
        return false;
    }

    m_pRenClass = new RendererOpenGL();
    if(!m_pRenClass)
    {
        Close();
        return false;
    }

    m_pWndClass->m_feedback.keyCall = 0;

    // create window
    if(iFlags & WF_FIT_TO_IMAGE)
        iStyle = WSTYLE_FIXED_SIZE;

    m_wndName  = sName;
    m_wndStyle = iStyle;

    if(vm_event_init(&m_wndInited, 0, 0) < VM_OK)
    {
        Close();
        return false;
    }

    if(vm_thread_create(&m_wndProcThread, &wndProcThread, this) < VM_OK)
    {
        Close();
        return false;
    }

    // Wait with 0.5 second intervals until thread exists
    vm_status statusThread = VM_OK;
    vm_status statusEvent  = VM_OK;
    do
    {
        statusThread = vm_thread_wait(&m_wndProcThread, 1);
        if(statusThread >= VM_OK)
            statusEvent = vm_event_wait(&m_wndInited, 500);
    } while(VM_TIMEOUT == statusThread && VM_TIMEOUT == statusEvent);
    if(statusThread < VM_OK || statusEvent < VM_OK)
    {
        Close();
        return false;
    }

    if(!m_pWndClass->IsWindowExist())
    {
        Close();
        return false;
    }

    RendererContext context;
    m_pWndClass->GetWindowContext(&context);
    if(m_pRenClass->Init(&context) != STS_OK)
    {
        Close();
        return false;
    }

    m_bInitialized = true;
    return true;
#else
    return false;
#endif
}

void WindowDraw::Close()
{
    if(m_pRenClass)
        delete m_pRenClass;

    m_bKillWndThread = true;

    vm_thread_destroy(&m_wndProcThread);
    vm_event_destroy(&m_wndInited);

    if(m_pWndClass)
        delete m_pWndClass;

    m_bInitialized = false;
    reset();
}

Status WindowDraw::DrawImage(Image *pImage, bool bForce, bool bDelayed)
{
    Status status;

    this->ProcWindow();
    if(!this->IsInitialized())
        return STS_ERR_NOT_INITIALIZED;

    if(!pImage)
        return STS_ERR_NULL_PTR;

    Rect rect(-1, -1, 0, 0);

    if(m_bFirstFrame || (m_flags & WF_FIT_TO_IMAGE))
    {
        m_pWndClass->WindowResize((int)pImage->m_size.width, (int)pImage->m_size.height);
        m_bFirstFrame = false;
    }

    rect.width  = pImage->m_size.width;
    rect.height = pImage->m_size.height;

    rect = m_pRenClass->ScaleViewRect(rect);
    m_pRenClass->SetViewport(rect);
    status = m_pRenClass->DrawImage(pImage, bForce);
    CHECK_STATUS_PRINT_RS(status, "BaseRenderer::DrawImage()", GetBaseStatusString(status));
    if(!bDelayed)
    {
        status = m_pRenClass->SwapBuffers();
        CHECK_STATUS_PRINT_RS(status, "BaseRenderer::SwapBuffers()", GetBaseStatusString(status));
    }

    return status;
}

Status WindowDraw::SwapBuffers()
{
    this->ProcWindow();
    if(!this->IsInitialized())
        return STS_ERR_NOT_INITIALIZED;

    return m_pRenClass->SwapBuffers();
}

bool WindowDraw::IsClosed()
{
    this->ProcWindow();
    if(!this->IsInitialized())
        return true;

    return false;
}

bool WindowDraw::IsInvalidated()
{
    this->ProcWindow();
    if(!this->IsInitialized())
        return false;

    if(m_pWndClass->m_feedback.iRepaint)
    {
        m_pWndClass->m_feedback.iRepaint = 0;
        return true;
    }
    return false;
}

int WindowDraw::CheckKey()
{
    this->ProcWindow();
    if(!this->IsInitialized())
        return STS_ERR_NOT_INITIALIZED;

    int iKey = m_pWndClass->m_feedback.iLastKey;
    m_pWndClass->m_feedback.iLastKey = -1;

    return iKey;
}

int WindowDraw::WaitKey()
{
    this->ProcWindow();
    if(!this->IsInitialized())
        return STS_ERR_NOT_INITIALIZED;

    while(!m_pWndClass->m_feedback.bDestroyMessage && m_pWndClass->m_feedback.iLastKey == -1)
        vm_time_sleep(10);

    int iKey = m_pWndClass->m_feedback.iLastKey;
    m_pWndClass->m_feedback.iLastKey = -1;

    return iKey;
}

Status WindowDraw::WaitQuit()
{
    this->ProcWindow();
    if(!this->IsInitialized())
        return STS_ERR_NOT_INITIALIZED;

    while(!m_pWndClass->m_feedback.bDestroyMessage)
        vm_time_sleep(10);

    // don't close we need to release renderer first
    m_pWndClass->WindowHide();

    return STS_OK;
}

void WindowDraw::ProcWindow()
{
    if(m_pWndClass)
    {
        if(m_pWndClass->m_feedback.bDestroyMessage)
            Close();
    }
}

Point WindowDraw::GetMouseCoordinates()
{
    this->ProcWindow();
    if(!this->IsInitialized())
        return Point(0, 0);

#if defined ENABLE_RENDERING && defined _WIN32
    POINT point;
    if(!GetCursorPos(&point))
        return Point(0, 0);

    RendererContext ctx;
    this->m_pWndClass->GetWindowContext(&ctx);
    if(!ScreenToClient(ctx.m_hWnd, &point))
        return Point(0, 0);

    return Point(point.x, point.y);
#else
    return Point(0, 0);
#endif
}

#endif
