#ifndef BLOCK_HPP_
#define BLOCK_HPP_

#include <string>
#include <memory>
#include <vector>

#include <BlockAtlas/BlockAtlas.hpp>
#include <BoundingBox/BoundingBox.hpp>
#include <ItemStack/ItemStack.hpp>
#include <Chunk/Chunk.hpp>
#include <Sound/Sound.hpp>
#include <Side/Side.hpp>
#include <BlockEffect/BlockEffect.hpp>
#include <Bitmap/Bitmap.hpp>
#include <World/IWorldActor.hpp>
#include <Utilities/RGBABytes.hpp>

class World;
class LocalWorld;
class Player;
class MovementPhysics;
class PhysicsEntity;

/**
 * @brief Describes a block.
 */
class Block
{
private:
    // Use the 0xFxxxxxxx range for generic block signals.
    static constexpr uint32_t sigNeighborUpdateBase = 0xF0000010;
    static constexpr uint32_t sigNeighborUpdateSideMask = 0xF;

    std::string grindingOutputName;
    Sound* breakingSound;
    Sound* placingSound;
    std::vector<Sound*> footstepSounds;
    Sound* fallingSound;

public:
    virtual ~Block() = default;

    Material iconMaterial;
    std::shared_ptr<Model> defaultModel;
    glm::vec4 mapColor = glm::vec4(1.0f, 0.0f, 1.0f, 1.0f);

    /**
     * @brief A container for variable names and values for the block details section of the debug menu.
     */
    struct Detail
    {
        std::string name;
        std::string value;
    };

    /**
     * @brief Create a signal code for a neighbor update on the specified side.
     */
    static constexpr uint32_t createNeighborUpdatedSignalCode(Side side)
    {
        return sigNeighborUpdateBase | ((uint32_t) side.asNumber());
    };

    /**
     * @brief The block name.
     */
    const std::string name;

    /**
     * @brief The block display name.
     */
    const std::string displayName;

    /**
     * @brief The hardness of this block (resistance to digging).
     */
    const float hardness;

    /**
     * @brief Construct a block.
     * 
     * @param name The block name.
     * @param displayName The display name.
     */
    Block(std::string name, std::string displayName, float hardness);

    /**
     * @brief Load the sprites needed by this block into the atlas, and set the default sprite (used by the entity).
     */
    virtual void loadBlockSprites(BlockAtlas &atlas, Bitmap &icon, VoxelSprite &defaultSprite) = 0;

    /**
     * @brief Get the block sprite to use for the given meta.
     *
     * @param pos The block's world position.
     * @param meta The current meta value of the block.
     * @param world The world handle (be careful as we are called from worker threads).
     */
    virtual BlockAtlas::SpriteID getBlockSprite(glm::ivec3 pos, uint8_t meta, World &world) = 0;

    /**
     * @brief Check whether the specified ray intersects this block.
     * 
     * By default always returns true.
     * 
     * You can change the value of `side` if you want a different side to be reported,
     * based on internal collision checking. In this case, the input `side` is the one
     * from which the block bounding box was hit.
     * 
     * @param world Reference to the world containing this block.
     * @param pos The block's position.
     * @param start A position on the ray.
     * @param dir The direction of the ray.
     * @param side The side from which the block was hit; can be changed.
     * @param collisionKey Output the color of the collision mask voxel which was clicked.
     */
    virtual bool isIntersectedByRay(World &world, glm::ivec3 pos, wfvec3 start, wfvec3 dir, Side &side, RGBABytes &collisionKey);

    /**
     * @brief Check whether the specified box intersects this block.
     */
    virtual bool isIntersectedBy(World &world, glm::ivec3 pos, const BoundingBox<wfixed> &box);

    /**
     * @brief Make an item stack with a specified amount of this block.
     * 
     * @param stackSize The amount of this block (1 by default).
     * @return The item stack.
     */
    ItemStack toItemStack(int stackSize = 1);

    /**
     * @brief Called when the player interacts with the block (by right-clicking).
     * 
     * The default response is to call `onUse()` on the item held by the player
     * (if any).
     * 
     * This is always called on the server.
     * 
     * @param world The world within which the interaction is occuring.
     * @param player The player interacting.
     * @param pos The position of this block.
     * @param side Specifies which the player is interacting with.
     * @param collisionKey The color of the collision mask voxel which was clicked.
     */
    virtual void onInteract(LocalWorld &world, Player &player, glm::ivec3 pos, Side side, const RGBABytes &collisionKey);

