package mcjty.mymod.tools;

import com.google.common.base.Predicate;
import com.google.common.base.Predicates;
import net.minecraft.entity.Entity;
import net.minecraft.entity.player.EntityPlayer;
import net.minecraft.util.EntitySelectors;
import net.minecraft.util.math.AxisAlignedBB;
import net.minecraft.util.math.RayTraceResult;
import net.minecraft.util.math.Vec3d;
import net.minecraft.world.World;
import org.apache.commons.lang3.tuple.Pair;

import javax.annotation.Nullable;
import java.util.ArrayList;
import java.util.Comparator;
import java.util.List;
import java.util.function.Function;

public class RayTraceTools {

    // Trace a beam from the player in the direction the player is looking. Stop if a block is hit. Call
    // the function on all hit entities sorted by distance (closest entities first). Stop if the function returns true
    public static void rayTrace(Beam beam, Function<Entity, Boolean> consumer) {
        // Based on EntityRender.getMouseOver(float partialTicks) which we can't use because that's client only
        Vec3d start = beam.getStart();
        Vec3d lookVec = beam.getLookVec();
        Vec3d end = beam.getEnd();
        double dist = beam.getDist();
        World world = beam.getWorld();
        EntityPlayer player = beam.getPlayer();
        List<Entity> targets = world.getEntitiesInAABBexcluding(player, player.getEntityBoundingBox().expand(lookVec.x * dist, lookVec.y * dist, lookVec.z * dist).grow(1.0D, 1.0D, 1.0D),
                Predicates.and(EntitySelectors.NOT_SPECTATING, ent -> ent != null && ent.canBeCollidedWith()));
        List<Pair<Entity, Double>> hitTargets = new ArrayList<>();
        for (Entity target : targets) {
            AxisAlignedBB targetBB = target.getEntityBoundingBox().grow(target.getCollisionBorderSize());
            if (targetBB.contains(start)) {
                hitTargets.add(Pair.of(target, 0.0));
            } else {
                RayTraceResult targetResult = targetBB.calculateIntercept(start, end);
                if (targetResult != null) {
                    double d3 = start.distanceTo(targetResult.hitVec);
                    if (d3 < dist) {
                        hitTargets.add(Pair.of(target, d3));
                    }
                }
            }
        }
        hitTargets.sort(Comparator.comparing(Pair::getRight));
        hitTargets.stream().filter(pair -> consumer.apply(pair.getLeft())).findFirst();
    }

    public static class Beam {
        private World world;
        private EntityPlayer player;
        private double maxDist;
        private Vec3d start;
        private Vec3d lookVec;
        private Vec3d end;
        private double dist;

        public Beam(World world, EntityPlayer player, double maxDist) {
            this.world = world;
            this.player = player;
            this.maxDist = maxDist;

            calculate();
        }

        private void calculate() {
            start = this.player.getPositionEyes(1.0f);
            lookVec = this.player.getLookVec();
            end = start.add(lookVec.x * this.maxDist, lookVec.y * this.maxDist, lookVec.z * this.maxDist);

            // Find out if there is a block blocking our beam
            RayTraceResult result = this.world.rayTraceBlocks(start, end);
            dist = this.maxDist;
            if (result != null && result.typeOfHit == RayTraceResult.Type.BLOCK) {
                // Adjust our maximum distance
                dist = result.hitVec.distanceTo(start);
                end = start.add(lookVec.x * dist, lookVec.y * dist, lookVec.z * dist);
            }
        }

        public Vec3d getStart() {
            return start;
        }

        public Vec3d getLookVec() {
            return lookVec;
        }

        public Vec3d getEnd() {
            return end;
        }

        public double getDist() {
            return dist;
        }

        public World getWorld() {
            return world;
        }

        public EntityPlayer getPlayer() {
            return player;
        }

        public double getMaxDist() {
            return maxDist;
        }
    }
}