//--------------------------------------------------------------------------------------
//
//
// Copyright 2015 ADVANCED MICRO DEVICES, INC.  All Rights Reserved.
//
// AMD is granting you permission to use this software and documentation (if
// any) (collectively, the "Materials") pursuant to the terms and conditions
// of the Software License Agreement included with the Materials.  If you do
// not have a copy of the Software License Agreement, contact your AMD
// representative for a copy.
// You agree that you will not reverse engineer or decompile the Materials,
// in whole or in part, except as allowed by applicable law.
//
// WARRANTY DISCLAIMER: THE SOFTWARE IS PROVIDED "AS IS" WITHOUT WARRANTY OF
// ANY KIND.  AMD DISCLAIMS ALL WARRANTIES, EXPRESS, IMPLIED, OR STATUTORY,
// INCLUDING BUT NOT LIMITED TO THE IMPLIED WARRANTIES OF MERCHANTABILITY,
// FITNESS FOR A PARTICULAR PURPOSE, TITLE, NON-INFRINGEMENT, THAT THE SOFTWARE
// WILL RUN UNINTERRUPTED OR ERROR-FREE OR WARRANTIES ARISING FROM CUSTOM OF
// TRADE OR COURSE OF USAGE.  THE ENTIRE RISK ASSOCIATED WITH THE USE OF THE
// SOFTWARE IS ASSUMED BY YOU.
// Some jurisdictions do not allow the exclusion of implied warranties, so
// the above exclusion may not apply to You.
//
// LIMITATION OF LIABILITY AND INDEMNIFICATION:  AMD AND ITS LICENSORS WILL
// NOT, UNDER ANY CIRCUMSTANCES BE LIABLE TO YOU FOR ANY PUNITIVE, DIRECT,
// INCIDENTAL, INDIRECT, SPECIAL OR CONSEQUENTIAL DAMAGES ARISING FROM USE OF
// THE SOFTWARE OR THIS AGREEMENT EVEN IF AMD AND ITS LICENSORS HAVE BEEN
// ADVISED OF THE POSSIBILITY OF SUCH DAMAGES.
// In no event shall AMD's total liability to You for all damages, losses,
// and causes of action (whether in contract, tort (including negligence) or
// otherwise) exceed the amount of $100 USD.  You agree to defend, indemnify
// and hold harmless AMD and its licensors, and any of their directors,
// officers, employees, affiliates or agents from and against any and all
// loss, damage, liability and other expenses (including reasonable attorneys'
// fees), resulting from Your use of the Software or violation of the terms and
// conditions of this Agreement.
//
// U.S. GOVERNMENT RESTRICTED RIGHTS: The Materials are provided with "RESTRICTED
// RIGHTS." Use, duplication, or disclosure by the Government is subject to the
// restrictions as set forth in FAR 52.227-14 and DFAR252.227-7013, et seq., or
// its successor.  Use of the Materials by the Government constitutes
// acknowledgement of AMD's proprietary rights in them.
//
// EXPORT RESTRICTIONS: The Materials may be subject to export restrictions as
// stated in the Software License Agreement.
//
//--------------------------------------------------------------------------------------


#include <iomanip>
#include <sstream>
#include <string>

#include "os_include.h"
#include <assert.h>
#include <GL/glew.h>


#include "defines.h"
#include "BufferQueue.h"
#include "GPUSink.h"



GPUSink::GPUSink(Euresys::EGenTL &gentl)
    : m_gentl(gentl)
    , m_grabber(gentl)
{
    m_uiWindowWidth = 0;
    m_uiWindowHeight = 0;

    m_uiTextureWidth = 0;
    m_uiTextureHeight = 0;
    m_uiTexture = 0;
    m_uiBufferSize = 0;

    m_nIntFormat = 0;
    m_nFormat = 0;
    m_nType  = 0;

    m_fAspectRatio = 1.0f;
    m_uiQuad = 0;

    m_pBusBuffers = NULL;
    m_uiNumBuffers = 0;
}


GPUSink::~GPUSink()
{
    m_grabber.stop();
    delete []m_pBusBuffers;
}


