// 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.
//
#pragma once

#include <algorithm>
#include <utility>

#include <carb/Defines.h>
#include <carb/Framework.h>
#include <carb/streamclient/StreamClient.h>


namespace carb
{
namespace streamclient
{

/**
 * Defines the wrapper around streamclient interface and context.
 */
class StreamClientWrapper
{
public:
    /**
     *  Constructs an uninitilized wrapper object.
     */
    inline StreamClientWrapper(){};

    /**
     *  Deinitialize and destroys wrapper object.
     */
    virtual ~StreamClientWrapper()
    {
        done();
    };

    inline void swap(StreamClientWrapper& other)
    {
        std::swap(m_interface, other.m_interface);
        std::swap(m_context, other.m_context);
    }

    inline std::string resolvePath(const std::string& path, const std::string& root = "")
    {
        auto tokens = carb::getCachedInterface<carb::tokens::ITokens>();

        carb::extras::Path resultPath = carb::tokens::resolveString(tokens, path.c_str());

        // If relative - relative to the root
        if (!root.empty() && resultPath.isRelative())
        {
            resultPath = carb::extras::Path(root).join(resultPath);
        }
        return resultPath;
    }

public:
    /**
     * Initialize the wrapper instance
     *
     * @param[in] framework A pointer to a framework interface to use.
     * @param[in] graphics A pointer to a graphics interface to use.
     * @param[in] cudaInterop A pointer to a CUDA interop interface.
     * @param[in] device A pointer to a graphics device instance.
     * @return True if successfull.
     */
    bool init(carb::Framework* framework,
              carb::graphics::Graphics* graphics,
              carb::cudainterop::CudaInterop* cudaInterop,
              carb::graphics::Device* device)
    {
        {
            // Ensure we load the plugin
            const std::string gfxPath = resolvePath("${kit}/plugins/gpu.foundation");
            const char* paths[] = { gfxPath.c_str() };

            const char* plugins[] = { "carb.livestream.plugin" };
            loadPluginsFromPatterns(plugins, countOf(plugins), paths, countOf(paths));
        }

        m_interface = framework->acquireInterface<carb::streamclient::StreamClient>();
        if (!m_interface)
        {
            return false;
        }

        m_context = m_interface->createContext(graphics, cudaInterop, device);
        if (!m_context)
        {
            return false;
        }

        return true;
    }

    /**
     * Deinitialize the wrapper instance.
     */
    void done()
    {
        if (m_interface && m_context)
        {
            m_interface->stopClient(m_context);
            m_interface->destroyContext(m_context);
            m_context = nullptr;
        }
    }

    /**
     * Starts stream client.
     *
     * @param[in] addr A pointer to a server IP address.
     * @param[in] port A server port number.
     * @return true if client started and connected successfully.
     */
    bool start(const char* addr, uint16_t port)
    {
        if (m_interface && m_context)
        {
            return m_interface->startClient(m_context, addr, port);
        }
        return false;
    }

    /**
     * Stops stream client.
     */
    void stop()
    {
        if (m_interface && m_context)
        {
            m_interface->stopClient(m_context);
        }
    }

    /**
     * Pushes keyboard input event to a remote stream server.
     *
     * @param[in] evt A reference to the keyboard input event description.
     */
    void pushKeyboardEvent(const carb::input::KeyboardEvent& evt)
    {
        if (m_interface && m_context)
        {
            m_interface->pushKeyboardEvent(m_context, evt);
        }
    }

    /**
     * Pushes mouse input event to a remote stream server.
     *
     * @param[in] evt A reference to the keyboard input event description.
     */
    void pushMouseEvent(const carb::input::MouseEvent& evt)
    {
        if (m_interface && m_context)
        {
            m_interface->pushMouseEvent(m_context, evt);
        }
    }

    /**
     * Pushes gamepad input event to a remote stream server.
     *
     * @param[in] evt A reference to the mouse input event description.
     */
    void pushGamepadEvent(const carb::input::GamepadEvent& evt)
    {
        if (m_interface && m_context)
        {
            m_interface->pushGamepadEvent(m_context, evt);
        }
    }

    /**
     * Pushes window resize event to a remote stream server.
     *
     * @param[in] width New window width.
     * @param[in] height New window height.
     */
    void pushResizeEvent(uint32_t width, uint32_t height)
    {
        if (m_interface && m_context)
        {
            m_interface->pushResizeEvent(m_context, width, height);
        }
    }

    /**
     * Fetches video frame from streaming queue.
     *
     * @param[in] cmdList A pointer to the graphics command list to store readback and transformation commands.
     * @return A pointer to a video frame object or nullptr.
     */
    Frame* fetchFrame(carb::graphics::CommandList* cmdList)
    {
        if (m_interface && m_context && cmdList)
        {
            return m_interface->fetchFrame(m_context, cmdList);
        }
        return nullptr;
    }

    /**
     * Renders video frame with built-in tiny renderer.
     *
     * @param[in] cmdList A pointer to the graphics command list to store rendering commands.
     * @param[in] frame A pointer to the video frame object or nullptr.
     */
    void renderFrame(carb::graphics::CommandList* cmdList, Frame* frame)
    {
        if (m_interface && m_context && cmdList && frame)
        {
            m_interface->renderFrame(m_context, cmdList, frame);
        }
    }

    /**
     * Returns a pointer to texture object from a video frame object.
     *
     * @param[in] frame A pointer to a frame object obtained from fetchFrame call.
     * @return a pointer to texture object or nullptr.
     */
    carb::graphics::Texture* getFrameTexture(const Frame* frame)
    {
        if (m_interface && frame)
        {
            return m_interface->getFrameTexture(frame);
        }
        return nullptr;
    }

    /**
     * Returns a pointer to texture descriptor object from a video frame object.
     *
     * @param[in] frame A pointer to a frame object obtained from fetchFrame call.
     * @return a pointer to texture descriptor object or nullptr.
     */
    carb::graphics::Descriptor* getFrameDescriptor(const Frame* frame)
    {
        if (m_interface && frame)
        {
            return m_interface->getFrameDescriptor(frame);
        }
        return nullptr;
    }

    /**
     * Returns an extent of a video frame.
     *
     * @param[in] frame A pointer to a frame object obtained from fetchFrame call.
     * @return a pair of { width, height } for a given frame.
     */
    carb::Uint2 getFrameExtent(const Frame* frame)
    {
        if (m_interface && frame)
        {
            return m_interface->getFrameExtent(frame);
        }
        return { 0, 0 };
    }

private:
    StreamClientWrapper(const StreamClientWrapper&) = delete;
    StreamClientWrapper& operator=(const StreamClientWrapper&) = delete;

private:
    carb::streamclient::StreamClient* m_interface = nullptr;
    carb::streamclient::Context* m_context = nullptr;
};

} // namespace streamclient
} // namespace carb
