using System;
using System.Collections.Generic;
using Server.Engines.CannedEvil;
using Server.Engines.ConPVP;
using Server.Engines.PartySystem;
using Server.Factions;
using Server.Guilds;
using Server.Items;
using Server.Misc;
using Server.Mobiles;
using Server.Multis;
using Server.Regions;
using Server.Spells.Fifth;
using Server.Spells.Necromancy;
using Server.Spells.Ninjitsu;
using Server.Spells.Seventh;
using Server.Targeting;

namespace Server
{
#pragma warning disable CA1052
    public class DefensiveSpell
    {
        public static void Nullify(Mobile from)
        {
            if (!from.CanBeginAction<DefensiveSpell>())
            {
                new InternalTimer(from).Start();
            }
        }

        private class InternalTimer : Timer
        {
            private readonly Mobile m_Mobile;

            public InternalTimer(Mobile m) : base(TimeSpan.FromMinutes(1.0))
            {
                m_Mobile = m;
            }

            protected override void OnTick()
            {
                m_Mobile.EndAction<DefensiveSpell>();
            }
        }
    }
#pragma warning restore CA1052
}

namespace Server.Spells
{
    public enum TravelCheckType
    {
        RecallFrom,
        RecallTo,
        GateFrom,
        GateTo,
        Mark,
        TeleportFrom,
        TeleportTo
    }

    public static class SpellHelper
    {
        private static readonly TimeSpan AosDamageDelay = TimeSpan.FromSeconds(1.25);
        private static readonly TimeSpan OldDamageDelay = TimeSpan.FromSeconds(0.5);

        private static readonly TimeSpan CombatHeatDelay = TimeSpan.FromSeconds(30.0);
        private static readonly bool RestrictTravelCombat = true;

        private static readonly int[] m_Offsets =
        {
            -1, -1,
            -1, 0,
            -1, 1,
            0, -1,
            0, 1,
            1, -1,
            1, 0,
            1, 1
        };

        private static readonly TravelValidator[] m_Validators =
        {
            IsFeluccaT2A,
            IsKhaldun,
            IsIlshenar,
            IsTrammelWind,
            IsFeluccaWind,
            IsFeluccaDungeon,
            IsTrammelSolenHive,
            IsFeluccaSolenHive,
            IsCrystalCave,
            IsDoomGauntlet,
            IsDoomFerry,
            IsSafeZone,
            IsFactionStronghold,
            IsChampionSpawn,
            IsTokunoDungeon,
            IsLampRoom,
            IsGuardianRoom,
            IsHeartwood,
            IsMLDungeon
        };

        // TODO: Move to configuration
        private static readonly bool[,] m_Rules =
        {
            /* T2A(Fel), Khaldun, Ilshenar, Wind(Tram), Wind(Fel), Dungeons(Fel), Solen(Tram), Solen(Fel), CrystalCave(Malas), Gauntlet(Malas), Gauntlet(Ferry), SafeZone, Stronghold, ChampionSpawn, Dungeons(Tokuno[Malas]), LampRoom(Doom), GuardianRoom(Doom), Heartwood, MLDungeons */
            /* Recall From */
            {
                false, false, true, true, false, false, true, false, false, false, false, true, true, false, true, false,
                false, false, false
            },
            /* Recall To */
            {
                false, false, false, false, false, false, false, false, false, false, false, false, false, false, false,
                false, false, false, false
            },
            /* Gate From */
            {
                false, false, false, false, false, false, false, false, false, false, false, false, false, false, false,
                false, false, false, false
            },
            /* Gate To */
            {
                false, false, false, false, false, false, false, false, false, false, false, false, false, false, false,
                false, false, false, false
            },
            /* Mark In */
            {
                false, false, false, false, false, false, false, false, false, false, false, false, false, false, false,
                false, false, false, false
            },
            /* Tele From */
            {
                true, true, true, true, true, true, true, true, false, true, true, true, false, true, true, true, true,
                false, true
            },
            /* Tele To */
            {
                true, true, true, true, true, true, true, true, false, true, false, false, false, true, true, true, true,
                false, false
            }
        };

        private static Mobile m_TravelCaster;
        private static TravelCheckType m_TravelType;

        public static TimeSpan GetDamageDelayForSpell(Spell sp) =>
            !sp.DelayedDamage ? TimeSpan.Zero :
            Core.AOS ? AosDamageDelay : OldDamageDelay;

        public static bool CheckMulti(Point3D p, Map map, bool houses = true, int housingrange = 0)
        {
            if (map == null || map == Map.Internal)
            {
                return false;
            }

            foreach (var multi in map.GetMultisInSector(p))
            {
                if (multi is BaseHouse bh)
                {
                    if (houses && bh.IsInside(p, 16) || housingrange > 0 && bh.InRange(p, housingrange))
                    {
                        return true;
                    }
                }
                else if (multi.Contains(p))
                {
                    return true;
                }
            }

            return false;
        }

