/* Copyright (c) 2020, NVIDIA CORPORATION. All rights reserved.
 *
 * Redistribution and use in source and binary forms, with or without
 * modification, are permitted provided that the following conditions
 * are met:
 *  * Redistributions of source code must retain the above copyright
 *    notice, this list of conditions and the following disclaimer.
 *  * Redistributions in binary form must reproduce the above copyright
 *    notice, this list of conditions and the following disclaimer in the
 *    documentation and/or other materials provided with the distribution.
 *  * Neither the name of NVIDIA CORPORATION nor the names of its
 *    contributors may be used to endorse or promote products derived
 *    from this software without specific prior written permission.
 *
 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS ``AS IS'' AND ANY
 * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
 * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
 * PURPOSE ARE DISCLAIMED.  IN NO EVENT SHALL THE COPYRIGHT OWNER OR
 * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
 * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
 * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
 * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY
 * OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
 * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
 * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
 */

#include <string.h>
#include <iostream>
/* Nvidia headers */
#include "nvmedia_utils/cmdline.h"
#include "nvmedia_image.h"
#include "nvmedia_2d.h"
#include "nvmedia_surface.h"
#include "nvmedia_utils/image_utils.h"
#include "nvmedia_image_nvscibuf.h"
#include "nvmedia_producer.h"
#include "nvmedia_2d_nvscisync.h"
#include "nvsci_setup.h"

NvMediaImage *
NvMediaImageCreateUsingNvScibuf(
    NvMediaDevice *device,
    NvMediaSurfaceType type,
    const NvMediaSurfAllocAttr *attrs,
    uint32_t numAttrs,
    uint32_t flags,
    NvSciBufObj &bufobj,
    int cudaDeviceId)
{
    NvSciBufModule module = NULL;
    NvSciError err = NvSciError_Success;
    NvMediaStatus status = NVMEDIA_STATUS_OK;
    NvSciBufAttrList attrlist = NULL;
    NvSciBufAttrList conflictlist = NULL;
    NvSciBufAttrValAccessPerm access_perm = NvSciBufAccessPerm_ReadWrite;
    NvSciBufAttrKeyValuePair attr_kvp = {NvSciBufGeneralAttrKey_RequiredPerm,
                                         &access_perm,
                                         sizeof(access_perm)};
    NvSciBufAttrKeyValuePair pairArrayOut[10];

    NvMediaImage *image = NULL;

    err = NvSciBufModuleOpen(&module);
    if(err != NvSciError_Success) {
        printf("%s: NvSciBuffModuleOpen failed. Error: %d \n", __func__, err);
        goto fail_cleanup;
    }

    err = NvSciBufAttrListCreate(module, &attrlist);
    if(err != NvSciError_Success) {
        printf("%s: SciBufAttrListCreate failed. Error: %d \n", __func__, err);
        goto fail_cleanup;
    }

    err = NvSciBufAttrListSetAttrs(attrlist, &attr_kvp, 1);
    if(err != NvSciError_Success) {
        printf("%s: AccessPermSetAttr failed. Error: %d \n", __func__, err);
        goto fail_cleanup;
    }

    status = NvMediaImageFillNvSciBufAttrs(device,
                                           type,
                                           attrs,
                                           numAttrs,
                                           0,
                                           attrlist);


    if(status != NVMEDIA_STATUS_OK) {
        printf("%s: ImageFillSciBufAttrs failed. Error: %d \n", __func__, err);
        goto fail_cleanup;
    }

    setupNvSciBuf(bufobj, attrlist, cudaDeviceId);

    status = NvMediaImageCreateFromNvSciBuf(device,
                                            bufobj,
                                            &image);

    if(status != NVMEDIA_STATUS_OK) {
        printf("%s: ImageCreatefromSciBuf failed. Error: %d \n", __func__, err);
        goto fail_cleanup;
    }

    NvSciBufAttrListFree(attrlist);

    if(module != NULL) {
        NvSciBufModuleClose(module);
    }

    return image;

fail_cleanup:
    if(attrlist != NULL) {
        NvSciBufAttrListFree(attrlist);
    }
    if(bufobj != NULL) {
        NvSciBufObjFree(bufobj);
        bufobj = NULL;
    }

    if(module != NULL) {
        NvSciBufModuleClose(module);
    }
    NvMediaImageDestroy(image);
    return NULL;
}

