#ifndef SIDE_HPP_
#define SIDE_HPP_

#include <array>

#include <Serialize/Serialize.hpp>
#include <Utilities/Extern.hpp>

/**
 * Represents an enumeration of the sides of a block or axis-aligned directions.
 * 
 * This class wraps an integer representation of a side, which is made from the
 * axis index in bits 1..2, and the direction on that axis in bit 0. This means:
 * 
 * - (sidenum >> 1) is the axis index.
 * - (sidenum & 1) == 0 for the negative direction.
 * - (sidenum & 1) == 1 for the positive direction.
 * 
 * This class makes it more conventient to deal with side numbers, and has the same
 * representation as an `int`, and is plain serializable.
 */
class Side
{
private:
    int sidenum;

public:
    static Side West;
    static Side East;
    static Side South;
    static Side North;
    static Side Bottom;
    static Side Top;

    /**
     * @brief An array of all possible sides.
     */
    static std::array<Side, 6> all;

    static std::array<Side, 2> allX;
    static std::array<Side, 2> allY;
    static std::array<Side, 2> allZ;

    Side() = default;

    /**
     * Construct a `Side` from a side number.
     * 
     * The side number must be valid, else the behavior is undefined.
     */
    explicit constexpr Side(int sidenum) : sidenum(sidenum) {}

    /**
     * Returns the side number.
     */
    constexpr int asNumber() const
    {
        return sidenum;
    }

    /**
     * Returns the vector pointing out of this side.
     */
    inline glm::ivec3 out() const
    {
        glm::ivec3 result(0);
        result[axis()] = sign();
        return result;
    }

    /**
     * Returns the vector pointing into this side.
     */
    inline glm::ivec3 in() const
    {
        return -out();
    }

    /**
     * Return the opposite side.
     */
    constexpr Side opposite() const
    {
        return Side(sidenum ^ 1);
    }

    /**
     * Returns the axis index (0 = X, 1 = Y, 2 = Z).
     * 
     * This can beused to index into a vector.
     */
    constexpr int axis() const
    {
        return sidenum >> 1;
    }

    /**
     * Returns the sign of this side (-1 = negative side, 1 = positive side).
     */
    constexpr int sign() const
    {
        return (sidenum & 1) ? 1 : -1;
    }

    /**
     * @brief Returns true if this side is on the X axis.
     */
    constexpr bool isX() const
    {
        return (sidenum >> 1) == 0;
    }

    /**
     * @brief Returns true if this side is on the Y axis.
     */
    constexpr bool isY() const
    {
        return (sidenum >> 1) == 1;
    }

    /**
     * @brief Returns true if this side is on the Z axis.
     */
    constexpr bool isZ() const
    {
        return (sidenum >> 1) == 2;
    }

    /**
     * @brief Returns the (capitalized, unlocalized) name of this side.
     *
     * E.g. "West", "Bottom".
     */
    const char* name() const;

    inline bool operator==(Side other) const
    {
        return sidenum == other.sidenum;
    }

    /**
     * @brief Return the mask for this side in a bitfield.
     */
    constexpr uint8_t mask() const
    {
        return 1 << sidenum;
    }

    /**
     * @brief Rotate the side around the specified axis.
     *
     * If the current side is the side an entity is facing, and we are looking at the `axis` side of this
     * entity, then this function returns the side that the entity would be facing if it turned `turns`
     * times 90 degrees clockwise. `times` can be negative to turn counter-clockwise.
     *
     * If the current side is the same as the `axis`, or the opposite of the `axis`, then this function
     * returns the current side unchanged.
     */
    Side rotate(Side axis, int turns) const;

    friend struct SerialCodec<Side>;
};

std::ostream& operator<<(std::ostream &os, Side side);

DERIVE_STRUCT_SERIALIZER(Side, self.sidenum);

#endif