package emu.grasscutter.game.avatar;

import static emu.grasscutter.config.Configuration.GAME_OPTIONS;

import java.util.ArrayList;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import java.util.Set;

import org.bson.types.ObjectId;

import dev.morphia.annotations.Entity;
import dev.morphia.annotations.Id;
import dev.morphia.annotations.Indexed;
import dev.morphia.annotations.PostLoad;
import dev.morphia.annotations.PrePersist;
import dev.morphia.annotations.Transient;
import emu.grasscutter.data.GameData;
import emu.grasscutter.data.binout.OpenConfigEntry;
import emu.grasscutter.data.binout.OpenConfigEntry.SkillPointModifier;
import emu.grasscutter.data.common.FightPropData;
import emu.grasscutter.data.excels.AvatarData;
import emu.grasscutter.data.excels.AvatarPromoteData;
import emu.grasscutter.data.excels.AvatarSkillData;
import emu.grasscutter.data.excels.AvatarSkillDepotData;
import emu.grasscutter.data.excels.AvatarTalentData;
import emu.grasscutter.data.excels.EquipAffixData;
import emu.grasscutter.data.excels.ProudSkillData;
import emu.grasscutter.data.excels.ReliquaryAffixData;
import emu.grasscutter.data.excels.ReliquaryLevelData;
import emu.grasscutter.data.excels.ReliquaryMainPropData;
import emu.grasscutter.data.excels.ReliquarySetData;
import emu.grasscutter.data.excels.WeaponCurveData;
import emu.grasscutter.data.excels.WeaponPromoteData;
import emu.grasscutter.data.excels.AvatarSkillDepotData.InherentProudSkillOpens;
import emu.grasscutter.data.excels.ItemData.WeaponProperty;
import emu.grasscutter.database.DatabaseHelper;
import emu.grasscutter.game.entity.EntityAvatar;
import emu.grasscutter.game.inventory.EquipType;
import emu.grasscutter.game.inventory.GameItem;
import emu.grasscutter.game.inventory.ItemType;
import emu.grasscutter.game.player.Player;
import emu.grasscutter.game.props.ElementType;
import emu.grasscutter.game.props.EntityIdType;
import emu.grasscutter.game.props.FetterState;
import emu.grasscutter.game.props.FightProperty;
import emu.grasscutter.game.props.PlayerProperty;
import emu.grasscutter.net.proto.AvatarFetterInfoOuterClass.AvatarFetterInfo;
import emu.grasscutter.net.proto.AvatarInfoOuterClass.AvatarInfo;
import emu.grasscutter.net.proto.AvatarSkillInfoOuterClass.AvatarSkillInfo;
import emu.grasscutter.net.proto.FetterDataOuterClass.FetterData;
import emu.grasscutter.net.proto.ShowAvatarInfoOuterClass;
import emu.grasscutter.net.proto.ShowAvatarInfoOuterClass.ShowAvatarInfo;
import emu.grasscutter.net.proto.ShowEquipOuterClass.ShowEquip;
import emu.grasscutter.server.packet.send.PacketAbilityChangeNotify;
import emu.grasscutter.server.packet.send.PacketAvatarEquipChangeNotify;
import emu.grasscutter.server.packet.send.PacketAvatarFightPropNotify;
import emu.grasscutter.utils.ProtoHelper;
import it.unimi.dsi.fastutil.ints.Int2FloatOpenHashMap;
import it.unimi.dsi.fastutil.ints.Int2IntOpenHashMap;
import it.unimi.dsi.fastutil.ints.Int2ObjectMap;
import it.unimi.dsi.fastutil.ints.Int2ObjectOpenHashMap;
import lombok.Getter;

@Entity(value = "avatars", useDiscriminator = false)
public class Avatar {
    @Id private ObjectId id;
    @Indexed private int ownerId;	// Id of player that this avatar belongs to

    @Transient private Player owner;
    @Transient private AvatarData data;
    @Transient private AvatarSkillDepotData skillDepot;
    @Transient private long guid;	// Player unique id
    private int avatarId;			// Id of avatar

    private int level = 1;
    private int exp;
    private int promoteLevel;
    private int satiation; // ?
    private int satiationPenalty; // ?
    private float currentHp;
    private float currentEnergy;

    @Transient private final Int2ObjectMap<GameItem> equips;
    @Transient private final Int2FloatOpenHashMap fightProp;
    @Transient @Getter private final Int2FloatOpenHashMap fightPropOverrides;
    @Transient private Set<String> extraAbilityEmbryos;

    private List<Integer> fetters;

