package tc.oc.pgm.mutation.types.targetable;

import com.google.common.collect.ImmutableMap;
import com.google.common.collect.Range;
import org.apache.commons.lang.math.Fraction;
import org.bukkit.Location;
import org.bukkit.Material;
import org.bukkit.entity.Creeper;
import org.bukkit.entity.Entity;
import org.bukkit.entity.EntityType;
import org.bukkit.entity.LivingEntity;
import org.bukkit.entity.PigZombie;
import org.bukkit.entity.Slime;
import org.bukkit.entity.TNTPrimed;
import org.bukkit.entity.Zombie;
import org.bukkit.inventory.EntityEquipment;
import org.bukkit.inventory.ItemStack;
import org.bukkit.util.Vector;
import tc.oc.commons.core.random.ImmutableWeightedRandomChooser;
import tc.oc.commons.core.random.WeightedRandomChooser;
import tc.oc.pgm.match.Match;
import tc.oc.pgm.match.MatchPlayer;
import tc.oc.pgm.match.Repeatable;
import tc.oc.pgm.mutation.types.EntityMutation;
import tc.oc.pgm.mutation.types.kit.EnchantmentMutation;
import tc.oc.pgm.mutation.types.TargetMutation;
import tc.oc.pgm.points.PointProviderAttributes;
import tc.oc.pgm.points.RandomPointProvider;
import tc.oc.pgm.points.RegionPointProvider;
import tc.oc.pgm.regions.CuboidRegion;

import javax.annotation.Nullable;
import java.time.Duration;
import java.time.Instant;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.Optional;

import static tc.oc.commons.core.random.RandomUtils.nextBoolean;

public class ApocalypseMutation extends EntityMutation<LivingEntity> implements TargetMutation {

    final static ImmutableMap<Integer, Integer> AMOUNT_MAP = new ImmutableMap.Builder<Integer, Integer>()
            .put(3,  25)
            .put(5,  20)
            .put(10, 15)
            .put(20, 5)
            .put(50, 1)
            .build();

    final static ImmutableMap<Integer, Integer> STACK_MAP = new ImmutableMap.Builder<Integer, Integer>()
            .put(1, 100)
            .put(2, 25)
            .build();

    final static ImmutableMap<EntityType, Integer> AERIAL_MAP = new ImmutableMap.Builder<EntityType, Integer>()
            .put(EntityType.VEX,   5)
            .put(EntityType.BLAZE, 1)
            .build();

    final static ImmutableMap<EntityType, Integer> GROUND_MAP = new ImmutableMap.Builder<EntityType, Integer>()
            .put(EntityType.SPIDER,      50)
            .put(EntityType.ZOMBIE,      40)
            .put(EntityType.CREEPER,     30)
            .put(EntityType.HUSK,        20)
            .put(EntityType.CAVE_SPIDER, 10)
            .put(EntityType.PIG_ZOMBIE,  1)
            .build();

    final static ImmutableMap<EntityType, Integer> RANGED_MAP = new ImmutableMap.Builder<EntityType, Integer>()
            .put(EntityType.SKELETON,        50)
            .put(EntityType.STRAY,           20)
            .put(EntityType.BLAZE,           20)
            .put(EntityType.GHAST,           10)
            .put(EntityType.SHULKER,         5)
            .put(EntityType.WITCH,           5)
            .put(EntityType.WITHER_SKELETON, 1)
            .build();

    final static ImmutableMap<EntityType, Integer> FLYABLE_MAP = new ImmutableMap.Builder<EntityType, Integer>()
            .putAll(AERIAL_MAP)
            .put(EntityType.BAT, 10)
            .build();

    final static ImmutableMap<EntityType, Integer> PASSENGER_MAP = new ImmutableMap.Builder<EntityType, Integer>()
            .putAll(RANGED_MAP)
            .put(EntityType.CREEPER,   40)
            .put(EntityType.PRIMED_TNT, 1)
            .build();

    final static ImmutableMap<EntityType, Integer> CUBE_MAP = new ImmutableMap.Builder<EntityType, Integer>()
            .put(EntityType.SLIME,       10)
            .put(EntityType.MAGMA_CUBE,  1)
            .build();