        public static void Turn(Mobile from, IPoint3D to)
        {
            if (from == null)
            {
                return;
            }

            var root = (to as Item)?.RootParent;
            if (from != root)
            {
                to = root;
            }

            if (!from.Equals(to))
            {
                from.Direction = from.GetDirectionTo(to);
            }
        }

        public static bool CheckCombat(Mobile m)
        {
            if (!RestrictTravelCombat)
            {
                return false;
            }

            for (var i = 0; i < m.Aggressed.Count; ++i)
            {
                var info = m.Aggressed[i];

                if (info.Defender.Player && Core.Now - info.LastCombatTime < CombatHeatDelay)
                {
                    return true;
                }
            }

            if (Core.AOS)
            {
                for (var i = 0; i < m.Aggressors.Count; ++i)
                {
                    var info = m.Aggressors[i];

                    if (info.Attacker.Player && info.Attacker == m && Core.Now - info.LastCombatTime < CombatHeatDelay)
                    {
                        return true;
                    }
                }
            }

            return false;
        }

        public static bool AdjustField(ref Point3D p, Map map, int height, bool mobsBlock)
        {
            if (map == null)
            {
                return false;
            }

            for (var offset = 0; offset < 10; ++offset)
            {
                var loc = new Point3D(p.X, p.Y, p.Z - offset);

                if (map.CanFit(loc, height, true, mobsBlock))
                {
                    p = loc;
                    return true;
                }
            }

            return false;
        }

        public static bool GetEastToWest(Point3D from, Point3D target)
        {
            var dx = from.X - target.X;
            var dy = from.Y - target.Y;
            var rx = (dx - dy) * 44;
            var ry = (dx + dy) * 44;

            return rx >= 0 && ry < 0 || ry >= 0 && rx < 0;
        }

        public static bool CanRevealCaster(Mobile m) => m is BaseCreature { Controlled: false };

        public static void GetSurfaceTop(ref IPoint3D p)
        {
            if (p is Item item)
            {
                p = item.GetSurfaceTop();
            }
            else if (p is StaticTarget t)
            {
                var z = t.Z;

                if ((t.Flags & TileFlag.Surface) == 0)
                {
                    z -= TileData.ItemTable[t.ItemID & TileData.MaxItemValue].CalcHeight;
                }

                p = new Point3D(t.X, t.Y, z);
            }
        }

        public static bool AddStatOffset(Mobile m, StatType type, int offset, TimeSpan duration) =>
            offset > 0
                ? AddStatBonus(m, m, type, offset, duration)
                : offset >= 0 || AddStatCurse(m, m, type, -offset, duration);

        public static bool AddStatBonus(Mobile caster, Mobile target, StatType type, TimeSpan duration, bool skillCheck = true) =>
            AddStatBonus(
                caster,
                target,
                type,
                GetOffset(caster, target, type, false),
                duration,
                skillCheck
            );

        public static bool AddStatBonus(
            Mobile caster, Mobile target, StatType type, int bonus, TimeSpan duration, bool skillCheck = false
        )
        {
            var name = $"[Magic] {type} Buff";

            var mod = target.GetStatMod(name);

            if (mod != null)
            {
                if (mod.Offset >= bonus)
                {
                    return false;
                }

                target.RemoveStatMod(mod);
            }

            if (skillCheck)
            {
                caster.CheckSkill(SkillName.EvalInt, 0.0, 120.0);
            }

            target.AddStatMod(new StatMod(type, name, bonus, duration));
            return true;
        }

        public static int GetCurse(Mobile caster, Mobile target, StatType type)
        {
            var name = $"[Magic] {type} Curse";

            var mod = target.GetStatMod(name);

            return mod?.Offset ?? 0;
        }

        public static bool AddStatCurse(Mobile caster, Mobile target, StatType type, TimeSpan duration, bool skillCheck = true) =>
            AddStatCurse(
                caster,
                target,
                type,
                GetOffset(caster, target, type, true),
                duration,
                skillCheck
            );

        public static bool AddStatCurse(
            Mobile caster, Mobile target, StatType type, int curse, TimeSpan duration, bool skillCheck = false
        )
        {
            var malus = -curse;
            var name = $"[Magic] {type} Curse";

            var mod = target.GetStatMod(name);

            if (mod != null)
            {
                if (mod.Offset <= malus)
                {
                    return false;
                }

                target.RemoveStatMod(mod);
            }

            if (skillCheck)
            {
                caster.CheckSkill(SkillName.EvalInt, 0.0, 120.0);
                target.CheckSkill(SkillName.MagicResist, 0.0, 120.0);
            }

            target.AddStatMod(new StatMod(type, name, malus, duration));
            return true;
        }

        public static TimeSpan GetDuration(Mobile caster, Mobile target) =>
            // TODO: Is this accurate for Curse? Sources say it is magery. Should confirm at least for newest era.
            TimeSpan.FromSeconds(6 * (Core.AOS ? caster.Skills.EvalInt.Value : caster.Skills.Magery.Value) / 5.0);

