package tc.oc.pgm.stamina;

import java.util.ArrayDeque;
import java.util.Deque;
import java.util.Map;
import javax.annotation.Nullable;

import com.google.common.base.Predicate;
import com.google.common.base.Strings;
import com.google.common.collect.ImmutableRangeMap;
import com.google.common.collect.ImmutableSet;
import com.google.common.collect.Iterables;
import com.google.common.collect.Range;
import com.google.common.collect.RangeMap;
import com.google.common.collect.Sets;
import net.md_5.bungee.api.ChatColor;
import net.md_5.bungee.api.chat.BaseComponent;
import net.md_5.bungee.api.chat.TranslatableComponent;
import org.bukkit.enchantments.Enchantment;
import org.bukkit.event.block.BlockBreakEvent;
import org.bukkit.event.block.BlockDamageEvent;
import org.bukkit.event.entity.EntityDamageByEntityEvent;
import org.bukkit.event.entity.EntityDamageEvent;
import org.bukkit.event.entity.ProjectileLaunchEvent;
import org.bukkit.event.player.PlayerAnimationEvent;
import org.bukkit.event.player.PlayerAnimationType;
import org.bukkit.event.player.PlayerMoveEvent;
import org.bukkit.inventory.ItemStack;
import org.bukkit.potion.PotionEffect;
import org.bukkit.potion.PotionEffectType;
import tc.oc.commons.bukkit.item.ItemUtils;
import tc.oc.commons.core.chat.Component;
import tc.oc.commons.core.util.Numbers;
import tc.oc.commons.core.util.DefaultMapAdapter;
import tc.oc.pgm.match.MatchPlayer;
import tc.oc.commons.bukkit.event.BlockPunchEvent;
import tc.oc.pgm.stamina.mutators.StaminaMutator;
import tc.oc.pgm.stamina.symptoms.PotionSymptom;
import tc.oc.pgm.stamina.symptoms.StaminaSymptom;

public class PlayerStaminaState {

    final StaminaOptions options;
    final MatchPlayer player;

    double stamina;             // player's stamina (0..1)
    boolean moving;             // moved during current tick
    boolean onGround = true;    // was on the ground for last movement

    static final long SWING_TICK_WINDOW = 2;
    boolean swinging;           // swung weapon within the last SWING_TICK_WINDOW
    long lastSwingTick;         // world tick time of last weapon swing
    
    static final long SPRINT_TICK_WINDOW = 20;
    long lastSprintTick;

    // Map of total amount of stamina consumed by each mutator (see explanation in #mutateStamina)
    final Map<StaminaMutator, Double> depletionCauses = new DefaultMapAdapter<>(0d);

    // last potion effect levels applied to the player from stamina symptoms (zero-based)
    Map<PotionEffectType, Integer> lastPotionLevels = new DefaultMapAdapter<>(-1);

    PlayerStaminaState(StaminaOptions options, MatchPlayer player) {
        this.options = options;
        this.player = player;
        mutateStamina(null, 1d);
    }

    Iterable<StaminaSymptom> getActiveSymptoms() {
        return Iterables.filter(options.symptoms, new Predicate<StaminaSymptom>() {
            @Override
            public boolean apply(StaminaSymptom symptom) {
                return symptom.range.contains(stamina);
            }
        });
    }

    void mutateStamina(@Nullable StaminaMutator mutator, double newStamina) {
        newStamina = Numbers.clamp(newStamina, 0, 1);
        if(stamina == newStamina) return;

        double oldStamina = stamina;
        stamina = newStamina;

        if(newStamina < oldStamina) {
            if(mutator != null) {
                // If stamina went down, add the loss to the total for the causing mutator.
                depletionCauses.put(mutator, depletionCauses.get(mutator) + oldStamina - newStamina);
            }
        } else {
            // If stamina went up, reduce all depletion causes proportionally.
            for(StaminaMutator cause : ImmutableSet.copyOf(depletionCauses.keySet())) {
                depletionCauses.put(cause, depletionCauses.get(cause) / (1 - oldStamina) * (1 - newStamina));
            }
        }

        applySymptoms(oldStamina);
        refreshPotions();
        refreshMeter();
    }

