#pragma once

#include "drape/color.hpp"
#include "drape/glyph_manager.hpp"
#include "drape/pointers.hpp"
#include "drape/stipple_pen_resource.hpp"  // for PenPatternT
#include "drape/texture.hpp"

#include "base/string_utils.hpp"
#include "base/timer.hpp"

#include <array>
#include <atomic>
#include <map>
#include <mutex>
#include <string>
#include <vector>

namespace dp
{
class HWTextureAllocator;

struct TexturePoolDesc
{
  uint32_t m_maxTextureCount = 0;
  uint32_t m_textureWidth = 0;
  uint32_t m_textureHeight = 0;
  dp::TextureFormat m_format = dp::TextureFormat::Unspecified;
  bool m_needMipMaps = false;
};

class TexturePool
{
public:
  using TextureId = size_t;

  explicit TexturePool(TexturePoolDesc const & desc);
  virtual ~TexturePool() = default;

  virtual size_t GetAvailableCount() const = 0;

  [[nodiscard]] virtual TextureId AcquireTexture(ref_ptr<dp::GraphicsContext> context) = 0;
  virtual void ReleaseTexture(ref_ptr<dp::GraphicsContext> context, TextureId id) = 0;
  virtual void UpdateTextureData(ref_ptr<dp::GraphicsContext> context, TextureId id, uint32_t x, uint32_t y,
                                 uint32_t width, uint32_t height, ref_ptr<void> data) = 0;

  virtual ref_ptr<dp::Texture> GetTexture(TextureId id) = 0;
  virtual bool IsHardwareTexture2dArrayUsed() const { return false; }

  inline TexturePoolDesc const & GetDesc() const { return m_desc; }

protected:
  TexturePoolDesc m_desc;
};

class TextureManager
{
public:
  class BaseRegion
  {
  public:
    BaseRegion();
    void SetResourceInfo(ref_ptr<Texture::ResourceInfo> info);
    void SetTexture(ref_ptr<Texture> texture);
    ref_ptr<Texture> GetTexture() const { return m_texture; }
    bool IsValid() const;

    m2::PointF GetPixelSize() const;
    float GetPixelHeight() const;
    m2::RectF const & GetTexRect() const;

  protected:
    ref_ptr<Texture::ResourceInfo> m_info;
    ref_ptr<Texture> m_texture;
  };

  class SymbolRegion : public BaseRegion
  {
  public:
    uint32_t GetTextureIndex() const { return m_textureIndex; }
    void SetTextureIndex(uint32_t index) { m_textureIndex = index; }

  private:
    uint32_t m_textureIndex = 0;
  };

  using GlyphRegion = BaseRegion;

  class StippleRegion : public BaseRegion
  {
  public:
    m2::PointU GetMaskPixelSize() const;
  };

  using ColorRegion = BaseRegion;

  struct Params
  {
    std::string m_resPostfix;
    double m_visualScale = 1.0;
    std::string m_colors;
    std::string m_patterns;
    GlyphManager::Params m_glyphMngParams;
    std::string m_arrowTexturePath;  // maybe empty if no custom texture
    bool m_arrowTextureUseDefaultResourceFolder = false;
  };

  TextureManager();
  void Release();

  void Init(ref_ptr<dp::GraphicsContext> context, Params const & params);
  void OnSwitchMapStyle(ref_ptr<dp::GraphicsContext> context);
  void OnVisualScaleChanged(ref_ptr<dp::GraphicsContext> context, Params const & params);

  std::vector<drape_ptr<HWTexture>> GetTexturesToCleanup();

  bool GetSymbolRegionSafe(std::string const & symbolName, SymbolRegion & region);
  void GetSymbolRegion(std::string const & symbolName, SymbolRegion & region);

  void GetStippleRegion(PenPatternT const & pen, StippleRegion & region);
  void GetColorRegion(Color const & color, ColorRegion & region);

  using TMultilineText = buffer_vector<strings::UniString, 4>;
  using TGlyphsBuffer = buffer_vector<GlyphRegion, 128>;
  using TMultilineGlyphsBuffer = buffer_vector<TGlyphsBuffer, 4>;