        public static double GetOffsetScalar(Mobile caster, Mobile target, bool curse)
        {
            var percent = curse
                ? 8 + (caster.Skills.EvalInt.Value - target.Skills.MagicResist.Value) / 10
                : 1 + caster.Skills.EvalInt.Value / 10;

            percent *= 0.01;

            return Math.Max(percent, 0);
        }

        public static int GetOffset(Mobile caster, Mobile target, StatType type, bool curse)
        {
            if (!Core.AOS)
            {
                return 1 + (int)(caster.Skills.Magery.Value * 0.1);
            }

            var percent = GetOffsetScalar(caster, target, curse);

            return type switch
            {
                StatType.Str => (int)(target.RawStr * percent),
                StatType.Dex => (int)(target.RawDex * percent),
                StatType.Int => (int)(target.RawInt * percent)
            };
        }

        public static Guild GetGuildFor(Mobile m)
        {
            var g = m.Guild as Guild;

            if (g == null && m is BaseCreature c)
            {
                m = c.ControlMaster;

                if (m != null)
                {
                    g = m.Guild as Guild;
                }

                if (g == null)
                {
                    m = c.SummonMaster;

                    if (m != null)
                    {
                        g = m.Guild as Guild;
                    }
                }
            }

            return g;
        }

        public static bool ValidIndirectTarget(Mobile from, Mobile to)
        {
            if (from == to)
            {
                return true;
            }

            if (to.Hidden && to.AccessLevel > from.AccessLevel)
            {
                return false;
            }

            var bcFrom = from as BaseCreature;
            var bcTarg = to as BaseCreature;

            var pmFrom = (bcFrom?.Summoned == true ? bcFrom.SummonMaster : from) as PlayerMobile;
            var pmTarg = (bcTarg?.Summoned == true ? bcTarg.SummonMaster : to) as PlayerMobile;

            if (pmFrom?.DuelContext != null && pmFrom.DuelContext == pmTarg?.DuelContext && pmFrom.DuelContext.Started &&
                pmFrom.DuelPlayer != null && pmTarg?.DuelPlayer != null)
            {
                return pmFrom.DuelPlayer.Participant != pmTarg.DuelPlayer.Participant;
            }

            var fromGuild = GetGuildFor(from);
            var toGuild = GetGuildFor(to);

            if (fromGuild != null && toGuild != null && (fromGuild == toGuild || fromGuild.IsAlly(toGuild)))
            {
                return false;
            }

            var p = Party.Get(from);

            if (p?.Contains(to) == true)
            {
                return false;
            }

            if (bcTarg != null && (bcTarg.Controlled || bcTarg.Summoned))
            {
                if (bcTarg.ControlMaster == from || bcTarg.SummonMaster == from)
                {
                    return false;
                }

                if (p != null && (p.Contains(bcTarg.ControlMaster) || p.Contains(bcTarg.SummonMaster)))
                {
                    return false;
                }
            }

            if (bcFrom != null && (bcFrom.Controlled || bcFrom.Summoned))
            {
                if (bcFrom.ControlMaster == to || bcFrom.SummonMaster == to)
                {
                    return false;
                }

                p = Party.Get(to);

                if (p != null && (p.Contains(bcFrom.ControlMaster) || p.Contains(bcFrom.SummonMaster)))
                {
                    return false;
                }
            }

            return bcTarg?.Controlled == false && bcTarg.InitialInnocent ||
                   Notoriety.Compute(from, to) != Notoriety.Innocent || from.Kills >= 5;
        }

        public static void Summon(
            BaseCreature creature, Mobile caster, int sound, TimeSpan duration, bool scaleDuration,
            bool scaleStats
        )
        {
            var map = caster.Map;

            if (map == null)
            {
                return;
            }

            var scale = 1.0 + (caster.Skills.Magery.Value - 100.0) / 200.0;

            if (scaleDuration)
            {
                duration *= scale;
            }

            if (scaleStats)
            {
                creature.RawStr = (int)(creature.RawStr * scale);
                creature.Hits = creature.HitsMax;

                creature.RawDex = (int)(creature.RawDex * scale);
                creature.Stam = creature.StamMax;

                creature.RawInt = (int)(creature.RawInt * scale);
                creature.Mana = creature.ManaMax;
            }

            var p = new Point3D(caster);

            if (FindValidSpawnLocation(map, ref p, true))
            {
                BaseCreature.Summon(creature, caster, p, sound, duration);
                return;
            }

            /*
            int offset = Utility.Random( 8 ) * 2;

            for ( int i = 0; i < m_Offsets.Length; i += 2 )
            {
              int x = caster.X + m_Offsets[(offset + i) % m_Offsets.Length];
              int y = caster.Y + m_Offsets[(offset + i + 1) % m_Offsets.Length];

              if (map.CanSpawnMobile( x, y, caster.Z ))
              {
                BaseCreature.Summon( creature, caster, new Point3D( x, y, caster.Z ), sound, duration );
                return;
              }
              else
              {
                int z = map.GetAverageZ( x, y );

                if (map.CanSpawnMobile( x, y, z ))
                {
                  BaseCreature.Summon( creature, caster, new Point3D( x, y, z ), sound, duration );
                  return;
                }
              }
            }
             * */

            creature.Delete();
            caster.SendLocalizedMessage(501942); // That location is blocked.
        }

