//===============================================================================
// Copyright (c) 2014-2024  Advanced Micro Devices, Inc. All rights reserved.
//===============================================================================
//
// Permission is hereby granted, free of charge, to any person obtaining a copy
// of this software and associated documentation files(the "Software"), to deal
// in the Software without restriction, including without limitation the rights
// to use, copy, modify, merge, publish, distribute, sublicense, and / or sell
// copies of the Software, and to permit persons to whom the Software is
// furnished to do so, subject to the following conditions :
//
// The above copyright notice and this permission notice shall be included in
// all copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.IN NO EVENT SHALL THE
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
// THE SOFTWARE.
//
//
//  File Name:   Codec_GT.cpp
//  Description: implementation of the CCodec_GT class
//
//////////////////////////////////////////////////////////////////////////////

#pragma warning(disable : 4100)  // Ignore warnings of unreferenced formal parameters

#ifdef _WIN32
#include "common.h"
#include "codec_gt.h"
#include <process.h>
#include "debug.h"

#ifdef GT_COMPDEBUGGER
#include "compclient.h"
#endif

//======================================================================================
// #define USE_PRINTF
// #define USE_NOMULTITHREADING

#ifdef USE_FILEIO
#include <stdio.h>
FILE* gt_File       = NULL;
int   gt_blockcount = 0;
int   gt_total_MSE  = 0;
#endif

// Gets the total numver of active processor cores on the running host system
extern CMP_INT CMP_GetNumberOfProcessors();

//
// Thread procedure for encoding a block
//
// The thread stays alive, and expects blocks to be pushed to it by a producer
// process that signals it when new work is available. When the producer is finished
// it should set the exit flag in the parameters to allow the tread to quit
//

unsigned int _stdcall GTCThreadProcEncode(void* param)
{
    GTCEncodeThreadParam* tp = (GTCEncodeThreadParam*)param;

    while (tp->exit == FALSE)
    {
        if (tp->run == TRUE)
        {
            tp->encoder->CompressBlock(tp->in, tp->out);
            tp->run = false;
        }

        std::this_thread::sleep_for(std::chrono::milliseconds(0));
    }

    return 0;
}

static GTCEncodeThreadParam* g_EncodeParameterStorage = NULL;

//////////////////////////////////////////////////////////////////////////////
// Construction/Destruction
//////////////////////////////////////////////////////////////////////////////

CCodec_GTC::CCodec_GTC()
    : CCodec_DXTC(CT_GTC)
{
    m_LibraryInitialized = false;

    m_Use_MultiThreading   = true;
    m_NumThreads           = 0;
    m_NumEncodingThreads   = 0;
    m_EncodingThreadHandle = NULL;
    m_LiveThreads          = 0;
    m_LastThread           = 0;

    m_quality = 0.05f;
}

bool CCodec_GTC::SetParameter(const CMP_CHAR* pszParamName, CMP_CHAR* sValue)
{
    if (sValue == NULL)
        return false;

    if (strcmp(pszParamName, CodecParameters::NumThreads) == 0)
    {
        m_NumThreads         = (CMP_BYTE)std::stoi(sValue) & 0xFF;
        m_Use_MultiThreading = m_NumThreads != 1;
    }
    else if (strcmp(pszParamName, "Quality") == 0)
    {
        m_quality = std::stof(sValue);
        if ((m_quality < 0) || (m_quality > 1.0))
        {
            return false;
        }
    }
    else
        return CCodec_DXTC::SetParameter(pszParamName, sValue);
    return true;
}

bool CCodec_GTC::SetParameter(const CMP_CHAR* pszParamName, CMP_DWORD dwValue)
{
    if (strcmp(pszParamName, CodecParameters::NumThreads) == 0)
    {
        m_NumThreads         = (CMP_BYTE)dwValue;
        m_Use_MultiThreading = m_NumThreads != 1;
    }
    else
        return CCodec_DXTC::SetParameter(pszParamName, dwValue);
    return true;
}

