/*
 * SPDX-FileCopyrightText: Copyright (c) 1993-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
 * SPDX-License-Identifier: Apache-2.0
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 * http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

//! \file sampleSafeMNISTInfer.cpp
//! \brief This file contains the implementation of the MNIST sample.
//!
//! It uses the prebuilt TensorRT engine to run inference on an input image of a digit.
//! It can be run with the following command line:
//! Command: ./sample_mnist_safe_infer

#include "NvInferSafeRuntime.h"
#include "safeCommon.h"
#include "safeErrorRecorder.h"

#include <algorithm>
#include <array>
#include <cassert>
#include <cmath>
#include <cuda_runtime_api.h>
#include <fstream>
#include <iostream>
#include <memory>
#include <numeric>
#include <random>
#include <string_view>
#include <thread>
#include <vector>

using namespace samplesSafeCommon;

namespace
{

//!
//! \brief Locate path to file by its filename. Will walk back MAX_DEPTH dirs from CWD to check for such a file path.
//!
std::string locateFile(std::string const& fileName, nvinfer2::safe::ISafeRecorder& recorder)
{
    constexpr uint32_t MAX_DEPTH{10U};
    std::array<std::string const, 2> const dirPatterns
        = {std::string{"data/samples/mnist/"}, std::string{"data/mnist/"}};
    std::string foundFile{};

    for (auto const& dir : dirPatterns)
    {
        std::string file{dir + fileName};
        bool found{false};
        for (uint32_t i = 0U; i < MAX_DEPTH; i++)
        {
            std::ifstream checkFile(file);
            found = checkFile.is_open();
            if (found)
            {
                break;
            }
            file = "../" + file; // Try again in parent dir.
        }
        if (found)
        {
            foundFile = file;
            break;
        }
    }

    if (foundFile.empty())
    {
        safeLogError(recorder, "Could not find " + fileName + " in data/samples/mnist/ or data/mnist.");
        safeLogError(recorder, "&&&& FAILED");
        exit(EXIT_FAILURE);
    }

    return foundFile;
}

//!
//! \brief Reads the input data, preprocesses, and stores the result in a managed buffer.
//!
bool processInput(void* input, int32_t const inputFileIdx, nvinfer2::safe::ISafeRecorder& recorder)
{
    std::stringstream ss;
    constexpr int32_t kINPUT_H{28};
    constexpr int32_t kINPUT_W{28};

    // Read the digit file according to the inputFileIdx.
    std::vector<uint8_t> fileData(kINPUT_H * kINPUT_W);
    readPGMFile(locateFile(std::to_string(inputFileIdx) + ".pgm", recorder), fileData.data(), kINPUT_H, kINPUT_W);

    // Print ASCII representation of digit.
    ss << "Input:\n";
    for (int32_t i = 0; i < kINPUT_H * kINPUT_W; i++)
    {
        ss << (" .:-=+*#%@"[fileData[i] / 26U]) << (((i + 1) % kINPUT_W) ? "" : "\n");
    }
    safeLogInfo(recorder, ss.str());

    float* hostInputBuffer = static_cast<float*>(input);
    std::copy(fileData.begin(), fileData.end(), hostInputBuffer);
    // Normalize to 0-1 with background at 0
    std::transform(hostInputBuffer, hostInputBuffer + kINPUT_H * kINPUT_W, hostInputBuffer,
        [](float v) -> float { return 1.0f - v / 255.0f; });

    return true;
}

//!
//! \brief Verifies that the output is correct and prints it.
//!
bool verifyOutput(void* output, int32_t groundTruthDigit, nvinfer2::safe::ISafeRecorder& recorder)
{
    float* prob = static_cast<float*>(output);

    // Print histogram of the output distribution.
    safeLogInfo(recorder, "Output:");
    float val{0.0f};
    int32_t idx{0};
    constexpr int32_t kDIGITS{10};

    // Calculate Softmax
    float sum{0.0f};
    for (int32_t i = 0; i < kDIGITS; ++i)
    {
        prob[i] = exp(prob[i]);
        sum += prob[i];
    }

    for (int32_t i = 0; i < kDIGITS; ++i)
    {
        std::stringstream ss;

        prob[i] /= sum;
        if (val < prob[i])
        {
            val = prob[i];
            idx = i;
        }

        ss << " Prob " << i << "  " << std::fixed << std::setw(5) << std::setprecision(4) << prob[i] << " Class " << i
           << ": " << std::string(int32_t(std::floor(prob[i] * 10 + 0.5f)), '*');
        safeLogInfo(recorder, ss.str());
    }

    return (idx == groundTruthDigit && val > 0.9f);
}

//!
//! \brief Loads the enginePlanFile from engineFile and returns it.
//!
std::vector<char> loadEnginePlanFile(std::string const& engineFile, int& size, nvinfer2::safe::ISafeRecorder& recorder)
{
    std::string const& filename = engineFile;
    std::vector<char> gieModelStream;
    std::ifstream file(filename, std::ios::binary);
    if (!file.good())
    {
        safeLogError(recorder, "Could not open input engine file or file is empty. File name: " + filename);
        return {};
    }
    file.seekg(0, std::ifstream::end);
    size = file.tellg();
    file.seekg(0, std::ifstream::beg);
    gieModelStream.resize(size);
    file.read(gieModelStream.data(), size);
    file.close();

    return gieModelStream;
}

//!
//! \brief Returns a random digit between 0 and 9
//!
int32_t getRandomDigit()
{
    std::random_device rd;
    std::default_random_engine generator{rd()};
    std::uniform_int_distribution<int32_t> distribution(0, 9);
    return distribution(generator);
}

//!
//! \brief Structure representing memory allocation for CUDA
//!
struct CudaMemory
{
    void* hostPtr = nullptr;
    void* devicePtr = nullptr;
    size_t size = 0;
};

//!
//! \brief Do inference
//!
void doInferenceThread(nvinfer2::safe::ITRTGraph* graph, int8_t& ret_status, nvinfer2::safe::ISafeRecorder* recorder)
{
    // Initialize to success; will be set to 0 on any error.
    ret_status = 1;

    int64_t nbIOs{};
    SAFE_API_CALL(graph->getNbIOTensors(nbIOs), *recorder);
    // This sample only has one input and one output.
    SAFE_ASSERT(nbIOs == 2);
    CudaMemory inputCudaMemory;
    CudaMemory outputCudaMemory;

    // Initialize main stream
    cudaStream_t stream;
    CUDA_CALL(cudaStreamCreateWithFlags(&stream, cudaStreamNonBlocking), *recorder);

    // Setup as many auxiliary streams as the graph requires - destroyed at scope end.
    auto auxStreamsDeleter = samplesSafeCommon::setUpAuxStreamsOn(*graph, *recorder);

    // Pick a random digit to try to infer.
    int32_t digit = getRandomDigit();

    // Iterate through all input/output tensors
    for (int64_t i = 0; i < nbIOs; ++i)
    {
        // Get the tensor name for the current I/O tensor
        char const* tensorName;
        SAFE_API_CALL(graph->getIOTensorName(tensorName, i), *recorder);
        // Get tensor descriptor which contains metadata like size and I/O mode
        nvinfer2::safe::TensorDescriptor desc;
        SAFE_API_CALL(graph->getIOTensorDescriptor(desc, tensorName), *recorder);

        // Allocate device and host memory for this tensor
        void* deviceBuf = nullptr;
        void* hostBuf = nullptr;
        CUDA_CALL(cudaMalloc(&deviceBuf, desc.sizeInBytes), *recorder);
        CUDA_CHECK(cudaHostAlloc(&hostBuf, desc.sizeInBytes, cudaHostAllocDefault));

        if (desc.ioMode == TensorIOMode::kINPUT)
        {
            // Read the input data into the managed buffers.
            processInput(hostBuf, digit, *recorder);

            // Asynchronously copy data from host input buffers to device input buffers.
            CUDA_CHECK(cudaMemcpyAsync(deviceBuf, hostBuf, desc.sizeInBytes, cudaMemcpyHostToDevice, stream));
            inputCudaMemory = {hostBuf, deviceBuf, desc.sizeInBytes};
        }
        else if (desc.ioMode == TensorIOMode::kOUTPUT)
        {
            CUDA_CALL(cudaMemsetAsync(deviceBuf, 0, desc.sizeInBytes, stream), *recorder);
            outputCudaMemory = {hostBuf, deviceBuf, desc.sizeInBytes};
        }
        else
        {
            safeLogError(*recorder, "Unexpected tensor IO mode");
            ret_status = 0;
        }
        SAFE_ASSERT(desc.dataType == DataType::kFLOAT);
        // Create a typed array for the tensor
        nvinfer2::safe::TypedArray tensor
            = nvinfer2::safe::TypedArray(static_cast<float*>(deviceBuf), desc.sizeInBytes);

        SAFE_API_CALL(graph->setIOTensorAddress(tensorName, tensor), *recorder);
    }

    cudaEvent_t inputConsumedEvent;
    cudaEventCreate(&inputConsumedEvent);
    SAFE_API_CALL(graph->setInputConsumedEvent(inputConsumedEvent), *recorder);

    // Run the graph
    SAFE_API_CALL(graph->executeAsync(stream), *recorder);

    cudaEvent_t retrievedEvent;
    SAFE_API_CALL(graph->getInputConsumedEvent(retrievedEvent), *recorder);
    SAFE_ASSERT(retrievedEvent != nullptr);
    cudaEventSynchronize(retrievedEvent);

    // Synchronize the network
    SAFE_API_CALL(graph->sync(), *recorder);

    // Asynchronously copy data from device output buffers to host output buffers.
    CUDA_CHECK(cudaMemcpyAsync(
        outputCudaMemory.hostPtr, outputCudaMemory.devicePtr, outputCudaMemory.size, cudaMemcpyDeviceToHost, stream));

    // Wait for the work in the stream to complete.
    CUDA_CHECK(cudaStreamSynchronize(stream));

    // Check and print the output of the inference.
    if (!verifyOutput(outputCudaMemory.hostPtr, digit, *recorder))
    {
        safeLogError(*recorder, "Failed to verify output");
        ret_status = 0;
    }

    // Release stream and buffers.
    CUDA_CHECK(cudaStreamDestroy(stream));
    CUDA_CHECK(cudaFreeHost(inputCudaMemory.hostPtr));
    CUDA_CHECK(cudaFreeHost(outputCudaMemory.hostPtr));
    CUDA_CHECK(cudaFree(inputCudaMemory.devicePtr));
    CUDA_CHECK(cudaFree(outputCudaMemory.devicePtr));
}

//!
//! \brief The SampleSafeMNISTInferArgs struct stores the additional arguments required by the sample
//!
struct SampleSafeMNISTInferArgs
{
    std::string engineFileName{"safe_mnist.engine"};
    int32_t threads{1};
    bool help{false};
};

//!
//! \brief Runs the TensorRT inference engine for this sample.
//!
//! \details This function is the main execution function of the sample. It allocates
//!          the buffer, sets inputs, executes the engine, and verifies the output.
//!
bool doInference(SampleSafeMNISTInferArgs const& args)
{
    int32_t const nbThreads = args.threads;
    std::vector<int8_t> ret_status(nbThreads);
    std::vector<std::unique_ptr<sample::SampleSafeRecorder>> recorders(nbThreads);
    for (int32_t i = 0; i < nbThreads; ++i)
    {
        recorders[i] = std::make_unique<sample::SampleSafeRecorder>(nvinfer2::safe::Severity::kINFO, i);
    }
    // Load safe engine blob
    int32_t engineFileSize = 0;
    auto gieModelStream = loadEnginePlanFile(args.engineFileName, engineFileSize, *recorders[0]);
    SAFE_ASSERT(engineFileSize != 0);

    // Configure executor(s)
    std::vector<nvinfer2::safe::ITRTGraph*> graphs(nbThreads);
    SAFE_API_CALL(nvinfer2::safe::createTRTGraph(graphs[0], gieModelStream.data(), engineFileSize, *recorders[0], true),
        *recorders[0]);

    for (int32_t i = 1; i < nbThreads; ++i)
    {
        SAFE_API_CALL(graphs[0]->clone(graphs[i], *recorders[i]), *recorders[0]);
    }

    // Run the graphs in independent threads
    std::vector<std::thread> threads(nbThreads);
    for (int32_t i = 0; i < nbThreads; ++i)
    {
        threads[i] = std::thread{doInferenceThread, graphs[i], std::ref(ret_status[i]), recorders[i].get()};
    }

    for (int32_t i = 0; i < nbThreads; ++i)
    {
        threads[i].join();
        if (!ret_status[i])
        {
            return false;
        }
    }

    for (int32_t i = 0; i < nbThreads; ++i)
    {
        SAFE_API_CALL(nvinfer2::safe::destroyTRTGraph(graphs[i]), *recorders[i]);
        graphs[i] = nullptr;
    }

    return true;
}

//!
//! \brief This function parses arguments specific to the sample
//!
bool parseSampleSafeMNISTInferArgs(SampleSafeMNISTInferArgs& args, int32_t argc, char* argv[])
{
    for (int32_t i = 1; i < argc; ++i)
    {
        std::string const arg = argv[i];
        if (auto const value = parseString(arg, "loadEngine"))
        {
            args.engineFileName = *value;
        }
        else if (auto const value = parseString(arg, "threads"))
        {
            args.threads = std::stoi(*value);
            if (args.threads <= 0)
            {
                SAFE_LOG << "Number of threads must be > 0, got: " << arg << "\n";
                return false;
            }
        }
        else if (parseBool(arg, "help", 'h'))
        {
            args.help = true;
        }
        else
        {
            SAFE_LOG << "Invalid Argument: " << arg << "\n";
            return false;
        }
    }
    return true;
}

//!
//! \brief Prints the help information for running this sample.
//!
void printHelpInfo()
{
    SampleSafeMNISTInferArgs const defArgs{};
    std::cout << R"(Usage: sample_mnist_safe_infer [options]
Options:
  --help, -h          Print this message and exit.
  --loadEngine=FILE   Load serialized engine from FILE (default = )"
              << defArgs.engineFileName << R"().
  --threads=N         Run inference in N threads concurrently (default = )"
              << defArgs.threads << R"().
)";
}
} // namespace

int32_t main(int32_t argc, char** argv)
{
    safetyCompliance::setPromgrAbility();
    SampleSafeMNISTInferArgs args;
    bool argsOK = parseSampleSafeMNISTInferArgs(args, argc, argv);
    if (!argsOK)
    {
        printHelpInfo();
        return EXIT_FAILURE;
    }
    if (args.help)
    {
        printHelpInfo();
        return EXIT_SUCCESS;
    }

    // Initialize SafeCuda before any other Cuda APIs are called. This may be skipped if createInferRuntime() is called
    // first as per DEEPLRN_RES_116
    safetyCompliance::initSafeCuda();

    if (!isSmSafe())
    {
        SAFE_LOG << "Skip safe mode test on unsupported platforms." << std::endl;
        return EXIT_SUCCESS;
    }

    TestResult result = doInference(args) ? TestResult::kPASSED : TestResult::kFAILED;
    reportTestResult("TensorRT.sample_mnist_safe_infer", result, argc, argv);

    return EXIT_SUCCESS;
}