        public static bool FindValidSpawnLocation(Map map, ref Point3D p, bool surroundingsOnly)
        {
            if (map == null) // sanity
            {
                return false;
            }

            if (!surroundingsOnly)
            {
                if (map.CanSpawnMobile(p)) // p's fine.
                {
                    p = new Point3D(p);
                    return true;
                }

                var z = map.GetAverageZ(p.X, p.Y);

                if (map.CanSpawnMobile(p.X, p.Y, z))
                {
                    p = new Point3D(p.X, p.Y, z);
                    return true;
                }
            }

            var offset = Utility.Random(8) * 2;

            for (var i = 0; i < m_Offsets.Length; i += 2)
            {
                var x = p.X + m_Offsets[(offset + i) % m_Offsets.Length];
                var y = p.Y + m_Offsets[(offset + i + 1) % m_Offsets.Length];

                if (map.CanSpawnMobile(x, y, p.Z))
                {
                    p = new Point3D(x, y, p.Z);
                    return true;
                }

                var z = map.GetAverageZ(x, y);

                if (map.CanSpawnMobile(x, y, z))
                {
                    p = new Point3D(x, y, z);
                    return true;
                }
            }

            return false;
        }

        private static TextDefinition _travelTo = 1019004;    // You are not allowed to travel there.
        private static TextDefinition _travelFrom = 502361;   // You cannot teleport into that area from here.
        private static TextDefinition _teleportTo = 501035;   // You cannot teleport from here to the destination.
        private static TextDefinition _genericSpell = 501802; // Thy spell doth not appear to work...

        public static TextDefinition InvalidTravelMessage(TravelCheckType type)
        {
            return type switch
            {
                TravelCheckType.RecallTo or TravelCheckType.GateTo         => _travelTo,
                TravelCheckType.RecallFrom or TravelCheckType.TeleportFrom => _travelFrom,
                TravelCheckType.TeleportTo                                 => _teleportTo,
                _                                                          => _genericSpell
            };
        }

        public static bool CheckTravel(Mobile caster, TravelCheckType type, out TextDefinition message) =>
            CheckTravel(caster, caster.Map, caster.Location, type, out message);

        public static bool CheckTravel(Map map, Point3D loc, TravelCheckType type, out TextDefinition message) =>
            CheckTravel(null, map, loc, type, out message);

        public static bool CheckTravel(Mobile caster, Map map, Point3D loc, TravelCheckType type, out TextDefinition message)
        {
            message = null;

            if (IsInvalid(map, loc)) // null, internal, out of bounds
            {
                message = InvalidTravelMessage(type);
                return false;
            }

            // Always allow monsters to teleport
            if (type is TravelCheckType.TeleportTo or TravelCheckType.TeleportFrom &&
                caster is BaseCreature { Controlled: false, Summoned: false })
            {
                return true;
            }

            m_TravelCaster = caster;
            m_TravelType = type;

            var v = (int)type;

            if (caster != null)
            {
                var destination = Region.Find(loc, map) as BaseRegion;
                var current = Region.Find(caster.Location, caster.Map) as BaseRegion;

                if (destination?.CheckTravel(caster, loc, type, out message) == false || current?.CheckTravel(caster, loc, type, out message) == false)
                {
                    message ??= InvalidTravelMessage(type);
                    return false;
                }
            }

            for (var i = 0; i < m_Validators.Length; ++i)
            {
                var isValid = m_Rules[v, i] || !m_Validators[i](map, loc);
                if (!isValid)
                {
                    message = InvalidTravelMessage(type);
                    return false;
                }
            }

            return true;
        }

        public static bool IsWindLoc(Point3D loc)
        {
            int x = loc.X, y = loc.Y;

            return x >= 5120 && y >= 0 && x < 5376 && y < 256;
        }

        public static bool IsFeluccaWind(Map map, Point3D loc) => map == Map.Felucca && IsWindLoc(loc);

        public static bool IsTrammelWind(Map map, Point3D loc) => map == Map.Trammel && IsWindLoc(loc);

        public static bool IsIlshenar(Map map, Point3D loc) => map == Map.Ilshenar;

        public static bool IsSolenHiveLoc(Point3D loc)
        {
            int x = loc.X, y = loc.Y;

            return x >= 5640 && y >= 1776 && x < 5935 && y < 2039;
        }

        public static bool IsTrammelSolenHive(Map map, Point3D loc) => map == Map.Trammel && IsSolenHiveLoc(loc);

        public static bool IsFeluccaSolenHive(Map map, Point3D loc) => map == Map.Felucca && IsSolenHiveLoc(loc);