    /**
     * @brief Push item stacks which this block harvests into, into the list.
     * 
     * By default simply pushes 1 of this block. This is always called on the server.
     * 
     * @param stacks List of stacks (initially empty).
     */
    virtual void getHarvestedStacks(LocalWorld &world, glm::ivec3 pos, std::vector<ItemStack> &stacks);

    /**
     * @brief Return true if this item can be dug with bare hands.
     * 
     * By default returns `false`.
     */
    virtual bool canManuallyDig();

    /**
     * @brief Get the name of this block without the module prefix.
     * 
     * E.g. for "main:stone" returns "stone".
     * 
     * @return The pure name.
     */
    std::string getPureName();

    /**
     * @brief Returns true if the pure name of this block starts with the prefix.
     * 
     * @param prefix The prefix to check for.
     */
    bool isPrefixed(std::string prefix);

    /**
     * @brief Returns true if the pure name of this block begins with a color name.
     */
    bool isDyedBlock();

    /**
     * @brief Returns true if the pure name of this blocks ends with the suffix.
     * 
     * @param suffix The suffix.
     */
    bool isSuffixed(std::string suffix);

    /**
     * @brief Returns true if this block is a fully opaque cube.
     */
    virtual bool isFullyOpaque() = 0;

    /**
     * @brief Get the color of light given block metadata.
     * 
     * @param meta The block metadata.
     * @return The color of light emitted.
     */
    virtual glm::vec3 getLightColor(uint8_t meta);

    /**
     * @brief Get the light variance given block metadata.
     * 
     * The variance is a value 0-1, where 0 indicates a static light level, and higher
     * values indicate more flickering. By default returns 0.
     */
    virtual float getLightVariance(uint8_t meta);

    /**
     * @brief Get the offset (in voxels units) into this block to the light source.
     * 
     * By default returns `glm::ivec3(4)`, meaning that the light will come directly
     * from the center of the block.
     */
    virtual glm::ivec3 getLightOffset(uint8_t meta);

    /**
     * @brief Return `true` if fluid below this block does not need to render.
     * 
     * By default returns the result of `isFullyOpaque()`.
     */
    virtual bool concealsFluidBelow();

    /**
     * @brief Returns `true` if this block can interact with fluids
     *
     * Default = false
     */
    virtual bool isFluidInteractable();

    /**
     * @brief Returns `true` if this block is a FluidBlock
     *
     * Default = false
     */
    virtual bool isFluid();

    /**
     * @brief Called when a neighbor of this block is updated in the world.
     * 
     * By default this does nothing.
     * 
     * @param world The world.
     * @param pos Position of _this_ block.
     * @param side Which side the updated neighbor is on.
     */
    virtual void onNeighborUpdated(LocalWorld &world, glm::ivec3 pos, Side side);

    /**
     * @brief Return the item stack resulting from grinding one instance of this block.
     */
    virtual ItemStack getGrindingOutput();

    /**
     * @brief Set the stack which this block grinds to.
     * 
     * This only works if `getGrindingOutput()` is not overriden.
     * 
     * @param name The name of the item to grind to.
     * @return this
     */
    Block& grindsTo(std::string name);

    /**
     * @brief Set up stone sounds for this block and return it.
     */
    Block& withStoneSounds();

    /**
     * @brief Set up dirt sounds for this block and return it.
     */
    Block& withDustSounds();

    /**
     * @brief Set up wood sounds for this block and return it.
     */
    Block& withWoodSounds();

    /**
     * @brief Set up metal sounds for this block and return it.
     */
    Block& withMetalSounds();

    /**
     * @brief Set up glass sounds for this block and return it.
     */
    Block& withGlassSounds();

    /**
     * @brief Set the breaking sound and return this block.
     * 
     * @param sound The breaking sound.
     * @return this
     */
    Block& withBreakingSound(Sound &sound);

    /**
     * @brief Set the placing sound and return this block.
     * 
     * @param sound The placing sound.
     * @return this
     */
    Block& withPlacingSound(Sound &sound);

    /**
     * @brief Add the specified footstep sound to the list and return this block.
     */
    Block& withFootstepSound(Sound &sound);

    /**
     * @brief Set the falling sound and return this block.
     * 
     * @param sound The falling sound.
     * @return this
     */
    Block& withFallingSound(Sound &sound);

    /**
     * @brief Copy sounds from another block.
     */
    Block& withSoundsFrom(const Block &otherBlock);

    /**
     * @brief Called when this block is placed at the specified coordinates.
     * 
     * By default does nothing.
     */
    virtual void onPlaced(LocalWorld &world, Chunk &chunk, glm::ivec3 pos);

    /**
     * @brief Called when a scheduled signal is received by this block.
     * 
     * If the signal is not handled, you should call the parent's `onSignal()`. The default
     * implementation in `Block` handles signals related to neighbor updates etc.
     */
    virtual void onSignal(LocalWorld &world, glm::ivec3 pos, uint32_t code);