    final static WeightedRandomChooser<Integer, Integer> AMOUNT = new ImmutableWeightedRandomChooser<>(AMOUNT_MAP);
    final static WeightedRandomChooser<Integer, Integer> STACK = new ImmutableWeightedRandomChooser<>(STACK_MAP);
    final static WeightedRandomChooser<EntityType, Integer> AERIAL = new ImmutableWeightedRandomChooser<>(AERIAL_MAP);
    final static WeightedRandomChooser<EntityType, Integer> GROUND = new ImmutableWeightedRandomChooser<>(GROUND_MAP);
    final static WeightedRandomChooser<EntityType, Integer> RANGED = new ImmutableWeightedRandomChooser<>(RANGED_MAP);
    final static WeightedRandomChooser<EntityType, Integer> FLYABLE = new ImmutableWeightedRandomChooser<>(FLYABLE_MAP);
    final static WeightedRandomChooser<EntityType, Integer> PASSENGER = new ImmutableWeightedRandomChooser<>(PASSENGER_MAP);
    final static WeightedRandomChooser<EntityType, Integer> CUBE = new ImmutableWeightedRandomChooser<>(CUBE_MAP);

    final static Range<Integer> FREQUENCY = Range.closed(5, 30); // Seconds between entity spawns
    final static int DISTANCE = 15; // Max distance entities spawn from players
    final static int PARTICIPANT_ENTITIES = 25; // Max entities on the field per participant
    final static int MAX_ENTITIES = 500; // Max total entities on the field
    final static Range<Integer> AIR_OFFSET = Range.closed(DISTANCE / 4, DISTANCE); // Y-axis offset for spawning flying entities
    final static Fraction SPECIAL_CHANCE = Fraction.ONE_FIFTH; // Chance of a special attribute occuring in an entity
    final static int SPECIAL_MULTIPLIER = 3; // Multiplier for special attributes

    long time; // world time
    Instant next; // next time to spawn entities
    final PointProviderAttributes attributes; // attributes to choosing random points

    public ApocalypseMutation(Match match) {
        super(match, LivingEntity.class, false);
        this.attributes = new PointProviderAttributes(null, null, true, false);
    }

    /**
     * Get the maximum amount of entities that can be spawned.
     */
    public int entitiesMax() {
        return Math.min((int) match().participants().count() * PARTICIPANT_ENTITIES, MAX_ENTITIES);
    }

    /**
     * Get the number of available slots are left for additional entities to spawn.
     */
    public int entitiesLeft() {
        return entitiesMax() - world().getLivingEntities().size() + (int) match().participants().count();
    }

    /**
     * Generate a random spawn point given two locations.
     */
    public Optional<Location> location(Location start, Location end) {
        return Optional.ofNullable(new RandomPointProvider(Collections.singleton(new RegionPointProvider(new CuboidRegion(start.position(), end.position()), attributes))).getPoint(match(), null));
    }

    /**
     * Spawn a cohort of entities at the given location.
     * @param location location to spawn the entity.
     * @param ground whether the location is on the ground.
     */
    public void spawn(Location location, boolean ground) {
        int slots = entitiesLeft();
        int queued = AMOUNT.choose(entropy());
        // Remove any entities that may be over the max limit
        despawn(queued - slots);
        // Determine whether the entities should be airborn
        int stack = STACK.choose(entropy());
        boolean air = !ground || nextBoolean(random(), SPECIAL_CHANCE);
        if(air) {
            stack += (stack == 1 && random().nextBoolean() ? 1 : 0);
            location.add(0, entropy().randomInt(AIR_OFFSET), 0);
        }
        // Select the random entity chooser based on ground, air, and stacked
        boolean stacked = stack > 1;
        WeightedRandomChooser<EntityType, Integer> chooser;
        if(air) {
            if(stacked) {
                if(ground) {
                    chooser = nextBoolean(random(), SPECIAL_CHANCE) ? CUBE : FLYABLE;
                } else {
                    chooser = FLYABLE;
                }
            } else {
                chooser = AERIAL;
            }
        } else {
            if(stacked) {
                chooser = GROUND;
            } else {
                chooser = random().nextBoolean() ? GROUND : RANGED;
            }
        }
        // Select the specific entity types for the spawn,
        // all entities will have the same sequence of entity type
        // but may have variations (like armor) between them.
        List<EntityType> types = new ArrayList<>();
        for(int i = 0; i < stack; i++) {
            types.add((i == 0 ? chooser : PASSENGER).choose(entropy()));
        }
        // Spawn the mobs and stack them if required
        for(int i = 0; i < queued; i++) {
            Entity last = null;
            for(EntityType type : types) {
                Entity entity = spawn(location, (Class<LivingEntity>) type.getEntityClass());
                if(last != null) {
                    last.setPassenger(entity);
                }
                last = entity;
            }
        }

    }

