using System;
using System.Collections.Generic;
using System.Linq;
using Content.Server.GameObjects.Components.Damage;
using Content.Server.GameObjects.Components.Mobs;
using Content.Server.GameObjects.Components.Projectiles;
using Content.Server.GameObjects.Components.Weapon.Ranged.Ammunition;
using Content.Shared.Audio;
using Content.Shared.GameObjects.Components.Weapons.Ranged;
using Content.Shared.GameObjects.EntitySystems;
using Content.Shared.Interfaces.GameObjects.Components;
using Content.Shared.Physics;
using Robust.Server.GameObjects.EntitySystems;
using Robust.Shared.Audio;
using Robust.Shared.GameObjects.Components;
using Robust.Shared.GameObjects.Systems;
using Robust.Shared.Interfaces.GameObjects;
using Robust.Shared.Interfaces.Map;
using Robust.Shared.Interfaces.Physics;
using Robust.Shared.Interfaces.Random;
using Robust.Shared.Interfaces.Timing;
using Robust.Shared.IoC;
using Robust.Shared.Localization;
using Robust.Shared.Log;
using Robust.Shared.Map;
using Robust.Shared.Maths;
using Robust.Shared.Prototypes;
using Robust.Shared.Random;
using Robust.Shared.Serialization;
using Robust.Shared.Utility;

namespace Content.Server.GameObjects.Components.Weapon.Ranged.Barrels
{
    /// <summary>
    /// All of the ranged weapon components inherit from this to share mechanics like shooting etc.
    /// Only difference between them is how they retrieve a projectile to shoot (battery, magazine, etc.)
    /// </summary>
    public abstract class ServerRangedBarrelComponent : SharedRangedBarrelComponent, IUse, IInteractUsing, IExamine
    {
        // There's still some of py01 and PJB's work left over, especially in underlying shooting logic,
        // it's just when I re-organised it changed me as the contributor
#pragma warning disable 649
        [Dependency] private IGameTiming _gameTiming;
        [Dependency] private IRobustRandom _robustRandom;
#pragma warning restore 649

        public override FireRateSelector FireRateSelector => _fireRateSelector;
        private FireRateSelector _fireRateSelector;
        public override FireRateSelector AllRateSelectors => _fireRateSelector;
        private FireRateSelector _allRateSelectors;
        public override float FireRate => _fireRate;
        private float _fireRate;

        // _lastFire is when we actually fired (so if we hold the button then recoil doesn't build up if we're not firing)
        private TimeSpan _lastFire;

        public abstract IEntity PeekAmmo();
        public abstract IEntity TakeProjectile(GridCoordinates spawnAtGrid, MapCoordinates spawnAtMap);

        // Recoil / spray control
        private Angle _minAngle;
        private Angle _maxAngle;
        private Angle _currentAngle = Angle.Zero;
        /// <summary>
        /// How slowly the angle's theta decays per second in radians
        /// </summary>
        private float _angleDecay;
        /// <summary>
        /// How quickly the angle's theta builds for every shot fired in radians
        /// </summary>
        private float _angleIncrease;
        // Multiplies the ammo spread to get the final spread of each pellet
        private float _spreadRatio;

        public bool CanMuzzleFlash => _canMuzzleFlash;
        private bool _canMuzzleFlash = true;

        // Sounds
        public string SoundGunshot
        {
            get => _soundGunshot;
            set => _soundGunshot = value;
        }
        private string _soundGunshot;
        public string SoundEmpty => _soundEmpty;
        private string _soundEmpty;