  using TShapedTextLines = buffer_vector<text::TextMetrics, 4>;
  text::TextMetrics ShapeSingleTextLine(float fontPixelHeight, std::string_view utf8, TGlyphsBuffer * glyphRegions);
  TShapedTextLines ShapeMultilineText(float fontPixelHeight, std::string_view utf8, char const * delimiters,
                                      TMultilineGlyphsBuffer & multilineGlyphRegions);

  // This method must be called only on Frontend renderer's thread.
  bool AreGlyphsReady(TGlyphs const & glyphs) const;

  GlyphFontAndId GetSpaceGlyph() const;

  // On some devices OpenGL driver can't resolve situation when we upload to a texture on a thread
  // and use this texture to render on another thread. By this we move UpdateDynamicTextures call
  // into render thread. If you implement some kind of dynamic texture, you must synchronize UploadData
  // and index creation operations.
  // The result of the method shows if there are some changes in texture manager.
  bool UpdateDynamicTextures(ref_ptr<dp::GraphicsContext> context);

  ref_ptr<Texture> GetSymbolsTexture() const;
  ref_ptr<Texture> GetTrafficArrowTexture() const;
  ref_ptr<Texture> GetHatchingTexture(std::string_view key) const;
  ref_ptr<Texture> GetArrowTexture() const;
  ref_ptr<Texture> GetSMAAAreaTexture() const;
  ref_ptr<Texture> GetSMAASearchTexture() const;

  void InvalidateArrowTexture(ref_ptr<dp::GraphicsContext> context, std::string const & texturePath = {},
                              bool useDefaultResourceFolder = false);
  // Apply must be called on FrontendRenderer.
  void ApplyInvalidatedStaticTextures();

  ref_ptr<HWTextureAllocator> GetTextureAllocator() const;

  ref_ptr<TexturePool> GetTexturePool(ref_ptr<dp::GraphicsContext> context, BackgroundMode mode,
                                      TexturePoolDesc const & desc);

private:
  void InitStipplePen(Params const & params);

  struct GlyphGroup
  {
    std::set<GlyphFontAndId> m_glyphKeys;
    ref_ptr<Texture> m_texture{nullptr};
  };

  uint32_t m_maxTextureSize;
  uint32_t m_maxGlypsCount;

  ref_ptr<Texture> AllocateGlyphTexture();
  void GetRegionBase(ref_ptr<Texture> tex, BaseRegion & region, Texture::Key const & key);

  size_t FindHybridGlyphsGroup(std::vector<text::GlyphMetrics> const & glyphs);

  static uint32_t GetNumberOfGlyphsNotInGroup(std::vector<text::GlyphMetrics> const & glyphs, GlyphGroup const & group);

  void UpdateGlyphTextures(ref_ptr<dp::GraphicsContext> context);

  static constexpr size_t GetInvalidGlyphGroup();

private:
  bool m_isInitialized = false;
  std::string m_resPostfix;
  std::vector<drape_ptr<Texture>> m_symbolTextures;
  drape_ptr<Texture> m_stipplePenTexture;
  drape_ptr<Texture> m_colorTexture;
  std::vector<drape_ptr<Texture>> m_glyphTextures;
  mutable std::mutex m_glyphTexturesMutex;

  std::map<std::string_view, drape_ptr<Texture>> m_hatchingTextures;

  drape_ptr<Texture> m_trafficArrowTexture;
  drape_ptr<Texture> m_arrowTexture;
  drape_ptr<Texture> m_smaaAreaTexture;
  drape_ptr<Texture> m_smaaSearchTexture;

  drape_ptr<Texture> m_newArrowTexture;

  drape_ptr<GlyphManager> m_glyphManager;
  drape_ptr<HWTextureAllocator> m_textureAllocator;

  buffer_vector<GlyphGroup, 4> m_glyphGroups;

  std::array<std::vector<drape_ptr<TexturePool>>, BackgroundMode::Count> m_pools;

  std::vector<drape_ptr<HWTexture>> m_texturesToCleanup;

  base::Timer m_uploadTimer;
  std::atomic_flag m_nothingToUpload = ATOMIC_FLAG_INIT;
  std::mutex m_calcGlyphsMutex;

  // TODO(AB): Make a more robust use of BreakIterator to split strings and get rid of this space glyph.
  GlyphFontAndId m_spaceGlyph;
};
}  // namespace dp