/* Create NvMediaImage surface based on the input attributes.
 * Returns NVMEDIA_STATUS_OK on success
 */
static NvMediaStatus
createSurface(Blit2DTest *ctx,
              NvMediaSurfFormatAttr *surfFormatAttrs,
              NvMediaSurfAllocAttr *surfAllocAttrs,
              uint32_t numSurfAllocAttrs,
              NvMediaImage **image,
              NvSciBufObj &bufObj,
              int cudaDeviceId)
{
    NvMediaSurfaceType surfType;

    /* create source image */
    surfType = NvMediaSurfaceFormatGetType(surfFormatAttrs, NVM_SURF_FMT_ATTR_MAX);
    *image = NvMediaImageCreateUsingNvScibuf(ctx->device,             /* device */
                                             surfType,                /* surface type */
                                             surfAllocAttrs,
                                             numSurfAllocAttrs,
                                             0,
                                             bufObj,
                                             cudaDeviceId);

    if(*image == NULL) {
        printf ("Unable to create image\n");
        return NVMEDIA_STATUS_ERROR;
    }
    InitImage (*image, surfAllocAttrs[0].value, surfAllocAttrs[1].value);

/*    printf("%s: NvMediaImageCreate:: Image size: %ux%u Image type: %d\n",
            __func__, surfAllocAttrs[0].value, surfAllocAttrs[1].value, surfType);*/

    return NVMEDIA_STATUS_OK;
}

/* Create NvMediaImage surface based on the input attributes.
 * Returns NVMEDIA_STATUS_OK on success
 */
static NvMediaStatus
createSurfaceNonNvSCI(Blit2DTest *ctx,
              NvMediaSurfFormatAttr *surfFormatAttrs,
              NvMediaSurfAllocAttr *surfAllocAttrs,
              uint32_t numSurfAllocAttrs,
              NvMediaImage **image)
{
    NvMediaSurfaceType surfType;

    /* create source image */
    surfType = NvMediaSurfaceFormatGetType(surfFormatAttrs, NVM_SURF_FMT_ATTR_MAX);

    *image = NvMediaImageCreateNew(ctx->device, surfType, surfAllocAttrs, numSurfAllocAttrs, 0);

    if(*image == NULL) {
        printf ("Unable to create image\n");
        return NVMEDIA_STATUS_ERROR;
    }
    InitImage (*image, surfAllocAttrs[0].value, surfAllocAttrs[1].value);

/*    printf("%s: NvMediaImageCreate:: Image size: %ux%u Image type: %d\n",
            __func__, surfAllocAttrs[0].value, surfAllocAttrs[1].value, surfType);*/

    return NVMEDIA_STATUS_OK;
}


static void destroySurface(NvMediaImage *image)
{
    NvMediaImageDestroy(image);
}


