#if SSC_ENTITIES
using System;
using Unity.Entities;
using Unity.Transforms;
using Unity.Mathematics;
using Unity.Collections;
using Unity.Jobs;
using UnityEngine;
using Unity.Burst;

// Sci-Fi Ship Controller. Copyright (c) 2018-2023 SCSM Pty Ltd. All rights reserved.
namespace SciFiShipController
{
    #region ProjectileComponent

    [Serializable]
    public struct Projectile : IComponentData
    {
        public float3 velocity;
        public float damageAmount;
        public float despawnTime;
        public float despawnTimer;
        public byte useGravity; // 0 = no, 1 = yes
        public float gravity;
        public float3 gravityDirection;
        public float speed;
        public float3 fwdDirection;
        public float3 upDirection;
        public int projectilePrefabID;
        public int effectsObjectPrefabID;
        public int shieldEffectsObjectPrefabID;
        public int sourceShipId;
        public int sourceSquadronId;
        public int damageTypeInt;
        public int _tempIdx;
    }

    #endregion

    #region ProjectileSystem

    /// <summary>
    /// ProjectileSystem is used to create and move projectiles with the
    /// Data-Orientated Tech Stack (DOTS). It uses C# Jobs, Entities and
    /// the Burst compiler.The default SimulationSystemGroup in Entities
    /// 0.0.12 preview 30 runs at end of Update rather than FixedUpdate.
    /// So disable auto creation and create and update it manually from
    /// SSCManager.
    /// U2019.1 - Entities 0.0.12-preview.30
    /// U2019.2 - Entities 0.1.1 preview
    /// U2019.3 - Entities 0.2.0 preview.18
    /// U2019.4 - Entities 0.5.2 preview.4
    /// U2020.1 - Entities 0.12.0 (untested)
    /// U2020.3 - Entities 0.51.0 preview.32+ (SSC 1.3.4 and earlier Entities 0.17.0-preview.42)
    /// </summary>
    [DisableAutoCreation]
    #if UNITY_2020_3_OR_NEWER
    public partial class ProjectileSystem : SystemBase
    #elif UNITY_2020_1_OR_NEWER
    public class ProjectileSystem : SystemBase
    #else
    public class ProjectileSystem : JobComponentSystem
    #endif
    {
        #region Public Properties

        // For testing only
        public static int GetTotalProjectiles { get; private set; }

        #endregion

        #region Private Variables
        // EntityQuery aka ComponentGroup pre-ECS 0.27
        private static EntityQuery projectileEntityQuery;

        private static EntityManager entityManager;

        private static ComponentType compTypeProjectile;

        private Vector3 pos, velo, frameMovement;

        private Entity projectileEntity;
        private int nProjectiles;

        private NativeArray<RaycastHit> raycastResults;
        private NativeArray<RaycastCommand> raycastCommands;
        private RaycastCommand raycastCommand;

        private NativeList<Entity> entitiesToDestroy;
        private RaycastHit hitInfo;

        private SSCManager sscManager;

        #if SSC_PHYSICS
        private Unity.Physics.Systems.BuildPhysicsWorld buildPhysicsWorld;
        private NativeArray<Unity.Physics.RaycastHit> physicsraycastResults;
        private int numPhysicsRaycastHits;
        private int numPhysicsBodies;
        //private NativeList<Unity.Physics.RaycastHit> physicsraycastResultList;
        #endif

        #endregion

        #region Projectile Movement Job

        [BurstCompile]
        struct ProjectileMoveJob : IJobChunk
        {
            public float deltaTime;

            #if UNITY_2020_1_OR_NEWER
            // Renamed from ArchetypeChunkComponentType to ComponentTypeHandle in Entities 0.12.0
            public ComponentTypeHandle<Translation> translationType;
            public ComponentTypeHandle<Projectile> projectileType;
            #else
            public ArchetypeChunkComponentType<Translation> translationType;
            public ArchetypeChunkComponentType<Projectile> projectileType;
            #endif

            public void Execute(ArchetypeChunk archetypeChunk, int chunkIndex, int firstEntityIndex)
            {
                NativeArray<Translation> chunkTranslations = archetypeChunk.GetNativeArray(translationType);
                NativeArray<Projectile> chunkProjectiles = archetypeChunk.GetNativeArray(projectileType);

                for (int i = 0; i < chunkTranslations.Length; i++)
                {
                    Projectile projectile = chunkProjectiles[i];
                    Translation position = chunkTranslations[i];

                    // Use Gravity?
                    if (projectile.useGravity == (byte)1)
                    {
                        projectile.velocity += projectile.gravity * deltaTime * projectile.gravityDirection;
                    }

                    // Update our current position using our velocity and frame time
                    position.Value += projectile.velocity * deltaTime;
                    chunkTranslations[i] = position;

                    // Update the timer so that in the OnUpdate we can destroy the entity when required.
                    projectile.despawnTimer += deltaTime;

                    chunkProjectiles[i] = projectile;

                    // Update our current rotation using our velocity (currently not required)
                    //rotation.Value = quaternion.LookRotationSafe(movement.Velocity, new float3(0f, 1f, 0f));
                }
            }
        }

        [BurstCompile]
        struct ProjectileTelePortJob: IJobChunk
        {
            public float3 deltaPosition;

            #if UNITY_2020_1_OR_NEWER
            // Renamed from ArchetypeChunkComponentType to ComponentTypeHandle in Entities 0.12.0
            public ComponentTypeHandle<Translation> translationType;
            #else
            public ArchetypeChunkComponentType<Translation> translationType;
            #endif

            public void Execute(ArchetypeChunk archetypeChunk, int chunkIndex, int firstEntityIndex)
            {
                NativeArray<Translation> chunkTranslations = archetypeChunk.GetNativeArray(translationType);

                for (int i = 0; i < chunkTranslations.Length; i++)
                {
                    Translation translation = chunkTranslations[i];
                    translation.Value += deltaPosition;
                    chunkTranslations[i] = translation;
                }
            }
        }

        #endregion

        #region Physics Raycast Job
        #if SSC_PHYSICS

        /// <summary>
        /// Parallel job for casting rays from projectiles using Unity.Physics
        /// </summary>
        [BurstCompile]
        struct ProjectilePhysicsRayJob : IJobForEach<Translation, Projectile>
        {
            public NativeArray<Unity.Physics.RaycastHit> raycastResultsInJob;
            [ReadOnly] public Unity.Physics.CollisionWorld collisionWorldInJob;
            [ReadOnly] public float deltaTime;

            public void Execute([ReadOnly] ref Translation position, ref Projectile projectile)
            {
                Unity.Physics.RaycastInput raycastInput = new Unity.Physics.RaycastInput
                {
                    Start = position.Value,                    
                    End = position.Value + (projectile.velocity * deltaTime),
                    Filter = Unity.Physics.CollisionFilter.Default
                };

                if (collisionWorldInJob.CastRay(raycastInput, out Unity.Physics.RaycastHit hit))
                {
                    if (hit.RigidBodyIndex == 0) { hit.Position = new float3(1f, 1f, 1f); }

                    raycastResultsInJob[projectile._tempIdx] = hit;
                }
                else
                {
                    // Dummy hit
                    Unity.Physics.RaycastHit raycastHit = new Unity.Physics.RaycastHit();
                    raycastHit.RigidBodyIndex = -1;
                    raycastResultsInJob[projectile._tempIdx] = raycastHit;
                }
            }
        }

        #endif
        #endregion

        #region Event Methods

        protected override void OnCreate()
        {
            // Get the current entity manager. If it doesn't exist, create one.
            entityManager = SSCManager.sscWorld.EntityManager;

            // Cache the component type to avoid ComponentType.op_Implicit in CreateProjectile(..)
            compTypeProjectile = typeof(Projectile);

            // Projectile entity query code
            // Get all the entities with a Translation and Projectile component
            projectileEntityQuery = GetEntityQuery(new EntityQueryDesc
            {
                All = new ComponentType[] { typeof(Translation), typeof(Rotation), compTypeProjectile }
            });

            // Initialise raycast command
            raycastCommand = new RaycastCommand(Vector3.zero, Vector3.forward, 1f);

            // Initialise entities to destroy list
            entitiesToDestroy = new NativeList<Entity>(Allocator.Persistent);

            sscManager = SSCManager.GetOrCreateManager();

            #if SSC_PHYSICS
            if (!DOTSHelper.GetBuildPhysicsWorld(SSCManager.sscWorld, ref buildPhysicsWorld))
            {
                #if UNITY_EDITOR
                Debug.Log("ERROR: ProjectileSystem.OnCreate() - could not get physicsworld - PLEASE REPORT");
                #endif
            }
            else
            {
                // Create empty resultset. Note the difference between Physics and UnityEngine RaycastHit.
                //physicsraycastResultList = new NativeList<Unity.Physics.RaycastHit>(Allocator.Persistent);
            }
            #endif
        }

        protected override void OnDestroy()
        {
            // Dispose of the entities to destroy native list
            entitiesToDestroy.Dispose();

            #if SSC_PHYSICS
            if (physicsraycastResults.IsCreated) { physicsraycastResults.Dispose(); }
            #endif
        }

        #endregion

        #region Update Methods

        // OnUpdate runs on the main thread.
        #if UNITY_2020_1_OR_NEWER
        protected override void OnUpdate()
        #else
        protected override JobHandle OnUpdate(JobHandle jobHandle)
        #endif
        {
            //int nProjectiles = 0;
            nProjectiles = 0;

            #region Physics Updates
            #if SSC_PHYSICS
            // Ensure physics world updates first
            #if UNITY_2020_1_OR_NEWER
            // FinalJobHandle deprecated in Unity.Physics 0.4.0-preview.5. Replaced with GetOutputDependency().
            this.Dependency = JobHandle.CombineDependencies(this.Dependency, buildPhysicsWorld.GetOutputDependency());
            #else
            jobHandle = JobHandle.CombineDependencies(jobHandle, buildPhysicsWorld.FinalJobHandle);
            #endif
            #endif
            #endregion

            // Get all of the projectile entities in the scene
            // wrap in using statement to automatically dispose after use
            using (NativeArray<Entity> nativeArray = projectileEntityQuery.ToEntityArray(Allocator.TempJob))
            {
                nProjectiles = nativeArray.Length;

                // for testing only
                GetTotalProjectiles = nProjectiles;
            }

            // Gather the types of the components that we want to manipulate
            #if UNITY_2020_1_OR_NEWER
            // Renamed from ArchetypeChunkComponentType to ComponentTypeHandle in Entities 0.12.0
            // Replace GetArchetypeChunkComponentType<T>() with GetComponentTypeHandle<T>()
            ComponentTypeHandle<Translation> positionType = GetComponentTypeHandle<Translation>();
            ComponentTypeHandle<Rotation> rotationType = GetComponentTypeHandle<Rotation>();
            ComponentTypeHandle<Projectile> projectileType = GetComponentTypeHandle<Projectile>();
            #else
            ArchetypeChunkComponentType<Translation> positionType = GetArchetypeChunkComponentType<Translation>();
            ArchetypeChunkComponentType<Rotation> rotationType = GetArchetypeChunkComponentType<Rotation>();
            ArchetypeChunkComponentType<Projectile> projectileType = GetArchetypeChunkComponentType<Projectile>();
            #endif

            // Get all the chunks (segments of data) that match this query (that have Translation and Projectile components)
            // The chunks will only contain our projectile entities.
            NativeArray<ArchetypeChunk> chunks = projectileEntityQuery.CreateArchetypeChunkArray(Allocator.TempJob);

            // Set up the command and result buffers
            raycastCommands = new NativeArray<RaycastCommand>(nProjectiles, Allocator.TempJob);
            raycastResults = new NativeArray<RaycastHit>(nProjectiles, Allocator.TempJob);

            // Get delta time once

            // This system is updated from FixedUpdate, so use PhysX time rather than ECS timing.
            float deltaTime = UnityEngine.Time.fixedDeltaTime;
            
            //#if UNITY_2019_3_OR_NEWER
            //float deltaTime = Time.DeltaTime;
            //#else
            //float deltaTime = Time.deltaTime;
            //#endif

            #region Raycast Job

            // TODO - investigate using a job to populate the raycastCommands NativeArray
            // Iterate through all projectile entities
            int raycastIndex = 0;
            // Loop through the chunks
            int chunksLength = chunks == null ? 0 : chunks.Length;
            for (int chunkIndex = 0; chunkIndex < chunksLength; chunkIndex++)
            {
                // Get the current chunk
                ArchetypeChunk chunk = chunks[chunkIndex];

                // Get an array of the position components from the entities (that match the projectileEntityQuery) within this chunk
                NativeArray<Translation> positionComponents = chunk.GetNativeArray(positionType);
                // Get an array of the projectile components from the entities within this chunk
                NativeArray<Projectile> projectileComponents = chunk.GetNativeArray(projectileType);

                // Loop through the entities in this chunk
                int chunkSize = chunk == null ? 0 : chunk.Count;
                for (int entityIndex = 0; entityIndex < chunkSize; entityIndex++)
                {
                    // Get the position of this projectile
                    pos = positionComponents[entityIndex].Value;
                    // Get the velocity of this projectile
                    velo = projectileComponents[entityIndex].velocity;

                    // Calculate raycast data
                    raycastCommand.from = pos;
                    raycastCommand.direction = velo;
                    raycastCommand.distance = (velo * deltaTime).magnitude;

                    // Set raycast data
                    raycastCommands[raycastIndex] = raycastCommand;

                    // Used in the Unity.Physics raycast job
                    Projectile _projectile = projectileComponents[entityIndex];
                    _projectile._tempIdx = raycastIndex;
                    projectileComponents[entityIndex] = _projectile;

                    //Debug.Log("tempidx: " + _projectile._tempIdx + " actual: " + raycastIndex);

                    // Increment the raycast index - we're doing things this way to make sure that
                    // in the second loop the indices all match up correctly
                    raycastIndex++;
                }
            }

            // Schedule the batch of raycasts
            // TODO - in Entities 12+, not sure if this a parallel job...
            JobHandle handle = RaycastCommand.ScheduleBatch(raycastCommands, raycastResults, 1, default(JobHandle));

            // Wait for the batch processing job to complete
            handle.Complete();

            #endregion

            #region Unity.Physics Raycast Job
            #if SSC_PHYSICS
            physicsraycastResults = new NativeArray<Unity.Physics.RaycastHit>(nProjectiles, Allocator.TempJob);

            if (physicsraycastResults.IsCreated)
            {
                
                #if UNITY_2020_1_OR_NEWER
                // Check if this is parallel...
                new ProjectilePhysicsRayJob()
                {
                    collisionWorldInJob = buildPhysicsWorld.PhysicsWorld.CollisionWorld,
                    raycastResultsInJob = physicsraycastResults,
                    deltaTime = deltaTime
                }.Schedule(this, this.Dependency).Complete();
                #else
                new ProjectilePhysicsRayJob()
                {
                    collisionWorldInJob = buildPhysicsWorld.PhysicsWorld.CollisionWorld,
                    raycastResultsInJob = physicsraycastResults,
                    deltaTime = deltaTime
                }.Schedule(this, jobHandle).Complete();
                #endif

                numPhysicsRaycastHits = physicsraycastResults.Length;
                numPhysicsBodies = buildPhysicsWorld.PhysicsWorld.CollisionWorld.NumBodies;
            }
            else
            {
                numPhysicsRaycastHits = 0;
                numPhysicsBodies = 0;
            }
            #endif
            #endregion

            #region Process Raycasts for collision and despawn old projectiles
            
            // Get an archetype for projectiles
            #if UNITY_2020_1_OR_NEWER
            // Renamed from ArchetypeChunkEntityType to EntityTypeHandle in Entities 0.12.0
            // Replace GetArchetypeChunkEntityType() with GetEntityTypeHandle()
            EntityTypeHandle projectileChunkEntityArchetype = GetEntityTypeHandle();
            #else
            ArchetypeChunkEntityType projectileChunkEntityArchetype = GetArchetypeChunkEntityType();
            #endif

            // Iterate through all projectile entities again
            // Reset raycastIndex
            raycastIndex = 0;
            Collider other;
            for (int chunkIndex = 0; chunkIndex < chunksLength; chunkIndex++)
            {
                // Get the current chunk of (projectile) entities
                ArchetypeChunk chunk = chunks[chunkIndex];

                // Get a native array of the entities in this chunk
                NativeArray<Entity> chunkEntities = chunk.GetNativeArray(projectileChunkEntityArchetype);

                // Get arrays of the projectile and rotations components from the entities within this chunk
                NativeArray<Projectile> projectileComponents = chunk.GetNativeArray(projectileType);
                NativeArray<Rotation> rotationComponents = chunk.GetNativeArray(rotationType);

                // Loop through the entities in this chunk
                int chunkSize = chunk == null ? 0 : chunk.Count;
                for (int entityIndex = 0; entityIndex < chunkSize; entityIndex++)
                {
                    Projectile projectile = projectileComponents[entityIndex];
                    Rotation rotation = rotationComponents[entityIndex];

                    #region Process Legacy Physics Raycast results
                    hitInfo = raycastResults[raycastIndex];
                    other = hitInfo.collider;
                    //Translation translation = translationComponents[entityIndex];

                    // If raycastResults[raycastIndex].collider == null there was no hit
                    if (other != null)
                    {

                        bool isShieldHit = false;
                        ShipControlModule shipControlModule = null;

                        // Do we need to check for ship shield hits?
                        if (projectile.shieldEffectsObjectPrefabID >= 0 && ProjectileModule.CheckShipHit(hitInfo, projectile.damageAmount, (ProjectileModule.DamageType)projectile.damageTypeInt, projectile.sourceShipId, projectile.sourceSquadronId, projectile.projectilePrefabID, out shipControlModule))
                        {
                            isShieldHit = shipControlModule.shipInstance.HasActiveShield(hitInfo.point);
                        }
                        // No shield effects so perform a regular CheckShipHit
                        else if (projectile.shieldEffectsObjectPrefabID < 0 && ProjectileModule.CheckShipHit(hitInfo, projectile.damageAmount, (ProjectileModule.DamageType)projectile.damageTypeInt, projectile.sourceShipId, projectile.sourceSquadronId, projectile.projectilePrefabID))
                        {
                            // No need to do anything else here
                        }
                        else
                        {
                            // If it hit an object with a DamageReceiver script attached, take appropriate action like call a custom method
                            ProjectileModule.CheckObjectHit(hitInfo, projectile.damageAmount, (ProjectileModule.DamageType)projectile.damageTypeInt, projectile.sourceShipId, projectile.sourceSquadronId, projectile.projectilePrefabID);
                        }

                        // OLD CODE pre v1.3.5
                        // Determine if it has hit a ship
                        //if (!ProjectileModule.CheckShipHit(hitInfo, projectile.damageAmount, (ProjectileModule.DamageType)projectile.damageTypeInt, projectile.sourceShipId, projectile.sourceSquadronId, projectile.projectilePrefabID))
                        //{
                        //    // If it hit an object with a DamageReceiver script attached, take appropriate action like call a custom method
                        //    ProjectileModule.CheckObjectHit(hitInfo, projectile.damageAmount, (ProjectileModule.DamageType)projectile.damageTypeInt, projectile.sourceShipId, projectile.sourceSquadronId, projectile.projectilePrefabID);
                        //}

                        // If required, use a shield EffectsObject.
                        if (isShieldHit && projectile.shieldEffectsObjectPrefabID >= 0)
                        {
                            if (sscManager != null)
                            {
                                InstantiateEffectsObjectParameters ieParms = new InstantiateEffectsObjectParameters
                                {
                                    effectsObjectPrefabID = projectile.shieldEffectsObjectPrefabID,
                                    position = hitInfo.point + (hitInfo.normal * 0.0005f),
                                    rotation = Quaternion.LookRotation(-hitInfo.normal),
                                };

                                // For projectiles we don't need to get the effectsObject key from ieParms.
                                sscManager.InstantiateEffectsObject(ref ieParms);
                            }
                        }
                        else if (!isShieldHit && projectile.effectsObjectPrefabID >= 0 && sscManager != null)
                        {
                            InstantiateEffectsObjectParameters ieParms = new InstantiateEffectsObjectParameters
                            {
                                effectsObjectPrefabID = projectile.effectsObjectPrefabID,
                                position = hitInfo.point,
                                rotation = rotation.Value
                            };

                            sscManager.InstantiateEffectsObject(ref ieParms);
                        }

                        // Mark this (projectile) entity to be destroyed
                        entitiesToDestroy.Add(chunkEntities[entityIndex]);
                    }
                    // Should this entity be despawned? Use and "else" so we don't try to destroy it twice
                    else
                    {
                        // We "could" create a job with a CommandBuffer which would queue all the DestroyEntity requests and then run
                        // it at the end of the job on the main thread, that just creates more overhead. It "might" be
                        // faster with Burst if there were a zillion projectiles but doing it directly on the main thread
                        // is simplier and probably just as performant.

                        // Is the projectile past it's use-by date?
                        if (projectile.despawnTimer + deltaTime > projectile.despawnTime)
                        {
                            entitiesToDestroy.Add(chunkEntities[entityIndex]);
                        }

                        // If the Entity is not being destroyed, we "could" update the despawnTimer here on the main thread
                        // using the following example code, however, we can do that more efficently in the projectileJob
                        // below. Example code: entityManager.SetComponentData(chunkEntities[entityIndex], projectile);
                    }
                    #endregion

                    #region Process Unity.Physics Raycast results
                    #if SSC_PHYSICS
                    if (numPhysicsRaycastHits > 0)
                    {
                        Unity.Physics.RaycastHit raycastHit = physicsraycastResults[projectile._tempIdx];

                        if (raycastHit.RigidBodyIndex >= 0 && raycastHit.RigidBodyIndex < numPhysicsBodies)
                        {
                            // If the surface normal vector has a length, we must have hit something
                            if (math.abs(math.lengthsq(raycastHit.SurfaceNormal)) > 0f)
                            {
                                //Debug.Log("[DEBUG] PhysicsHit: " + raycastHit.Position.ToString() + " RigidBodyIndex: " + raycastHit.RigidBodyIndex);

                                if (sscManager != null && projectile.effectsObjectPrefabID >= 0)
                                {
                                    InstantiateEffectsObjectParameters ieParms = new InstantiateEffectsObjectParameters
                                    {
                                        effectsObjectPrefabID = projectile.effectsObjectPrefabID,
                                        position = raycastHit.Position,
                                        rotation = rotation.Value
                                    };

                                    sscManager.InstantiateEffectsObject(ref ieParms);
                                }

                                // Mark this (projectile) entity to be destroyed
                                entitiesToDestroy.Add(chunkEntities[entityIndex]);

                                // Potentially destroy the object that was hit.
                                //entitiesToDestroy.Add(buildPhysicsWorld.PhysicsWorld.CollisionWorld.Bodies[raycastHit.RigidBodyIndex].Entity);
                            }
                        }
                    }
                    #endif
                    #endregion

                    // Increment the raycast index - we're doing things this way to make sure that
                    // in the second loop the indices all match up correctly
                    // TODO: Need to check that this works - it might not work if, for instance, the chunks
                    // get populated in a different order each time
                    raycastIndex++;
                }
            }

            // It is faster to destroy a native array of projectiles, than one at a time
            entityManager.DestroyEntity(entitiesToDestroy.AsArray());
            entitiesToDestroy.Clear();

            #endregion

            // Dispose of the native array
            if (raycastResults.IsCreated) { raycastResults.Dispose(); }
            if (raycastCommands.IsCreated) { raycastCommands.Dispose(); }
            if (chunks.IsCreated) { chunks.Dispose(); }

            #region Unity.Physics Raycast Cleanup
            #if SSC_PHYSICS
            if (physicsraycastResults.IsCreated)
            {
                physicsraycastResults.Dispose();
            }
            #endif
            #endregion

            #region Parallel Job to move the Projectiles
            // Create a new IJobChunk projectile move job, passing in the current frame time as
            // an argument along with the chunk component types.
            // Notice that we cannot used cached chunk component types.
            ProjectileMoveJob projectileMoveJob = new ProjectileMoveJob
            {
                deltaTime = deltaTime,

                #if UNITY_2020_1_OR_NEWER
                // Replace GetArchetypeChunkComponentType<T>() with GetComponentTypeHandle<T>() in Entities 0.12.0
                translationType = GetComponentTypeHandle<Translation>(),
                projectileType = GetComponentTypeHandle<Projectile>()
                #else
                translationType = GetArchetypeChunkComponentType<Translation>(),
                projectileType = GetArchetypeChunkComponentType<Projectile>()
                #endif
            };

            // Schedule the parallel projectile move job
            #if UNITY_2020_1_OR_NEWER
            this.Dependency = projectileMoveJob.ScheduleParallel(projectileEntityQuery, this.Dependency);
            #else
            return projectileMoveJob.Schedule(projectileEntityQuery, jobHandle);
            #endif

            #endregion
        }

        #endregion

        #region Private and Internal Methods

        /// <summary>
        /// Teleport (move) the projectiles a particular amount on x,y,z axes.
        /// </summary>
        /// <param name="deltaPosition"></param>
        internal void TelePortProjectiles(Vector3 deltaPosition)
        {
            // Create a new job
            ProjectileTelePortJob projectileTelePortJob = new ProjectileTelePortJob
            {
                deltaPosition = deltaPosition,

                #if UNITY_2020_1_OR_NEWER
                // Replace GetArchetypeChunkComponentType<T>() with GetComponentTypeHandle<T>() in Entities 0.12.0
                translationType = GetComponentTypeHandle<Translation>()
                #else
                translationType = GetArchetypeChunkComponentType<Translation>()
                #endif
            };

            // Schedule the parallel teleport job
            #if UNITY_2020_1_OR_NEWER
            this.Dependency = projectileTelePortJob.ScheduleParallel(projectileEntityQuery, this.Dependency);
            #else
            JobHandle jobHandle = projectileTelePortJob.Schedule(projectileEntityQuery);
            #endif
        }

        #endregion

        #region Public Static Methods

        /// <summary>
        /// Create a new projectile entity in the scene, based on a prefab that has already been converted
        /// from a gameobject prefab to an entity.
        /// Add a Projectile component if it wasn't already attached to the original gameobject prefab.
        /// Update the array of all projectile entities.
        /// It "might" be better to pass the ProjectModule instance reference which would simplify
        /// maintenance.
        /// NOTE: This method runs on the main thread.
        /// </summary>
        /// <param name="position"></param>
        /// <param name="weaponVelocity"></param>
        /// <param name="startFwdDirection"></param>
        /// <param name="startUpDirection"></param>
        /// <param name="startSpeed"></param>
        /// <param name="deltaTime"></param>
        /// <param name="useGravity"></param>
        /// <param name="gravity"></param>
        /// <param name="gravityDirection"></param>
        /// <param name="damageAmount"></param>
        /// <param name="despawnTime"></param>
        /// <param name="projectilePrefabID"></param>
        /// <param name="effectsObjectPrefabID"></param>
        /// <param name="shieldEffectsObjectPrefabID"></param>
        /// <param name="shipId"></param>
        /// <param name="squadronId"></param>
        /// <param name="damageTypeInt"></param>
        /// <param name="projectilePrefabEntity"></param>
        public static void CreateProjectile
        (
            Vector3 position,
            float3 weaponVelocity,
            float3 startFwdDirection,
            float3 startUpDirection,
            float startSpeed,
            float deltaTime,
            bool useGravity,
            float gravity,
            float3 gravityDirection,
            float damageAmount,
            float despawnTime,
            int projectilePrefabID,
            int effectsObjectPrefabID,
            int shieldEffectsObjectPrefabID,
            int shipId,
            int squadronId,
            int damageTypeInt,
            Entity projectilePrefabEntity
        )
        {
            // The prefab is converted to an entity when the ProjectileTemplate is created.
            // Translation, Rotation, and RenderMesh components are automatically added to the input projectPrefabEntity
            // when it is converted from the gameobject prefab. So, we don't need to add them here.

            Entity entity = entityManager.Instantiate(projectilePrefabEntity);

            // Add a Projectile component if it wasn't on the template prefab
            if (!entityManager.HasComponent(entity, compTypeProjectile))
            {
                entityManager.AddComponent(entity, compTypeProjectile);
            }

            //Debug.Log("[DEBUG] has projectilemodule: " + entityManager.HasComponent(entity, typeof(ProjectileModule)));
            //Debug.Log("[DEBUG] has RenderMesh: " + entityManager.HasComponent(entity, typeof(Unity.Rendering.RenderMesh)));

            float3 _velocity = startFwdDirection * startSpeed;

            // Set their position, rotation and projectile properties
            // Shift the position forward by the weapon velocity, so that projectiles don't ever end up behind the ship
            entityManager.SetComponentData(entity, new Translation() { Value = (float3)position + (_velocity * deltaTime) });
            entityManager.SetComponentData(entity, new Rotation() { Value = quaternion.LookRotation(startFwdDirection, startUpDirection) });

            entityManager.SetComponentData(entity, new Projectile()
            {
                // Initialise the velocity based on the forwards direction
                // The forwards direction should have been set correctly prior to enabling the object
                velocity = _velocity + weaponVelocity,

                // Store the current speed and forwards direction incase we want to change
                // these if gravity is being applied to the projectile
                useGravity = useGravity ? (byte)1 : (byte)0,
                gravity = gravity,
                gravityDirection = gravityDirection,
                damageAmount = damageAmount,
                despawnTime = despawnTime,
                despawnTimer = 0f,
                speed = startSpeed,
                fwdDirection = startFwdDirection,
                upDirection = startUpDirection,
                projectilePrefabID = projectilePrefabID,
                effectsObjectPrefabID = effectsObjectPrefabID,
                shieldEffectsObjectPrefabID = shieldEffectsObjectPrefabID,
                sourceShipId = shipId,
                sourceSquadronId = squadronId,
                damageTypeInt = damageTypeInt,
                _tempIdx = -1
            }
            );
        }

        #endregion
    }

    #endregion
}
#endif