bool CCodec_GTC::SetParameter(const CMP_CHAR* pszParamName, CODECFLOAT fValue)
{
    if (strcmp(pszParamName, "Quality") == 0)
        m_quality = fValue;
    else
        return CCodec_DXTC::SetParameter(pszParamName, fValue);

    return true;
}

CCodec_GTC::~CCodec_GTC()
{
    if (m_LibraryInitialized)
    {
        if (m_Use_MultiThreading)
        {
            // Tell all the live threads that they can exit when they have finished any current work
            for (int i = 0; i < m_LiveThreads; i++)
            {
                // If a thread is in the running state then we need to wait for it to finish
                // any queued work from the producer before we can tell it to exit.
                //
                // If we don't wait then there is a race condition here where we have
                // told the thread to run but it hasn't yet been scheduled - if we set
                // the exit flag before it runs then its block will not be processed.
#pragma warning(push)
#pragma warning(disable : 4127)  //warning C4127: conditional expression is constant
                while (1)
                {
                    if (m_EncodeParameterStorage[i].run != TRUE)
                    {
                        break;
                    }
                }
#pragma warning(pop)
                // Signal to the thread that it can exit
                m_EncodeParameterStorage[i].exit = TRUE;
            }

            // Now wait for all threads to have exited
            if (m_LiveThreads > 0)
            {
                for (CMP_DWORD dwThread = 0; dwThread < m_LiveThreads; dwThread++)
                {
                    std::thread& curThread = m_EncodingThreadHandle[dwThread];

                    curThread.join();
                }
            }

            for (unsigned int i = 0; i < m_LiveThreads; i++)
            {
                std::thread& curThread = m_EncodingThreadHandle[i];

                curThread = std::thread();
            }

        }  // MultiThreading

        m_EncodingThreadHandle = NULL;

        if (m_EncodeParameterStorage)
            delete[] m_EncodeParameterStorage;
        m_EncodeParameterStorage = NULL;

        for (int i = 0; i < m_NumEncodingThreads; i++)
        {
            if (m_encoder[i])
            {
                delete m_encoder[i];
                m_encoder[i] = NULL;
            }
        }

        if (m_decoder)
        {
            delete m_decoder;
            m_decoder = NULL;
        }

        m_LibraryInitialized = false;
    }
}

CodecError CCodec_GTC::InitializeGTCLibrary()
{
    if (!m_LibraryInitialized)
    {
        for (CMP_DWORD i = 0; i < MAX_GT_THREADS; i++)
        {
            m_encoder[i] = NULL;
        }

        // Create threaded encoder instances
        m_LiveThreads        = 0;
        m_LastThread         = 0;
        m_NumEncodingThreads = min(m_NumThreads, MAX_GT_THREADS);
        if (m_NumEncodingThreads == 0)
        {
            m_NumEncodingThreads = CMP_GetNumberOfProcessors();
            if (m_NumEncodingThreads <= 2)
                m_NumEncodingThreads = 8;  // fallback to a default!
            if (m_NumEncodingThreads > 128)
                m_NumEncodingThreads = 128;
        }
        m_Use_MultiThreading = (m_NumEncodingThreads != 1);

        m_EncodeParameterStorage = new GTCEncodeThreadParam[m_NumEncodingThreads];
        if (!m_EncodeParameterStorage)
        {
            return CE_Unknown;
        }

        m_EncodingThreadHandle = new std::thread[m_NumEncodingThreads];
        if (!m_EncodingThreadHandle)
        {
            delete[] m_EncodeParameterStorage;
            m_EncodeParameterStorage = NULL;

            return CE_Unknown;
        }

        CMP_INT i;

        for (i = 0; i < m_NumEncodingThreads; i++)
        {
            // Create single encoder instance
            m_encoder[i] = new GTCBlockEncoder(m_quality);

            // Cleanup if problem!
            if (!m_encoder[i])
            {
                delete[] m_EncodeParameterStorage;
                m_EncodeParameterStorage = NULL;

                delete[] m_EncodingThreadHandle;
                m_EncodingThreadHandle = NULL;

                for (CMP_INT j = 0; j < i; j++)
                {
                    delete m_encoder[j];
                    m_encoder[j] = NULL;
                }

                return CE_Unknown;
            }

#ifdef USE_DBGTRACE
            //DbgTrace(("Encoder[%d]:ModeMask %X, Quality %f",i,m_ModeMask,m_Quality));
#endif
        }

        // Create the encoding threads in the suspended state
        for (i = 0; i < m_NumEncodingThreads; i++)
        {
            // Initialize thread parameters.
            m_EncodeParameterStorage[i].encoder = m_encoder[i];
            // Inform the thread that at the moment it doesn't have any work to do
            // but that it should wait for some and not exit
            m_EncodeParameterStorage[i].run  = FALSE;
            m_EncodeParameterStorage[i].exit = FALSE;

            m_EncodingThreadHandle[i] = std::thread(GTCThreadProcEncode, (void*)&m_EncodeParameterStorage[i]);
            m_LiveThreads++;
        }

        // Create single decoder instance
        m_decoder = new GTCBlockDecoder();
        if (!m_decoder)
        {
            for (CMP_INT j = 0; j < m_NumEncodingThreads; j++)
            {
                delete m_encoder[j];
                m_encoder[j] = NULL;
            }
            return CE_Unknown;
        }

        m_LibraryInitialized = true;
    }
    return CE_OK;
}

