// This file is part of Noggit3, licensed under GNU General Public License (version 3).

#pragma once

#include <math/frustum.hpp>
#include <math/trig.hpp>
#include <noggit/rendering/CursorRender.hpp>
#include <noggit/Misc.h>
#include <noggit/Model.h> // ModelManager
#include <noggit/Selection.h>
#include <noggit/Sky.h> // Skies, OutdoorLighting, OutdoorLightStats
#include <noggit/WMO.h> // WMOManager
#include <noggit/map_horizon.h>
#include <noggit/map_index.hpp>
#include <noggit/TileIndex.hpp>
#include <noggit/tool_enums.hpp>
#include <noggit/world_tile_update_queue.hpp>
#include <noggit/world_model_instances_storage.hpp>
#include <noggit/ui/MinimapCreator.hpp>
#include <noggit/ContextObject.hpp>
#include <noggit/rendering/Primitives.hpp>
#include <opengl/shader.fwd.hpp>
#include <opengl/types.hpp>
#include <noggit/rendering/LiquidTextureManager.hpp>
#include <optional>
#include <QtCore/QSettings>
#include <map>
#include <string>
#include <unordered_set>
#include <vector>
#include <array>
#include <noggit/project/ApplicationProject.h>
#include <noggit/rendering/WorldRender.hpp>
#include <QProgressDialog>

namespace Noggit
{
  struct object_paste_params;
  struct VertexSelectionCache;

  namespace Rendering
  {
    class WorldRender;
  }
}

class Brush;
class MapTile;
class QPixmap;

static const float detail_size = 8.0f;

using StripType = uint16_t;


class World
{
  friend class Noggit::Rendering::WorldRender;

protected:
  std::unordered_set<unsigned int> selected_uids; // fast lookup
  std::vector<selection_type> _current_selection;
  // std::unordered_map<std::string, std::vector<ModelInstance*>> _models_by_filename;
  Noggit::world_model_instances_storage _model_instance_storage;
  Noggit::world_tile_update_queue _tile_update_queue;
public:
  std::vector<selection_group> _selection_groups;

  MapIndex mapIndex;
  Noggit::map_horizon horizon;

  // Temporary variables for loading a WMO, if we have a global WMO.
  std::string mWmoFilename;
  ENTRY_MODF mWmoEntry;

  unsigned int getMapID() const;

  // Time of the day.
  float animtime;
  float time;

  //! \brief Name of this map.
  std::string basename;

  explicit World(const std::string& name, int map_id, Noggit::NoggitRenderContext context, bool create_empty = false);

  void LoadSavedSelectionGroups();

  void saveSelectionGroups();

  void setBasename(const std::string& name);

  SceneObject* getObjectInstance(std::uint32_t uid);

  void update_models_emitters(float dt);

  unsigned int getAreaID (glm::vec3 const&);
  void setAreaID(glm::vec3 const& pos, int id, bool adt,  float radius = -1.0f);

  Noggit::NoggitRenderContext getRenderContext() { return _context; };

  selection_result intersect (glm::mat4x4 const& model_view
                             , math::ray const&
                             , bool only_map
                             , bool do_objects
                             , bool draw_terrain
                             , bool draw_wmo
                             , bool draw_models
                             , bool draw_hidden_models
                             , bool draw_wmo_exterior
                             , bool animate
                             );

  MapChunk* getChunkAt(glm::vec3 const& pos);

  bool isInIndoorWmoGroup(std::array<glm::vec3, 2> obj_bounds, glm::mat4x4 obj_transform);

protected:
  // Information about the currently selected model / WMO / triangle.
  int _selected_model_count = 0;
  std::optional<glm::vec3> _multi_select_pivot;
public:

  Noggit::Rendering::WorldRender* renderer() { return &_renderer; }

  void update_selection_pivot();
  std::optional<glm::vec3> const& multi_select_pivot() const { return _multi_select_pivot; }

