package org.valkyrienskies.mod.common.util;

import java.util.Optional;
import javax.annotation.Nullable;
import javax.annotation.ParametersAreNonnullByDefault;
import lombok.experimental.UtilityClass;
import mcp.MethodsReturnNonnullByDefault;
import net.minecraft.entity.Entity;
import net.minecraft.util.math.AxisAlignedBB;
import net.minecraft.util.math.BlockPos;
import net.minecraft.world.World;
import org.valkyrienskies.fixes.IPhysicsChunk;
import org.valkyrienskies.mod.common.ValkyrienSkiesMod;
import org.valkyrienskies.mod.common.coordinates.CoordinateSpaceType;
import org.valkyrienskies.mod.common.entity.EntityMountable;
import org.valkyrienskies.mod.common.math.Vector;
import org.valkyrienskies.mod.common.physics.collision.polygons.Polygon;
import org.valkyrienskies.mod.common.physics.management.PhysicsObject;
import org.valkyrienskies.mod.common.physmanagement.shipdata.IValkyrienSkiesWorldData;
import org.valkyrienskies.mod.common.physmanagement.shipdata.QueryableShipData;
import valkyrienwarfare.api.TransformType;

/**
 * This class contains various helper functions for Valkyrien Skies.
 */
@UtilityClass
@MethodsReturnNonnullByDefault
@ParametersAreNonnullByDefault
public class ValkyrienUtils {

    /**
     * The liver of this mod. Returns the PhysicsObject that managed the given pos in the given
     * world.
     *
     * @param world The World we are in
     * @param pos   A BlockPos within the physics object space.
     * @return The PhysicsObject that owns the chunk at pos within the given world.
     */
    public static Optional<PhysicsObject> getPhysicsObject(@Nullable World world,
        @Nullable BlockPos pos) {
        return getPhysicsObject(world, pos, false);
    }

    public static Optional<PhysicsObject> getPhysicsObject(@Nullable World world,
        @Nullable BlockPos pos, boolean includePartiallyLoaded) {
        // No physics object manages a null world or a null pos.
        if (world != null && pos != null && world.isBlockLoaded(pos)) {
            IPhysicsChunk physicsChunk = (IPhysicsChunk) world.getChunk(pos);
            Optional<PhysicsObject> physicsObject = physicsChunk.getPhysicsObjectOptional();
            if (physicsObject.isPresent()) {
                if (includePartiallyLoaded || physicsObject.get()
                    .isFullyLoaded()) {
                    return physicsObject;
                }
            }
        }
        return Optional.empty();
    }

    /**
     * If the given AxisAlignedBB is in ship space, then this will return that AxisAlignedBB
     * transformed to global space. Otherwise it just returns the input AxisAlignedBB.
     */
    public static AxisAlignedBB getAABBInGlobal(AxisAlignedBB axisAlignedBB,
        @Nullable World world, @Nullable BlockPos pos) {
        Optional<PhysicsObject> physicsObject = ValkyrienUtils.getPhysicsObject(world, pos);
        if (physicsObject.isPresent()) {
            // We're in a physics object; convert the bounding box to a polygon; put its coordinates
            // in global space, and then return the bounding box that encloses all the points.
            Polygon bbAsPoly = new Polygon(axisAlignedBB, physicsObject.get()
                .getShipTransformationManager()
                .getCurrentTickTransform(), TransformType.SUBSPACE_TO_GLOBAL);
            return bbAsPoly.getEnclosedAABB();
        } else {
            return axisAlignedBB;
        }
    }

    public static EntityShipMountData getMountedShipAndPos(Entity entity) {
        Entity ridingEntity = entity.ridingEntity;
        if (ridingEntity instanceof EntityMountable) {
            EntityMountable mountable = (EntityMountable) ridingEntity;
            Optional<PhysicsObject> mountedShip = mountable.getMountedShip();
            if (mountedShip.isPresent()) {
                return new EntityShipMountData(mountedShip.get(), mountable.getMountPos());
            }
        }
        return new EntityShipMountData();
    }

    public static void fixEntityToShip(Entity toFix, Vector posInLocal,
        PhysicsObject mountingShip) {
        World world = mountingShip.world();
        EntityMountable entityMountable = new EntityMountable(world, posInLocal.toVec3d(),
            CoordinateSpaceType.SUBSPACE_COORDINATES, mountingShip.getReferenceBlockPos());
        world.spawnEntity(entityMountable);
        toFix.startRiding(entityMountable);
    }

    /**
     * This method basically grabs the {@link IValkyrienSkiesWorldData} capability from the world
     * and then returns the QueryableShipData associated with it
     *
     * @param world The world we are getting the QueryableShipData from
     * @return The QueryableShipData corresponding to the given world
     */
    public static QueryableShipData getQueryableData(World world) {
        IValkyrienSkiesWorldData worldData = world
            .getCapability(ValkyrienSkiesMod.VS_WORLD_DATA, null);
        if (worldData == null) {
            // I hate it when other mods add their custom worlds without calling the forge world
            // load events, so I don't feel bad crashing the game here. Although we could also get
            // away with just adding the capability to world instead of crashing.
            throw new IllegalStateException(
                "World " + world + " doesn't have an IVSWorldDataCapability. This is wrong!");
        }
        return worldData.getQueryableShipData();
    }
}