package me.km.utils;

import java.util.List;
import net.minecraft.entity.Entity;
import net.minecraft.entity.LivingEntity;
import net.minecraft.entity.player.PlayerEntity;
import net.minecraft.entity.player.ServerPlayerEntity;
import net.minecraft.entity.projectile.ArrowEntity;
import net.minecraft.entity.projectile.DamagingProjectileEntity;
import net.minecraft.entity.projectile.ThrowableEntity;
import net.minecraft.server.MinecraftServer;
import net.minecraft.util.math.AxisAlignedBB;
import net.minecraft.util.math.Vec3d;
import net.minecraft.world.IWorld;
import net.minecraft.util.DamageSource;
import net.minecraft.util.math.RayTraceContext;
import net.minecraft.util.math.RayTraceResult;
import net.minecraft.world.World;

public class Utils {
    public static Entity getEntityFromProjectile(Entity ent) {
        if(ent instanceof ArrowEntity) {
            return ((ArrowEntity) ent).getShooter();
        } else if(ent instanceof ThrowableEntity) {
            return ((ThrowableEntity) ent).getThrower();
        } else if(ent instanceof DamagingProjectileEntity) {
            return ( (DamagingProjectileEntity) ent).shootingEntity;
        }
        return null;
    }

    public static PlayerEntity getDamager(DamageSource ds) {
        Entity ent = ds.getTrueSource();
        if(ent == null) {
            return null;
        }
        if(ent instanceof PlayerEntity) {
            return (PlayerEntity) ent;
        }
        return null;
    }

    private static AxisAlignedBB buildBoxAround(double x, double y, double z, double radius) {
        return new AxisAlignedBB(x - radius, y - radius, z - radius, x + radius, y + radius, z + radius);
    }

    @SuppressWarnings("unchecked")
    public static <T extends Entity> List<T> getEntities(IWorld w, double x, double y, double z, double radius, Class<T> type) {
        double sqareRadius = radius * radius;
        return w.getWorld().getEntitiesWithinAABB(type, buildBoxAround(x, y, z, radius), ent -> ent.getDistanceSq(x, y, z) <= sqareRadius);
    }
    
    public static List<Entity> getEntities(Entity ent, double radius) {
        double sqareRadius = radius * radius;
        double x = ent.getPosX();
        double y = ent.getPosY();
        double z = ent.getPosZ();
        return ent.world.getEntitiesInAABBexcluding(ent, buildBoxAround(x, y, z, radius), e -> e.getDistanceSq(x, y, z) <= sqareRadius);
    }
    
    public static List<Entity> getEntities(Location loc, double radius) {
        double sqareRadius = radius * radius;
        double x = loc.getX();
        double y = loc.getY();
        double z = loc.getZ();
        return loc.getWorld().getEntitiesInAABBexcluding(null, buildBoxAround(x, y, z, radius), ent -> ent.getDistanceSq(x, y, z) <= sqareRadius);
    }

    public static List<PlayerEntity> getPlayers(IWorld w, double x, double y, double z, double radius) {
        double sqareRadius = radius * radius;
        return w.getEntitiesWithinAABB(PlayerEntity.class, buildBoxAround(x, y, z, radius), p -> p.getDistanceSq(x, y, z) <= sqareRadius);
    }

    public static List<PlayerEntity> getPlayers(Entity not, double radius) {
        double sqareRadius = radius * radius;
        return not.world.getEntitiesWithinAABB(PlayerEntity.class, buildBoxAround(not.getPosX(), not.getPosY(), not.getPosZ(), radius),
                p -> p != not && p.getDistanceSq(not) <= sqareRadius);
    }

    public static List<LivingEntity> getLiving(Entity not, double radius) {
        double sqareRadius = radius * radius;
        return not.world.getEntitiesWithinAABB(LivingEntity.class, buildBoxAround(not.getPosX(), not.getPosY(), not.getPosZ(), radius),
                p -> p != not && p.getDistanceSq(not) <= sqareRadius);
    }
    
    public static List<LivingEntity> getLiving(Location loc, double radius) {
        double sqareRadius = radius * radius;
        double x = loc.getX();
        double y = loc.getY();
        double z = loc.getZ();
        return loc.getWorld().getEntitiesWithinAABB(LivingEntity.class, buildBoxAround(x, y, z, radius),
                p -> p.getDistanceSq(x, y, z) <= sqareRadius);
    }