  // Selection related methods.
  bool is_selected(selection_type selection);
  bool is_selected(std::uint32_t uid) const;
  std::vector<selection_type> const& current_selection() const { return _current_selection; }
  std::vector<selected_object_type> const get_selected_objects() const;
  std::optional<selection_type> get_last_selected_model() const;
  bool has_selection() const { return !_current_selection.empty(); }
  bool has_multiple_model_selected() const { return _selected_model_count > 1; }
  int get_selected_model_count() const { return _selected_model_count; }
  // Unused in Red, models are now iterated by adt because of the occlusion check
  // std::unordered_map<std::string, std::vector<ModelInstance*>> get_models_by_filename() const& { return _models_by_filename;  } 
  void set_current_selection(selection_type entry);
  bool add_to_selection(selection_type entry, bool skip_group = false, bool update_pivot = true);
  void remove_from_selection(selection_type entry, bool skip_group = false, bool update_pivot = true);
  void remove_from_selection(std::uint32_t uid, bool skip_group = false, bool update_pivot = true);
  void reset_selection();
  void delete_selected_models();
  glm::vec3 get_ground_height(glm::vec3 pos);
  void range_add_to_selection(glm::vec3 const& pos, float radius, bool remove);
  Noggit::world_model_instances_storage& getModelInstanceStorage() { return _model_instance_storage; };

  enum class object_scaling_type
  {
    set,
    add,
    mult
  };

  void snap_selected_models_to_the_ground();
  void scale_selected_models(float v, object_scaling_type type);
  void move_selected_models(float dx, float dy, float dz);
  void move_model(selection_type entry, float dx, float dy, float dz);
  void move_selected_models(glm::vec3 const& delta)
  {
    move_selected_models(delta.x, delta.y, delta.z);
  }
  void set_selected_models_pos(float x, float y, float z, bool change_height = true)
  {
    return set_selected_models_pos({x,y,z}, change_height);
  }
  void set_selected_models_pos(glm::vec3 const& pos, bool change_height = true);
  void set_model_pos(selection_type entry, glm::vec3 const& pos, bool change_height = true);
  void rotate_selected_models(math::degrees rx, math::degrees ry, math::degrees rz, bool use_pivot);
  void rotate_selected_models_randomly(float minX, float maxX, float minY, float maxY, float minZ, float maxZ);
  void set_selected_models_rotation(math::degrees rx, math::degrees ry, math::degrees rz);

  void update_selected_model_groups();

  // Checks the normal of the terrain on model origin and rotates to that spot.
  void rotate_selected_models_to_ground_normal(bool smoothNormals);
  void rotate_model_to_ground_normal(SceneObject* obj, bool smoothNormals);

  bool GetVertex(float x, float z, glm::vec3 *V) const;

  // check if the cursor is under map or in an unloaded tile
  bool isUnderMap(glm::vec3 const& pos) const;

  template<typename Fun>
  bool for_all_chunks_in_range ( glm::vec3 const& pos
                               , float radius
                               , Fun&& /* MapChunk* -> bool changed */
                               );
  template<typename Fun, typename Post>
  bool for_all_chunks_in_range ( glm::vec3 const& pos
                               , float radius
                               , Fun&& /* MapChunk* -> bool changed */
                               , Post&& /* MapChunk* -> void; called for all changed chunks */
                               );

  template<typename Fun>
  bool for_all_chunks_in_rect ( glm::vec3 const& pos
    , float radius
    , Fun&& /* MapChunk* -> bool changed */
  );

  template<typename Fun, typename Post>
  bool for_all_chunks_in_rect (glm::vec3 const& pos
    , float radius
    , Fun&& /* MapChunk* -> bool changed */
    , Post&& /* MapChunk* -> void; called for all changed chunks */
  );

  template<typename Fun>
    void for_all_chunks_on_tile (glm::vec3 const& pos, Fun&&);

    template<typename Fun>
    void for_all_chunks_on_tile(MapTile* tile, Fun&& fun);