    void mutateStamina(StaminaMutator mutator) {
        mutateStamina(mutator, mutator.getNumericModifier().credit(stamina));
    }

    void mutateStaminaTick(StaminaMutator mutator) {
        mutateStamina(mutator, mutator.getNumericModifier().credit(stamina, 1d / 20d));
    }

    void applySymptoms(double oldStamina) {
        for(StaminaSymptom symptom : options.symptoms) {
            if(!symptom.range.contains(oldStamina) && symptom.range.contains(stamina)) {
                symptom.apply(player);
            } else if(symptom.range.contains(oldStamina) && !symptom.range.contains(stamina)) {
                symptom.remove(player);
            }
        }
    }

    // Number of bars
    private static final int METER_SCALE = 88;
    private static final RangeMap<Double, ChatColor> METER_COLORS = ImmutableRangeMap.<Double, ChatColor>builder()
        .put(Range.closedOpen(0.2, 0.3), ChatColor.BLUE)
        .put(Range.closedOpen(0.3, 0.5), ChatColor.DARK_AQUA)
        .put(Range.closedOpen(0.5, 0.7), ChatColor.AQUA)
        .put(Range.closedOpen(0.7, 1.0), ChatColor.WHITE)
        .put(Range.singleton(1.0), ChatColor.YELLOW)
        .build();

    private static final BaseComponent SPACE = new Component(" ");

    void refreshMeter() {
        BaseComponent label;
        ChatColor color = METER_COLORS.get(stamina);

        if(color != null) {
            int segments = METER_COLORS.asMapOfRanges().size();
            Deque<BaseComponent> parts = new ArrayDeque<>(segments * 2 + 3);

            parts.add(SPACE);
            parts.add(new TranslatableComponent("stamina.label"));
            parts.add(SPACE);

            for(Map.Entry<Range<Double>, ChatColor> entry : METER_COLORS.asMapOfRanges().entrySet()) {
                int length = (int) (METER_SCALE * (Numbers.clamp(stamina, entry.getKey()) - entry.getKey().lowerEndpoint()));
                Component segment = new Component(Strings.repeat("\u23d0", length), entry.getValue());
                parts.addFirst(segment);
                parts.addLast(segment);
            }

            label = new Component(color).extra(parts);
        } else {
            StaminaMutator cause = getDepletionCause();
            if(cause != null) {
                label = new Component(new TranslatableComponent("stamina.depletedFromMutator",
                                                                new Component(cause.getDescription(), ChatColor.AQUA)),
                                      ChatColor.RED);
            } else {
                label = new Component(new TranslatableComponent("stamina.depleted"), ChatColor.RED);
            }
        }

        player.sendHotbarMessage(label);
    }

    @Nullable StaminaMutator getDepletionCause() {
        StaminaMutator cause = null;
        double max = 0;
        for(Map.Entry<StaminaMutator, Double> entry : depletionCauses.entrySet()) {
            if(entry.getValue() > max) {
                max = entry.getValue();
                cause = entry.getKey();
            }
        }
        return cause;
    }

