using System;
using System.Collections.Generic;
using System.Linq;
using RimWorld;
using Verse;
using UnityEngine;

namespace CombatExtended;
public static class ArmorUtilityCE
{
    #region Constants

    private const float PenetrationRandVariation = 0.05f;    // Armor penetration will be randomized by +- this amount
    private const float SoftArmorMinDamageFactor = 0.2f;    // Soft body armor will always take at least original damage * this number from sharp attacks
    private const float HardArmorDamageFactor = 0.5f;       // Hard body armor final health damage multiplier
    private const float SpikeTrapAPModifierBlunt = 0.65f;
    private const float BulletImpactForceMultiplier = 0.2f; //Bullet has less mass => less impluse comparing to melee => less Blunt penetration

    #endregion

    #region Properties


    private static readonly StuffCategoryDef[] softStuffs = { StuffCategoryDefOf.Fabric, StuffCategoryDefOf.Leathery };

    #endregion

    #region Methods

    /// <summary>
    /// Calculates damage through armor, depending on damage type, target and natural resistance. Also calculates deflection and adjusts damage type and impacted body part accordingly.
    /// </summary>
    /// <param name="originalDinfo">The pre-armor damage info</param>
    /// <param name="pawn">The damaged pawn</param>
    /// <param name="hitPart">The pawn's body part that has been hit</param>
    /// <param name="armorReduced">Whether sharp damage was deflected by armor</param>
    /// <param name="shieldAbsorbed">Returns true if attack did not penetrate pawn's melee shield</param>
    /// <param name="armorDeflected">Whether the attack was completely absorbed by the armor</param>
    /// <returns>If shot is deflected returns a new dinfo cloned from the original with damage amount, Def and ForceHitPart adjusted for deflection, otherwise a clone with only the damage adjusted</returns>
    public static DamageInfo GetAfterArmorDamage(DamageInfo originalDinfo, Pawn pawn, BodyPartRecord hitPart, out bool armorDeflected, out bool armorReduced, out bool shieldAbsorbed)
    {
        shieldAbsorbed = false;
        armorDeflected = false;
        armorReduced = false;

        var deflectionComp = pawn.TryGetComp<Comp_BurnDamageCalc>();

        if (originalDinfo.Def.armorCategory == null
                || (!(originalDinfo.Weapon?.projectile is ProjectilePropertiesCE)
                    && Verb_MeleeAttackCE.LastAttackVerb == null
                    && originalDinfo.Weapon == null
                    && originalDinfo.Instigator == null))
        {
            return originalDinfo;
        }

        var dinfo = new DamageInfo(originalDinfo);
        var dmgAmount = dinfo.Amount;
        var penAmount = dinfo.ArmorPenetrationInt; //GetPenetrationValue(originalDinfo);
        var involveArmor = dinfo.Def.harmAllLayersUntilOutside || hitPart.depth == BodyPartDepth.Outside;
        bool isAmbientDamage = dinfo.IsAmbientDamage();

        // In case of ambient damage (fire, electricity) we apply a percentage reduction formula based on the sum of all applicable armor, using the penetration from the type of damagedef
        if (isAmbientDamage)
        {
            penAmount = dinfo.defInt.defaultArmorPenetration;
            dinfo.SetAmount(Mathf.CeilToInt(GetAmbientPostArmorDamage(dmgAmount, penAmount, originalDinfo.Def.armorCategory.armorRatingStat, pawn, hitPart)));
            armorDeflected = dinfo.Amount <= 0;
            return dinfo;
        }
        else if (deflectionComp != null)
        {
            deflectionComp.deflectedSharp = false;
        }


        // Apply worn armor
        if (involveArmor && pawn.apparel != null && !pawn.apparel.WornApparel.NullOrEmpty())
        {
            var apparel = pawn.apparel.WornApparel;

            // Check for shields first
            Apparel_Shield shield = null;
            for (int i = 0; i < apparel.Count; i++)
            {
                if (apparel[i] is Apparel_Shield loopShield)
                {
                    shield = loopShield;
                    break;
                }
            }

            if (shield != null)
            {

                var blockedByShield = false;

                bool isMeleeOrNull = dinfo.Tool != null || (dinfo.Weapon?.IsMeleeWeapon ?? true);
                if (!isMeleeOrNull)
                {
                    var shieldDef = shield.def.GetModExtension<ShieldDefExtension>();
                    if (shieldDef == null)
                    {
                        Log.ErrorOnce("Combat Extended :: shield " + shield.def.ToString() + " is Apparel_Shield but has no ShieldDefExtension", shield.def.GetHashCode() + 12748102);
                    }
                    else
                    {
                        var hasCoverage = shieldDef.PartIsCoveredByShield(hitPart, pawn.IsCrouching());
                        if (hasCoverage)
                        {
                            // Right arm is vulnerable during warmup/attack/cooldown
                            blockedByShield = !((pawn.stances?.curStance as Stance_Busy)?.verb != null && hitPart.IsInGroup(CE_BodyPartGroupDefOf.RightArm));
                        }
                    }
                }

                // Try to penetrate the shield
                if (blockedByShield && !TryPenetrateArmor(dinfo.Def, shield.GetStatValue(dinfo.Def.armorCategory.armorRatingStat), ref penAmount, ref dmgAmount, shield))
                {
                    //Deflected by sharp armor, check for blunt armor
                    if (dinfo.Def.armorCategory.armorRatingStat == StatDefOf.ArmorRating_Sharp)
                    {
                        if (deflectionComp != null)
                        {
                            deflectionComp.deflectedSharp = true;
                        }
                    }
                    // Hit was deflected, convert damage type
                    dinfo = GetDeflectDamageInfo(dinfo, hitPart, ref dmgAmount, ref penAmount);
                    //Applies multiplier to bullet.
                    penAmount *= dinfo.Weapon?.IsMeleeWeapon ?? false ? 1.0f : BulletImpactForceMultiplier;
                    //Check if converted blunt damage could penetrate the shield
                    if (!TryPenetrateArmor(dinfo.Def, shield.GetStatValue(dinfo.Def.armorCategory.armorRatingStat), ref penAmount, ref dmgAmount))
                    {
                        shieldAbsorbed = true;
                        armorDeflected = true;
                        dinfo.SetAmount(0);
                        // Apply secondary damage to shield
                        if (dinfo.Weapon?.projectile is ProjectilePropertiesCE props && !props.secondaryDamage.NullOrEmpty())
                        {
                            foreach (var sec in props.secondaryDamage)
                            {
                                if (shield.Destroyed)
                                {
                                    break;
                                }
                                if (!Rand.Chance(sec.chance))
                                {
                                    continue;
                                }
                                var secDinfo = sec.GetDinfo();
                                var pen = secDinfo.ArmorPenetrationInt; //GetPenetrationValue(originalDinfo);
                                var dmg = (float)secDinfo.Amount;
                                TryPenetrateArmor(secDinfo.Def, shield.GetStatValue(secDinfo.Def.armorCategory.armorRatingStat), ref pen, ref dmg, shield);
                            }
                        }

                        return dinfo;
                    }
                    //Blunt damage penetrated the shield, apply the damage to left arm
                    //Could add a check for having weapon equipped, if not, pawns should be able to hold the shield with both arms, increasing their defence
                    else
                    {
                        shieldAbsorbed = true;
                        //Priority: Left Arm > Right Arm > Left Shoulder
                        //It seems that losing left shoulder makes shield unequippable, so no need to add a null check (for now)
                        //FirstOrFallback returns null when nothing satisfies, effectively brings the program into the if statement
                        BodyPartRecord PartToHit = pawn.health.hediffSet.GetNotMissingParts(depth: BodyPartDepth.Outside, tag: BodyPartTagDefOf.ManipulationLimbCore).FirstOrFallback(x => x.IsInGroup(CE_BodyPartGroupDefOf.LeftArm) || x.IsInGroup(CE_BodyPartGroupDefOf.RightArm));
                        if (PartToHit == null)
                        {
                            PartToHit = pawn.health.hediffSet.GetNotMissingParts(depth: BodyPartDepth.Outside, tag: BodyPartTagDefOf.ManipulationLimbSegment).First(x => x.IsInGroup(CE_BodyPartGroupDefOf.LeftShoulder));
                        }
                        dinfo.SetHitPart(PartToHit);
                        dinfo.SetAmount(dmgAmount);
                        // Apply secondary damage to shield
                        if (dinfo.Weapon?.projectile is ProjectilePropertiesCE props && !props.secondaryDamage.NullOrEmpty())
                        {
                            foreach (var sec in props.secondaryDamage)
                            {
                                if (shield.Destroyed)
                                {
                                    break;
                                }
                                if (!Rand.Chance(sec.chance))
                                {
                                    continue;
                                }
                                var secDinfo = sec.GetDinfo();
                                var pen = secDinfo.ArmorPenetrationInt; //GetPenetrationValue(originalDinfo);
                                var dmg = (float)secDinfo.Amount;
                                TryPenetrateArmor(secDinfo.Def, shield.GetStatValue(secDinfo.Def.armorCategory.armorRatingStat), ref pen, ref dmg, shield);
                            }
                        }
                        return dinfo;
                    }

                }
            }

            // Apparel is arranged in draw order, we run through reverse to go from Shell -> OnSkin
            for (var i = apparel.Count - 1; i >= 0; i--)
            {
                var app = apparel[i];
                if (app == shield)
                {
                    continue;
                }
                if (app.def.apparel.CoversBodyPart(hitPart) && !TryPenetrateArmor(dinfo.Def, app.PartialStat(dinfo.Def.armorCategory.armorRatingStat, hitPart), ref penAmount, ref dmgAmount, app))
                {
                    if (dinfo.Def.armorCategory.armorRatingStat == StatDefOf.ArmorRating_Sharp)
                    {
                        if (deflectionComp != null)
                        {
                            deflectionComp.deflectedSharp = true;
                        }
                    }

                    // Hit was deflected, convert damage type
                    //armorReduced = true;
                    dinfo = GetDeflectDamageInfo(dinfo, hitPart, ref dmgAmount, ref penAmount);
                    if (app == apparel.ElementAtOrDefault(i))   //Check whether the "deflecting" apparel is still in the WornApparel - if not, the next loop checks again and errors out because the index is out of range
                    {
                        i++;    // We apply this piece of apparel twice on conversion, this means we can't use deflection on Blunt or else we get an infinite loop of eternal deflection
                    }
                }
                if (dmgAmount <= 0)
                {
                    dinfo.SetAmount(0);
                    armorDeflected = true;
                    return dinfo;
                }
            }
        }

        // Apply natural armor
        var partsToHit = new List<BodyPartRecord>()
        {
            hitPart
        };
        if (dinfo.Def.harmAllLayersUntilOutside)
        {
            var curPart = hitPart;
            while (curPart.parent != null && curPart.depth == BodyPartDepth.Inside)
            {
                curPart = curPart.parent;
                partsToHit.Add(curPart);
            }
        }

        var isSharp = dinfo.Def.armorCategory.armorRatingStat == StatDefOf.ArmorRating_Sharp;
        var partDensityStat = isSharp
                              ? CE_StatDefOf.BodyPartSharpArmor
                              : CE_StatDefOf.BodyPartBluntArmor;
        var partDensity = pawn.GetStatValue(partDensityStat);   // How much armor is provided by sheer meat
        for (var i = partsToHit.Count - 1; i >= 0; i--)
        {
            var curPart = partsToHit[i];
            var coveredByArmor = curPart.IsInGroup(CE_BodyPartGroupDefOf.CoveredByNaturalArmor);
            var armorAmount = coveredByArmor ? pawn.PartialStat(dinfo.Def.armorCategory.armorRatingStat, curPart) : 0;

            // Only apply damage reduction when penetrating armored body parts
            if (!TryPenetrateArmor(dinfo.Def, armorAmount, ref penAmount, ref dmgAmount, null, partDensity))
            {
                dinfo.SetHitPart(curPart);
                if (isSharp && coveredByArmor)
                {
                    if (dinfo.Def.armorCategory.armorRatingStat == StatDefOf.ArmorRating_Sharp)
                    {
                        if (deflectionComp != null)
                        {
                            deflectionComp.deflectedSharp = true;

                            deflectionComp.weapon = originalDinfo.Weapon;
                        }
                    }
                    // For Mechanoid natural armor, apply deflection and blunt armor
                    dinfo = GetDeflectDamageInfo(dinfo, curPart, ref dmgAmount, ref penAmount);

                    // Fetch armor rating stat again in case of deflection conversion to blunt
                    TryPenetrateArmor(dinfo.Def, pawn.GetStatValue(dinfo.Def.armorCategory.armorRatingStat), ref penAmount, ref dmgAmount, null, partDensity);
                }
                break;
            }
            if (dmgAmount <= 0)
            {
                dinfo.SetAmount(0);
                armorDeflected = true;
                return dinfo;
            }
        }
        // Applies blunt damage from partial penetrations.
        if (isSharp && (dinfo.Amount > dmgAmount))
        {
            pawn.TakeDamage(GetDeflectDamageInfo(dinfo, hitPart, ref dmgAmount, ref penAmount, true));
        }
        // Return damage info.
        dinfo.SetAmount(dmgAmount);
        return dinfo;
    }