    @Override
    public LivingEntity spawn(Location location, Class<LivingEntity> entityClass, @Nullable MatchPlayer owner) {
        LivingEntity entity = super.spawn(location, entityClass, owner);
        EnchantmentMutation enchant = new EnchantmentMutation(match());
        EntityEquipment equipment = entity.getEquipment();
        entity.setVelocity(Vector.getRandom());
        ItemStack held = null;
        switch(entity.getType()) {
            case SKELETON:
            case WITHER_SKELETON:
            case STRAY:
                held = item(Material.BOW);
                break;
            case ZOMBIE:
            case ZOMBIE_VILLAGER:
            case HUSK:
                Zombie zombie = (Zombie) entity;
                zombie.setBaby(nextBoolean(random(), SPECIAL_CHANCE));
                break;
            case PIG_ZOMBIE:
                PigZombie pigZombie = (PigZombie) entity;
                pigZombie.setAngry(true);
                pigZombie.setAnger(Integer.MAX_VALUE);
                held = item(Material.GOLD_SWORD);
                break;
            case CREEPER:
                Creeper creeper = (Creeper) entity;
                creeper.setPowered(nextBoolean(random(), SPECIAL_CHANCE));
                world().strikeLightningEffect(location);
                break;
            case PRIMED_TNT:
                TNTPrimed tnt = (TNTPrimed) entity;
                tnt.setFuseTicks(tnt.getFuseTicks() * SPECIAL_MULTIPLIER);
                break;
            case SLIME:
            case MAGMA_CUBE:
                Slime slime = (Slime) entity;
                slime.setSize(slime.getSize() * SPECIAL_MULTIPLIER);
                break;
            case SKELETON_HORSE:
                world().strikeLightning(location);
                break;
        }
        if(held != null && random().nextBoolean()) {
            enchant.apply(held, equipment);
            equipment.setItemInMainHand(held);
        }
        return entity;
    }

    /**
     * Select the entities that have lived the longest and remove them
     * to make room for new entities.
     * @param amount the amount of entities to despawn.
     */
    public void despawn(long amount) {
        entitiesByTime().limit(Math.max(0, amount)).forEachOrdered(this::despawn);
    }

    @Override
    public void target(List<MatchPlayer> players) {
        // At least one player is required to spawn mobs
        if(players.size() >= 1) {
            Location start, end;
            start = players.get(0).getLocation(); // player 1 is the first location
            if(players.size() >= 2) {
                end = players.get(1).getLocation(); // if player 2 exists, they are the second location
            } else { // if no player 2, generate a random location near player 1
                end = start.clone().add(Vector.getRandom().multiply(DISTANCE));
            }
            Optional<Location> location = location(start, end);
            if(location.isPresent()) { // if the location is safe (on ground)
                spawn(location.get(), true);
            } else { // if the location was not safe, generate a simple midpoint location
                spawn(start.position().midpoint(end.position()).toLocation(world()), false);
            }
        }
    }

    @Override
    public int targets() {
        return 2; // Always require 2 targets to generate a spawn location between them
    }

    @Override
    public Instant next() {
        return next;
    }

    @Override
    public void next(Instant time) {
        next = time;
    }

    @Override
    public Duration frequency() {
        return Duration.ofSeconds(entropy().randomInt(FREQUENCY));
    }

    @Override
    public void enable() {
        super.enable();
        TargetMutation.super.enable();
        time = world().getTime();
    }

    @Repeatable
    public void tick() {
        TargetMutation.super.tick();
        world().setTime(16000); // Night time to prevent flaming entities
    }

    @Override
    public void disable() {
        world().setTime(time);
        despawn(entities().count());
        super.disable();
    }

}
