/*
 * Copyright (c) 2020, NVIDIA CORPORATION.  All rights reserved.
 *
 * NVIDIA CORPORATION and its licensors retain all intellectual property
 * and proprietary rights in and to this software, related documentation
 * and any modifications thereto.  Any use, reproduction, disclosure or
 * distribution of this software and related documentation without an express
 * license agreement from NVIDIA CORPORATION is strictly prohibited.
 */

// Standard header files
#include <iostream>
#include <memory>
#include <thread>
#include <mutex>

// SIPL header files
#include "NvSIPLCommon.hpp"
#include "NvSIPLCamera.hpp"
#include "NvSIPLClient.hpp"
#include "NvSIPLPipelineMgr.hpp"

// Sample application header files
#include "CUtils.hpp"
#include "CImageManager.hpp"

using namespace nvsipl;

#define SIPL_PIPELINE_ID (0U)
#define IMAGE_QUEUE_TIMEOUT_US (1000000U)
#define EVENT_QUEUE_TIMEOUT_US (1000000U)
#define INPUT_LINE_READ_SIZE (16U)

struct ThreadData {
    std::string threadName;
    INvSIPLFrameCompletionQueue *imageQueue;
    INvSIPLNotificationQueue *eventQueue;
    std::mutex *printMutex;
};

enum ThreadIndex {
    THREAD_INDEX_ICP = 0U,
    THREAD_INDEX_ISP0,
    THREAD_INDEX_ISP1,
    THREAD_INDEX_EVENT,
    THREAD_INDEX_COUNT
};

static PlatformCfg platformCfg = {
    .platform = "SF3324_CPHY_x4",
    .platformConfig = "SF3324_CPHY_x4",
    .description = "SF3324 module in 4 lane CPHY mode",
    .numDeviceBlocks = 1U,
    .deviceBlockList = {
        {
            .csiPort = NVMEDIA_IMAGE_CAPTURE_CSI_INTERFACE_TYPE_CSI_AB,
            .phyMode = NVMEDIA_ICP_CSI_CPHY_MODE,
            .i2cDevice = 0U,
            .deserInfo = {
                .name = "MAX96712",
#if !NV_IS_SAFETY
                .description = "Maxim 96712 Aggregator",
#endif // !NV_IS_SAFETY
                .i2cAddress = 0x29
            },
            .numCameraModules = 1U,
            .cameraModuleInfoList = {
                {
                    .name = "SF3324",
#if !NV_IS_SAFETY
                    .description = "Sekonix SF3324 module - 120-deg FOV, DVP AR0231-RCCB, MAX96705",
#endif // !NV_IS_SAFETY
                    .linkIndex = 0U,
                    .serInfo = {
                        .name = "MAX96705",
#if !NV_IS_SAFETY
                        .description = "Maxim 96705 Serializer",
#endif // !NV_IS_SAFETY
                        .i2cAddress = 0x40
                    },
                    .isEEPROMSupported = false,
                    .eepromInfo = {
                    },
                    .sensorInfo = {
                            .id = SIPL_PIPELINE_ID,
                            .name = "AR0231",
#if !NV_IS_SAFETY
                            .description = "OnSemi AR0231 Sensor",
#endif // !NV_IS_SAFETY
                            .i2cAddress = 0x10,
                            .vcInfo = {
                                    .cfa = NVM_SURF_ATTR_COMPONENT_ORDER_CRBC,
                                    .embeddedTopLines = 24U,
                                    .embeddedBottomLines = 4U,
                                    .inputFormat = NVMEDIA_IMAGE_CAPTURE_INPUT_FORMAT_TYPE_RAW12,
                                    .resolution = {
                                        .width = 1920U,
                                        .height = 1208U
                                    },
                                    .fps = 30.0,
                                    .isEmbeddedDataTypeEnabled = false
                            },
                            .isTriggerModeEnabled = true
                    }
                }
            }
        }
    }
};

