#ifndef MACHINE_HPP_
#define MACHINE_HPP_

#include <MachineConfig/MachineConfig.hpp>
#include <cstdlib>
#include <string_view>
#include <unordered_map>

#include <Serialize/Serialize.hpp>
#include <Machine/IElectricMachine.hpp>
#include <ChunkHandle/ChunkHandle.hpp>
#include <MachineComponent/MachineComponent.hpp>

class LocalWorld;
class Chunk;

/**
 * @brief Base class for all machines.
 */
class Machine
{
private:
    std::unordered_map<std::string, std::unique_ptr<MachineComponent>> components;
    bool detached = false;

protected:
    /**
     * @brief The world containing this machine.
     */
    LocalWorld &m_world;

    /**
     * @brief The chunk which this machine is on.
     */
    ChunkHandle ourChunk;

public:
    /**
     * @brief Enumeration of generic attribution actions.
     */
    struct Action
    {
        /**
         * @brief Attributed to the player that placed the machine block.
         */
        static constexpr std::string_view Placed = "main:Machine:Placed";
    };

    /**
     * @brief Attach a component to this machine.s
     *
     * This should be called from the constructor.
     * 
     * @tparam TComponent The component class (must extend `MachineComponent`).
     */
    template<typename TComponent>
    TComponent& attach()
    {
        auto &storedComp = components[TComponent::tagName];
        if (storedComp != nullptr)
        {
            return static_cast<TComponent&>(*storedComp);
        }

        std::unique_ptr<TComponent> comp = std::make_unique<TComponent>(*this);
        TComponent &result = *comp;
        storedComp = std::move(comp);
        return result;
    }

    /**
     * @brief The path to the file where we save this machine's state.
     */
    std::string stateFilePath;

    /**
     * @brief The machine's position in the world.
     */
    const glm::ivec3 pos;

    /**
     * @brief The block ID for this machine's block.
     */
    int blockId;

    /**
     * @brief Construct the machine.
     * 
     * @param world The world handle.
     * @param pos The machine position.
     */
    Machine(LocalWorld &world, glm::ivec3 pos);

    virtual ~Machine() = default;

    /**
     * @brief Returns true if the machine is ready.
     */
    bool isReady();

    /**
     * @brief Called on every tick when this machine is loaded.
     * 
     * Does nothing by default.
     */
    virtual void update();

    /**
     * @brief Get the world.
     */
    LocalWorld& getWorld();

    /**
     * @brief Serialize this machine's state. Used for saving to disk.
     * 
     * @param outBytes The vector to which this machine's state will be appended.
     */
    virtual void serializeState(std::vector<uint8_t> &outBytes) = 0;

    /**
     * @brief Deserialize this machine's state. Used for saving to disk.
     */
    virtual void deserializeState(const uint8_t *data, size_t size) = 0;

    /**
     * @brief Serialize machine file data (the machine state and every component state).
     */
    void serializeMachineFile(std::vector<uint8_t> &outBytes);

    /**
     * @brief Deserialize machine file data (the machine state and every component state).
     */
    void deserializeMachineFile(const uint8_t *data, size_t size);

    /**
     * @brief Tell the machine's components that it is being removed.
     */
    void onRemoving();

    /**
     * @brief Update the components and the machine.
     */
    void updateAll();

    /**
     * @brief Return the chunk containing this machine.
     */
    Chunk& getChunk();

    /**
     * @brief Returns true if this machine has been removed from the world.
     */
    bool isDetached();

    /**
     * @brief Serialize this machine's main (machine-specific) config.
     * 
     * @param consumedItems You can push items to consume for this config here.
     * @param data Output the config of this machine here.
     */
    virtual void saveMainConfig(MachineConfig &config);

    /**
     * @brief Deserialize this machine's main (machine-specific) config.
     * 
     * @param data The config to deserialize.
     * @param consumed `true` if the config's required items were consumed; otherwise load only non-item config.
     */
    virtual void loadMainConfig(const MachineConfig &config, bool consumed);

    /**
     * @brief Save all of this machine's config into `config`.
     * 
     * @param config Config to output; must be initialized to empty.
     */
    void saveConfig(MachineConfig &config);

    /**
     * @brief Load all of this machine's config into `config`.
     * 
     * @param config The config to load from.
     * @param consumed Whether the required items were consumed.
     */
    void loadConfig(MachineConfig &config, bool consumed);

    /**
     * @brief Returns a reference to the specified component.
     *
     * The component must have been added by the constructor using `attach()`.
     */
    template<typename TComponent>
    TComponent& getComponent()
    {
        return dynamic_cast<TComponent&>(*components[TComponent::tagName]);
    }

    template<typename TComponent>
    bool hasComponent()
    {
        return components.contains(TComponent::tagName);
    }

    friend class BeltMotorComponent;
};

#endif