    private Map<Integer, Integer> skillLevelMap; // Talent levels
    private Map<Integer, Integer> skillExtraChargeMap; // Charges
    private Map<Integer, Integer> proudSkillBonusMap; // Talent bonus levels (from const)
    private int skillDepotId;
    private int coreProudSkillLevel; // Constellation level
    private Set<Integer> talentIdList; // Constellation id list
    private Set<Integer> proudSkillList; // Character passives

    private int flyCloak;
    private int costume;
    private int bornTime;

    private int fetterLevel = 1;
    private int fetterExp;

    private int nameCardRewardId;
    private int nameCardId;

    @Deprecated // Do not use. Morhpia only!
    public Avatar() {
        this.equips = new Int2ObjectOpenHashMap<>();
        this.fightProp = new Int2FloatOpenHashMap();
        this.fightPropOverrides = new Int2FloatOpenHashMap();
        this.extraAbilityEmbryos = new HashSet<>();
        this.proudSkillBonusMap = new HashMap<>();
        this.fetters = new ArrayList<>(); // TODO Move to avatar
    }

    // On creation
    public Avatar(int avatarId) {
        this(GameData.getAvatarDataMap().get(avatarId));
    }

    public Avatar(AvatarData data) {
        this();
        this.avatarId = data.getId();
        this.nameCardRewardId = data.getNameCardRewardId();
        this.nameCardId = data.getNameCardId();
        this.data = data;
        this.bornTime = (int) (System.currentTimeMillis() / 1000);
        this.flyCloak = 140001;

        this.skillLevelMap = new HashMap<>();
        this.skillExtraChargeMap = new HashMap<>();
        this.talentIdList = new HashSet<>();
        this.proudSkillList = new HashSet<>();

        // Combat properties
        for (FightProperty prop : FightProperty.values()) {
            if (prop.getId() <= 0 || prop.getId() >= 3000) {
                continue;
            }
            this.setFightProperty(prop, 0f);
        }

        // Skill depot
        this.setSkillDepotData(getAvatarData().getSkillDepot());

        // Set stats
        this.recalcStats();
        this.currentHp = getFightProperty(FightProperty.FIGHT_PROP_MAX_HP);
        setFightProperty(FightProperty.FIGHT_PROP_CUR_HP, this.currentHp);
        this.currentEnergy = 0f;
        // Load handler
        this.onLoad();
    }

    public Player getPlayer() {
        return this.owner;
    }

    public ObjectId getObjectId() {
        return id;
    }

    public AvatarData getAvatarData() {
        return data;
    }

    protected void setAvatarData(AvatarData data) {
        if (this.data != null) return;
        this.data = data; // Used while loading this from the database
    }

    public int getOwnerId() {
        return ownerId;
    }

    public void setOwner(Player player) {
        this.owner = player;
        this.ownerId = player.getUid();
        this.guid = player.getNextGameGuid();
    }

    public int getSatiation() {
        return satiation;
    }

    public void setSatiation(int satiation) {
        this.satiation = satiation;
    }

    public int getNameCardRewardId() {
        return nameCardRewardId;
    }

    public void setNameCardRewardId(int nameCardRewardId) {
        this.nameCardRewardId = nameCardRewardId;
    }

    public int getSatiationPenalty() {
        return satiationPenalty;
    }

    public void setSatiationPenalty(int satiationPenalty) {
        this.satiationPenalty = satiationPenalty;
    }

    public AvatarData getData() {
        return data;
    }

    public long getGuid() {
        return guid;
    }

    public int getAvatarId() {
        return avatarId;
    }

    public int getLevel() {
        return level;
    }

    public void setLevel(int level) {
        this.level = level;
    }

    public int getExp() {
        return exp;
    }

    public void setExp(int exp) {
        this.exp = exp;
    }

    public int getPromoteLevel() {
        return promoteLevel;
    }

    public void setPromoteLevel(int promoteLevel) {
        this.promoteLevel = promoteLevel;
    }

    static public int getMinPromoteLevel(int level) {
        if (level > 80) {
            return 6;
        } else if (level > 70) {
            return 5;
        } else if (level > 60) {
            return 4;
        } else if (level > 50) {
            return 3;
        } else if (level > 40) {
            return 2;
        } else if (level > 20) {
            return 1;
        }
        return 0;
    }

    public Int2ObjectMap<GameItem> getEquips() {
        return equips;
    }

    public GameItem getEquipBySlot(EquipType slot) {
        return this.getEquips().get(slot.getValue());
    }