static void ImageQueueThreadFunc(ThreadData *threadData)
{
    SIPLStatus status = NVSIPL_STATUS_OK;
    INvSIPLClient::INvSIPLBuffer *pBuffer = nullptr;
    INvSIPLClient::INvSIPLNvMBuffer *pNvMBuffer = nullptr;
    INvSIPLClient::ImageMetaData metadata;
    if (threadData == nullptr) {
        LOG_ERR("Invalid thread data\n");
        status = NVSIPL_STATUS_EOF;
    } else if (threadData->imageQueue == nullptr) {
        LOG_ERR("Invalid image queue\n");
        status = NVSIPL_STATUS_EOF;
    }
    while (status != NVSIPL_STATUS_EOF) {
        status = threadData->imageQueue->Get(pBuffer, IMAGE_QUEUE_TIMEOUT_US);
        if (status == NVSIPL_STATUS_OK) {
            pNvMBuffer = static_cast<INvSIPLClient::INvSIPLNvMBuffer *>(pBuffer);
            if (pNvMBuffer == nullptr) {
                LOG_ERR("Invalid buffer\n");
            } else {
                metadata = pNvMBuffer->GetImageData();
                if (!metadata.frameSeqNumInfo.frameSeqNumValid) {
                    LOG_ERR("Invalid frame sequence number\n");
                } else {
                    threadData->printMutex->lock();
                    std::cout << threadData->threadName << ": " \
                              << metadata.frameSeqNumInfo.frameSequenceNumber << std::endl;
                    threadData->printMutex->unlock();
                }
                status = pNvMBuffer->Release();
                if (status != NVSIPL_STATUS_OK) {
                    LOG_ERR("Buffer release failed\n");
                }
            }
        } else if (status == NVSIPL_STATUS_TIMED_OUT) {
            LOG_WARN("Queue timeout\n");
        } else if (status == NVSIPL_STATUS_EOF) {
            LOG_INFO("Queue shutdown\n");
        } else {
            LOG_ERR("Unexpected queue return status\n");
        }
    }

    return;
}

static void EventQueueThreadFunc(ThreadData *threadData)
{
    SIPLStatus status = NVSIPL_STATUS_OK;
    NvSIPLPipelineNotifier::NotificationData event;
    const char *eventName = nullptr;
    if (threadData == nullptr) {
        LOG_ERR("Invalid thread data\n");
        status = NVSIPL_STATUS_EOF;
    } else if (threadData->eventQueue == nullptr) {
        LOG_ERR("Invalid event queue\n");
        status = NVSIPL_STATUS_EOF;
    }
    while (status != NVSIPL_STATUS_EOF) {
        status = threadData->eventQueue->Get(event, EVENT_QUEUE_TIMEOUT_US);
        if (status == NVSIPL_STATUS_OK) {
            status = GetEventName(event, eventName);
            if ((status != NVSIPL_STATUS_OK) || (eventName == nullptr)) {
                LOG_ERR("Failed to get event name\n");
            } else {
                threadData->printMutex->lock();
                std::cout << threadData->threadName << ": " << eventName << std::endl;
                threadData->printMutex->unlock();
                eventName = nullptr;
            }
        } else if (status == NVSIPL_STATUS_TIMED_OUT) {
            LOG_WARN("Queue timeout\n");
        } else if (status == NVSIPL_STATUS_EOF) {
            LOG_INFO("Queue shutdown\n");
        } else {
            LOG_ERR("Unexpected queue return status\n");
        }
    }

    return;
}