  template<typename Fun>
    void for_chunk_at(glm::vec3 const& pos, Fun&& fun);
  template<typename Fun>
    auto for_maybe_chunk_at (glm::vec3 const& pos, Fun&& fun) -> std::optional<decltype (fun (nullptr))>;

  template<typename Fun>
    void for_tile_at(const TileIndex& pos, Fun&&);

  template<typename Fun>
    void for_tile_at_force(const TileIndex& pos, Fun&&);

  void changeObjectsWithTerrain(glm::vec3 const& pos, float change, float radius, int BrushType, float inner_radius, bool iter_wmos_ = true, bool iter_m2s = true);
  void changeTerrain(glm::vec3 const& pos, float change, float radius, int BrushType, float inner_radius);
  std::vector<selected_object_type> getObjectsInRange(glm::vec3 const& pos, float radius, bool ignore_height = true, bool iter_wmos_ = true, bool iter_m2s = true);
  void changeShader(glm::vec3 const& pos, glm::vec4 const& color, float change, float radius, bool editMode);
  void stampShader(glm::vec3 const& pos, glm::vec4 const& color, float change, float radius, bool editMode, QImage* img, bool paint, bool use_image_colors);
  glm::vec3 pickShaderColor(glm::vec3 const& pos);
  void flattenTerrain(glm::vec3 const& pos, float remain, float radius, int BrushType, flatten_mode const& mode, const glm::vec3& origin, math::degrees angle, math::degrees orientation);
  std::vector<std::pair<SceneObject*, float>> getObjectsGroundDistance(glm::vec3 const& pos, float radius, bool iter_wmos_, bool iter_m2s);
  void blurTerrain(glm::vec3 const& pos, float remain, float radius, int BrushType, flatten_mode const& mode);
  bool paintTexture(glm::vec3 const& pos, Brush *brush, float strength, float pressure, scoped_blp_texture_reference texture);
  bool stampTexture(glm::vec3 const& pos, Brush *brush, float strength, float pressure, scoped_blp_texture_reference texture, QImage* img, bool paint);
  bool sprayTexture(glm::vec3 const& pos, Brush *brush, float strength, float pressure, float spraySize, float sprayPressure, scoped_blp_texture_reference texture);
  bool replaceTexture(glm::vec3 const& pos, float radius, scoped_blp_texture_reference const& old_texture, scoped_blp_texture_reference new_texture, bool entire_chunk = false, bool entire_tile = false);

  void eraseTextures(glm::vec3 const& pos);
  void overwriteTextureAtCurrentChunk(glm::vec3 const& pos, scoped_blp_texture_reference const& oldTexture, scoped_blp_texture_reference newTexture);
  void paintGroundEffectExclusion(glm::vec3 const& pos, float radius, bool exclusion);
  void setBaseTexture(glm::vec3 const& pos);
  void clear_shadows(glm::vec3 const& pos);
  void clearTextures(glm::vec3 const& pos);
  void swapTexture(glm::vec3 const& pos, scoped_blp_texture_reference tex);
  void swapTextureGlobal(scoped_blp_texture_reference tex);
  void removeTexture(glm::vec3 const& pos, scoped_blp_texture_reference tex);
  void removeTexDuplicateOnADT(glm::vec3 const& pos);
  void change_texture_flag(glm::vec3 const& pos, scoped_blp_texture_reference const& tex, std::size_t flag, bool add);

  void setHole(glm::vec3 const& pos, float radius, bool big, bool hole);
  void setHoleADT(glm::vec3 const& pos, bool hole);

  void exportADTAlphamap(glm::vec3 const& pos);
  void exportADTNormalmap(glm::vec3 const& pos);
  void exportADTAlphamap(glm::vec3 const& pos, std::string const& filename);
  void exportADTHeightmap(glm::vec3 const& pos, float min_height, float max_height);
  void exportADTVertexColorMap(glm::vec3 const& pos);
  void exportAllADTsAlphamap();
  void exportAllADTsAlphamap(std::string const& filename);
  void exportAllADTsHeightmap();
  void exportAllADTsVertexColorMap();