static NvMediaStatus blit2DImage(Blit2DTest *ctx, TestArgs* args, NvSciSyncObj &nvMediaSignalerSyncObj,
                                NvSciSyncFence *preSyncFence, NvSciSyncFence *fence)
{
    NvMediaStatus status;
    NvMediaImageSurfaceMap surfaceMap;

    status = ReadImage(args->inputFileName,                         /* fileName */
                       0,                                           /* frameNum */
                       args->srcSurfAllocAttrs[0].value,            /* source image width */
                       args->srcSurfAllocAttrs[1].value,            /* source image height */
                       ctx->srcImage,                               /* srcImage */
                       NVMEDIA_TRUE,                                /* uvOrderFlag */
                       1,                                           /* bytesPerPixel */
                       MSB_ALIGNED);                                /* pixelAlignment */

    if (status != NVMEDIA_STATUS_OK) {
        printf("%s: ReadImage failed for input buffer: %d\n", __func__, status);
        return status;
    }

    if ((args->srcRect.x1 <= args->srcRect.x0) || (args->srcRect.y1 <= args->srcRect.y0)) {
        ctx->srcRect = NULL;
    } else {
        ctx->srcRect = &(args->srcRect);
    }

    if ((args->dstRect.x1 <= args->dstRect.x0) || (args->dstRect.y1 <= args->dstRect.y0)) {
        ctx->dstRect = NULL;
    } else {
        ctx->dstRect = &(args->dstRect);
    }

    static int64_t launch = 0;
    // Start inserting pre-fence from second launch inorder to for NvMedia2Blit to wait
    // for cuda signal on fence.
    if (launch)
    {
        status = NvMedia2DInsertPreNvSciSyncFence(ctx->i2d, preSyncFence);
        if(status != NVMEDIA_STATUS_OK) {
          printf("%s: NvMedia2DSetNvSciSyncObjforEOF   failed: %d\n", __func__, status);
          return status;
        }
        NvSciSyncFenceClear(preSyncFence);
    }
    launch++;

    status = NvMedia2DSetNvSciSyncObjforEOF(ctx->i2d, nvMediaSignalerSyncObj);
    if(status != NVMEDIA_STATUS_OK) {
        printf("%s: NvMedia2DSetNvSciSyncObjforEOF   failed: %d\n", __func__, status);
        return status;
    }

    /* 2DBlit processing on input image */
    status = NvMedia2DBlitEx(ctx->i2d,           /* i2d */
                             ctx->dstImage,      /* dstSurface */
                             ctx->dstRect,       /* dstRect */
                             ctx->srcImage,      /* srcSurface */
                             ctx->srcRect,       /* srcRect */
                             &args->blitParams,   /* params */
                             NULL);              /* paramsOut */

    if(status != NVMEDIA_STATUS_OK) {
        printf("%s: NvMedia2DBlitEx failed: %d\n", __func__, status);
        return status;
    }

    status = NvMedia2DGetEOFNvSciSyncFence(ctx->i2d, nvMediaSignalerSyncObj, fence);
    if(status != NVMEDIA_STATUS_OK) {
        printf("%s: NvMedia2DGetEOFNvSciSyncFence failed: %d\n", __func__, status);
        return status;
    }

    return NVMEDIA_STATUS_OK;
}

static NvMediaStatus blit2DImageNonNvSCI(Blit2DTest *ctx, TestArgs* args)
{
    NvMediaStatus status;
    NvMediaImageSurfaceMap surfaceMap;

    status = ReadImage(args->inputFileName,                         /* fileName */
                       0,                                           /* frameNum */
                       args->srcSurfAllocAttrs[0].value,            /* source image width */
                       args->srcSurfAllocAttrs[1].value,            /* source image height */
                       ctx->srcImage,                               /* srcImage */
                       NVMEDIA_TRUE,                                /* uvOrderFlag */
                       1,                                           /* bytesPerPixel */
                       MSB_ALIGNED);                                /* pixelAlignment */

    if (status != NVMEDIA_STATUS_OK) {
        printf("%s: ReadImage failed for input buffer: %d\n", __func__, status);
        return status;
    }

    if ((args->srcRect.x1 <= args->srcRect.x0) || (args->srcRect.y1 <= args->srcRect.y0)) {
        ctx->srcRect = NULL;
    } else {
        ctx->srcRect = &(args->srcRect);
    }

    if ((args->dstRect.x1 <= args->dstRect.x0) || (args->dstRect.y1 <= args->dstRect.y0)) {
        ctx->dstRect = NULL;
    } else {
        ctx->dstRect = &(args->dstRect);
    }

    /* 2DBlit processing on input image */
    status = NvMedia2DBlitEx(ctx->i2d,           /* i2d */
                             ctx->dstImage,      /* dstSurface */
                             ctx->dstRect,       /* dstRect */
                             ctx->srcImage,      /* srcSurface */
                             ctx->srcRect,       /* srcRect */
                             &args->blitParams,   /* params */
                             NULL);              /* paramsOut */
    if(status != NVMEDIA_STATUS_OK) {
        printf("%s: NvMedia2DBlitEx failed: %d\n", __func__, status);
        return status;
    }

    /* Write output image into buffer */
    ctx->bytesPerPixel = 1;
    WriteImageToAllocatedBuffer(ctx, ctx->dstImage,
                              NVMEDIA_TRUE,
                              NVMEDIA_FALSE,
                              ctx->bytesPerPixel);

    return NVMEDIA_STATUS_OK;
}