static SIPLStatus SiplMain()
{
    SIPLStatus status = NVSIPL_STATUS_OK;
    NvSIPLPipelineConfiguration pipelineCfg = {
        .captureOutputRequested = true,
        .isp0OutputRequested = true,
        .isp1OutputRequested = true
    };
    NvSIPLPipelineQueues queues;
    CImageManager imageManager;
    std::vector<std::uint8_t> blob;
    bool defaultNitoLoaded = true;
    std::mutex threadPrintMutex;
    ThreadData threadDataStructs[THREAD_INDEX_COUNT];
    std::thread threads[THREAD_INDEX_COUNT];
    bool quit = false;

    std::unique_ptr<INvSIPLCamera> siplCamera = INvSIPLCamera::GetInstance();
    CHK_PTR_AND_RETURN(siplCamera, "INvSIPLCamera::GetInstance()");

    status = siplCamera->SetPlatformCfg(&platformCfg);
    CHK_STATUS_AND_RETURN(status, "INvSIPLCamera::SetPlatformCfg()");

    status = siplCamera->SetPipelineCfg(SIPL_PIPELINE_ID, pipelineCfg, queues);
    CHK_STATUS_AND_RETURN(status, "INvSIPLCamera::SetPipelineCfg()");

    status = siplCamera->Init();
    CHK_STATUS_AND_RETURN(status, "INvSIPLCamera::Init()");

    status = imageManager.Init(siplCamera.get(), SIPL_PIPELINE_ID);
    CHK_STATUS_AND_RETURN(status, "CImageManager::Init()");

    status = LoadNitoFile("/opt/nvidia/nvmedia/nit/", "sf3324", blob, defaultNitoLoaded);
    CHK_STATUS_AND_RETURN(status, "LoadNitoFile()");
    // The NVIDIA plugin relies on the module-specific NITO and does not work with default.nito
    if (defaultNitoLoaded) {
        LOG_ERR("Module-specific NITO file not found\n");
        return NVSIPL_STATUS_NOT_SUPPORTED;
    }

    status = siplCamera->RegisterAutoControlPlugin(SIPL_PIPELINE_ID, NV_PLUGIN, nullptr, blob);
    CHK_STATUS_AND_RETURN(status, "INvSIPLCamera::RegisterAutoControlPlugin()");

    for (uint32_t i = 0U; i < THREAD_INDEX_COUNT; i++) {
        void (*threadFunc)(ThreadData *) = nullptr;
        threadDataStructs[i].imageQueue = nullptr;
        threadDataStructs[i].eventQueue = nullptr;
        threadDataStructs[i].printMutex = &threadPrintMutex;
        switch (i) {
            case THREAD_INDEX_ICP:
                threadDataStructs[i].threadName = "ICP";
                threadDataStructs[i].imageQueue = queues.captureCompletionQueue;
                threadFunc = &ImageQueueThreadFunc;
                break;
            case THREAD_INDEX_ISP0:
                threadDataStructs[i].threadName = "ISP0";
                threadDataStructs[i].imageQueue = queues.isp0CompletionQueue;
                threadFunc = &ImageQueueThreadFunc;
                break;
            case THREAD_INDEX_ISP1:
                threadDataStructs[i].threadName = "ISP1";
                threadDataStructs[i].imageQueue = queues.isp1CompletionQueue;
                threadFunc = &ImageQueueThreadFunc;
                break;
            case THREAD_INDEX_EVENT:
                threadDataStructs[i].threadName = "Event";
                threadDataStructs[i].eventQueue = queues.notificationQueue;
                threadFunc = &EventQueueThreadFunc;
                break;
            default:
                LOG_ERR("Unexpected thread index\n");
                return NVSIPL_STATUS_ERROR;
        }
        threads[i] = std::thread(threadFunc, &threadDataStructs[i]);
    }

    status = siplCamera->Start();
    CHK_STATUS_AND_RETURN(status, "INvSIPLCamera::Start()");

    // Wait for the user's quit command
    while (!quit) {
        std::cout << "Enter 'q' to quit the application\n-\n";
        char line[INPUT_LINE_READ_SIZE];
        std::cin.getline(line, INPUT_LINE_READ_SIZE);
        if (line[0] == 'q') {
            quit = true;
        }
    }

    status = siplCamera->Stop();
    CHK_STATUS_AND_RETURN(status, "INvSIPLCamera::Stop()");

    status = siplCamera->Deinit();
    CHK_STATUS_AND_RETURN(status, "INvSIPLCamera::Deinit()");

    for (uint32_t i = 0U; i < THREAD_INDEX_COUNT; i++) {
        threads[i].join();
    }

    std::cout << "SUCCESS!" << std::endl;
    return status;
}

int main(int argc, char *argv[])
{
    SIPLStatus status = SiplMain();
    return static_cast<int>(status);
}