        public static bool IsFeluccaT2A(Map map, Point3D loc)
        {
            int x = loc.X, y = loc.Y;

            return map == Map.Felucca && x >= 5120 && y >= 2304 && x < 6144 && y < 4096;
        }

        public static bool IsAnyT2A(Map map, Point3D loc)
        {
            int x = loc.X, y = loc.Y;

            return (map == Map.Trammel || map == Map.Felucca) && x >= 5120 && y >= 2304 && x < 6144 && y < 4096;
        }

        public static bool IsFeluccaDungeon(Map map, Point3D loc)
        {
            var region = Region.Find(loc, map);
            return region.IsPartOf<DungeonRegion>() && region.Map == Map.Felucca;
        }

        public static bool IsKhaldun(Map map, Point3D loc) => Region.Find(loc, map).Name == "Khaldun";

        public static bool IsCrystalCave(Map map, Point3D loc)
        {
            if (map != Map.Malas || loc.Z >= -80)
            {
                return false;
            }

            int x = loc.X, y = loc.Y;

            return x >= 1182 && y >= 437 && x < 1211 && y < 470
                   || x >= 1156 && y >= 470 && x < 1211 && y < 503
                   || x >= 1176 && y >= 503 && x < 1208 && y < 509
                   || x >= 1188 && y >= 509 && x < 1201 && y < 513;
        }

        public static bool IsSafeZone(Map map, Point3D loc) =>
            Region.Find(loc, map).IsPartOf<SafeZone>() &&
            m_TravelType is TravelCheckType.TeleportTo or TravelCheckType.TeleportFrom
            && (m_TravelCaster as PlayerMobile)?.DuelPlayer?.Eliminated == false;

        public static bool IsFactionStronghold(Map map, Point3D loc) => Region.Find(loc, map).IsPartOf<StrongholdRegion>();

        public static bool IsChampionSpawn(Map map, Point3D loc) => Region.Find(loc, map).IsPartOf<ChampionSpawnRegion>();

        public static bool IsDoomFerry(Map map, Point3D loc)
        {
            if (map != Map.Malas)
            {
                return false;
            }

            int x = loc.X, y = loc.Y;

            return x >= 426 && y >= 314 && x <= 430 && y <= 331 || x >= 406 && y >= 247 && x <= 410 && y <= 264;
        }

        public static bool IsTokunoDungeon(Map map, Point3D loc)
        {
            // The tokuno dungeons are really inside malas
            if (map != Map.Malas)
            {
                return false;
            }

            int x = loc.X, y = loc.Y, z = loc.Z;

            var r1 = x >= 0 && y >= 0 && x <= 128 && y <= 128;
            var r2 = x >= 45 && y >= 320 && x < 195 && y < 710;

            return r1 || r2;
        }

        public static bool IsDoomGauntlet(Map map, Point3D loc)
        {
            if (map != Map.Malas)
            {
                return false;
            }

            int x = loc.X - 256, y = loc.Y - 304;

            return x >= 0 && y >= 0 && x < 256 && y < 256;
        }

        public static bool IsLampRoom(Map map, Point3D loc)
        {
            if (map != Map.Malas)
            {
                return false;
            }

            int x = loc.X, y = loc.Y;

            return x >= 465 && y >= 92 && x < 474 && y < 102;
        }

        public static bool IsGuardianRoom(Map map, Point3D loc)
        {
            if (map != Map.Malas)
            {
                return false;
            }

            int x = loc.X, y = loc.Y;

            return x >= 356 && y >= 5 && x < 375 && y < 25;
        }

        public static bool IsHeartwood(Map map, Point3D loc)
        {
            int x = loc.X, y = loc.Y;

            return (map == Map.Trammel || map == Map.Felucca) && x >= 6911 && y >= 254 && x < 7167 && y < 511;
        }

        public static bool IsMLDungeon(Map map, Point3D loc) => MondainsLegacy.IsMLRegion(Region.Find(loc, map));

        public static bool IsInvalid(Map map, Point3D loc)
        {
            if (map == null || map == Map.Internal)
            {
                return true;
            }

            int x = loc.X, y = loc.Y;

            return x < 0 || y < 0 || x >= map.Width || y >= map.Height;
        }

        public static bool IsTown(Point3D loc, Mobile caster)
        {
            var map = caster.Map;

            if (map == null)
            {
                return false;
            }

            if (Region.Find(loc, map).GetRegion<SafeZone>() != null)
            {
                if (caster is PlayerMobile pm && (pm.DuelContext?.Started != true || pm.DuelPlayer?.Eliminated != false))
                {
                    return true;
                }
            }

            var reg = Region.Find(loc, map).GetRegion<GuardedRegion>();

            return reg?.IsDisabled() == false;
        }

        public static bool CheckTown(IPoint3D ip, Mobile caster) =>
            CheckTown((ip as Item)?.GetWorldLocation() ?? new Point3D(ip), caster);