    private GameItem getEquipBySlot(int slotId) {
        return this.getEquips().get(slotId);
    }

    public GameItem getWeapon() {
        return this.getEquipBySlot(EquipType.EQUIP_WEAPON);
    }

    public int getSkillDepotId() {
        return skillDepotId;
    }

    public AvatarSkillDepotData getSkillDepot() {
        return skillDepot;
    }

    protected void setSkillDepot(AvatarSkillDepotData skillDepot) {
        if (this.skillDepot != null) return;
        this.skillDepot = skillDepot; // Used while loading this from the database
    }

    public void setSkillDepotData(AvatarSkillDepotData skillDepot) {
        // Set id and depot
        this.skillDepotId = skillDepot.getId();
        this.skillDepot = skillDepot;
        // Clear, then add skills
        getSkillLevelMap().clear();
        if (skillDepot.getEnergySkill() > 0) {
            getSkillLevelMap().put(skillDepot.getEnergySkill(), 1);
        }
        for (int skillId : skillDepot.getSkills()) {
            if (skillId > 0) {
                getSkillLevelMap().put(skillId, 1);
            }
        }
        // Add proud skills
        this.getProudSkillList().clear();
        for (InherentProudSkillOpens openData : skillDepot.getInherentProudSkillOpens()) {
            if (openData.getProudSkillGroupId() == 0) {
                continue;
            }
            if (openData.getNeedAvatarPromoteLevel() <= this.getPromoteLevel()) {
                int proudSkillId = (openData.getProudSkillGroupId() * 100) + 1;
                if (GameData.getProudSkillDataMap().containsKey(proudSkillId)) {
                    this.getProudSkillList().add(proudSkillId);
                }
            }
        }
    }

    public Map<Integer, Integer> getSkillLevelMap() {
        return skillLevelMap;
    }

    public Map<Integer, Integer> getSkillExtraChargeMap() {
        if (skillExtraChargeMap == null) {
            skillExtraChargeMap = new HashMap<>();
        }
        return skillExtraChargeMap;
    }

    public Map<Integer, Integer> getProudSkillBonusMap() {
        return proudSkillBonusMap;
    }

    public Set<String> getExtraAbilityEmbryos() {
        return extraAbilityEmbryos;
    }

    public void setFetterList(List<Integer> fetterList) {
        this.fetters = fetterList;
    }

    public List<Integer> getFetterList() {
        return fetters;
    }

    public int getFetterLevel() {
        return fetterLevel;
    }

    public void setFetterLevel(int fetterLevel) {
        this.fetterLevel = fetterLevel;
    }

    public int getFetterExp() {
        return fetterExp;
    }

    public void setFetterExp(int fetterExp) {
        this.fetterExp = fetterExp;
    }

    public int getNameCardId() {
        return nameCardId;
    }

    public void setNameCardId(int nameCardId) {
        this.nameCardId = nameCardId;
    }

    public float getCurrentHp() {
        return currentHp;
    }

    public void setCurrentHp(float currentHp) {
        this.currentHp = currentHp;
    }

    public void setCurrentEnergy() {
        if (GAME_OPTIONS.energyUsage) {
            this.setCurrentEnergy(this.currentEnergy);
        }
    }

    public void setCurrentEnergy(float currentEnergy) {
        if (this.getSkillDepot() != null && this.getSkillDepot().getEnergySkillData() != null) {
            ElementType element = this.getSkillDepot().getElementType();
            this.setFightProperty(element.getMaxEnergyProp(), this.getSkillDepot().getEnergySkillData().getCostElemVal());

            if (GAME_OPTIONS.energyUsage) {
                this.setFightProperty(element.getCurEnergyProp(), currentEnergy);
            }
            else {
                this.setFightProperty(element.getCurEnergyProp(), this.getSkillDepot().getEnergySkillData().getCostElemVal());
            }
        }
    }

    public void setCurrentEnergy(FightProperty curEnergyProp, float currentEnergy) {
        if (GAME_OPTIONS.energyUsage) {
            this.setFightProperty(curEnergyProp, currentEnergy);
            this.currentEnergy = currentEnergy;
            this.save();
        }
    }

    public Int2FloatOpenHashMap getFightProperties() {
        return fightProp;
    }

    public void setFightProperty(FightProperty prop, float value) {
        this.getFightProperties().put(prop.getId(), value);
    }

    private void setFightProperty(int id, float value) {
        this.getFightProperties().put(id, value);
    }

    public void addFightProperty(FightProperty prop, float value) {
        this.getFightProperties().put(prop.getId(), getFightProperty(prop) + value);
    }