    void refreshPotions() {
        // Get the maximum level of every potion type caused by current symptoms.
        // It would be nice if PotionSymptom could do this itself, but there doesn't
        // seem to be an easy way.
        Map<PotionEffectType, Integer> newPotionLevels = new DefaultMapAdapter<>(-1);
        for(StaminaSymptom symptom : getActiveSymptoms()) {
            if(symptom instanceof PotionSymptom) {
                PotionSymptom potionSymptom = (PotionSymptom) symptom;
                int maxLevel = newPotionLevels.get(potionSymptom.effect);
                if(maxLevel < potionSymptom.amplifier) {
                    newPotionLevels.put(potionSymptom.effect, potionSymptom.amplifier);
                }
            }
        }

        // Sync the client's effects
        for(PotionEffectType effect : ImmutableSet.copyOf(Sets.union(lastPotionLevels.keySet(), newPotionLevels.keySet()))) {
            int newLevel = newPotionLevels.get(effect);
            if(newLevel != lastPotionLevels.get(effect)) {
                if(newLevel > -1) {
                    lastPotionLevels.put(effect, newLevel);
                    player.getBukkit().addPotionEffect(new PotionEffect(effect, Integer.MAX_VALUE, newLevel, true), true);
                } else {
                    // We remove an effect by setting its time to 2 seconds. This provides a bit of
                    // hysteresis, and also avoids a bug that causes a nether portal to appear when
                    // a nausea potion is removed suddenly.
                    int oldLevel = lastPotionLevels.remove(effect);
                    player.getBukkit().addPotionEffect(new PotionEffect(effect, 40, oldLevel, true), true);
                }
            }
        }
    }

    void tick() {
        long now = player.getMatch().getClock().now().tick;

        if(player.getBukkit().isSprinting()) {
            lastSprintTick = now;
        }
        
        // To detect movement, we just set the moving flag on every move event,
        // and check and clear it here every tick.
        if(moving) {
            moving = false;
            if(lastSprintTick + SPRINT_TICK_WINDOW > now) {
                mutateStaminaTick(options.mutators.run);
            } else if(player.getBukkit().isSneaking()) {
                mutateStaminaTick(options.mutators.sneak);
            } else {
                mutateStaminaTick(options.mutators.walk);
            }
        } else {
            mutateStaminaTick(options.mutators.stand);
        }

        // If a swing was not explained by some other event within the time window,
        // assume it was a swing at the air.
        if(swinging && lastSwingTick + SWING_TICK_WINDOW <= now) {
            swinging = false;
            mutateStamina(options.mutators.meleeMiss);
        }

        refreshMeter();
    }

    void onEvent(PlayerMoveEvent event) {
        moving = true;

        if(onGround && !player.getBukkit().isOnGround() && event.getFrom().getY() < event.getTo().getY()) {
            if(player.getBukkit().isSprinting()) {
                mutateStamina(options.mutators.runJump);
            } else {
                mutateStamina(options.mutators.jump);
            }
        }

        onGround = event.getPlayer().isOnGround();
    }

    boolean isHoldingWeapon() {
        ItemStack holding = player.getBukkit().getItemInHand();
        return holding != null && (ItemUtils.isWeapon(holding) ||
                                   holding.getEnchantmentLevel(Enchantment.DAMAGE_ALL) > 0);
    }

    void onEvent(PlayerAnimationEvent event) {
        if(event.getAnimationType() != PlayerAnimationType.ARM_SWING) return;
        if(event.getPlayer().isDigging()) return;
        if(!isHoldingWeapon()) return;

        swinging = true;
        lastSwingTick = player.getMatch().getClock().now().tick;
    }

    void onEvent(BlockPunchEvent event) {
        swinging = false;
    }

    void onEvent(BlockDamageEvent event) {
        swinging = false;
    }

    void onEvent(BlockBreakEvent event) {
        swinging = false;
    }

    void onEvent(EntityDamageEvent event) {
        if(event.getEntity() == player.getBukkit()) {
            // Player took damage
            mutateStamina(options.mutators.injury);

        } else if(event.getCause() == EntityDamageEvent.DamageCause.ENTITY_ATTACK &&
                  event instanceof EntityDamageByEntityEvent &&
                  ((EntityDamageByEntityEvent) event).getDamager() == player.getBukkit()) {

            // Player is damager and attack is melee
            swinging = false;

            for(StaminaSymptom symptom : getActiveSymptoms()) {
                symptom.onAttack(event);
            }

            mutateStamina(options.mutators.meleeHit);
        }
    }

    void onEvent(ProjectileLaunchEvent event) {
        for(StaminaSymptom symptom : getActiveSymptoms()) {
            symptom.onShoot(event);
        }

        mutateStamina(options.mutators.archery);
    }
}