        public static bool CheckTown(Point3D loc, Mobile caster)
        {
            if (IsTown(loc, caster))
            {
                caster.SendLocalizedMessage(500946); // You cannot cast this in town!
                return false;
            }

            return true;
        }

        // magic reflection
        public static bool CheckReflect(int circle, Mobile caster, ref Mobile target) =>
            CheckReflect(circle, ref caster, ref target);

        public static bool CheckReflect(int circle, ref Mobile caster, ref Mobile target)
        {
            var reflect = false;

            if (target.MagicDamageAbsorb > 0)
            {
                ++circle;

                target.MagicDamageAbsorb -= circle;

                // This order isn't very intuitive, but you have to nullify reflect before target gets switched
                reflect = target.MagicDamageAbsorb >= 0;
                if (target.MagicDamageAbsorb <= 0)
                {
                    target.MagicDamageAbsorb = 0;
                    DefensiveSpell.Nullify(target);
                }
            }

            if (target is BaseCreature creature)
            {
                creature.CheckReflect(caster, ref reflect);
            }

            if (reflect)
            {
                target.FixedEffect(0x37B9, 10, 5);
                (caster, target) = (target, caster);
            }

            return reflect;
        }

        public static void Damage(Spell spell, Mobile target, double damage)
        {
            var ts = GetDamageDelayForSpell(spell);

            Damage(spell, ts, target, spell.Caster, damage);
        }

        public static void Damage(TimeSpan delay, Mobile target, double damage)
        {
            Damage(delay, target, null, damage);
        }

        public static void Damage(TimeSpan delay, Mobile target, Mobile from, double damage)
        {
            Damage(null, delay, target, from, damage);
        }

        public static void Damage(Spell spell, TimeSpan delay, Mobile target, Mobile from, double damage)
        {
            var damageGiven = (int)damage;

            if (delay == TimeSpan.Zero)
            {
                var bcFrom = from as BaseCreature;
                var bcTarget = target as BaseCreature;

                bcFrom?.AlterSpellDamageTo(target, ref damageGiven);
                bcTarget?.AlterSpellDamageFrom(from, ref damageGiven);

                target.Damage(damageGiven, from);

                bcFrom?.OnDamageSpell(target, damageGiven);

                if (from != null)
                {
                    bcTarget?.OnHarmfulSpell(from);
                    bcTarget?.OnDamagedBySpell(from, damageGiven);
                }
            }
            else
            {
                new SpellDamageTimer(spell, target, from, damageGiven, delay).Start();
            }
        }

        public static void Damage(
            Spell spell, Mobile target, double damage, int phys, int fire, int cold, int pois,
            int nrgy, int chaos = 0, DFAlgorithm dfa = DFAlgorithm.Standard
        )
        {
            Damage(
                spell,
                GetDamageDelayForSpell(spell),
                target,
                spell.Caster,
                damage,
                phys,
                fire,
                cold,
                pois,
                nrgy,
                chaos,
                dfa
            );
        }

        public static void Damage(
            TimeSpan delay, Mobile target, double damage, int phys, int fire, int cold, int pois,
            int nrgy, int chaos = 0, DFAlgorithm dfa = DFAlgorithm.Standard
        )
        {
            Damage(delay, target, null, damage, phys, fire, cold, pois, nrgy, chaos, dfa);
        }

        public static void Damage(
            TimeSpan delay, Mobile target, Mobile from, double damage, int phys, int fire, int cold,
            int pois, int nrgy, int chaos = 0, DFAlgorithm dfa = DFAlgorithm.Standard
        )
        {
            Damage(null, delay, target, from, damage, phys, fire, cold, pois, nrgy, chaos, dfa);
        }

        public static void Damage(
            Spell spell, TimeSpan delay, Mobile target, Mobile from, double damage, int phys, int fire,
            int cold, int pois, int nrgy, int chaos = 0, DFAlgorithm dfa = DFAlgorithm.Standard
        )
        {
            var dmg = (int)damage;

            if (delay == TimeSpan.Zero)
            {
                var bcFrom = from as BaseCreature;
                var bcTarget = target as BaseCreature;
                bcFrom?.AlterSpellDamageTo(target, ref dmg);

                bcTarget?.AlterSpellDamageFrom(from, ref dmg);

                if (Feint.GetDamageReduction(from, target, out int feintReduction))
                {
                    // example: 35 damage * 50 / 100 = 17 damage
                    dmg -= dmg * feintReduction / 100;
                }

                StaminaSystem.DFA = dfa;

                var damageGiven = AOS.Damage(target, from, dmg, phys, fire, cold, pois, nrgy, chaos);
                Mysticism.SpellPlagueSpell.OnMobileDamaged(target);

                StaminaSystem.DFA = DFAlgorithm.Standard;

                bcFrom?.OnDamageSpell(target, damageGiven);

                if (bcTarget != null && from != null && delay == TimeSpan.Zero)
                {
                    bcTarget.OnHarmfulSpell(from);
                    bcTarget.OnDamagedBySpell(from, damageGiven);
                }
            }
            else
            {
                new SpellDamageTimerAOS(spell, delay, target, from, dmg, phys, fire, cold, pois, nrgy, chaos, dfa).Start();
            }
        }