    public float getFightProperty(FightProperty prop) {
        return getFightProperties().getOrDefault(prop.getId(), 0f);
    }

    public Set<Integer> getTalentIdList() {
        return talentIdList;
    }

    public int getCoreProudSkillLevel() {
        return coreProudSkillLevel;
    }

    public void setCoreProudSkillLevel(int constLevel) {
        this.coreProudSkillLevel = constLevel;
    }

    public Set<Integer> getProudSkillList() {
        return proudSkillList;
    }

    public int getFlyCloak() {
        return flyCloak;
    }

    public void setFlyCloak(int flyCloak) {
        this.flyCloak = flyCloak;
    }

    public int getCostume() {
        return costume;
    }

    public void setCostume(int costume) {
        this.costume = costume;
    }

    public int getBornTime() {
        return bornTime;
    }

    public boolean equipItem(GameItem item, boolean shouldRecalc) {
        // Sanity check equip type
        EquipType itemEquipType = item.getItemData().getEquipType();
        if (itemEquipType == EquipType.EQUIP_NONE) {
            return false;
        }

        // Check if other avatars have this item equipped
        Avatar otherAvatar = getPlayer().getAvatars().getAvatarById(item.getEquipCharacter());
        if (otherAvatar != null) {
            // Unequip other avatar's item
            if (otherAvatar.unequipItem(item.getItemData().getEquipType())) {
                getPlayer().sendPacket(new PacketAvatarEquipChangeNotify(otherAvatar, item.getItemData().getEquipType()));
            }
            // Swap with other avatar
            if (getEquips().containsKey(itemEquipType.getValue())) {
                GameItem toSwap = this.getEquipBySlot(itemEquipType);
                otherAvatar.equipItem(toSwap, false);
            }
            // Recalc
            otherAvatar.recalcStats();
        } else if (getEquips().containsKey(itemEquipType.getValue())) {
            // Unequip item in current slot if it exists
            unequipItem(itemEquipType);
        }

        // Set equip
        getEquips().put(itemEquipType.getValue(), item);

        if (itemEquipType == EquipType.EQUIP_WEAPON && getPlayer().getWorld() != null) {
            item.setWeaponEntityId(this.getPlayer().getWorld().getNextEntityId(EntityIdType.WEAPON));
        }

        item.setEquipCharacter(this.getAvatarId());
        item.save();

        if (this.getPlayer().hasSentLoginPackets()) {
            this.getPlayer().sendPacket(new PacketAvatarEquipChangeNotify(this, item));
        }

        if (shouldRecalc) {
            this.recalcStats();
        }

        return true;
    }

    public boolean unequipItem(EquipType slot) {
        GameItem item = getEquips().remove(slot.getValue());

        if (item != null) {
            item.setEquipCharacter(0);
            item.save();
            return true;
        }

        return false;
    }

    public void recalcStats() {
        recalcStats(false);
    }

