/*
 *  Copyright (C) 2017-2018 Team Kodi
 *  This file is part of Kodi - https://kodi.tv
 *
 *  SPDX-License-Identifier: GPL-2.0-or-later
 *  See LICENSES/README.md for more information.
 */

#pragma once

#include "IRenderManager.h"
#include "RenderVideoSettings.h"
#include "cores/RetroPlayer/guibridge/IRenderCallback.h"
#include "threads/CriticalSection.h"

extern "C" {
#include <libavutil/pixfmt.h>
}

#include <atomic>
#include <map>
#include <memory>
#include <set>
#include <vector>

struct SwsContext;

namespace KODI
{
namespace RETRO
{
  class CGUIRenderTargetFactory;
  class CRenderContext;
  class CRenderSettings;
  class CRPBaseRenderer;
  class CRPProcessInfo;
  class IGUIRenderSettings;
  class IRenderBuffer;
  class IRenderBufferPool;

  /*!
   * \brief Renders video frames provided by the game loop
   *
   * Generally, buffer pools are registered by the windowing subsystem. A buffer
   * pool provides a software or hardware buffer to store the added frame. When
   * RenderManager is created, it instantiates all registered buffer pools.
   *
   * When a frame arrives, it is copied into a buffer from each buffer pool with
   * a visible renderer. For example, if a GLES and MMAL renderer are both
   * visible in the GUI, then the frame will be copied into two buffers.
   *
   * When it is time to render the frame, the GUI control or window calls into
   * this class through the IRenderManager interface. RenderManager selects an
   * appropriate renderer to use to render the frame. The renderer is then
   * given the buffer that came from its buffer pool.
   *
   * Special behavior is needed when the game is paused. As no new frames are
   * delivered, a newly created renderer will stay black. For this scenario,
   * when we detect a pause event, the frame is preemptively cached so that a
   * newly created renderer will have something to display.
   */
  class CRPRenderManager : public IRenderManager,
                           public IRenderCallback
  {
  public:
    CRPRenderManager(CRPProcessInfo &processInfo);
    ~CRPRenderManager() override = default;

    void Initialize();
    void Deinitialize();

    /*!
     * \brief Access the factory for creating GUI render targets
     */
    CGUIRenderTargetFactory *GetGUIRenderTargetFactory() { return m_renderControlFactory.get(); }

    // Functions called from game loop
    bool Configure(AVPixelFormat format, unsigned int nominalWidth, unsigned int nominalHeight, unsigned int maxWidth, unsigned int maxHeight);
    bool GetVideoBuffer(unsigned int width, unsigned int height, AVPixelFormat &format, uint8_t *&data, size_t &size);
    void AddFrame(const uint8_t* data, size_t size, unsigned int width, unsigned int height, unsigned int orientationDegCW);
    void Flush();

    // Functions called from the player
    void SetSpeed(double speed);

    // Functions called from render thread
    void FrameMove();

    // Implementation of IRenderManager
    void RenderWindow(bool bClear, const RESOLUTION_INFO &coordsRes) override;
    void RenderControl(bool bClear, bool bUseAlpha, const CRect &renderRegion, const IGUIRenderSettings *renderSettings) override;
    void ClearBackground() override;

    // Implementation of IRenderCallback
    bool SupportsRenderFeature(RENDERFEATURE feature) const override;
    bool SupportsScalingMethod(SCALINGMETHOD method) const override;

  private:
    /*!
     * \brief Get or create a renderer compatible with the given render settings
     */
    std::shared_ptr<CRPBaseRenderer> GetRenderer(const IGUIRenderSettings *renderSettings);

    /*!
     * \brief Get or create a renderer for the given buffer pool and render settings
     */
    std::shared_ptr<CRPBaseRenderer> GetRenderer(IRenderBufferPool *bufferPool, const CRenderSettings &renderSettings);

    /*!
     * \brief Render a frame using the given renderer
     */
    void RenderInternal(const std::shared_ptr<CRPBaseRenderer> &renderer, bool bClear, uint32_t alpha);

    /*!
     * \brief Return true if we have a render buffer belonging to the specified pool
     */
    bool HasRenderBuffer(IRenderBufferPool *bufferPool);

    /*!
     * \brief Get a render buffer belonging to the specified pool
     */
    IRenderBuffer *GetRenderBuffer(IRenderBufferPool *bufferPool);

    /*!
     * \brief Create a render buffer for the specified pool from a cached frame
     */
    void CreateRenderBuffer(IRenderBufferPool *bufferPool);

    /*!
     * \brief Create a render buffer and copy the cached data into it
     *
     * The cached frame is accessed by both the game and rendering threads,
     * and therefore requires synchronization.
     *
     * However, assuming the memory copy is expensive, we must avoid holding
     * the mutex during the copy.
     *
     * To allow for this, the function is permitted to invalidate its
     * cachedFrame parameter, as long as it is restored upon exit. While the
     * mutex is exited inside this function, cachedFrame is guaranteed to be
     * empty.
     *
     * \param cachedFrame The cached frame
     * \param width The width of the cached frame
     * \param height The height of the cached frame
     * \param bufferPool The buffer pool used to create the render buffer
     * \param mutex The locked mutex, to be unlocked during memory copy
     *
     * \return The render buffer if one was created from the cached frame,
     *         otherwise nullptr
     */
    IRenderBuffer *CreateFromCache(std::vector<uint8_t> &cachedFrame, unsigned int width, unsigned int height, IRenderBufferPool *bufferPool, CCriticalSection &mutex);

    /*!
     * \brief Utility function to copy a frame and rescale pixels if necessary
     */
    void CopyFrame(IRenderBuffer *renderBuffer, AVPixelFormat format, const uint8_t *data, size_t size, unsigned int width, unsigned int height);

    CRenderVideoSettings GetEffectiveSettings(const IGUIRenderSettings *settings) const;

    void CheckFlush();

    // Construction parameters
    CRPProcessInfo &m_processInfo;
    CRenderContext &m_renderContext;

    // Subsystems
    std::shared_ptr<IGUIRenderSettings> m_renderSettings;
    std::shared_ptr<CGUIRenderTargetFactory> m_renderControlFactory;

    // Stream properties
    AVPixelFormat m_format = AV_PIX_FMT_NONE;
    unsigned int m_maxWidth = 0;
    unsigned int m_maxHeight = 0;

    // Render resources
    std::set<std::shared_ptr<CRPBaseRenderer>> m_renderers;
    std::vector<IRenderBuffer*> m_pendingBuffers; // Only access from game thread
    std::vector<IRenderBuffer*> m_renderBuffers;
    std::map<AVPixelFormat, SwsContext*> m_scalers;
    std::vector<uint8_t> m_cachedFrame;
    unsigned int m_cachedWidth = 0;
    unsigned int m_cachedHeight = 0;

    // State parameters
    enum class RENDER_STATE
    {
      UNCONFIGURED,
      CONFIGURING,
      CONFIGURED,
    };
    RENDER_STATE m_state = RENDER_STATE::UNCONFIGURED;
    bool m_bHasCachedFrame = false; // Invariant: m_cachedFrame is empty if false
    std::set<std::string> m_failedShaderPresets;
    std::atomic<bool> m_bFlush = {false};

    // Playback parameters
    std::atomic<double> m_speed = {1.0};

    // Synchronization parameters
    CCriticalSection m_stateMutex;
    CCriticalSection m_bufferMutex;
  };
}
}