CodecError CCodec_GTC::EncodeGTCBlock(CMP_BYTE* in, CMP_BYTE* out)
{
#ifdef USE_NOMULTITHREADING
    m_Use_MultiThreading = false;
#endif

    if (m_Use_MultiThreading)
    {
        CMP_WORD threadIndex;

        // Loop and look for an available thread
        CMP_BOOL found = FALSE;
        threadIndex    = m_LastThread;
        while (found == FALSE)
        {
            if (m_EncodeParameterStorage == NULL)
                return CE_Unknown;

            if (m_EncodeParameterStorage[threadIndex].run == FALSE)
            {
                found = TRUE;
                break;
            }

            // Increment and wrap the thread index
            threadIndex++;
            if (threadIndex == m_LiveThreads)
            {
                threadIndex = 0;
            }
        }

        m_LastThread = threadIndex;

        // Copy the input data into the thread storage
        memcpy(m_EncodeParameterStorage[threadIndex].in, in, m_xdim * m_ydim * 4 * sizeof(CMP_BYTE));

        // Set the output pointer for the thread to the provided location
        m_EncodeParameterStorage[threadIndex].out = out;

        // Tell the thread to start working
        m_EncodeParameterStorage[threadIndex].run = TRUE;
    }
    else
    {
        // Copy the input data into the thread storage
        memcpy(m_EncodeParameterStorage[0].in, in, m_xdim * m_ydim * 4 * sizeof(CMP_BYTE));

        // Set the output pointer for the thread to write
        m_EncodeParameterStorage[0].out = out;
        m_encoder[0]->CompressBlock(m_EncodeParameterStorage[0].in, m_EncodeParameterStorage[0].out);
    }
    return CE_OK;
}

CodecError CCodec_GTC::FinishGTCEncoding(void)
{
    if (!m_LibraryInitialized)
    {
        return CE_Unknown;
    }

    if (!m_EncodeParameterStorage)
    {
        return CE_Unknown;
    }

    if (m_Use_MultiThreading)
    {
        // Wait for all the live threads to finish any current work
        for (CMP_DWORD i = 0; i < m_LiveThreads; i++)
        {
            // If a thread is in the running state then we need to wait for it to finish
            // its work from the producer
            while (m_EncodeParameterStorage[i].run == TRUE)
            {
                std::this_thread::sleep_for(std::chrono::milliseconds(1));
            }
        }
    }
    return CE_OK;
}