  void importADTAlphamap(glm::vec3 const& pos, QImage const& image, unsigned layer, bool cleanup);
  void importADTAlphamap(glm::vec3 const& pos, bool cleanup);
  void importADTHeightmap(glm::vec3 const& pos, QImage const& image, float min_height, float max_height, unsigned mode, bool tiledEdges);
  void importADTHeightmap(glm::vec3 const& pos, float min_height, float max_height, unsigned mode, bool tiledEdges);
  void importADTWatermap(glm::vec3 const& pos, QImage const& image, float min_height, float max_height, unsigned mode, bool tiledEdges);
  void importADTVertexColorMap(glm::vec3 const& pos, int mode, bool tiledEdges);
  void importADTVertexColorMap(glm::vec3 const& pos, QImage const& image, int mode, bool tiledEdges);

  void importAllADTsAlphamaps(QProgressDialog* progress_dialog);
  void importAllADTsHeightmaps(QProgressDialog* progress_dialog, float min_height, float max_height, unsigned mode, bool tiledEdges);
  void importAllADTVertexColorMaps(unsigned mode, bool tiledEdges);

  void ensureAllTilesetsADT(glm::vec3 const& pos);
  void ensureAllTilesetsAllADTs();

  void notifyTileRendererOnSelectedTextureChange();

  void addM2 ( BlizzardArchive::Listfile::FileKey const& file_key
             , glm::vec3 newPos
             , float scale, math::degrees::vec3 rotation
             , Noggit::object_paste_params*
             , bool action
             );
  void addWMO ( BlizzardArchive::Listfile::FileKey const& file_key
              , glm::vec3 newPos
              , float scale, math::degrees::vec3 rotation
              , Noggit::object_paste_params*
              , bool action
              );

  ModelInstance* addM2AndGetInstance ( BlizzardArchive::Listfile::FileKey const& file_key
      , glm::vec3 newPos
      , float scale, math::degrees::vec3 rotation
      , Noggit::object_paste_params*
      , bool ignore_params
      , bool action
  );

  WMOInstance* addWMOAndGetInstance ( BlizzardArchive::Listfile::FileKey const& file_key
      , glm::vec3 newPos
      , math::degrees::vec3 rotation
      , float scale
      , bool action
  );

  auto stamp(glm::vec3 const& pos, float dt, QImage const* img, float radiusOuter
  , float radiusInner, int BrushType, bool sculpt) -> void;

  // add a m2 instance to the world (needs to be positioned already), return the uid
  std::uint32_t add_model_instance(ModelInstance model_instance, bool from_reloading, bool action);
  // add a wmo instance to the world (needs to be positioned already), return the uid
  std::uint32_t add_wmo_instance(WMOInstance wmo_instance, bool from_reloading, bool action);

  std::optional<selection_type> get_model(std::uint32_t uid);
  void remove_models_if_needed(std::vector<uint32_t> const& uids);

  void reload_tile(TileIndex const& tile);

  void updateTilesEntry(selection_type const& entry, model_update type);
  void updateTilesEntry(SceneObject* entry, model_update type);
  void updateTilesWMO(WMOInstance* wmo, model_update type);
  void updateTilesModel(ModelInstance* m2, model_update type);
  void wait_for_all_tile_updates();

  void deleteModelInstance(int uid, bool action);
  void deleteWMOInstance(int uid, bool action);
  void deleteInstance(int uid, bool action);

  bool uid_duplicates_found() const;
  void delete_duplicate_model_and_wmo_instances();
  // used after the uid fix all
  void unload_every_model_and_wmo_instance();

	static bool IsEditableWorld(BlizzardDatabaseLib::Structures::BlizzardDatabaseRow& record);