    public void recalcStats(boolean forceSendAbilityChange) {
        // Setup
        AvatarData data = this.getAvatarData();
        AvatarPromoteData promoteData = GameData.getAvatarPromoteData(data.getAvatarPromoteId(), this.getPromoteLevel());
        Int2IntOpenHashMap setMap = new Int2IntOpenHashMap();

        // Extra ability embryos
        Set<String> prevExtraAbilityEmbryos = this.getExtraAbilityEmbryos();
        this.extraAbilityEmbryos = new HashSet<>();

        // Fetters
        this.setFetterList(data.getFetters());
        this.setNameCardRewardId(data.getNameCardRewardId());
        this.setNameCardId(data.getNameCardId());

        // Get hp percent, set to 100% if none
        float hpPercent = this.getFightProperty(FightProperty.FIGHT_PROP_MAX_HP) <= 0 ? 1f : this.getFightProperty(FightProperty.FIGHT_PROP_CUR_HP) / this.getFightProperty(FightProperty.FIGHT_PROP_MAX_HP);

        // Store current energy value for later
        float currentEnergy = (this.getSkillDepot() != null) ? this.getFightProperty(this.getSkillDepot().getElementType().getCurEnergyProp()) : 0f;

        // Clear properties
        this.getFightProperties().clear();

        // Base stats
        this.setFightProperty(FightProperty.FIGHT_PROP_BASE_HP, data.getBaseHp(this.getLevel()));
        this.setFightProperty(FightProperty.FIGHT_PROP_BASE_ATTACK, data.getBaseAttack(this.getLevel()));
        this.setFightProperty(FightProperty.FIGHT_PROP_BASE_DEFENSE, data.getBaseDefense(this.getLevel()));
        this.setFightProperty(FightProperty.FIGHT_PROP_CRITICAL, data.getBaseCritical());
        this.setFightProperty(FightProperty.FIGHT_PROP_CRITICAL_HURT, data.getBaseCriticalHurt());
        this.setFightProperty(FightProperty.FIGHT_PROP_CHARGE_EFFICIENCY, 1f);

        if (promoteData != null) {
            for (FightPropData fightPropData : promoteData.getAddProps()) {
                this.addFightProperty(fightPropData.getProp(), fightPropData.getValue());
            }
        }

        // Set energy usage
        setCurrentEnergy(currentEnergy);

        // Artifacts
        for (int slotId = 1; slotId <= 5; slotId++) {
            // Get artifact
            GameItem equip = this.getEquipBySlot(slotId);
            if (equip == null) {
                continue;
            }
            // Artifact main stat
            ReliquaryMainPropData mainPropData = GameData.getReliquaryMainPropDataMap().get(equip.getMainPropId());
            if (mainPropData != null) {
                ReliquaryLevelData levelData = GameData.getRelicLevelData(equip.getItemData().getRankLevel(), equip.getLevel());
                if (levelData != null) {
                    this.addFightProperty(mainPropData.getFightProp(), levelData.getPropValue(mainPropData.getFightProp()));
                }
            }
            // Artifact sub stats
            for (int appendPropId : equip.getAppendPropIdList()) {
                ReliquaryAffixData affixData = GameData.getReliquaryAffixDataMap().get(appendPropId);
                if (affixData != null) {
                    this.addFightProperty(affixData.getFightProp(), affixData.getPropValue());
                }
            }
            // Set bonus
            if (equip.getItemData().getSetId() > 0) {
                setMap.addTo(equip.getItemData().getSetId(), 1);
            }
        }

        // Set stuff
        for (Int2IntOpenHashMap.Entry e : setMap.int2IntEntrySet()) {
            ReliquarySetData setData = GameData.getReliquarySetDataMap().get(e.getIntKey());
            if (setData == null) {
                continue;
            }

            // Calculate how many items are from the set
            int amount = e.getIntValue();

            // Add affix data from set bonus
            for (int setIndex = 0; setIndex < setData.getSetNeedNum().length; setIndex++) {
                if (amount >= setData.getSetNeedNum()[setIndex]) {
                    int affixId = (setData.getEquipAffixId() * 10) + setIndex;

                    EquipAffixData affix = GameData.getEquipAffixDataMap().get(affixId);
                    if (affix == null) {
                        continue;
                    }

                    // Add properties from this affix to our avatar
                    for (FightPropData prop : affix.getAddProps()) {
                        this.addFightProperty(prop.getProp(), prop.getValue());
                    }

                    // Add any skill strings from this affix
                    this.addToExtraAbilityEmbryos(affix.getOpenConfig(), true);
                } else {
                    break;
                }
            }
        }

        // Weapon
        GameItem weapon = this.getWeapon();
        if (weapon != null) {
            // Add stats
            WeaponCurveData curveData = GameData.getWeaponCurveDataMap().get(weapon.getLevel());
            if (curveData != null) {
                for (WeaponProperty weaponProperty : weapon.getItemData().getWeaponProperties()) {
                    this.addFightProperty(weaponProperty.getPropType(), weaponProperty.getInitValue() * curveData.getMultByProp(weaponProperty.getType()));
                }
            }
            // Weapon promotion stats
            WeaponPromoteData wepPromoteData = GameData.getWeaponPromoteData(weapon.getItemData().getWeaponPromoteId(), weapon.getPromoteLevel());
            if (wepPromoteData != null) {
                for (FightPropData prop : wepPromoteData.getAddProps()) {
                    if (prop.getValue() == 0f || prop.getProp() == null) {
                        continue;
                    }
                    this.addFightProperty(prop.getProp(), prop.getValue());
                }
            }
            // Add weapon skill from affixes
            if (weapon.getAffixes() != null && weapon.getAffixes().size() > 0) {
                // Weapons usually dont have more than one affix but just in case...
                for (int af : weapon.getAffixes()) {
                    if (af == 0) {
                        continue;
                    }
                    // Calculate affix id
                    int affixId = (af * 10) + weapon.getRefinement();
                    EquipAffixData affix = GameData.getEquipAffixDataMap().get(affixId);
                    if (affix == null) {
                        continue;
                    }

                    // Add properties from this affix to our avatar
                    for (FightPropData prop : affix.getAddProps()) {
                        this.addFightProperty(prop.getProp(), prop.getValue());
                    }

                    // Add any skill strings from this affix
                    this.addToExtraAbilityEmbryos(affix.getOpenConfig(), true);
                }
            }
        }

        // Add proud skills and unlock them if needed
        AvatarSkillDepotData skillDepot = GameData.getAvatarSkillDepotDataMap().get(this.getSkillDepotId());
        this.getProudSkillList().clear();
        for (InherentProudSkillOpens openData : skillDepot.getInherentProudSkillOpens()) {
            if (openData.getProudSkillGroupId() == 0) {
                continue;
            }
            if (openData.getNeedAvatarPromoteLevel() <= this.getPromoteLevel()) {
                int proudSkillId = (openData.getProudSkillGroupId() * 100) + 1;
                if (GameData.getProudSkillDataMap().containsKey(proudSkillId)) {
                    this.getProudSkillList().add(proudSkillId);
                }
            }
        }

        // Proud skills
        for (int proudSkillId : this.getProudSkillList()) {
            ProudSkillData proudSkillData = GameData.getProudSkillDataMap().get(proudSkillId);
            if (proudSkillData == null) {
                continue;
            }

            // Add properties from this proud skill to our avatar
            for (FightPropData prop : proudSkillData.getAddProps()) {
                this.addFightProperty(prop.getProp(), prop.getValue());
            }

            // Add any skill strings from this proud skill
            this.addToExtraAbilityEmbryos(proudSkillData.getOpenConfig(), true);
        }

        // Constellations
        if (this.getTalentIdList().size() > 0) {
            for (int talentId : this.getTalentIdList()) {
                AvatarTalentData avatarTalentData = GameData.getAvatarTalentDataMap().get(talentId);
                if (avatarTalentData == null) {
                    return;
                }

                // Add any skill strings from this constellation
                this.addToExtraAbilityEmbryos(avatarTalentData.getOpenConfig(), false);
            }
        }

        // Set % stats
        this.setFightProperty(
            FightProperty.FIGHT_PROP_MAX_HP,
            (getFightProperty(FightProperty.FIGHT_PROP_BASE_HP) * (1f + getFightProperty(FightProperty.FIGHT_PROP_HP_PERCENT))) + getFightProperty(FightProperty.FIGHT_PROP_HP)
        );
        this.setFightProperty(
            FightProperty.FIGHT_PROP_CUR_ATTACK,
            (getFightProperty(FightProperty.FIGHT_PROP_BASE_ATTACK) * (1f + getFightProperty(FightProperty.FIGHT_PROP_ATTACK_PERCENT))) + getFightProperty(FightProperty.FIGHT_PROP_ATTACK)
        );
        this.setFightProperty(
            FightProperty.FIGHT_PROP_CUR_DEFENSE,
            (getFightProperty(FightProperty.FIGHT_PROP_BASE_DEFENSE) * (1f + getFightProperty(FightProperty.FIGHT_PROP_DEFENSE_PERCENT))) + getFightProperty(FightProperty.FIGHT_PROP_DEFENSE)
        );

        // Reapply all overrides
        this.fightProp.putAll(this.fightPropOverrides);

        // Set current hp
        this.setFightProperty(FightProperty.FIGHT_PROP_CUR_HP, this.getFightProperty(FightProperty.FIGHT_PROP_MAX_HP) * hpPercent);

        // Packet
        if (getPlayer() != null && getPlayer().hasSentLoginPackets()) {
            // Update stats for client
            getPlayer().sendPacket(new PacketAvatarFightPropNotify(this));
            // Update client abilities
            EntityAvatar entity = this.getAsEntity();
            if (entity != null && (!this.getExtraAbilityEmbryos().equals(prevExtraAbilityEmbryos) || forceSendAbilityChange)) {
                getPlayer().sendPacket(new PacketAbilityChangeNotify(entity));
            }
        }
    }