bool GPUSink::initGL()
{
    const float pArray [] = {  1.0f,  1.0f,        0.0f, 0.0f,
                              -1.0f,  1.0f,        1.0f, 0.0f,
                              -1.0f, -1.0f,        1.0f, 1.0f,
                               1.0f, -1.0f,        0.0f, 1.0f,
                            };

    glMatrixMode(GL_MODELVIEW);
    glLoadIdentity();

    glDisable(GL_DEPTH_TEST);
    glEnable(GL_TEXTURE_2D);

    glShadeModel(GL_SMOOTH);

    glPolygonMode(GL_FRONT, GL_FILL);

    // create quad that is used to map texture to
    glGenBuffers(1, &m_uiQuad);
    glBindBuffer(GL_ARRAY_BUFFER, m_uiQuad);

    glBufferData(GL_ARRAY_BUFFER, 16*sizeof(float), pArray, GL_STATIC_DRAW);

    glBindBuffer(GL_ARRAY_BUFFER, m_uiQuad);
    glEnableClientState(GL_VERTEX_ARRAY);
    glEnableClientState(GL_TEXTURE_COORD_ARRAY);

    glVertexPointer(2, GL_FLOAT,   4*sizeof(float), 0);
    glTexCoordPointer(2, GL_FLOAT, 4*sizeof(float), (char*)NULL + 2*sizeof(float));

    glBindBuffer(GL_ARRAY_BUFFER, 0);

    // Create texture that will be used to store frames from remote device
    glGenTextures(1, &m_uiTexture);

    glBindTexture(GL_TEXTURE_2D, m_uiTexture);

    glTexEnvi(GL_TEXTURE_ENV, GL_TEXTURE_ENV_MODE, GL_COMBINE);
    glTexEnvi(GL_TEXTURE_ENV, GL_COMBINE_RGB, GL_MODULATE);
    glTexEnvi(GL_TEXTURE_ENV, GL_SOURCE0_RGB, GL_TEXTURE0);
    glTexEnvi(GL_TEXTURE_ENV, GL_OPERAND0_RGB, GL_SRC_COLOR);
    glTexEnvi(GL_TEXTURE_ENV, GL_SOURCE1_RGB, GL_CONSTANT);

    glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR);
    glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR);
    glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_BORDER);
    glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_BORDER);

    glBindTexture(GL_TEXTURE_2D, 0);

    glClearColor(0.0, 0.0, 0.25, 1.0);

    // Activate Sync to VBlank to avoid tearing
    // wglSwapIntervalEXT(1);

    if (glGetError() != GL_NO_ERROR)
        return false;
    
    return true;
}

bool GPUSink::initFG()
{
    m_grabber.runScript("config.js");
    return true;
}

void GPUSink::resize()
{
    resize(m_uiWindowWidth, m_uiWindowHeight);
}


void GPUSink::resize(unsigned int w, unsigned int h)
{
    m_uiWindowWidth  = w;
    m_uiWindowHeight = h;

    glViewport(0, 0, w, h);

    if (!m_uiWindowWidth || !m_uiWindowHeight || !m_uiTextureWidth || !m_uiTextureHeight) return;
    
    float fWinAspectRatio = (float)w / (float)h;
    float mj_k, mn_k;

    glMatrixMode(GL_PROJECTION);
    glLoadIdentity();

    if (m_fAspectRatio > fWinAspectRatio)
    {
        // fill in X and gaps in Y
        mj_k = (float)m_uiWindowWidth / (float)m_uiTextureWidth;
        mn_k = (float)m_uiTextureHeight / ((float)m_uiWindowHeight / mj_k);
        glScalef(1.0f, mn_k, 1.0f);
    }
    else
    {
        // fill in Y and gaps in X
        mj_k = (float)m_uiWindowHeight / (float)m_uiTextureHeight;
        mn_k = (float)m_uiTextureWidth / ((float)m_uiWindowWidth / mj_k);
        glScalef(mn_k, 1.0f, 1.0f);
    }
}