  static bool IsWMOWorld(BlizzardDatabaseLib::Structures::BlizzardDatabaseRow& record);

  void clearHeight(glm::vec3 const& pos);
  void clearAllModelsOnADT(TileIndex const& tile, bool action);

  // liquids
  void paintLiquid( glm::vec3 const& pos
                  , float radius
                  , int liquid_id
                  , bool add
                  , math::radians const& angle
                  , math::radians const& orientation
                  , bool lock
                  , glm::vec3 const& origin
                  , bool override_height
                  , bool override_liquid_id
                  , float opacity_factor
                  );
  void CropWaterADT(const TileIndex& pos);
  void setWaterType(const TileIndex& pos, int type, int layer);
  int getWaterType(const TileIndex& tile, int layer) const;
  void autoGenWaterTrans(const TileIndex&, float factor);


  void fixAllGaps();

  void CleanupEmptyTexturesChunks();
  void convert_alphamap(QProgressDialog* progress_dialog, bool to_big_alpha);

  bool deselectVertices(glm::vec3 const& pos, float radius);
  void selectVertices(glm::vec3 const& pos, float radius);
  void moveVertices(float h);
  void orientVertices ( glm::vec3 const& ref_pos
                      , math::degrees vertex_angle
                      , math::degrees vertex_orientation
                      );
  void flattenVertices (float height);

  void updateSelectedVertices();
  void updateVertexCenter();
  void clearVertexSelection();

  void deleteObjects(std::vector<selected_object_type> const& types, bool action);

  float getMaxTileHeight(const TileIndex& tile);

  glm::vec3 const& vertexCenter();

  void recalc_norms (MapChunk*) const;

  Noggit::VertexSelectionCache getVertexSelectionCache();
  void setVertexSelectionCache(Noggit::VertexSelectionCache& cache);

  bool need_model_updates = false;

  void loadAllTiles(glm::vec3& camera_pos);
  unsigned getNumLoadedTiles() const { return _n_loaded_tiles; };
  unsigned getNumRenderedTiles() const { return _n_rendered_tiles; };
  unsigned getNumRenderedObjects() const { return _n_rendered_objects; };

  void select_objects_in_area(
      const std::array<glm::vec2, 2>& selection_box, 
      bool reset_selection,
      const glm::mat4x4& view,
      const glm::mat4x4& projection,
      int viewport_width,
      int viewport_height,
      float user_depth,
      const glm::vec3& camera_position
  );


  void add_object_group_from_selection();
  // void remove_selection_group(selection_group* group);

  void clear_selection_groups();

private:
  bool is_point_occluded_by_terrain(const glm::vec3& point
    , const glm::mat4x4& view
    , const glm::mat4& VPmatrix
    , float viewport_width
    , float viewport_height
    , const glm::vec3& camera_position
    , float distance_override = 0.0f);

public:
  bool paste_model_random_rotation = false;
  bool paste_model_random_tilt = false;
  bool paste_model_random_size = false;

protected:
  // void update_models_by_filename();

  std::unordered_set<MapChunk*>& vertexBorderChunks();

  std::unordered_set<MapTile*> _vertex_tiles;
  std::unordered_set<MapChunk*> _vertex_chunks;
  std::unordered_set<MapChunk*> _vertex_border_chunks;
  std::unordered_set<glm::vec3*> _vertices_selected;
  glm::vec3 _vertex_center;
  bool _vertex_center_updated = false;
  bool _vertex_border_updated = false;

  QSettings* _settings;

  Noggit::NoggitRenderContext _context;

  std::array<std::pair<std::pair<int, int>, MapTile*>, 64 * 64 > _loaded_tiles_buffer;

  Noggit::Rendering::WorldRender _renderer;

  // Debug metrics
  unsigned _n_loaded_tiles = 0;
  unsigned _n_rendered_tiles = 0;;

  // unsigned _n_loaded_objects; // done from instance storage size currently
  unsigned _n_rendered_objects = 0;

};