    public void addToExtraAbilityEmbryos(String openConfig, boolean forceAdd) {
        if (openConfig == null || openConfig.length() == 0) {
            return;
        }

        OpenConfigEntry entry = GameData.getOpenConfigEntries().get(openConfig);
        if (entry == null) {
            if (forceAdd) {
                // Add config string to ability skill list anyways
                this.getExtraAbilityEmbryos().add(openConfig);
            }
            return;
        }

        if (entry.getAddAbilities() != null) {
            for (String ability : entry.getAddAbilities()) {
                this.getExtraAbilityEmbryos().add(ability);
            }
        }
    }

    public void recalcConstellations() {
        // Clear first
        this.getProudSkillBonusMap().clear();
        this.getSkillExtraChargeMap().clear();

        // Sanity checks
        if (getData() == null || getData().getSkillDepot() == null) {
            return;
        }

        if (this.getTalentIdList().size() > 0) {
            for (int talentId : this.getTalentIdList()) {
                AvatarTalentData avatarTalentData = GameData.getAvatarTalentDataMap().get(talentId);

                if (avatarTalentData == null || avatarTalentData.getOpenConfig() == null || avatarTalentData.getOpenConfig().length() == 0) {
                    continue;
                }

                // Get open config to find which skill should be boosted
                OpenConfigEntry entry = GameData.getOpenConfigEntries().get(avatarTalentData.getOpenConfig());
                if (entry == null) {
                    continue;
                }

                // Check if we can add charges to a skill
                if (entry.getSkillPointModifiers() != null) {
                    for (SkillPointModifier mod : entry.getSkillPointModifiers()) {
                        AvatarSkillData skillData = GameData.getAvatarSkillDataMap().get(mod.getSkillId());

                        if (skillData == null) continue;

                        int charges = skillData.getMaxChargeNum() + mod.getDelta();

                        this.getSkillExtraChargeMap().put(mod.getSkillId(), charges);
                    }
                    continue;
                }

                // Check if a skill can be boosted by +3 levels
                int skillId = 0;

                if (entry.getExtraTalentIndex() == 2 && this.getData().getSkillDepot().getSkills().size() >= 2) {
                    // E skill
                    skillId = this.getData().getSkillDepot().getSkills().get(1);
                } else if (entry.getExtraTalentIndex() == 9) {
                    // Ult skill
                    skillId = this.getData().getSkillDepot().getEnergySkill();
                }

                // Sanity check
                if (skillId == 0) {
                    continue;
                }

                // Get proud skill group id
                AvatarSkillData skillData = GameData.getAvatarSkillDataMap().get(skillId);

                if (skillData == null) {
                    continue;
                }

                // Add to bonus list
                this.getProudSkillBonusMap().put(skillData.getProudSkillGroupId(), 3);
            }
        }
    }