        public static void DoLeech(int damageGiven, Mobile from, Mobile target)
        {
            if (target == null)
            {
                return;
            }

            var context = TransformationSpellHelper.GetContext(from);

            if (context == null) /* cleanup */
            {
                return;
            }

            if (context.Type == typeof(WraithFormSpell))
            {
                WraithFormSpell.DoWraithLeech(from, target, damageGiven);
            }
            else if (context.Type == typeof(VampiricEmbraceSpell))
            {
                from.Hits += Math.Min(target.Hits, AOS.Scale(damageGiven, 20));
                from.PlaySound(0x44D);
            }
        }
        public static void Heal(int amount, Mobile target, Mobile from, bool message = true)
        {
            // TODO: All Healing *spells* go through ArcaneEmpowerment
            target.Heal(amount, from, message);
        }

        private delegate bool TravelValidator(Map map, Point3D loc);

        private class SpellDamageTimer : Timer
        {
            private readonly Mobile m_From;
            private readonly Spell m_Spell;
            private readonly Mobile m_Target;
            private int m_Damage;

            public SpellDamageTimer(Spell s, Mobile target, Mobile from, int damage, TimeSpan delay) : base(delay)
            {
                m_Target = target;
                m_From = from;
                m_Damage = damage;
                m_Spell = s;

                if (m_Spell?.DelayedDamage == true)
                {
                    m_Spell.StartDelayedDamageContext(target, this);
                }
            }

            protected override void OnTick()
            {
                (m_From as BaseCreature)?.AlterSpellDamageTo(m_Target, ref m_Damage);
                (m_Target as BaseCreature)?.AlterSpellDamageFrom(m_From, ref m_Damage);

                m_Target.Damage(m_Damage);
                m_Spell?.RemoveDelayedDamageContext(m_Target);
            }
        }

        private class SpellDamageTimerAOS : Timer
        {
            private readonly int m_Chaos;
            private readonly int m_Cold;
            private readonly DFAlgorithm m_DFA;
            private readonly int m_Fire;
            private readonly Mobile m_From;
            private readonly int m_Nrgy;
            private readonly int m_Phys;
            private readonly int m_Pois;
            private readonly Spell m_Spell;
            private readonly Mobile m_Target;
            private int m_Damage;

            public SpellDamageTimerAOS(
                Spell s, TimeSpan delay, Mobile target, Mobile from, int damage, int phys, int fire, int cold,
                int pois, int nrgy, int chaos, DFAlgorithm dfa
            ) : base(delay)
            {
                m_Target = target;
                m_From = from;
                m_Damage = damage;
                m_Phys = phys;
                m_Fire = fire;
                m_Cold = cold;
                m_Pois = pois;
                m_Nrgy = nrgy;
                m_Chaos = chaos;
                m_DFA = dfa;
                m_Spell = s;

                if (m_Spell?.DelayedDamage == true)
                {
                    m_Spell.StartDelayedDamageContext(target, this);
                }
            }

            protected override void OnTick()
            {
                Damage(
                    m_Spell,
                    TimeSpan.Zero,
                    m_Target,
                    m_From,
                    m_Damage,
                    m_Phys,
                    m_Fire,
                    m_Cold,
                    m_Pois,
                    m_Nrgy,
                    m_Chaos,
                    m_DFA
                );

                m_Spell?.RemoveDelayedDamageContext(m_Target);
            }
        }
    }

    public static class TransformationSpellHelper
    {
        private static readonly Dictionary<Mobile, TransformContext> _table = new();

        public static bool CheckCast(Mobile caster, Spell spell)
        {
            if (Sigil.ExistsOn(caster))
            {
                caster.SendLocalizedMessage(1061632); // You can't do that while carrying the sigil.
                return false;
            }

            if (!caster.CanBeginAction<PolymorphSpell>())
            {
                caster.SendLocalizedMessage(1061628); // You can't do that while polymorphed.
                return false;
            }

            if (AnimalForm.UnderTransformation(caster))
            {
                caster.SendLocalizedMessage(1061091); // You cannot cast that spell in this form.
                return false;
            }

            return true;
        }