    /// <summary>
    /// Calculates armor for penetrating damage types (Blunt, Sharp). Applies damage reduction based on armor penetration to armor ratio and calculates damage accordingly, with the difference being applied to the armor Thing. Also calculates whether a Sharp attack is deflected.
    /// </summary>
    /// <param name="def">The DamageDef of the attack</param>
    /// <param name="armorAmount">The amount of armor to apply</param>
    /// <param name="penAmount">How much penetration the attack still has</param>
    /// <param name="dmgAmount">The pre-armor amount of damage</param>
    /// <param name="armor">The armor apparel</param>
    /// <param name="partDensity">When penetrating body parts, the body part density</param>
    /// <returns>False if the attack is deflected, true otherwise</returns>
    private static bool TryPenetrateArmor(DamageDef def, float armorAmount, ref float penAmount, ref float dmgAmount, Thing armor = null, float partDensity = 0)
    {
        // Calculate deflection
        var isSharpDmg = def.armorCategory == DamageArmorCategoryDefOf.Sharp;
        var isFireDmg = def.armorCategory == CE_DamageArmorCategoryDefOf.Heat;
        //var rand = UnityEngine.Random.Range(penAmount - PenetrationRandVariation, penAmount + PenetrationRandVariation);
        var deflected = isSharpDmg && armorAmount > penAmount;

        // Apply damage reduction
        var defCE = def.GetModExtension<DamageDefExtensionCE>() ?? new DamageDefExtensionCE();
        var noDmg = deflected && defCE.noDamageOnDeflect;
        var newPenAmount = penAmount - armorAmount;

        var dmgMult = noDmg ? 0 : penAmount == 0 ? 1 : Mathf.Clamp01(newPenAmount / penAmount);
        deflected = deflected || dmgMult == 0;
        var newDmgAmount = dmgAmount * dmgMult;
        newPenAmount -= partDensity;    // Factor partDensity only after damage calculations

        // Apply damage to armor
        if (armor != null)
        {
            var isSoftArmor = (armor.Stuff != null && armor.Stuff.stuffProps.categories.Any(s => softStuffs.Contains(s))) || (armor.def.GetModExtension<ApparelDefExtension>()?.isSoftArmor ?? false);
            float armorDamage = 0;
            if (isSoftArmor)
            {
                // Soft armor takes absorbed damage from sharp and no damage from blunt
                if (isFireDmg)
                {
                    armorDamage = armor.GetStatValue(StatDefOf.Flammability, true) * dmgAmount;
                }
                if (isSharpDmg)
                {
                    armorDamage = Mathf.Max(dmgAmount * SoftArmorMinDamageFactor, dmgAmount - newDmgAmount);
                    TryDamageArmor(def, penAmount, armorAmount, ref armorDamage, armor);
                }
            }
            else
            {
                // Hard armor takes damage depending on the damage amount and damage penetration
                // Such armor takes the most damage when the attack has the same amount of armor penetration as the armor has armor amount
                // Otherwise it's a non-penetration (was unable to perforate the armor) or an over-penetration (creates a nice hole in the armor)
                // It is assumed that elastic deformation (no damage) occurs when the attack is blunt and has less armor penetration than the armor amount divided by 2
                if (!isSharpDmg && (penAmount / armorAmount) < 0.5f)
                {
                    armorDamage = 0;
                }
                else
                {
                    if (isFireDmg)
                    {
                        armorDamage = armor.GetStatValue(StatDefOf.Flammability, true) * dmgAmount;
                    }
                    else
                    {
                        if (penAmount == 0 || armorAmount == 0)
                        {
                            if (armor.GetStatValue(StatDefOf.ArmorRating_Sharp) == 0 && armor.GetStatValue(StatDefOf.ArmorRating_Blunt) == 0 && armor.GetStatValue(StatDefOf.ArmorRating_Heat) == 0)
                            {
                                Log.ErrorOnce($"penAmount or armorAmount are zero for {def.armorCategory} on {armor}", armor.def.GetHashCode() + 846532021);
                            }
                        }
                        else
                        {
                            armorDamage = (dmgAmount - newDmgAmount) * Mathf.Min(1.0f, (penAmount * penAmount) / (armorAmount * armorAmount)) + newDmgAmount * Mathf.Clamp01(armorAmount / penAmount);
                        }
                    }
                    armorDamage *= HardArmorDamageFactor;
                }

                TryDamageArmor(def, penAmount, armorAmount, ref armorDamage, armor);
            }
        }

        if (!deflected || !isSharpDmg)
        {
            dmgAmount = Mathf.Max(0, newDmgAmount);
            penAmount = Mathf.Max(0, newPenAmount);
        }
        return !deflected;
    }