        public override void ExposeData(ObjectSerializer serializer)
        {
            base.ExposeData(serializer);

            serializer.DataField(ref _fireRateSelector, "currentSelector", FireRateSelector.Safety);
            serializer.DataField(ref _fireRate, "fireRate", 2.0f);

            // This hard-to-read area's dealing with recoil
            // Use degrees in yaml as it's easier to read compared to "0.0125f"
            serializer.DataReadWriteFunction(
                "minAngle",
                0,
                angle => _minAngle = Angle.FromDegrees(angle / 2f),
                () => _minAngle.Degrees * 2);

            // Random doubles it as it's +/- so uhh we'll just half it here for readability
            serializer.DataReadWriteFunction(
                "maxAngle",
                45,
                angle => _maxAngle = Angle.FromDegrees(angle / 2f),
                () => _maxAngle.Degrees * 2);

            serializer.DataReadWriteFunction(
                "angleIncrease",
                40 / _fireRate,
                angle => _angleIncrease = angle * (float) Math.PI / 180f,
                () => MathF.Round(_angleIncrease / ((float) Math.PI / 180f), 2));

            serializer.DataReadWriteFunction(
                "angleDecay",
                20f,
                angle => _angleDecay = angle * (float) Math.PI / 180f,
                () => MathF.Round(_angleDecay / ((float) Math.PI / 180f), 2));

            serializer.DataField(ref _spreadRatio, "ammoSpreadRatio", 1.0f);

            serializer.DataReadWriteFunction(
                "allSelectors",
                new List<FireRateSelector>(),
                selectors => selectors.ForEach(selector => _allRateSelectors |= selector),
                () =>
                {
                    var types = new List<FireRateSelector>();

                    foreach (FireRateSelector selector in Enum.GetValues(typeof(FireRateSelector)))
                    {
                        if ((_allRateSelectors & selector) != 0)
                        {
                            types.Add(selector);
                        }
                    }

                    return types;
                });

            // For simplicity we'll enforce it this way; ammo determines max spread
            if (_spreadRatio > 1.0f)
            {
                Logger.Error("SpreadRatio must be <= 1.0f for guns");
                throw new InvalidOperationException();
            }

            serializer.DataField(ref _canMuzzleFlash, "canMuzzleFlash", true);

            // Sounds
            serializer.DataField(ref _soundGunshot, "soundGunshot", null);
            serializer.DataField(ref _soundEmpty, "soundEmpty", "/Audio/Weapons/Guns/Empty/empty.ogg");
        }

        public override void OnAdd()
        {
            base.OnAdd();
            var rangedWeaponComponent = Owner.GetComponent<ServerRangedWeaponComponent>();
            rangedWeaponComponent.Barrel = this;
            rangedWeaponComponent.FireHandler += Fire;
            rangedWeaponComponent.WeaponCanFireHandler += WeaponCanFire;
        }

        public override void OnRemove()
        {
            base.OnRemove();
            if (Owner.TryGetComponent(out ServerRangedWeaponComponent rangedWeaponComponent))
            {
                rangedWeaponComponent.Barrel = null;
                rangedWeaponComponent.FireHandler -= Fire;
                rangedWeaponComponent.WeaponCanFireHandler -= WeaponCanFire;
            }
        }

        private Angle GetRecoilAngle(Angle direction)
        {
            var currentTime = _gameTiming.CurTime;
            var timeSinceLastFire = (currentTime - _lastFire).TotalSeconds;
            var newTheta = FloatMath.Clamp(_currentAngle.Theta + _angleIncrease - _angleDecay * timeSinceLastFire, _minAngle.Theta, _maxAngle.Theta);
            _currentAngle = new Angle(newTheta);

            var random = (_robustRandom.NextDouble() - 0.5) * 2;
            var angle = Angle.FromDegrees(direction.Degrees + _currentAngle.Degrees * random);
            return angle;
        }

        public abstract bool UseEntity(UseEntityEventArgs eventArgs);
        public abstract bool InteractUsing(InteractUsingEventArgs eventArgs);

        public void ChangeFireSelector(FireRateSelector rateSelector)
        {
            if ((rateSelector & AllRateSelectors) != 0)
            {
                _fireRateSelector = rateSelector;
                return;
            }

            throw new InvalidOperationException();
        }

        protected virtual bool WeaponCanFire()
        {
            // If the ServerRangedWeaponComponent gets re-done probably need to add the checks here
            return true;
        }