    public static <T extends Entity> T getEntity(IWorld w, double x, double y, double z, double radius, Class<T> type) {
        return getEntities(w, x, y, z, radius, type).stream()
                .min((e1, e2) -> Double.compare(e1.getDistanceSq(x, y, z), e2.getDistanceSq(x, y, z)))
                .orElse(null);
    }

    private static boolean doesIntersect(AxisAlignedBB bound, Vec3d start, Vec3d unit) {
        double lowerX = Double.NEGATIVE_INFINITY;
        double upperX = Double.POSITIVE_INFINITY;
        if(unit.x != 0) {
            if(unit.x > 0) {
                lowerX = (bound.minX - start.x) / unit.x;
                upperX = (bound.maxX - start.x) / unit.x;
            } else {
                lowerX = (bound.maxX - start.x) / unit.x;
                upperX = (bound.minX - start.x) / unit.x;
            }
        }

        double lowerY = Double.NEGATIVE_INFINITY;
        double upperY = Double.POSITIVE_INFINITY;
        if(unit.y != 0) {
            if(unit.y > 0) {
                lowerY = (bound.minY - start.y) / unit.y;
                upperY = (bound.maxY - start.y) / unit.y;
            } else {
                lowerY = (bound.maxY - start.y) / unit.y;
                upperY = (bound.minY - start.y) / unit.y;
            }
        }

        double lowerZ = Double.NEGATIVE_INFINITY;
        double upperZ = Double.POSITIVE_INFINITY;
        if(unit.z != 0) {
            if(unit.z > 0) {
                lowerZ = (bound.minZ - start.z) / unit.z;
                upperZ = (bound.maxZ - start.z) / unit.z;
            } else {
                lowerZ = (bound.maxZ - start.z) / unit.z;
                upperZ = (bound.minZ - start.z) / unit.z;
            }
        }

        return Math.max(Math.max(lowerX, lowerY), lowerZ) < Math.min(Math.min(upperX, upperY), upperZ);
    }

    private static Vec3d getTargetVector(Entity ent, double range) {
        if(range > 128) {
            range = 128;
        }
        World w = ent.getEntityWorld().getWorld();
        Vec3d start = ent.getEyePosition(1.0f);
        Vec3d look = ent.getLookVec();
        Vec3d end = start.add(look.x * range, look.y * range, look.z * range);

        RayTraceResult ray = w.rayTraceBlocks(new RayTraceContext(start, end, RayTraceContext.BlockMode.COLLIDER, RayTraceContext.FluidMode.NONE, ent));
        if(ray == null || ray.getHitVec() == null) {
            return end;
        }
        return ray.getHitVec();
    }

    @SuppressWarnings("unchecked")
    public static <T extends Entity> T getTargetedEntity(PlayerEntity p, double radius, Class<T> type) {
        World w = p.getEntityWorld().getWorld();
        Vec3d l = getTargetVector(p, radius);
        Vec3d eye = p.getEyePosition(1.0f);
        Vec3d unit = new Vec3d(l.x - eye.x, l.y - eye.y, l.z - eye.z);

        List<Entity> col = w.getEntitiesWithinAABBExcludingEntity(p, new AxisAlignedBB(eye.x, eye.y, eye.z, l.x, l.y, l.z));
        col.removeIf(ent -> !type.isAssignableFrom(ent.getClass()));
        // removes entities, which do not intersect with the sight vector
        col.removeIf(ent -> !doesIntersect(ent.getBoundingBox().grow(0.1), eye, unit));

        return (T) col.stream().sorted((Entity e1, Entity e2) -> {
            return Double.compare(e1.getDistanceSq(eye.x, eye.y, eye.z), e2.getDistanceSq(eye.x, eye.y, eye.z));
        }).findFirst().orElse(null);
    }

    // -------------------------------------------------------------------------
    // Player-Tools
    // -------------------------------------------------------------------------
    public static ServerPlayerEntity getPlayerByName(MinecraftServer server, String name) {
        String nameLower = name.toLowerCase();
        return server.getPlayerList().getPlayers().stream()
                .filter(pl -> pl.getName().getUnformattedComponentText().toLowerCase().contains(nameLower))
                .findFirst().orElse(null);
    }
}