CodecError CCodec_GTC::Compress(CCodecBuffer& bufferIn, CCodecBuffer& bufferOut, Codec_Feedback_Proc pFeedbackProc, CMP_DWORD_PTR pUser1, CMP_DWORD_PTR pUser2)
{
    CodecError err = InitializeGTCLibrary();
    if (err != CE_OK)
        return err;

    // Source image size
    int xsize = bufferIn.GetWidth();
    int ysize = bufferIn.GetHeight();
    int zsize = 1;  //todo: add depth to support 3d textures

    // Block sizes to partition the source data into for compression
    m_xdim = bufferOut.GetBlockWidth();
    m_ydim = bufferOut.GetBlockHeight();
    m_zdim = 1;

    CodecError result = CE_OK;
    int        xdim   = m_xdim;
    int        ydim   = m_ydim;

    g_GTCEncode.m_xdim = m_xdim;
    g_GTCEncode.m_ydim = m_ydim;
    g_GTCEncode.m_zdim = m_zdim;

    uint8_t* bufferOutput = bufferOut.GetData();

    int x, y, z;
    int xblocks = (xsize + m_xdim - 1) / m_xdim;
    int yblocks = (ysize + m_ydim - 1) / m_ydim;
    int zblocks = (zsize + m_zdim - 1) / m_zdim;
    int offset;
    int processingBlock = 0;

    float TotalBlocks = (float)(yblocks * xblocks);

    CMP_BYTE* srcBlock = (CMP_BYTE*)malloc(xdim * ydim * 4);

    for (z = 0; z < zblocks; z++)
    {
        for (y = 0; y < yblocks; y++)
        {
            for (x = 0; x < xblocks; x++)
            {
                processingBlock++;

                // Output block size for GTC is fixed at 16 bytes
                offset      = ((z * yblocks + y) * xblocks + x) * 16;
                uint8_t* bp = bufferOutput + offset;
                memset(srcBlock, 0, sizeof(srcBlock));
                bufferIn.ReadBlockRGBA(x * m_xdim, y * m_ydim, (CMP_BYTE)m_xdim, (CMP_BYTE)m_ydim, srcBlock);

                EncodeGTCBlock(srcBlock, bp);
            }

            if (pFeedbackProc)
            {
                if ((processingBlock % 10) == 0)
                {
                    float fProgress = 100.f * ((float)(processingBlock) / TotalBlocks);
                    if (pFeedbackProc(fProgress, pUser1, pUser2))
                    {
                        result = CE_Aborted;
                        break;
                    }
                }
            }
        }
    }

    free(srcBlock);

    CodecError EncodeResult = FinishGTCEncoding();

    if (result != CE_Aborted)
        result = EncodeResult;

    return result;
}

#ifdef USE_FILEIO_DECODE
FILE* gt_File_Decode = NULL;
char  ModesUsed[CMP_MAXGTMODES + 1];
#endif