    /// <summary>
    /// Damages the armor, where the damage amount is modified, with damages rounding either to ceiling or floor based off of the fraction of the number and damages less than 1 having an increased chance to round to ceiling based off the ratio of armor penetration to armor amount.
    /// </summary>
    /// <param name="def">The DamageDef of the attack</param>
    /// <param name="armorAmount">The amount of armor to apply</param>
    /// <param name="penAmount">How much penetration the attack still has</param>
    /// <param name="dmgAmount">The pre-armor amount of damage</param>
    /// <param name="armor">The armor apparel</param>
    /// <returns>Returns true if the armor takes damage, false if it doesn't.</returns>
    private static bool TryDamageArmor(DamageDef def, float penAmount, float armorAmount, ref float armorDamage, Thing armor)
    {
        if (armorDamage == 0)
        {
            return false;
        }

        // Any fractional armor damage has a chance to get rounded to the largest nearest whole number
        // Combined with the previous dice roll, values between 0 and 1 have an increased chance to get rounded up
        if (Rand.Value < (armorDamage - Mathf.Floor(armorDamage)))
        {
            armorDamage = Mathf.Ceil(armorDamage);
        }

        armorDamage = Mathf.Floor(armorDamage);
        // Don't call TakeDamage() with 0 damage as that would be a waste
        if (armorDamage != 0f)
        {
            armor.TakeDamage(new DamageInfo(def, armorDamage));
            return true;
        }
        return false;
    }