        /// <summary>
        /// Fires a round of ammo out of the weapon.
        /// </summary>
        /// <param name="shooter">Entity that is operating the weapon, usually the player.</param>
        /// <param name="targetPos">Target position on the map to shoot at.</param>
        private void Fire(IEntity shooter, Vector2 targetPos)
        {
            var soundSystem = EntitySystem.Get<AudioSystem>();
            if (ShotsLeft == 0)
            {
                if (_soundEmpty != null)
                {
                    soundSystem.PlayAtCoords(_soundEmpty, Owner.Transform.GridPosition);
                }
                return;
            }

            var ammo = PeekAmmo();
            var projectile = TakeProjectile(shooter.Transform.GridPosition, shooter.Transform.MapPosition);
            if (projectile == null)
            {
                soundSystem.PlayAtCoords(_soundEmpty, Owner.Transform.GridPosition);
                return;
            }

            // At this point firing is confirmed
            var direction = (targetPos - shooter.Transform.WorldPosition).ToAngle();
            var angle = GetRecoilAngle(direction);
            // This should really be client-side but for now we'll just leave it here
            if (shooter.TryGetComponent(out CameraRecoilComponent recoilComponent))
            {
                recoilComponent.Kick(-angle.ToVec() * 0.15f);
            }


            // This section probably needs tweaking so there can be caseless hitscan etc.
            if (projectile.TryGetComponent(out HitscanComponent hitscan))
            {
                FireHitscan(shooter, hitscan, angle);
            }
            else if (projectile.HasComponent<ProjectileComponent>())
            {
                var ammoComponent = ammo.GetComponent<AmmoComponent>();

                FireProjectiles(shooter, projectile, ammoComponent.ProjectilesFired, ammoComponent.EvenSpreadAngle, angle, ammoComponent.Velocity);

                if (CanMuzzleFlash)
                {
                    ammoComponent.MuzzleFlash(Owner.Transform.GridPosition, angle);
                }

                if (ammoComponent.Caseless)
                {
                    ammo.Delete();
                }
            }
            else
            {
                // Invalid types
                throw new InvalidOperationException();
            }

            soundSystem.PlayAtCoords(_soundGunshot, Owner.Transform.GridPosition);
            _lastFire = _gameTiming.CurTime;

            return;
        }

        /// <summary>
        /// Drops a single cartridge / shell
        /// Made as a static function just because multiple places need it
        /// </summary>
        /// <param name="entity"></param>
        /// <param name="playSound"></param>
        /// <param name="robustRandom"></param>
        /// <param name="prototypeManager"></param>
        /// <param name="ejectDirections"></param>
        public static void EjectCasing(
            IEntity entity,
            bool playSound = true,
            IRobustRandom robustRandom = null,
            IPrototypeManager prototypeManager = null,
            Direction[] ejectDirections = null)
        {
            if (robustRandom == null)
            {
                robustRandom = IoCManager.Resolve<IRobustRandom>();
            }

            if (ejectDirections == null)
            {
                ejectDirections = new[] {Direction.East, Direction.North, Direction.South, Direction.West};
            }

            const float ejectOffset = 0.2f;
            var ammo = entity.GetComponent<AmmoComponent>();
            var offsetPos = (robustRandom.NextFloat() * ejectOffset, robustRandom.NextFloat() * ejectOffset);
            entity.Transform.GridPosition = entity.Transform.GridPosition.Offset(offsetPos);
            entity.Transform.LocalRotation = robustRandom.Pick(ejectDirections).ToAngle();

            if (ammo.SoundCollectionEject == null || !playSound)
            {
                return;
            }

            if (prototypeManager == null)
            {
                prototypeManager = IoCManager.Resolve<IPrototypeManager>();
            }

            var soundCollection = prototypeManager.Index<SoundCollectionPrototype>(ammo.SoundCollectionEject);
            var randomFile = robustRandom.Pick(soundCollection.PickFiles);
            EntitySystem.Get<AudioSystem>().PlayAtCoords(randomFile, entity.Transform.GridPosition, AudioParams.Default.WithVolume(-1));
        }

        /// <summary>
        /// Drops multiple cartridges / shells on the floor
        /// Wraps EjectCasing to make it less toxic for bulk ejections
        /// </summary>
        /// <param name="entities"></param>
        public static void EjectCasings(IEnumerable<IEntity> entities)
        {
            var robustRandom = IoCManager.Resolve<IRobustRandom>();
            var prototypeManager = IoCManager.Resolve<IPrototypeManager>();
            var ejectDirections = new[] {Direction.East, Direction.North, Direction.South, Direction.West};
            var soundPlayCount = 0;
            var playSound = true;

            foreach (var entity in entities)
            {
                EjectCasing(entity, playSound, robustRandom, prototypeManager, ejectDirections);
                soundPlayCount++;
                if (soundPlayCount > 3)
                {
                    playSound = false;
                }
            }
        }