static void cleanup(Blit2DTest* ctx, NvMediaStatus status = NVMEDIA_STATUS_OK)
{
    if (ctx->srcImage != NULL) {
        NvMedia2DImageUnRegister(ctx->i2d, ctx->srcImage);
        destroySurface(ctx->srcImage);
    }
    if (ctx->dstImage != NULL) {
        NvMedia2DImageUnRegister(ctx->i2d, ctx->dstImage);
        destroySurface(ctx->dstImage);
    }
    if (status != NVMEDIA_STATUS_OK) {
        exit(EXIT_FAILURE);
    }
}

void cleanupNvMedia(Blit2DTest* ctx, NvSciSyncObj &syncObj, NvSciSyncObj &preSyncObj)
{
    NvMediaStatus status;
    cleanup(ctx);
    status = NvMedia2DUnregisterNvSciSyncObj(ctx->i2d, syncObj);
    if(status != NVMEDIA_STATUS_OK) {
        printf("%s: NvMediaImageSciBufInit failed\n",__func__);
        exit(EXIT_FAILURE);
    }
    status = NvMedia2DUnregisterNvSciSyncObj(ctx->i2d, preSyncObj);
    if(status != NVMEDIA_STATUS_OK) {
        printf("%s: NvMediaImageSciBufInit failed\n",__func__);
        exit(EXIT_FAILURE);
    }
    NvMediaImageNvSciBufDeinit();
}

void cleanupNvMedia(Blit2DTest* ctx)
{
    cleanup(ctx);
    free(ctx->dstBuffPitches);
    free(ctx->dstBuffer);
    free(ctx->dstBuff);
}

void setupNvMedia(TestArgs* args, Blit2DTest* ctx, NvSciBufObj &srcNvSciBufobj,
                  NvSciBufObj& dstNvSciBufobj, NvSciSyncObj &syncObj, NvSciSyncObj &preSyncObj,
                  int cudaDeviceId)
{
    NvMediaStatus status;
    status = NvMediaImageNvSciBufInit();
    if(status != NVMEDIA_STATUS_OK) {
        printf("%s: NvMediaImageSciBufInit failed\n",__func__);
        cleanup(ctx, status);
    }

    // Create source surface
    status = createSurface(ctx,
                           args->srcSurfFormatAttrs,
                           args->srcSurfAllocAttrs,
                           args->numSurfAllocAttrs,
                           &ctx->srcImage,
                           srcNvSciBufobj,
                           cudaDeviceId);
    if(status != NVMEDIA_STATUS_OK) {
        printf("%s: Unable to create buffer pools\n", __func__);
        cleanup(ctx, status);
    }

    // Create destination surface
    status = createSurface(ctx,
                           args->dstSurfFormatAttrs,
                           args->dstSurfAllocAttrs,
                           args->numSurfAllocAttrs,
                           &ctx->dstImage,
                           dstNvSciBufobj, 
                           cudaDeviceId);
    if(status != NVMEDIA_STATUS_OK) {
        printf("%s: Unable to create buffer pools\n", __func__);
        cleanup(ctx, status);
    }

    //Register source  Surface
    status = NvMedia2DImageRegister(ctx->i2d, ctx->srcImage, NVMEDIA_ACCESS_MODE_READ);
    if ( status != NVMEDIA_STATUS_OK) {
        printf("%s: Unable to register source surface\n", __func__);
        cleanup(ctx, status);
    }
    //Register destination Surface
    status = NvMedia2DImageRegister(ctx->i2d, ctx->dstImage, NVMEDIA_ACCESS_MODE_READ_WRITE);
    if ( status != NVMEDIA_STATUS_OK) {
        printf("%s: Unable to register destination surface\n", __func__);
        cleanup(ctx, status);
    }

    status = NvMedia2DRegisterNvSciSyncObj(ctx->i2d, NVMEDIA_EOFSYNCOBJ, syncObj);
    if (status != NVMEDIA_STATUS_OK) {
        printf("%s: Unable to NvMedia2DRegisterNvSciSyncObj\n", __func__);
    }

    status = NvMedia2DRegisterNvSciSyncObj(ctx->i2d, NVMEDIA_PRESYNCOBJ, preSyncObj);
    if (status != NVMEDIA_STATUS_OK) {
        printf("%s: Unable to NvMedia2DRegisterNvSciSyncObj\n", __func__);
    }
}