    /// <summary>
    /// Calculates damage reduction for ambient damage types (fire, electricity) versus natural and worn armor of a pawn. Adds up the total armor percentage (clamped at 0-100%) and multiplies damage by that amount.
    /// </summary>
    /// <param name="dmgAmount">The original amount of damage</param>
    /// <param name="armorRatingStat">The armor stat to use for damage reduction</param>
    /// <param name="pawn">The damaged pawn</param>
    /// <param name="part">The body part affected</param>
    /// <returns>The post-armor damage ranging from 0 to the original amount</returns>
    private static float GetAmbientPostArmorDamage(float dmgAmount, float penAmount, StatDef armorRatingStat, Pawn pawn, BodyPartRecord part)
    {
        if (penAmount < 0f)
        {
            penAmount = 0f;
        }
        var dmgMult = 1f + penAmount;
        if (part.IsInGroup(CE_BodyPartGroupDefOf.CoveredByNaturalArmor))
        {
            dmgMult -= pawn.GetStatValue(armorRatingStat);
        }

        if (dmgMult <= 0)
        {
            return 0;
        }
        if (pawn.apparel != null && !pawn.apparel.WornApparel.NullOrEmpty())
        {
            var apparelList = pawn.apparel.WornApparel;
            foreach (var apparel in apparelList)
            {
                if (apparel.def.apparel.CoversBodyPart(part))
                {
                    dmgMult -= apparel.GetStatValue(armorRatingStat);
                }
                if (dmgMult <= 0)
                {
                    dmgMult = 0;
                    break;
                }
            }
        }
        if (dmgMult > 1f)
        {
            dmgMult = 1f;
        }
        var deflectionComp = pawn.TryGetComp<Comp_BurnDamageCalc>();
        if (deflectionComp != null)
        {
            if (armorRatingStat == StatDefOf.ArmorRating_Heat)
            {
                if (deflectionComp.deflectedSharp)
                {
                    dmgMult /= 2f;
                }
            }
            deflectionComp.deflectedSharp = false;
        }

        return (float)Math.Floor(dmgAmount * dmgMult);
    }