// format can be GL_LUMINANCE for mono, GL_RG for Bayer and GL_RGB for RGB
bool GPUSink::createBuffers()
{
    StreamDesc streamDesc = { 0 };
    streamDesc.uiWidth = (unsigned int)m_grabber.getWidth();
    streamDesc.uiHeight = (unsigned int)m_grabber.getHeight();
    std::string fmt = m_grabber.getString<Euresys::StreamModule>("PixelFormat");
    if (fmt == "Mono8") {
        streamDesc.nBits = 8;
    } else if (fmt == "Mono10") {
        streamDesc.nBits = 10;
    } else if (fmt == "Mono12") {
        streamDesc.nBits = 12;
    } else if (fmt == "Mono14") {
        streamDesc.nBits = 14;
    } else if (fmt == "Mono16") {
        streamDesc.nBits = 16;
    } else {
        std::string s("Pixel format '" + fmt + "' is not handled in this program");
        m_gentl.memento(s);
        return false;
    }
    streamDesc.nFormat = GL_LUMINANCE;
    std::ostringstream ss;
    ss << "Creating " << streamDesc.uiWidth << "x" << streamDesc.uiHeight << " " << streamDesc.nBits << "-bit buffers";
    m_gentl.memento(ss.str());

    m_uiTextureWidth  = streamDesc.uiWidth;
    m_uiTextureHeight = streamDesc.uiHeight;

    if (!m_uiTextureWidth || !m_uiTextureHeight)
        return false;

    int bpp;

    switch (streamDesc.nBits)
    {
        case 8:
            m_nSourceBits = 8;
            m_fSourceDataScaleFactor = 1.0;
            m_nType = GL_UNSIGNED_BYTE;
            bpp = 1;
            break;
        case 10:
        case 12:
        case 14:
        case 16:
            m_nSourceBits = streamDesc.nBits;
            m_fSourceDataScaleFactor = (float)((1 << 16) - 1) / (float)(float)((1 << m_nSourceBits) - 1);
            m_nType = GL_UNSIGNED_SHORT;
            bpp = 2;
            break;
        case 32:
            m_nSourceBits = 8;
            m_fSourceDataScaleFactor = 1.0;
            m_nType = GL_UNSIGNED_BYTE;
            bpp = 4;
            break;
        default:
            return false;
    }

    switch (streamDesc.nFormat)
    {
        case GL_LUMINANCE:
        case GL_RG:
            m_nFormat = GL_LUMINANCE;
            m_nIntFormat = streamDesc.nBits > 8 ? GL_LUMINANCE16 : GL_LUMINANCE8;
            break;
        case GL_RGBA:
            m_nFormat = GL_RGBA;
            switch (streamDesc.nBits)
            {
                case 32:
                    m_nIntFormat = GL_RGBA8;
                    break;
                default:
                    return false;
            }
            break;
        default:
            return false;
    }

    glBindTexture(GL_TEXTURE_2D, m_uiTexture);

    glTexImage2D(GL_TEXTURE_2D, 0, m_nIntFormat, m_uiTextureWidth, m_uiTextureHeight, 0, m_nFormat, m_nType, NULL);

    float c[4] = { m_fSourceDataScaleFactor, m_fSourceDataScaleFactor, m_fSourceDataScaleFactor, 1.0 };
    glTexEnvfv(GL_TEXTURE_ENV, GL_TEXTURE_ENV_COLOR, (GLfloat*)&c);

    glBindTexture(GL_TEXTURE_2D, 0);

    // Create transfer buffer
    if (!createBusBuffers(NUM_BUFFERS, bpp * m_uiTextureWidth * m_uiTextureHeight))
        return false;

    // Update synchronization buffers
    m_uiTransferId = 1;
    m_grabber.setInteger<Euresys::StreamModule>("SyncMarkerValue", m_uiTransferId);
    m_grabber.setInteger<Euresys::StreamModule>("SyncMarkerValueIncrement", 1);
    for (unsigned int i = 0; i < NUM_BUFFERS; i++)
    {
        m_grabber.setInteger<Euresys::StreamModule>("SyncMarkerBusAddress", m_pBusBuffers[i].ullMarkerBusAddress);
        m_grabber.announceAndQueue(Euresys::BusMemory(m_pBusBuffers[i].ullBufferBusAddress, m_uiBufferSize, &m_pBusBuffers[i]));
        m_pBufferQueue.push(&m_pBusBuffers[i]);
    }

    m_fAspectRatio = (float)m_uiTextureWidth / (float)m_uiTextureHeight;

    return true;
}