        public static bool OnCast(Mobile caster, Spell spell)
        {
            if (spell is not ITransformationSpell transformSpell)
            {
                return false;
            }

            if (Sigil.ExistsOn(caster))
            {
                caster.SendLocalizedMessage(1061632); // You can't do that while carrying the sigil.
            }
            else if (!caster.CanBeginAction<PolymorphSpell>())
            {
                caster.SendLocalizedMessage(1061628); // You can't do that while polymorphed.
            }
            else if (DisguisePersistence.IsDisguised(caster))
            {
                caster.SendLocalizedMessage(1061631); // You can't do that while disguised.
                return false;
            }
            else if (AnimalForm.UnderTransformation(caster))
            {
                caster.SendLocalizedMessage(1061091); // You cannot cast that spell in this form.
            }
            else if (!caster.CanBeginAction<IncognitoSpell>() || caster.IsBodyMod && GetContext(caster) == null)
            {
                spell.DoFizzle();
            }
            else if (spell.CheckSequence())
            {
                var context = GetContext(caster);
                var ourType = spell.GetType();

                var wasTransformed = context != null;
                var ourTransform = wasTransformed && context.Type == ourType;

                if (wasTransformed)
                {
                    RemoveContext(caster, context, ourTransform);

                    if (ourTransform)
                    {
                        caster.PlaySound(0xFA);
                        caster.FixedParticles(0x3728, 1, 13, 5042, EffectLayer.Waist);
                    }
                }

                if (!ourTransform)
                {
                    if (transformSpell.PhysResistOffset != 0)
                    {
                        caster.AddResistanceMod(new ResistanceMod(ResistanceType.Physical, "TransformSpell", transformSpell.PhysResistOffset));
                    }

                    if (transformSpell.FireResistOffset != 0)
                    {
                        caster.AddResistanceMod(new ResistanceMod(ResistanceType.Fire, "TransformSpell", transformSpell.FireResistOffset));
                    }

                    if (transformSpell.ColdResistOffset != 0)
                    {
                        caster.AddResistanceMod(new ResistanceMod(ResistanceType.Cold, "TransformSpell", transformSpell.ColdResistOffset));
                    }

                    if (transformSpell.PoisResistOffset != 0)
                    {
                        caster.AddResistanceMod(new ResistanceMod(ResistanceType.Poison, "TransformSpell", transformSpell.PoisResistOffset));
                    }

                    if (transformSpell.NrgyResistOffset != 0)
                    {
                        caster.AddResistanceMod(new ResistanceMod(ResistanceType.Energy, "TransformSpell", transformSpell.NrgyResistOffset));
                    }

                    if (!((Body)transformSpell.Body).IsHuman)
                    {
                        var mt = caster.Mount;

                        if (mt != null)
                        {
                            mt.Rider = null;
                        }
                    }

                    caster.BodyMod = transformSpell.Body;
                    caster.HueMod = transformSpell.Hue;

                    transformSpell.DoEffect(caster);

                    Timer timer = new TransformTimer(caster, transformSpell);
                    timer.Start();

                    AddContext(caster, new TransformContext(timer, ourType, transformSpell));
                    return true;
                }
            }

            return false;
        }

        public static void AddContext(Mobile m, TransformContext context)
        {
            _table[m] = context;
        }

        public static void RemoveContext(Mobile m, bool resetGraphics)
        {
            var context = GetContext(m);

            if (context != null)
            {
                RemoveContext(m, context, resetGraphics);
            }
        }

        public static void RemoveContext(Mobile m, TransformContext context, bool resetGraphics)
        {
            if (!_table.Remove(m))
            {
                return;
            }

            m.RemoveResistanceMod("TransformSpell");

            if (resetGraphics)
            {
                m.HueMod = -1;
                m.BodyMod = 0;
            }

            context.Timer.Stop();
            context.Spell.RemoveEffect(m);
        }

        public static TransformContext GetContext(Mobile m)
        {
            _table.TryGetValue(m, out var context);

            return context;
        }

        public static bool UnderTransformation(Mobile m) => GetContext(m) != null;

        public static bool UnderTransformation(Mobile m, Type type) => GetContext(m)?.Type == type;
    }

    public interface ITransformationSpell
    {
        int Body { get; }
        int Hue { get; }

        int PhysResistOffset { get; }
        int FireResistOffset { get; }
        int ColdResistOffset { get; }
        int PoisResistOffset { get; }
        int NrgyResistOffset { get; }

        double TickRate { get; }
        void OnTick(Mobile m);

        void DoEffect(Mobile m);
        void RemoveEffect(Mobile m);
    }

    public class TransformContext
    {
        public TransformContext(Timer timer, Type type, ITransformationSpell spell)
        {
            Timer = timer;
            Type = type;
            Spell = spell;
        }

        public Timer Timer { get; }

        public Type Type { get; }

        public ITransformationSpell Spell { get; }
    }

    public class TransformTimer : Timer
    {
        private readonly Mobile m_Mobile;
        private readonly ITransformationSpell m_Spell;

        public TransformTimer(Mobile from, ITransformationSpell spell)
            : base(TimeSpan.FromSeconds(spell.TickRate), TimeSpan.FromSeconds(spell.TickRate))
        {
            m_Mobile = from;
            m_Spell = spell;
        }

        protected override void OnTick()
        {
            if (m_Mobile.Deleted || !m_Mobile.Alive || m_Mobile.Body != m_Spell.Body || m_Mobile.Hue != m_Spell.Hue)
            {
                TransformationSpellHelper.RemoveContext(m_Mobile, true);
                Stop();
            }
            else
            {
                m_Spell.OnTick(m_Mobile);
            }
        }
    }
}