    /// <summary>
    /// Creates a new DamageInfo from a deflected one. Changes damage type to Blunt and hit part to the outermost parent of the originally hit part.
    /// </summary>
    /// <param name="dinfo">The dinfo that was deflected</param>
    /// <param name="hitPart">The originally hit part</param>
    /// <param name="partialPen">Is this is supposed to be a partial penetration</param>
    /// <returns>DamageInfo copied from dinfo with Def and forceHitPart adjusted</returns>
    private static DamageInfo GetDeflectDamageInfo(DamageInfo dinfo, BodyPartRecord hitPart, ref float dmgAmount, ref float penAmount, bool partialPen = false)
    {
        if (dinfo.Def.armorCategory != DamageArmorCategoryDefOf.Sharp)
        {
            if (!partialPen)
            {
                dmgAmount = 0;
                penAmount = 0;
            }
            dinfo.SetAmount(0);
            return dinfo;
        }

        //Creating local variables as we don't want to edit the pass-by-reference parameters in the case of partial penetrations.
        float localDmgAmount = dmgAmount;
        float localPenAmount = penAmount;

        //Calculating blunt damage from sharp damage: if it's a deflected sharp attack, then the penetration amount is directly localPenAmount.
        //However, if it's a partially-penetrating sharp attack, then we're using the blocked values of penetration amount and damage amount instead
        //and because localPenAmount is the sharp attack's remaining penetration amount and localDmgAmount is the sharp attack's remaining damage amount,
        //we have to take that amount away from the base penetration amount and damage amount.
        float penMulti = (partialPen ? ((dinfo.ArmorPenetrationInt - localPenAmount) * (dinfo.Amount - localDmgAmount) / dinfo.Amount) : localPenAmount) / dinfo.ArmorPenetrationInt;
        if (dinfo.Weapon?.projectile is ProjectilePropertiesCE projectile)
        {
            localPenAmount = projectile.armorPenetrationBlunt * penMulti;
        }
        else if (dinfo.Instigator?.def.thingClass == typeof(Building_TrapDamager))
        {
            //Temporarily deriving spike trap blunt AP based on their vanilla stats, just so they're not entirely broken
            //TODO proper integration
            var trapAP = dinfo.Instigator.GetStatValue(StatDefOf.TrapMeleeDamage, true) * SpikeTrapAPModifierBlunt;
            localPenAmount = trapAP * penMulti;
        }
        else
        {
            if (Verb_MeleeAttackCE.LastAttackVerb != null)
            {
                localPenAmount = Verb_MeleeAttackCE.LastAttackVerb.ArmorPenetrationBlunt;
            }
            else if (dinfo.defInt.defaultArmorPenetration > 0)
            {
                localPenAmount = dinfo.defInt.defaultArmorPenetration;
            }
            else
            {
                //LastAttackVerb is already checked in GetAfterArmorDamage(). Only known case of code arriving here is with the ancient soldiers
                //spawned at the start of the game: their wounds are usually applied with Weapon==null and Instigator==null, so they skip CE's armor system,
                //but on rare occasions, one of the soldiers gets Bite injuries with with Weapon==null and the instigator set as *himself*.
                //Warning message below to identify any other situations where this might be happening. -LX7
                Log.Warning($"[CE] Deflection for Instigator:{dinfo.Instigator} Target:{dinfo.IntendedTarget} DamageDef:{dinfo.Def} Weapon:{dinfo.Weapon} has null verb, overriding AP.");
                localPenAmount = 50;
            }
        }
        localDmgAmount = Mathf.Pow(localPenAmount * 10000, 1 / 3f) / 10;

        //Fragment damage from large fragments often splits up into multiple attacks with reduced damage, which have the same sharp pen I believe,
        //therefore the deflected damage should also be with reduced damage, but the same blunt pen.
        //The damage info's damage amount isn't set anywhere within this class, therefore it is a good gauge on how many times the damage split.
        if (dinfo.Weapon?.projectile is ProjectilePropertiesCE)
        {
            localDmgAmount *= dinfo.Amount / (float)dinfo.Weapon.projectile.damageAmountBase;
        }


        var newDinfo = new DamageInfo(DamageDefOf.Blunt,
                                      localDmgAmount,
                                      localPenAmount,
                                      dinfo.Angle,
                                      dinfo.Instigator,
                                      GetFirstOuterPart(hitPart),
                                      partialPen ? null : dinfo.Weapon, //To not apply the secondary damage twice on partial penetrations.
                                      instigatorGuilty: dinfo.InstigatorGuilty);
        newDinfo.SetBodyRegion(dinfo.Height, dinfo.Depth);
        newDinfo.SetWeaponBodyPartGroup(dinfo.WeaponBodyPartGroup);
        newDinfo.SetWeaponHediff(dinfo.WeaponLinkedHediff);
        newDinfo.SetInstantPermanentInjury(dinfo.InstantPermanentInjury);
        newDinfo.SetAllowDamagePropagation(dinfo.AllowDamagePropagation);
        if (!partialPen) //If it was a deflect, update dmgAmount and penAmount.
        {
            dmgAmount = localDmgAmount;
            penAmount = localPenAmount;
        }
        return newDinfo;
    }