        #region Firing
        /// <summary>
        /// Handles firing one or many projectiles
        /// </summary>
        private void FireProjectiles(IEntity shooter, IEntity baseProjectile, int count, float evenSpreadAngle, Angle angle, float velocity)
        {
            List<Angle> sprayAngleChange = null;
            if (count > 1)
            {
                evenSpreadAngle *= _spreadRatio;
                sprayAngleChange = Linspace(-evenSpreadAngle / 2, evenSpreadAngle / 2, count);
            }

            for (var i = 0; i < count; i++)
            {
                IEntity projectile;

                if (i == 0)
                {
                    projectile = baseProjectile;
                }
                else
                {
                    projectile =
                        Owner.EntityManager.SpawnEntity(baseProjectile.Prototype.ID, Owner.Transform.GridPosition);
                }

                Angle projectileAngle;

                if (sprayAngleChange != null)
                {
                    projectileAngle = angle + sprayAngleChange[i];
                }
                else
                {
                    projectileAngle = angle;
                }

                var collidableComponent = projectile.GetComponent<ICollidableComponent>();
                collidableComponent.Status = BodyStatus.InAir;
                projectile.Transform.GridPosition = Owner.Transform.GridPosition;

                var projectileComponent = projectile.GetComponent<ProjectileComponent>();
                projectileComponent.IgnoreEntity(shooter);

                projectile
                    .GetComponent<ICollidableComponent>()
                    .EnsureController<BulletController>()
                    .LinearVelocity = projectileAngle.ToVec() * velocity;

                projectile.Transform.LocalRotation = projectileAngle.Theta;
            }
        }

        /// <summary>
        ///     Returns a list of numbers that form a set of equal intervals between the start and end value. Used to calculate shotgun spread angles.
        /// </summary>
        private List<Angle> Linspace(double start, double end, int intervals)
        {
            DebugTools.Assert(intervals > 1);

            var linspace = new List<Angle>(intervals);

            for (var i = 0; i <= intervals - 1; i++)
            {
                linspace.Add(Angle.FromDegrees(start + (end - start) * i / (intervals - 1)));
            }
            return linspace;
        }

        /// <summary>
        /// Fires hitscan entities and then displays their effects
        /// </summary>
        private void FireHitscan(IEntity shooter, HitscanComponent hitscan, Angle angle)
        {
            var ray = new CollisionRay(Owner.Transform.GridPosition.Position, angle.ToVec(), (int) hitscan.CollisionMask);
            var physicsManager = IoCManager.Resolve<IPhysicsManager>();
            var rayCastResults = physicsManager.IntersectRay(Owner.Transform.MapID, ray, hitscan.MaxLength, shooter, false).ToList();

            if (rayCastResults.Count >= 1)
            {
                var result = rayCastResults[0];
                var distance = result.HitEntity != null ? result.Distance : hitscan.MaxLength;
                hitscan.FireEffects(shooter, distance, angle, result.HitEntity);

                if (result.HitEntity == null || !result.HitEntity.TryGetComponent(out DamageableComponent damageable))
                {
                    return;
                }

                damageable.TakeDamage(
                    hitscan.DamageType,
                    (int)Math.Round(hitscan.Damage, MidpointRounding.AwayFromZero),
                    Owner,
                    shooter);
                //I used Math.Round over Convert.toInt32, as toInt32 always rounds to
                //even numbers if halfway between two numbers, rather than rounding to nearest
            }
            else
            {
                hitscan.FireEffects(shooter, hitscan.MaxLength, angle);
            }
        }
        #endregion

        public virtual void Examine(FormattedMessage message, bool inDetailsRange)
        {
            var fireRateMessage = Loc.GetString(FireRateSelector switch
            {
                FireRateSelector.Safety => "Its safety is enabled.",
                FireRateSelector.Single => "It's in single fire mode.",
                FireRateSelector.Automatic => "It's in automatic fire mode.",
                _ => throw new IndexOutOfRangeException()
            });

            message.AddText(fireRateMessage);
        }
    }
}