    public EntityAvatar getAsEntity() {
        for (EntityAvatar entity : getPlayer().getTeamManager().getActiveTeam()) {
            if (entity.getAvatar() == this) {
                return entity;
            }
        }
        return null;
    }

    public int getEntityId() {
        EntityAvatar entity = getAsEntity();
        return entity != null ? entity.getId() : 0;
    }

    public void save() {
        DatabaseHelper.saveAvatar(this);
    }

    public AvatarInfo toProto() {
        int fetterLevel = this.getFetterLevel();
        AvatarFetterInfo.Builder avatarFetter = AvatarFetterInfo.newBuilder()
                .setExpLevel(fetterLevel);

        if (fetterLevel != 10) {
            avatarFetter.setExpNumber(this.getFetterExp());
        }


        if (this.getFetterList() != null) {
            for (int i = 0; i < this.getFetterList().size(); i++) {
                avatarFetter.addFetterList(
                    FetterData.newBuilder()
                        .setFetterId(this.getFetterList().get(i))
                        .setFetterState(FetterState.FINISH.getValue())
                );
            }
        }

        int cardId = this.getNameCardId();

        if (this.getPlayer().getNameCardList().contains(cardId)) {
            avatarFetter.addRewardedFetterLevelList(10);
        }

        AvatarInfo.Builder avatarInfo = AvatarInfo.newBuilder()
                .setAvatarId(this.getAvatarId())
                .setGuid(this.getGuid())
                .setLifeState(1)
                .addAllTalentIdList(this.getTalentIdList())
                .putAllFightPropMap(this.getFightProperties())
                .setSkillDepotId(this.getSkillDepotId())
                .setCoreProudSkillLevel(this.getCoreProudSkillLevel())
                .putAllSkillLevelMap(this.getSkillLevelMap())
                .addAllInherentProudSkillList(this.getProudSkillList())
                .putAllProudSkillExtraLevelMap(getProudSkillBonusMap())
                .setAvatarType(1)
                .setBornTime(this.getBornTime())
                .setFetterInfo(avatarFetter)
                .setWearingFlycloakId(this.getFlyCloak())
                .setCostumeId(this.getCostume());

        for (Entry<Integer, Integer> entry : this.getSkillExtraChargeMap().entrySet()) {
            avatarInfo.putSkillMap(entry.getKey(), AvatarSkillInfo.newBuilder().setMaxChargeCount(entry.getValue()).build());
        }

        for (GameItem item : this.getEquips().values()) {
            avatarInfo.addEquipGuidList(item.getGuid());
        }

        avatarInfo.putPropMap(PlayerProperty.PROP_LEVEL.getId(), ProtoHelper.newPropValue(PlayerProperty.PROP_LEVEL, this.getLevel()));
        avatarInfo.putPropMap(PlayerProperty.PROP_EXP.getId(), ProtoHelper.newPropValue(PlayerProperty.PROP_EXP, this.getExp()));
        avatarInfo.putPropMap(PlayerProperty.PROP_BREAK_LEVEL.getId(), ProtoHelper.newPropValue(PlayerProperty.PROP_BREAK_LEVEL, this.getPromoteLevel()));
        avatarInfo.putPropMap(PlayerProperty.PROP_SATIATION_VAL.getId(), ProtoHelper.newPropValue(PlayerProperty.PROP_SATIATION_VAL, 0));
        avatarInfo.putPropMap(PlayerProperty.PROP_SATIATION_PENALTY_TIME.getId(), ProtoHelper.newPropValue(PlayerProperty.PROP_SATIATION_PENALTY_TIME, 0));

        return avatarInfo.build();
    }