    /// <summary>
    /// Determines whether a dinfo is of an ambient (i.e. heat, electric) damage type and should apply percentage reduction, as opposed to deflection-based reduction
    /// </summary>
    /// <param name="dinfo"></param>
    /// <returns>True if dinfo armor category is Heat or Electric, false otherwise</returns>
    private static bool IsAmbientDamage(this DamageInfo dinfo)
    {
        return (dinfo.Def.GetModExtension<DamageDefExtensionCE>() ?? new DamageDefExtensionCE()).isAmbientDamage;
    }

    /// <summary>
    /// Applies damage to a parry object based on its armor values. For ambient damage, percentage reduction is applied, direct damage uses deflection formulas.
    /// </summary>
    /// <param name="dinfo">DamageInfo to apply to parryThing</param>
    /// <param name="parryThing">Thing taking the damage</param>
    public static void ApplyParryDamage(DamageInfo dinfo, Thing parryThing)
    {
        var pawn = parryThing as Pawn;
        if (pawn != null)
        {
            // Pawns run their own armor calculations
            dinfo.SetAmount(dinfo.Amount * Mathf.Clamp01(Rand.Range(0.5f - pawn.GetStatValue(CE_StatDefOf.MeleeParryChance), 1f - pawn.GetStatValue(CE_StatDefOf.MeleeParryChance) * 1.25f)));
            pawn.TakeDamage(dinfo);
            return;
        }
        else if (dinfo.IsAmbientDamage())
        {
            var dmgAmount = Mathf.CeilToInt(dinfo.Amount * Mathf.Clamp01(parryThing.GetStatValue(dinfo.Def.armorCategory.armorRatingStat)));
            dinfo.SetAmount(dmgAmount);
            parryThing.TakeDamage(dinfo);
        }
        else
        {
            float parryThingArmor;
            var dmgAmount = dinfo.Amount * 0.5f;

            // For apparel
            if (parryThing.def.IsApparel)
            {
                parryThingArmor = parryThing.GetStatValue(dinfo.Def.armorCategory.armorRatingStat);
            }
            // Special case for weapons
            else
            {
                parryThingArmor = parryThing.GetStatValue(CE_StatDefOf.ToughnessRating);
                // Compensation for blunt damage against weapons
                if (dinfo.Def.armorCategory != DamageArmorCategoryDefOf.Sharp)
                {
                    parryThingArmor *= 1.5f;
                }
            }

            var penAmount = dinfo.ArmorPenetrationInt; //GetPenetrationValue(dinfo);

            bool partialPen = TryPenetrateArmor(dinfo.Def, parryThingArmor, ref penAmount, ref dmgAmount, parryThing);

            if (dinfo.Def.armorCategory == DamageArmorCategoryDefOf.Sharp && dmgAmount > 0)
            {
                var ndi = GetDeflectDamageInfo(dinfo, dinfo.HitPart, ref dmgAmount, ref penAmount, partialPen);
                if (dmgAmount > 0)
                {
                    ApplyParryDamage(ndi, parryThing);
                }
            }

        }
    }