// Create NvMedia src & dst image without NvSciBuf
void setupNvMedia(TestArgs* args, Blit2DTest* ctx)
{
    NvMediaStatus status;

    // Create source surface
    status = createSurfaceNonNvSCI(ctx,
                           args->srcSurfFormatAttrs,
                           args->srcSurfAllocAttrs,
                           args->numSurfAllocAttrs,
                           &ctx->srcImage);
    if(status != NVMEDIA_STATUS_OK) {
        printf("%s: Unable to create buffer pools\n", __func__);
        cleanup(ctx, status);
    }

    // Create destination surface
    status = createSurfaceNonNvSCI(ctx,
                           args->dstSurfFormatAttrs,
                           args->dstSurfAllocAttrs,
                           args->numSurfAllocAttrs,
                           &ctx->dstImage);
    if(status != NVMEDIA_STATUS_OK) {
        printf("%s: Unable to create buffer pools\n", __func__);
        cleanup(ctx, status);
    }

    //Register source  Surface
    status = NvMedia2DImageRegister(ctx->i2d, ctx->srcImage, NVMEDIA_ACCESS_MODE_READ);
    if ( status != NVMEDIA_STATUS_OK) {
        printf("%s: Unable to register source surface\n", __func__);
        cleanup(ctx, status);
    }

    //Register destination Surface
    status = NvMedia2DImageRegister(ctx->i2d, ctx->dstImage, NVMEDIA_ACCESS_MODE_READ_WRITE);
    if ( status != NVMEDIA_STATUS_OK) {
        printf("%s: Unable to register destination surface\n", __func__);
        cleanup(ctx, status);
    }

    // Allocate buffer for writing image & set image parameters in Blit2DTest.
    ctx->bytesPerPixel = 1;
    AllocateBufferToWriteImage(ctx, 
                              ctx->dstImage,
                              NVMEDIA_TRUE,         /* uvOrderFlag */
                              NVMEDIA_FALSE);       /* appendFlag */
}

void runNvMediaBlit2D(TestArgs* args, Blit2DTest* ctx)
{
    // Blit2D function
    NvMediaStatus status = blit2DImageNonNvSCI(ctx, args);
    if(status != NVMEDIA_STATUS_OK) {
        printf("%s: Blit2D failed\n", __func__);
        cleanup(ctx, status);
    }
}

void runNvMediaBlit2D(TestArgs* args, Blit2DTest* ctx, NvSciSyncObj &nvMediaSignalerSyncObj, 
                      NvSciSyncFence *preSyncFence, NvSciSyncFence *fence)
{
     // Blit2D function
    NvMediaStatus status = blit2DImage(ctx, args, nvMediaSignalerSyncObj, preSyncFence, fence);
    if(status != NVMEDIA_STATUS_OK) {
        printf("%s: Blit2D failed\n", __func__);
        cleanup(ctx, status);
    }
}
