package tc.oc.pgm.damage;

import java.util.HashSet;
import java.util.Set;
import javax.inject.Inject;

import org.bukkit.Location;
import org.bukkit.World;
import org.bukkit.entity.Entity;
import org.bukkit.entity.LivingEntity;
import org.bukkit.entity.Player;
import org.bukkit.inventory.EquipmentSlot;
import tc.oc.commons.bukkit.util.NMSHacks;
import tc.oc.pgm.map.MapInfo;
import tc.oc.pgm.match.Match;
import tc.oc.pgm.match.MatchPlayer;
import tc.oc.pgm.match.MatchPlayerFacet;
import tc.oc.pgm.match.MatchScope;
import tc.oc.pgm.match.Repeatable;

public class HitboxPlayerFacet implements MatchPlayerFacet {

    private static final double FAKE_WIDTH = 0.6;
    private static final double FAKE_HEIGHT = 1.8;
    private static final double PLAYER_WIDTH = 0.6;
    private static final double PLAYER_HEIGHT = 1.8;
    private static final double VIEW_RADIUS = 12;

    private final NMSHacks.FakeEntity[] fakes = new NMSHacks.FakeEntity[4];
    private final Location[] locations = new Location[4];

    private final Match match;
    private final HitboxMatchModule mm;
    private final World world;
    private final MatchPlayer matchPlayer;
    private final Player player;
    private final MapInfo mapInfo;
    private final Set<Player> viewers = new HashSet<>();
    private double width = PLAYER_WIDTH;

    @Inject HitboxPlayerFacet(Match match, World world, MatchPlayer matchPlayer, Player player, MapInfo mapInfo) {
        this.match = match;
        this.mapInfo = mapInfo;
        this.mm = match.needMatchModule(HitboxMatchModule.class);
        this.world = world;
        this.matchPlayer = matchPlayer;
        this.player = player;
    }

    @Override
    public void enable() {
        for(int i = 0; i < fakes.length; i++) {
            fakes[i] = new NMSHacks.FakeZombie(world, true);
            locations[i] = new Location(world, 0, 0, 0);
            mm.facets.put(fakes[i].entityId(), this);
        }
    }

    @Override
    public void disable() {
        for(NMSHacks.FakeEntity fake : fakes) {
            mm.facets.remove(fake.entityId());
        }
    }

    public double getWidth() {
        return width;
    }

    public void setWidth(double width) {
        this.width = width;
    }

    public Cuboid hitbox() {
        if(hasFakeHitbox()) {
            final Location location = player.getLocation();
            final double radius = width / 2;
            return Cuboid.between(location.position().minus(radius, 0, radius),
                                  location.position().plus(radius, PLAYER_HEIGHT, radius));
        } else {
            return player.getBoundingBox();
        }
    }

    public static Cuboid hitbox(Match match, LivingEntity victim) {
        if(victim instanceof Player) {
            final MatchPlayer matchVictim = match.getPlayer(victim);
            if(matchVictim != null) {
                return matchVictim.facet(HitboxPlayerFacet.class).hitbox();
            }
        }
        return victim.getBoundingBox();
    }

    public boolean hasFakeHitbox() {
        return matchPlayer.canInteract() && width > PLAYER_WIDTH;
    }

    public Location meleeHitLocation(LivingEntity attacker, double minDistanceFromAttacker, double offsetFromVictim) {
        return meleeHitLocation(match, player, attacker, minDistanceFromAttacker, offsetFromVictim);
    }

    public static Location meleeHitLocation(Match match, LivingEntity victim, LivingEntity attacker, double minDistanceFromAttacker, double offsetFromVictim) {
        // Find the attacker's line-of-sight distance to the victim
        final Ray ray = attacker.getEyeRay();
        double distance = hitbox(match, victim).intersectionDistance(ray);

        // If attacker is not looking at the victim, take a rough guess at the distance
        if(Double.isNaN(distance)) {
            distance = ray.origin().distance(victim.getEyeLocation().position());
        }

        distance = Math.max(minDistanceFromAttacker, distance + offsetFromVictim);
        return ray.atDistance(distance).toLocation(victim.getWorld());
    }

    void onUse(Player user, boolean attack, EquipmentSlot hand) {
        NMSHacks.useEntity(user, player, attack, hand);
    }

    @Repeatable(scope = MatchScope.RUNNING)
    public void updateViewers() {
        final Location location = player.getLocation();
        updateFakeLocations(location);

        final Set<Player> remove = new HashSet<>(viewers);
        if(hasFakeHitbox()) {
            for(Entity entity : world.getNearbyEntities(location, VIEW_RADIUS, VIEW_RADIUS, VIEW_RADIUS)) {
                if(entity instanceof Player && !entity.equals(player)) {
                    final Player viewer = (Player) entity;
                    if(isAttacker(viewer)) {
                        if(viewers.add(viewer)) {
                            spawnFakes(viewer);
                        } else {
                            remove.remove(viewer);
                            moveFakes(viewer);
                        }
                    }
                }
            }
        }

        for(Player viewer : remove) {
            destroyFakes(viewer);
        }
        viewers.removeAll(remove);
    }

    private boolean isAttacker(Player attacker) {
        final MatchPlayer mp = match.getPlayer(attacker);
        return mp != null &&
               mp.canInteract() &&
               (mapInfo.friendlyFire ||
                !mp.getParty().equals(matchPlayer.getParty()));
    }

    private void updateFakeLocations(Location c) {
        final double y = c.getY() - (FAKE_HEIGHT - PLAYER_HEIGHT) / 2;

        final double h = (width - FAKE_WIDTH) / 2;
        final double px = c.getX() + h;
        final double mx = c.getX() - h;
        final double pz = c.getZ() + h;
        final double mz = c.getZ() - h;

        locations[0].setPosition(mx, y, mz);
        locations[1].setPosition(px, y, mz);
        locations[2].setPosition(mx, y, pz);
        locations[3].setPosition(px, y, pz);
    }

    private void destroyFakes(Player viewer) {
        for(NMSHacks.FakeEntity fake : fakes) {
            fake.destroy(viewer);
        }
    }

    private void spawnFakes(Player viewer) {
        for(int i = 0; i < fakes.length; i++) {
            fakes[i].spawn(viewer, locations[i]);
        }
    }

    private void moveFakes(Player viewer) {
        for(int i = 0; i < fakes.length; i++) {
            fakes[i].teleport(viewer, locations[i]);
        }
    }
}