    /// <summary>
    /// Retrieves the first body part with Outside depth, tavelling up the part's ancestors until finding one.
    /// </summary>
    /// <param name="part">The part to get the parent of</param>
    /// <returns>The first parent part with depth Outside, the original part if it already is Outside or doesn't have a parent, the root part if no parents are Outside</returns>
    public static BodyPartRecord GetFirstOuterPart(BodyPartRecord part)
    {
        var curPart = part;
        if (curPart != null)
        {
            while (curPart.parent != null && curPart.depth != BodyPartDepth.Outside)
            {
                curPart = curPart.parent;
            }
        }
        return curPart;
    }

    /// <summary>
    /// Gets the true rating of armor with partial stats taken into account
    /// </summary>
    public static float PartialStat(this Apparel apparel, StatDef stat, BodyPartRecord part)
    {
        float result = apparel.GetStatValue(stat);
        if (part == null)
        {
            return result;
        }

        if (!apparel.def.apparel.CoversBodyPart(part))
        {
            var shieldDef = apparel.def.GetModExtension<ShieldDefExtension>();
            if (shieldDef == null || !shieldDef.PartIsCoveredByShield(part, true))
            {
                return 0f;
            }
        }

        if (!Controller.settings.PartialStat)
        {
            return result;
        }

        PartialArmorExt partialArmorExt = apparel.def.GetModExtension<PartialArmorExt>();
        if (partialArmorExt == null)
        {
            return result;
        }

        foreach (ApparelPartialStat partial in partialArmorExt.stats)
        {
            if (partial.stat != stat || !partial.parts.Contains(GetFirstOuterPart(part).def))
            {
                continue;
            }
            result = partial.GetStatValue(result);
        }
        return result;
    }

