package com.minelittlepony.unicopia.ability;

import java.util.Collection;
import java.util.EnumMap;
import java.util.List;
import java.util.Map;
import java.util.Optional;

import org.jetbrains.annotations.Nullable;

import com.minelittlepony.unicopia.*;
import com.minelittlepony.unicopia.ability.data.Hit;
import com.minelittlepony.unicopia.entity.player.Pony;
import com.minelittlepony.unicopia.network.MsgPlayerAbility;
import com.minelittlepony.unicopia.network.Channel;
import com.minelittlepony.unicopia.util.NbtSerialisable;
import com.minelittlepony.unicopia.util.Tickable;

import net.minecraft.nbt.NbtCompound;
import net.minecraft.util.Identifier;

public class AbilityDispatcher implements Tickable, NbtSerialisable {

    private final Pony player;

    private final Map<AbilitySlot, Stat> stats = new EnumMap<>(AbilitySlot.class);

    @Nullable
    private Race prevRace;
    private long maxPage = -1;

    public AbilityDispatcher(Pony player) {
        this.player = player;
    }

    public void clear(AbilitySlot slot, ActivationType pressType, long page) {
        Stat stat = getStat(slot);

        if (stat.canSwitchStates()) {
            stat.clear(pressType, page);
        }
    }

    public Optional<Ability<?>> activate(AbilitySlot slot, long page) {
        Stat stat = getStat(slot);
        if (stat.canSwitchStates()) {
            return stat.getAbility(page).flatMap(stat::setActiveAbility);
        }
        return Optional.empty();
    }

    public Collection<Stat> getStats() {
        return stats.values();
    }

    public Optional<Stat> getActiveStat() {
        return stats.values().stream().filter(stat -> stat.getFillProgress() > 0).findFirst();
    }

    public Stat getStat(AbilitySlot slot) {
        return stats.computeIfAbsent(slot, Stat::new);
    }

    public boolean isFilled(AbilitySlot slot) {
        return getStat(slot).getMaxPage() > 0;
    }

    public int getMaxPage() {
        Race newRace = player.getCompositeRace().collapsed();
        if (maxPage < 0 || prevRace != newRace) {
            prevRace = newRace;
            maxPage = 0;
            for (AbilitySlot slot : AbilitySlot.values()) {
                maxPage = Math.max(maxPage, getStat(slot).getMaxPage() - 1);
            }
        }
        return (int)maxPage;
    }

    @Override
    public void tick() {
        stats.values().forEach(Stat::tick);
    }

    @Override
    public void toNBT(NbtCompound compound) {
        if (compound.contains("stats")) {
            stats.clear();
            NbtCompound li = compound.getCompound("stats");
            li.getKeys().forEach(key -> {
                getStat(AbilitySlot.valueOf(key)).fromNBT(li.getCompound(key));
            });
        }
    }

    @Override
    public void fromNBT(NbtCompound compound) {
        NbtCompound li = new NbtCompound();
        stats.forEach((key, value) -> li.put(key.name(), value.toNBT()));
        compound.put("stats", li);
    }

    public class Stat implements NbtSerialisable {

        /**
         * Ticks of warmup before an ability is triggered.
         */
        private int warmup;
        private int maxWarmup;

        /**
         * Ticks of cooldown after an ability has been triggered.
         */
        private int cooldown;
        private int maxCooldown;

        public final AbilitySlot slot;

        /**
         * True once the current ability has been triggered.
         */
        private boolean triggered;

        private Optional<Ability<?>> activeAbility = Optional.empty();

        private Stat(AbilitySlot slot) {
            this.slot = slot;
        }

        /**
         * Returns true if the current ability can we swapped out.
         */
        boolean canSwitchStates() {
            return !activeAbility.isPresent() || (warmup != 0) || (triggered && cooldown == 0);
        }

        public int getRemainingCooldown() {
            return cooldown;
        }

        public float getFillProgress() {
            float cooldown = getWarmup();
            if (cooldown <= 0 || cooldown >= 1) {
                return getCooldown();
            }
            return 1 - cooldown;
        }

        public float getCooldown() {
            return maxCooldown <= 0 ? 0 : ((float)cooldown / (float)maxCooldown);
        }

        public void setCooldown(int value) {
            cooldown = value;
            maxCooldown = value;
        }

        public float getWarmup() {
            return maxWarmup <= 0 ? 0 : ((float)warmup / (float)maxWarmup);
        }