void GPUSink::start()
{
    m_grabber.start();
}


void GPUSink::draw()
{
    if (m_pBufferQueue.empty())
    {
        return;
    }

    glClear(GL_COLOR_BUFFER_BIT);

    // Update stream
    glBindTexture(GL_TEXTURE_2D, m_uiTexture);
    
    BusBufferDesc* pBusBuffer = m_pBufferQueue.front();
    m_pBufferQueue.pop();

    glBindBuffer(GL_PIXEL_UNPACK_BUFFER, pBusBuffer->uiBufferGLId);
    // Instruct the GPU to wait until the marker value is m_uiTransferId before processing the subsequent instructions
    glWaitMarkerAMD(pBusBuffer->uiBufferGLId, m_uiTransferId++);
    
    // Copy bus addressable buffer into texture object
    glTexSubImage2D(GL_TEXTURE_2D, 0, 0, 0, m_uiTextureWidth, m_uiTextureHeight, m_nFormat, m_nType, NULL);

    // Insert fence to determine when the buffer was copied into the texture and we can release the buffer
    GLsync Fence = glFenceSync(GL_SYNC_GPU_COMMANDS_COMPLETE, 0);

    // Draw quad with mapped texture
    glBindBuffer(GL_ARRAY_BUFFER, m_uiQuad);
    glDrawArrays(GL_QUADS, 0, 4);
    glBindBuffer(GL_ARRAY_BUFFER, 0);

    glBindTexture(GL_TEXTURE_2D, 0);

    // glClientWaitSync does a busy-wait, so we first pop the buffer (EGrabber::pop blocks without busy-waiting)
    Euresys::Buffer filled(m_grabber.pop());
    // Wait until buffer is no longer needed by the GPU
    if (glIsSync(Fence))
    {
        glClientWaitSync(Fence, GL_SYNC_FLUSH_COMMANDS_BIT, OneSecond);
        glDeleteSync(Fence);
    }
    // Re-queue the buffer
    filled.push(m_grabber);
    m_pBufferQueue.push(pBusBuffer);
}



bool GPUSink::createBusBuffers(unsigned int uiNumBuffers, unsigned int uiBufferSize)
{
    // Check if buffers were already allocated. If so delete them
    if (m_pBusBuffers)
    {
        for (unsigned int i = 0; i < m_uiNumBuffers; i++)
        {
            glDeleteBuffers(1, &m_pBusBuffers[i].uiBufferGLId);
        }

        delete[] m_pBusBuffers;
        m_pBusBuffers = NULL;
        m_uiNumBuffers = 0;
    }

    m_uiNumBuffers = uiNumBuffers;
    m_uiBufferSize = uiBufferSize;

    // Generate GL Buffers
    m_pBusBuffers = new BusBufferDesc[m_uiNumBuffers];
    
    GLenum nUsage = GL_DYNAMIC_DRAW;

    /////////////////////////////////////////////
    // Create bus addressable memory buffers

    for (unsigned int i = 0; i < m_uiNumBuffers; i++)
    {
        BusBufferDesc *pBuffer = &m_pBusBuffers[i];

        glGenBuffers(1, &pBuffer->uiBufferGLId);

        glBindBuffer(GL_BUS_ADDRESSABLE_MEMORY_AMD, pBuffer->uiBufferGLId);
        glBufferData(GL_BUS_ADDRESSABLE_MEMORY_AMD, m_uiBufferSize, 0, nUsage);

        // Call makeResident when all BufferData calls were submitted.
        glMakeBuffersResidentAMD(1, &pBuffer->uiBufferGLId, &pBuffer->ullBufferBusAddress, &pBuffer->ullMarkerBusAddress);

    }

    // Make sure that the buffer creation really succeed
    GLenum error = glGetError();
    if (error != GL_NO_ERROR)
    {
        std::ostringstream ss;
        ss << "GPUSink::createBusBuffers: glGetError() = 0x" << std::hex << std::setfill('0') << std::setw(8) << error << " (" << glewGetErrorString(error) << ")";
        m_gentl.memento(ss.str());
        return false;
    }

    glBindBuffer(GL_BUS_ADDRESSABLE_MEMORY_AMD, 0);

    return true;
}