CodecError CCodec_GTC::Decompress(CCodecBuffer&       bufferIn,
                                  CCodecBuffer&       bufferOut,
                                  Codec_Feedback_Proc pFeedbackProc,
                                  CMP_DWORD_PTR       pUser1,
                                  CMP_DWORD_PTR       pUser2)
{
    CodecError err = InitializeGTCLibrary();
    if (err != CE_OK)
        return err;

    m_xdim = bufferIn.GetBlockWidth();
    m_ydim = bufferIn.GetBlockHeight();
    m_zdim = 1;

    if (m_xdim == 0)
        m_xdim = 4;
    if (m_ydim == 0)
        m_ydim = 4;

    // Our Compressed data Blocks are always 128 bit long (4x4 blocks)
    const CMP_DWORD imageWidth  = bufferIn.GetWidth();
    const CMP_DWORD imageHeight = bufferIn.GetHeight();
    const CMP_DWORD imageDepth  = 1;
    const CMP_BYTE  bitness     = 8;

    const CMP_DWORD CompBlockX   = m_xdim;
    const CMP_DWORD CompBlockY   = m_ydim;
    CMP_BYTE        Block_Width  = (CMP_BYTE)m_xdim;
    CMP_BYTE        Block_Height = (CMP_BYTE)m_ydim;

    const CMP_DWORD dwBlocksX       = ((bufferIn.GetWidth() + (CompBlockX - 1)) / CompBlockX);
    const CMP_DWORD dwBlocksY       = ((bufferIn.GetHeight() + (CompBlockY - 1)) / CompBlockY);
    const CMP_DWORD dwBlocksZ       = 1;
    const CMP_DWORD dwBufferInDepth = 1;

    // Override the current input buffer Pitch size  (Since it will be set according to the Compressed Block Sizes
    // and not to the Compressed Codec data which is for GTC 16 Bytes per block x Number of blocks per row
    bufferIn.SetPitch(16 * dwBlocksX);

    // Output data size Pitch
    CMP_DWORD dwPitch = bufferOut.GetPitch();

    // Output Buffer
    CMP_BYTE* pDataOut = bufferOut.GetData();

    const CMP_DWORD dwBlocksXY = dwBlocksX * dwBlocksY;

    for (CMP_DWORD cmpRowY = 0; cmpRowY < dwBlocksY; cmpRowY++)
    {  // Compressed images row = height
        for (CMP_DWORD cmpColX = 0; cmpColX < dwBlocksX; cmpColX++)
        {  // Compressed images Col = width
            union FBLOCKS
            {
                CMP_BYTE decodedBlock[144][4];  // max 12x12 block size
                CMP_BYTE destBlock[576];        // max 12x12x4
            } DecData;

            union BBLOCKS
            {
                CMP_DWORD compressedBlock[4];
                CMP_BYTE  out[16];
                CMP_BYTE  in[16];
            } CompData;

            bufferIn.ReadBlock(cmpColX * 4, cmpRowY * 4, CompData.compressedBlock, 4);

            // Encode to the appropriate location in the compressed image
            m_decoder->DecompressBlock(DecData.decodedBlock, CompData.in);

            // Now that we have a decoded block lets copy that data over to the target image buffer
            CMP_DWORD outCol    = cmpColX * Block_Width;
            CMP_DWORD outRow    = cmpRowY * Block_Height;
            CMP_DWORD outImgRow = outRow;
            CMP_DWORD outImgCol = outCol;

            for (int row = 0; row < Block_Height; row++)
            {
                CMP_DWORD nextRowCol = (outRow + row) * dwPitch + (outCol * 4);
                CMP_BYTE* pData      = (CMP_BYTE*)(pDataOut + nextRowCol);
                if ((outImgRow + row) < imageHeight)
                {
                    outImgCol = outCol;
                    for (int col = 0; col < Block_Width; col++)
                    {
                        CMP_DWORD w = outImgCol + col;
                        if (w < imageWidth)
                        {
                            int index = row * Block_Width + col;
                            *pData++  = (CMP_BYTE)DecData.decodedBlock[index][BC_COMP_RED];
                            *pData++  = (CMP_BYTE)DecData.decodedBlock[index][BC_COMP_GREEN];
                            *pData++  = (CMP_BYTE)DecData.decodedBlock[index][BC_COMP_BLUE];
                            *pData++  = (CMP_BYTE)DecData.decodedBlock[index][BC_COMP_ALPHA];
                        }
                        else
                            break;
                    }
                }
            }
        }

        if (pFeedbackProc)
        {
            float fProgress = 100.f * (cmpRowY * dwBlocksX) / dwBlocksXY;
            if (pFeedbackProc(fProgress, pUser1, pUser2))
            {
                return CE_Aborted;
            }
        }
    }

    /**

        const CMP_DWORD dwBlocksX = ((bufferIn.GetWidth() + 3) >> 2);
        const CMP_DWORD dwBlocksY = ((bufferIn.GetHeight() + 3) >> 2);
        const CMP_DWORD dwBlocksXY = dwBlocksX*dwBlocksY;

    #ifdef USE_FILEIO_DECODE
        gt_File_Decode = fopen("gt_report_decode.txt","w");
        memset(ModesUsed,'.',CMP_MAXGTMODES);
    #endif

        for(CMP_DWORD j = 0; j < dwBlocksY; j++)
        {
            for(CMP_DWORD i = 0; i < dwBlocksX; i++)
            {

                union FBLOCKS
                {
                    CMP_BYTE decodedBlock[16][4];
                    CMP_BYTE destBlock[BLOCK_SIZE_4X4X4];
                } DecData;

                union BBLOCKS
                {
                    CMP_DWORD       compressedBlock[4];
                    CMP_BYTE            out[16];
                    CMP_BYTE            in[16];
                } CompData;

                CMP_BYTE destBlock[BLOCK_SIZE_4X4X4];


    #ifdef USE_FILEIO_DECODE
                // Mode Used
                int mode = CompData.in[CMP_GT_MODE];
                if (mode > CMP_MAXGTMODES)
                {
                    printf("Err mode = %d\n",mode);
                }
                else
                ModesUsed[mode] = 'x';
    #endif

                bufferIn.ReadBlock(i*4, j*4, CompData.compressedBlock, 4);

                // Encode to the appropriate location in the compressed image
                m_decoder->DecompressBlock(DecData.decodedBlock,CompData.in);

                // Create the block for decoding
                int srcIndex = 0;
                for(int row=0; row < BLOCK_SIZE_4; row++)
                {
                    for(int col=0; col<BLOCK_SIZE_4; col++)
                    {
                        destBlock[srcIndex]   = (CMP_BYTE)DecData.decodedBlock[row*BLOCK_SIZE_4+col][BC_COMP_RED];
                        destBlock[srcIndex+1] = (CMP_BYTE)DecData.decodedBlock[row*BLOCK_SIZE_4+col][BC_COMP_GREEN];
                        destBlock[srcIndex+2] = (CMP_BYTE)DecData.decodedBlock[row*BLOCK_SIZE_4+col][BC_COMP_BLUE];
                        destBlock[srcIndex+3] = (CMP_BYTE)DecData.decodedBlock[row*BLOCK_SIZE_4+col][BC_COMP_ALPHA];
                        srcIndex+=4;
                    }
                }

                bufferOut.WriteBlockRGBA(i*4, j*4, 4, 4, destBlock);

            }

            if (pFeedbackProc)
            {
                float fProgress = 100.f * (j * dwBlocksX) / dwBlocksXY;
                if (pFeedbackProc(fProgress, pUser1, pUser2))
                {
                    return CE_Aborted;
                }
            }
        }

    #ifdef USE_FILEIO_DECODE
        if (gt_File_Decode)
        {
            fprintf(gt_File_Decode, "Modes\n");
            int lf=0;
            for (int i=0; i<=CMP_MAXGTMODES; i++)
            {
                if (ModesUsed[i] =='x')
                {
                    lf++;
                    fprintf(gt_File_Decode,"%3d,",i);
                    if (lf == 10) {
                        fprintf(gt_File_Decode,"\n");
                        lf = 0;
                    }
                }
            }
            fprintf(gt_File_Decode, "\nModes Not used\n");
            lf = 0;
            for (int i = 0; i <= CMP_MAXGTMODES; i++)
            {
                if (ModesUsed[i] == '.')
                {
                    lf++;
                    fprintf(gt_File_Decode, "%3d,", i);
                    if (lf == 10) {
                        fprintf(gt_File_Decode, "\n");
                        lf = 0;
                    }
                }
            }

            fprintf(gt_File_Decode, "\nDone\n");
            fclose(gt_File_Decode);
            gt_File_Decode = NULL;
        }
    #endif
    ******************************************/

    return CE_OK;
}

// Not implemented
CodecError CCodec_GTC::Compress_Fast(CCodecBuffer&       bufferIn,
                                     CCodecBuffer&       bufferOut,
                                     Codec_Feedback_Proc pFeedbackProc,
                                     CMP_DWORD_PTR       pUser1,
                                     CMP_DWORD_PTR       pUser2)
{
    return CE_OK;
}

// Not implemented
CodecError CCodec_GTC::Compress_SuperFast(CCodecBuffer&       bufferIn,
                                          CCodecBuffer&       bufferOut,
                                          Codec_Feedback_Proc pFeedbackProc,
                                          CMP_DWORD_PTR       pUser1,
                                          CMP_DWORD_PTR       pUser2)
{
    return CE_OK;
}
#endif