    /**
     * @brief Get the collision boxes of this block into the `boxes` vector.
     * 
     * This is used to figure out where to draw digging cracks etc, as well as in the default
     * implementations of collision functions.
     * 
     * By default simply returns a box that encompasses the whole block.
     */
    virtual void getCollisionBoxes(World &world, glm::ivec3 pos, std::vector<BoundingBox<wfixed>> &boxes);

    /**
     * @brief Returns true if a block can be placed to overwrite this one.
     * 
     * By default returns false. Blocks such as air return true.
     */
    virtual bool canReplaceWithPlacedBlock();

    /**
     * @brief Determine if the player can place this block, and optionally set metadata.
     * 
     * By defualt this function simply returns `true`. You can override this to restrict where
     * a player can place this type of block. The `meta` reference points to a value initially
     * set to 0; you can overwrite this to set different metadata for the block being placed.
     * 
     * If this function returns `true`, the block will then be written to its new position with
     * the desired metadata (0 by default). If this function returns `false`, the block is not
     * placed and is not taken from the player.
     * 
     * @param world The world we are placing in.
     * @param actor The actor placing the block.
     * @param pos The block's to-be position.
     * @param side The side of this block which connects to the clicked block.
     * @param meta Set this to the metadata you want (0 by default).
     * @return true If the block should be placed.
     * @return false If the player should be prevented from placing the block.
     */
    virtual bool canPlace(LocalWorld &world, IWorldActor &actor, glm::ivec3 pos, Side side, uint8_t &meta);

    /**
     * @brief Check if a block protects another block.
     * 
     * We are the block at `myPos`; do we protect the casing block at
     * `otherPos`?
     * 
     * You can assume that the other block is a casing block, as only they ever call
     * this function. This allows some optimisations with multi-block machines
     * protecting their casings.
     * 
     * By defaults this returns false.
     *
     * TODO: Remove me, use chunk monitors instead??
     */
    virtual bool isProtectorOf(World &world, glm::ivec3 myPos, glm::ivec3 otherPos);

    /**
     * @brief Get the movement physics for the player when inside this block.
     * 
     * By default, returns `AirMovementPhysics`, which produces the usual physics
     * of moving through the air. But, for example, the `FluidBlock` returns different
     * physics, which allows the player to swim.
     */
    virtual MovementPhysics& getMovementPhysicsInside();

    /**
     * @brief Get the background sound to be played by this block given metadata.
     * 
     * By default returns `Sound::NONE`, so no sound is played.
     */
    virtual Sound& getBackgroundSound(uint8_t meta);

    /**
     * @brief Get the sound to play while digging this block manually.
     */
    virtual Sound& getManualDiggingSound();

    /**
     * @brief Get a vector of block information in the format `variableName` and `variableValue`
     */
    virtual std::vector<Detail> getDetails();

    /**
     * @brief Get the sound to play when this block breaks.
     */
    Sound& getBreakingSound();

    /**
     * @brief Get the sound to play when this block is placed.
     */
    Sound& getPlacingSound();

    /**
     * @brief Returns a random footstep sound.
     */
    Sound& getRandomFootstepSound();

    /**
     * @brief Get the sound to play when this block is fallen on.
     */
    Sound& getFallingSound();

    /**
     * @brief Called after this block was removed.
     * 
     * When this method is called, the position is already replaced with the new block.
     * By default does nothing.
     * 
     * @param world The world.
     * @param chunk The chunk on which this block was placed.
     * @param pos The position.
     * @param meta The meta value for the block before it was removed.
     */
    virtual void onRemoved(LocalWorld &world, Chunk &chunk, glm::ivec3 pos, uint8_t meta);

    /**
     * @brief Called when a physics entity walks over this block.
     */
    virtual void onWalkingOver(World &world, glm::ivec3 pos, PhysicsEntity &entity);

    /**
     * @brief Called when a physics entity enters this block.
     */
    virtual void onEntering(World &world, PhysicsEntity &entity);

    /**
     * @brief Called when a physics entity leaves this block.
     */
    virtual void onLeaving(World &world, PhysicsEntity &entity);

    /**
     * @brief Given meta, pushes the effects this block should generate into the specified effects vector.
     * 
     * By default does nothing.
     */
    virtual void getEffects(uint8_t meta, glm::vec3 offset, std::vector<std::shared_ptr<BlockEffect>> &effects);

    /**
     * @brief Returns true if this block can be sweeped away by fluids.
     * 
     * By default returns false.
     */
    virtual bool canSweepAway();
};

#endif