    /// <summary>
    /// Gets the true rating of armor with partial stats taken into account
    /// </summary>
    public static float PartialStat(this Pawn pawn, StatDef stat, BodyPartRecord part)
    {
        float result = pawn.GetStatValue(stat);
        if (part == null)
        {
            return result;
        }

        if (!part.IsInGroup(CE_BodyPartGroupDefOf.CoveredByNaturalArmor))
        {
            return 0f;
        }

        if (!Controller.settings.PartialStat)
        {
            return result;
        }

        PartialArmorExt partialArmorExt = pawn.def.GetModExtension<PartialArmorExt>();
        if (partialArmorExt == null)
        {
            return result;
        }

        foreach (ApparelPartialStat partial in partialArmorExt.stats)
        {
            if (partial.stat != stat || !partial.parts.Contains(GetFirstOuterPart(part).def))
            {
                continue;
            }
            result = partial.GetStatValue(result);
        }
        return result;
    }

    /// <summary>
    /// version of PartialStat used for display in StatWorker_ArmorPartial
    /// </summary>
    public static float PartialStat(this Apparel apparel, StatDef stat, BodyPartDef part)
    {
        float result = apparel.GetStatValue(stat);
        PartialArmorExt partialArmorExt = apparel.def.GetModExtension<PartialArmorExt>();
        if (partialArmorExt == null)
        {
            return result;
        }

        foreach (ApparelPartialStat partial in partialArmorExt.stats)
        {
            if (partial.stat != stat || !partial.parts.Contains(part))
            {
                continue;
            }
            result = partial.GetStatValue(result);
        }
        return result;
    }

    /// <summary>
    /// version of PartialStat used for display in StatWorker_ArmorPartial
    /// </summary>
    public static float PartialStat(this Pawn pawn, StatDef stat, BodyPartDef part)
    {
        float result = pawn.GetStatValue(stat);
        PartialArmorExt partialArmorExt = pawn.def.GetModExtension<PartialArmorExt>();
        if (partialArmorExt == null)
        {
            return result;
        }

        foreach (ApparelPartialStat partial in partialArmorExt.stats)
        {
            if (partial.stat != stat || !partial.parts.Contains(part))
            {
                continue;
            }
            result = partial.GetStatValue(result);
        }
        return result;
    }


    #endregion
}