    // used only in character showcase
    public ShowAvatarInfo toShowAvatarInfoProto() {
        AvatarFetterInfo.Builder avatarFetter = AvatarFetterInfo.newBuilder()
                .setExpLevel(this.getFetterLevel());

        ShowAvatarInfo.Builder showAvatarInfo = ShowAvatarInfoOuterClass.ShowAvatarInfo.newBuilder()
                .setAvatarId(avatarId)
                .addAllTalentIdList(this.getTalentIdList())
                .putAllFightPropMap(this.getFightProperties())
                .setSkillDepotId(this.getSkillDepotId())
                .setCoreProudSkillLevel(this.getCoreProudSkillLevel())
                .addAllInherentProudSkillList(this.getProudSkillList())
                .putAllSkillLevelMap(this.getSkillLevelMap())
                .putAllProudSkillExtraLevelMap(this.getProudSkillBonusMap())
                .setFetterInfo(avatarFetter)
                .setCostumeId(this.getCostume());

        showAvatarInfo.putPropMap(PlayerProperty.PROP_LEVEL.getId(), ProtoHelper.newPropValue(PlayerProperty.PROP_LEVEL, this.getLevel()));
        showAvatarInfo.putPropMap(PlayerProperty.PROP_EXP.getId(), ProtoHelper.newPropValue(PlayerProperty.PROP_EXP, this.getExp()));
        showAvatarInfo.putPropMap(PlayerProperty.PROP_BREAK_LEVEL.getId(), ProtoHelper.newPropValue(PlayerProperty.PROP_BREAK_LEVEL, this.getPromoteLevel()));
        showAvatarInfo.putPropMap(PlayerProperty.PROP_SATIATION_VAL.getId(), ProtoHelper.newPropValue(PlayerProperty.PROP_SATIATION_VAL, this.getSatiation()));
        showAvatarInfo.putPropMap(PlayerProperty.PROP_SATIATION_PENALTY_TIME.getId(), ProtoHelper.newPropValue(PlayerProperty.PROP_SATIATION_VAL, this.getSatiationPenalty()));
        int maxStamina = this.getPlayer().getProperty(PlayerProperty.PROP_MAX_STAMINA);
        showAvatarInfo.putPropMap(PlayerProperty.PROP_MAX_STAMINA.getId(), ProtoHelper.newPropValue(PlayerProperty.PROP_MAX_STAMINA, maxStamina));

        for (GameItem item : this.getEquips().values()) {
            if (item.getItemType() == ItemType.ITEM_RELIQUARY) {
                showAvatarInfo.addEquipList(ShowEquip.newBuilder()
                        .setItemId(item.getItemId())
                        .setReliquary(item.toReliquaryProto()));
            } else if (item.getItemType() == ItemType.ITEM_WEAPON) {
                showAvatarInfo.addEquipList(ShowEquip.newBuilder()
                        .setItemId(item.getItemId())
                        .setWeapon(item.toWeaponProto()));
            }
        }

        return showAvatarInfo.build();
    }

    @PostLoad
    private void onLoad() {

    }

    @PrePersist
    private void prePersist() {
        this.currentHp = this.getFightProperty(FightProperty.FIGHT_PROP_CUR_HP);
    }
}
