#pragma once

#include "StarPeriodicFunction.hpp"
#include "StarTtlCache.hpp"
#include "StarGameTypes.hpp"
#include "StarItemDescriptor.hpp"
#include "StarParticle.hpp"
#include "StarSet.hpp"
#include "StarTileDamage.hpp"
#include "StarDamageTypes.hpp"
#include "StarStatusTypes.hpp"
#include "StarEntityRendering.hpp"
#include "StarTileEntity.hpp"

namespace Star {

STAR_CLASS(World);
STAR_CLASS(Image);
STAR_CLASS(ItemDatabase);
STAR_CLASS(RecipeDatabase);
STAR_CLASS(Object);
STAR_STRUCT(ObjectOrientation);
STAR_STRUCT(ObjectConfig);
STAR_CLASS(ObjectDatabase);

STAR_EXCEPTION(ObjectException, StarException);

struct ObjectOrientation {
  struct Anchor {
    TileLayer layer;
    Vec2I position;
    bool tilled;
    bool soil;
    Maybe<MaterialId> material;
  };

  struct ParticleEmissionEntry {
    float particleEmissionRate;
    float particleEmissionRateVariance;
    // Particle positions are considered relative to image pixels, and are
    // flipped with image flipping
    Particle particle;
    Particle particleVariance;
    bool placeInSpaces;
  };

  // The JSON values that were used to configure this orientation.
  Json config;

  EntityRenderLayer renderLayer;
  List<Drawable> imageLayers;
  bool flipImages;

  // Offset of image from (0, 0) object position, in tile coordinates
  Vec2F imagePosition;

  // If an object has frames > 1, then the image name will have the marker
  // "{frame}" replaced with an integer in [0, frames)
  unsigned frames;
  float animationCycle;

  // Spaces the object occupies.  By default, this is simply the single space
  // at the object position, but can be specified in config as either a list of
  // Vec2I, or by setting a threshold value using "spaceScanning", which will
  // scan the image (frame 1) for non-transparent pixels.
  List<Vec2I> spaces;
  RectI boundBox;

  // Allow an orientation to override the metaboundbox in case you don't want to
  // specify spaces
  Maybe<RectF> metaBoundBox;

  // Anchors of the object to place it in the world
  // For background tiles set in order for the object to
  // remain placed.  Must be within 1 space of the bounding box of spaces.
  // For foreground tiles this cannot logically contain any position
  // also in spaces, as objects cannot overlap with foreground tiles.
  List<Anchor> anchors;

  // if true, only one anchor needs to be valid for the orientation to be valid,
  // otherwise all anchors must be valid
  bool anchorAny;

  Maybe<Direction> directionAffinity;

  // Optional list of material spaces
  List<MaterialSpace> materialSpaces;

  // optionally override the default spaces used for interaction
  Maybe<List<Vec2I>> interactiveSpaces;

  Vec2F lightPosition;
  float beamAngle;

  List<ParticleEmissionEntry> particleEmitters;

  Maybe<PolyF> statusEffectArea;
  Json touchDamageConfig;

  static ParticleEmissionEntry parseParticleEmitter(String const& path, Json const& config);
  bool placementValid(World const* world, Vec2I const& position) const;
  bool anchorsValid(World const* world, Vec2I const& position) const;
};

// TODO: This is used very strangely and inconsistently. We go to all the trouble of populating
// this ObjectConfig structure from the JSON, but then keep around the JSON anyway. In some
// places we access the objectConfig, but in many more we use the object's configValue method
// to access the raw config JSON which means it's inconsistent which parameters can be overridden
// by instance values at various levels. This whole system needs reevaluation.
struct ObjectConfig {
  // Returns the index of the best valid orientation.  If no orientations are
  // valid, returns NPos
  size_t findValidOrientation(World const* world, Vec2I const& position, Maybe<Direction> directionAffinity = Maybe<Direction>()) const;

  String path;
  // The JSON values that were used to configure this Object
  Json config;

  String name;
  String type;
  String race;
  String category;
  StringList colonyTags;
  StringList scripts;
  StringList animationScripts;

  unsigned price;
  bool printable;
  bool scannable;

  bool interactive;

  StringMap<Color> lightColors;
  bool pointLight;
  float pointBeam;
  float beamAmbience;
  Maybe<PeriodicFunction<float>> lightFlickering;

  String soundEffect;
  float soundEffectRangeMultiplier;

  List<PersistentStatusEffect> statusEffects;
  Json touchDamageConfig;

  bool hasObjectItem;
  bool retainObjectParametersInItem;

  bool smashable;
  bool smashOnBreak;
  bool unbreakable;
  String smashDropPool;
  List<List<ItemDescriptor>> smashDropOptions;
  StringList smashSoundOptions;
  JsonArray smashParticles;

  String breakDropPool;
  List<List<ItemDescriptor>> breakDropOptions;

  TileDamageParameters tileDamageParameters;
  float damageShakeMagnitude;
  String damageMaterialKind;

  EntityDamageTeam damageTeam;

  Maybe<float> minimumLiquidLevel;
  Maybe<float> maximumLiquidLevel;
  float liquidCheckInterval;

  float health;

  Json animationConfig;

  List<ObjectOrientationPtr> orientations;

  // If true, the object will root - it will prevent the blocks it is
  // anchored to from being destroyed directly, and damage from those
  // blocks will be redirected to the object
  bool rooting;

  bool biomePlaced;
};

class ObjectDatabase {
public:
  static List<Vec2I> scanImageSpaces(ImageConstPtr const& image, Vec2F const& position, float fillLimit, bool flip = false);
  static Json parseTouchDamage(String const& path, Json const& touchDamage);
  static List<ObjectOrientationPtr> parseOrientations(String const& path, Json const& configList);

  ObjectDatabase();

  void cleanup();

  StringList allObjects() const;
  bool isObject(String const& name) const;

  ObjectConfigPtr getConfig(String const& objectName) const;
  List<ObjectOrientationPtr> const& getOrientations(String const& objectName) const;

  ObjectPtr createObject(String const& objectName, Json const& objectParameters = JsonObject()) const;
  ObjectPtr diskLoadObject(Json const& diskStore) const;
  ObjectPtr netLoadObject(ByteArray const& netStore) const;

  bool canPlaceObject(World const* world, Vec2I const& position, String const& objectName) const;
  // If the object is placeable in the given position, creates the given object
  // and sets its position and direction and returns it, otherwise returns
  // null.
  ObjectPtr createForPlacement(World const* world, String const& objectName, Vec2I const& position,
      Direction direction, Json const& parameters = JsonObject()) const;

  List<Drawable> cursorHintDrawables(World const* world, String const& objectName, Vec2I const& position,
      Direction direction, Json parameters = {}) const;

private:
  static ObjectConfigPtr readConfig(String const& path);

  StringMap<String> m_paths;
  mutable Mutex m_cacheMutex;
  mutable HashTtlCache<String, ObjectConfigPtr> m_configCache;
};

}