        public double getCost(long page) {
            if (warmup <= 0) {
                return 0;
            }
            return getAbility(page).map(ability -> ability.getCostEstimate(player)).orElse(0D);
        }

        public void setWarmup(int value) {
            maxWarmup = value;
            warmup = value;
        }

        public void tick() {
            getActiveAbility().ifPresent(ability -> {
                if (warmup > 0) {
                    warmup--;
                    ability.warmUp(player, slot);
                    return;
                }

                if (cooldown > 100 && player.asEntity().isCreative()) {
                    cooldown = Math.max(10, cooldown - 100);
                }

                if (cooldown > 0 && cooldown-- > 0) {
                    ability.coolDown(player, slot);

                    if (cooldown <= 0) {
                        setActiveAbility(null);
                    }
                    return;
                }

                tryFire(ability);
            });
        }

        private <T extends Hit> void tryFire(Ability<T> ability) {
            if (triggered) {
                return;
            }

            triggered = true;
            setCooldown(ability.getCooldownTime(player));

            if (player.isClientPlayer()) {
                Optional<T> data = ability.prepare(player);
                warmup = 0;

                if (data.isPresent()) {
                    InteractionManager.instance().sendPlayerLookAngles(player.asEntity());
                    Channel.CLIENT_PLAYER_ABILITY.sendToServer(new MsgPlayerAbility<>(ability, data, ActivationType.NONE));
                } else {
                    player.asEntity().playSound(USounds.GUI_ABILITY_FAIL, 1, 1);
                    setCooldown(0);
                }
            }

            if (cooldown <= 0) {
                setActiveAbility(null);
            }
        }

        public Optional<Ability<?>> getAbility(long page) {
            List<Ability<?>> found = Abilities.BY_SLOT_AND_COMPOSITE_RACE.apply(slot, player.getCompositeRace());
            if (found.isEmpty()) {
                return Optional.empty();
            }

            return Optional.ofNullable(found.get((int)Math.min(found.size() - 1, page)));
        }

        public void clear(ActivationType pressType, long page) {
            if (pressType == ActivationType.NONE
                    || getAbility(page).filter(ability -> !triggerQuickAction(ability, pressType)).isEmpty()) {
                if (warmup > 0) {
                    getActiveAbility().filter(Ability::activateOnEarlyRelease).ifPresentOrElse(this::tryFire, () -> setActiveAbility(null));
                } else {
                    setActiveAbility(null);
                }
            }
        }

        private <T extends Hit> boolean triggerQuickAction(Ability<T> ability, ActivationType pressType) {
            Optional<T> data = ability.prepareQuickAction(player, pressType);
            if (ability.onQuickAction(player, pressType, data)) {
                Channel.CLIENT_PLAYER_ABILITY.sendToServer(new MsgPlayerAbility<>(ability, data, pressType));
                return true;
            }
            return false;
        }

        public long getMaxPage() {
            return Abilities.BY_SLOT_AND_COMPOSITE_RACE.apply(slot, player.getCompositeRace()).size();
        }

        protected synchronized Optional<Ability<?>> setActiveAbility(@Nullable Ability<?> power) {
            if (activeAbility.orElse(null) != power) {
                triggered = false;
                activeAbility = Optional.ofNullable(power);
                setWarmup(activeAbility.map(p -> p.getWarmupTime(player)).orElse(0));
                setCooldown(0);
                return activeAbility;
            }
            return Optional.empty();
        }

        public synchronized Optional<Ability<?>> getActiveAbility() {
            return activeAbility.filter(ability -> {
                return (!(ability == null || (triggered && warmup == 0 && cooldown == 0)) && ability.canUse(player.getCompositeRace()));
            });
        }

        @Override
        public void toNBT(NbtCompound compound) {
            compound.putInt("warmup", warmup);
            compound.putInt("cooldown", cooldown);
            compound.putInt("maxWarmup", maxWarmup);
            compound.putInt("maxCooldown", maxCooldown);
            compound.putBoolean("triggered", triggered);
            getActiveAbility().ifPresent(ability -> {
                compound.putString("activeAbility", Abilities.REGISTRY.getId(ability).toString());
            });
        }

        @Override
        public void fromNBT(NbtCompound compound) {
            warmup = compound.getInt("warmup");
            cooldown = compound.getInt("cooldown");
            maxWarmup = compound.getInt("maxWarmup");
            maxCooldown = compound.getInt("maxCooldown");
            triggered = compound.getBoolean("triggered");
            activeAbility = Abilities.REGISTRY.getOrEmpty(new Identifier(compound.getString("activeAbility")));
        }
    }
}
