using CodeWalker.GameFiles;
using SharpDX;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;

namespace CodeWalker.World
{
    public class Space
    {

        public LinkedList<Entity> TemporaryEntities = new LinkedList<Entity>();
        public LinkedList<Entity> PersistentEntities = new LinkedList<Entity>();
        public List<Entity> EnabledEntities = new List<Entity>(); //built each frame

        private GameFileCache GameFileCache = null;




        public SpaceMapDataStore MapDataStore;
        public SpaceBoundsStore BoundsStore;



        private Dictionary<MetaHash, MetaHash> interiorLookup = new Dictionary<MetaHash, MetaHash>();
        private Dictionary<MetaHash, YmfInterior> interiorManifest = new Dictionary<MetaHash, YmfInterior>();
        private Dictionary<SpaceBoundsKey, CInteriorProxy> interiorProxies = new Dictionary<SpaceBoundsKey, CInteriorProxy>();
        private Dictionary<MetaHash, YmfMapDataGroup> dataGroupDict = new Dictionary<MetaHash, YmfMapDataGroup>();
        private Dictionary<MetaHash, MapDataStoreNode> nodedict = new Dictionary<MetaHash, MapDataStoreNode>();
        private Dictionary<SpaceBoundsKey, BoundsStoreItem> boundsdict = new Dictionary<SpaceBoundsKey, BoundsStoreItem>();
        private Dictionary<MetaHash, BoundsStoreItem> usedboundsdict = new Dictionary<MetaHash, BoundsStoreItem>();

        private Dictionary<MetaHash, uint> ymaptimes = new Dictionary<MetaHash, uint>();
        private Dictionary<MetaHash, MetaHash[]> ymapweathertypes = new Dictionary<MetaHash, MetaHash[]>();

        public bool Inited = false;


        public SpaceNodeGrid NodeGrid;
        private Dictionary<uint, YndFile> AllYnds = new Dictionary<uint, YndFile>();

        public SpaceNavGrid NavGrid;

        public List<SpaceEntityCollision> Collisions = new List<SpaceEntityCollision>();
        private bool[] CollisionLayers = new[] { true, false, false };

        private int CurrentHour;
        private MetaHash CurrentWeather;


        public void Init(GameFileCache gameFileCache, Action<string> updateStatus)
        {
            GameFileCache = gameFileCache;


            updateStatus("Scanning manifests...");

            InitManifestData();


            updateStatus("Scanning caches...");

            InitCacheData();


            updateStatus("Building map data store...");

            InitMapDataStore();


            updateStatus("Building bounds store...");

            InitBoundsStore();


            updateStatus("Loading paths...");

            InitNodeGrid();


            updateStatus("Loading nav meshes...");

            InitNavGrid();


            Inited = true;
            updateStatus("World initialised.");
        }


        private void InitManifestData()
        {
            interiorLookup.Clear();
            interiorManifest.Clear();
            ymaptimes.Clear();
            ymapweathertypes.Clear();
            dataGroupDict.Clear();

            var manifests = GameFileCache.AllManifests;
            foreach (var manifest in manifests)
            {
                //build interior lookup - maps child->parent interior bounds
                if (manifest.Interiors != null)
                {
                    foreach (var interior in manifest.Interiors)
                    {
                        var intname = interior.Interior.Name;
                        if (interiorManifest.ContainsKey(intname))
                        { }
                        interiorManifest[intname] = interior;

                        if (interior.Bounds != null)
                        {
                            foreach (var intbound in interior.Bounds)
                            {
                                if (interiorLookup.ContainsKey(intbound))
                                { }//updates can hit here
                                interiorLookup[intbound] = intname;
                            }
                        }
                        else
                        { }
                    }
                }

                //these appear to be all the dynamic "togglable" ymaps....
                if (manifest.MapDataGroups != null)
                {
                    foreach (var mapgroup in manifest.MapDataGroups)
                    {
                        if (mapgroup.HoursOnOff != 0)
                        {
                            ymaptimes[mapgroup.Name] = mapgroup.HoursOnOff;
                        }
                        if (mapgroup.WeatherTypes != null)
                        {
                            ymapweathertypes[mapgroup.Name] = mapgroup.WeatherTypes;
                        }

                        if (dataGroupDict.ContainsKey(mapgroup.DataGroup.Name))
                        {
                            if (mapgroup.Bounds != null)
                            {
                                var ex = dataGroupDict[mapgroup.DataGroup.Name];
                                if (ex.Bounds != null)
                                { } //only 1 hit here - redcarpet
                            }
                            else
                            {
                                continue;//nothing to replace with
                            }
                        }
                        dataGroupDict[mapgroup.DataGroup.Name] = mapgroup;
                    }
                }
            }

            //YmfMapDataGroups string
            //StringBuilder sb = new StringBuilder();
            //foreach (var dg in dataGroupDict.Values)
            //{
            //    sb.AppendLine(dg.ToString());
            //    if (dg.Bounds != null)
            //    {
            //        foreach (var ybnh in dg.Bounds)
            //        {
            //            sb.AppendLine("   " + ybnh.ToString());
            //        }
            //    }
            //}
            //string str = sb.ToString();

        }

        private void InitCacheData()
        {
            //build the grid from the cached data
            var caches = GameFileCache.AllCacheFiles;
            nodedict = new Dictionary<MetaHash, MapDataStoreNode>();
            MetaHash inthash;
            List<BoundsStoreItem> intlist = new List<BoundsStoreItem>();
            boundsdict = new Dictionary<SpaceBoundsKey, BoundsStoreItem>();
            usedboundsdict = new Dictionary<MetaHash, BoundsStoreItem>();
            interiorProxies = new Dictionary<SpaceBoundsKey, CInteriorProxy>();

            Dictionary<MetaHash, CacheFileDate> filedates = new Dictionary<MetaHash, CacheFileDate>();
            Dictionary<uint, CacheFileDate> filedates2 = new Dictionary<uint, CacheFileDate>();

            foreach (var cache in caches)
            {
                foreach (var filedate in cache.FileDates)
                {
                    CacheFileDate exdate;
                    if (filedates.TryGetValue(filedate.FileName, out exdate))
                    {
                        if (filedate.TimeStamp >= exdate.TimeStamp)
                        {
                            filedates[filedate.FileName] = filedate;
                        }
                        else //if (filedate.TimeStamp < exdate.TimeStamp)
                        { }
                    }
                    else
                    {
                        filedates[filedate.FileName] = filedate;
                    }

                    if (filedates2.TryGetValue(filedate.FileID, out exdate))
                    {
                        if (filedate.FileName != exdate.FileName)
                        { }
                        if (filedate.TimeStamp >= exdate.TimeStamp)
                        {
                            filedates2[filedate.FileID] = filedate;
                        }
                        else
                        { }
                    }
                    else
                    {
                        filedates2[filedate.FileID] = filedate;
                    }

                }



                foreach (var node in cache.AllMapNodes)
                {
                    if (!GameFileCache.YmapDict.ContainsKey(node.Name))
                    { continue; }
                    nodedict[node.Name] = node;
                }

                foreach (var intprx in cache.AllCInteriorProxies)
                {
                    //these might need to go into the grid. which grid..?
                    //but might need to map back to the bounds store... this has more info though!
                    SpaceBoundsKey key = new SpaceBoundsKey(intprx.Name, intprx.Position);
                    if (interiorProxies.ContainsKey(key))
                    { }//updates/dlc hit here
                    interiorProxies[key] = intprx;
                }

                foreach (var item in cache.AllBoundsStoreItems)
                {
                    if (!GameFileCache.YbnDict.ContainsKey(item.Name))
                    { continue; }

                    if ((item.Layer < 0) || (item.Layer > 3))
                    { } //won't hit here..
                    if (interiorLookup.TryGetValue(item.Name, out inthash))
                    {
                        //it's an interior... the vectors are in local space...
                        intlist.Add(item);//handle it later? use the parent for a dict?
                    }
                    else //interiors filtered out
                    {
                        SpaceBoundsKey key = new SpaceBoundsKey(item.Name, item.Min);
                        if (boundsdict.ContainsKey(key))
                        { }//updates/dlc hit here
                        boundsdict[key] = item;

                    }
                    usedboundsdict[item.Name] = item;
                }
            }




            //try and generate the cache data for uncached ymaps... mainly for mod maps!
            var maprpfs = GameFileCache.ActiveMapRpfFiles;
            foreach (var maprpf in maprpfs.Values)
            {
                foreach (var entry in maprpf.AllEntries)
                {
                    if (entry.NameLower.EndsWith(".ymap"))
                    {
                        if (!nodedict.ContainsKey(new MetaHash(entry.ShortNameHash)))
                        {
                            //non-cached ymap. mostly only mods... but some interesting test things also
                            var ymap = GameFileCache.RpfMan.GetFile<YmapFile>(entry);
                            if (ymap != null)
                            {
                                MapDataStoreNode dsn = new MapDataStoreNode(ymap);
                                if (dsn.Name != 0)
                                {
                                    nodedict[dsn.Name] = dsn;//perhaps should add as entry.ShortNameHash?
                                }
                                else
                                { }
                            }
                            else
                            { }
                        }
                    }
                    if (entry.NameLower.EndsWith(".ybn"))
                    {
                        MetaHash ehash = new MetaHash(entry.ShortNameHash);
                        if (!usedboundsdict.ContainsKey(ehash))
                        {
                            if (interiorLookup.ContainsKey(ehash))
                            {
                            }
                            else
                            {
                                //exterior ybn's that aren't already cached... only noncached modded bounds hit here...
                                //load the ybn and cache its extents.
                                var ybn = GameFileCache.RpfMan.GetFile<YbnFile>(entry);
                                BoundsStoreItem item = new BoundsStoreItem(ybn.Bounds);
                                item.Name = ehash;
                                SpaceBoundsKey key = new SpaceBoundsKey(ehash, item.Min);
                                if (boundsdict.ContainsKey(key))
                                { }
                                boundsdict[key] = item;
                            }
                        }
                    }
                }
            }


        }

        private void InitMapDataStore()
        {

            MapDataStore = new SpaceMapDataStore();

            MapDataStore.Init(nodedict.Values.ToList());

        }

        private void InitBoundsStore()
        {

            BoundsStore = new SpaceBoundsStore();

            BoundsStore.Init(boundsdict.Values.ToList());

        }

        private void InitNodeGrid()
        {

            NodeGrid = new SpaceNodeGrid();
            AllYnds.Clear();

            var rpfman = GameFileCache.RpfMan;
            Dictionary<uint, RpfFileEntry> yndentries = new Dictionary<uint, RpfFileEntry>();
            foreach (var rpffile in GameFileCache.BaseRpfs) //load nodes from base rpfs
            {
                AddRpfYnds(rpffile, yndentries);
            }
            if (GameFileCache.EnableDlc)
            {
                var updrpf = rpfman.FindRpfFile("update\\update.rpf"); //load nodes from patch area...
                if (updrpf != null)
                {
                    foreach (var rpffile in updrpf.Children)
                    {
                        AddRpfYnds(rpffile, yndentries);
                    }
                }
                foreach (var dlcrpf in GameFileCache.DlcActiveRpfs) //load nodes from current dlc rpfs
                {
                    if (dlcrpf.Path.StartsWith("x64")) continue; //don't override update.rpf YNDs with x64 ones! *hack
                    foreach (var rpffile in dlcrpf.Children)
                    {
                        AddRpfYnds(rpffile, yndentries);
                    }
                }
            }


            Vector3 corner = new Vector3(-8192, -8192, -2048);
            Vector3 cellsize = new Vector3(512, 512, 4096);

            for (int x = 0; x < NodeGrid.CellCountX; x++)
            {
                for (int y = 0; y < NodeGrid.CellCountY; y++)
                {
                    var cell = NodeGrid.Cells[x, y];
                    string fname = "nodes" + cell.ID + ".ynd";
                    uint fnhash = JenkHash.GenHash(fname);
                    RpfFileEntry fentry = null;
                    if (yndentries.TryGetValue(fnhash, out fentry))
                    {
                        cell.Ynd = rpfman.GetFile<YndFile>(fentry);
                        cell.Ynd.BBMin = corner + (cellsize * new Vector3(x, y, 0));
                        cell.Ynd.BBMax = cell.Ynd.BBMin + cellsize;
                        cell.Ynd.CellX = x;
                        cell.Ynd.CellY = y;
                        cell.Ynd.Loaded = true;

                        AllYnds[fnhash] = cell.Ynd;


                        #region node flags test

                        //if (cell.Ynd == null) continue;
                        //if (cell.Ynd.NodeDictionary == null) continue;
                        //if (cell.Ynd.NodeDictionary.Nodes == null) continue;
                        //var na = cell.Ynd.NodeDictionary.Nodes;

                        //for (int i = 0; i < na.Length; i++)
                        //{
                        //    var node = na[i];

                        //    int nodetype = node.Unk25Type & 7;
                        //    int linkcount = node.Unk25Type >> 3;
                        //    int nxtlink = node.LinkID + linkcount;
                        //    if (i < na.Length - 1)
                        //    {
                        //        var nxtnode = na[i + 1];
                        //        if (nxtnode.LinkID != nxtlink)
                        //        { }
                        //    }
                        //    else
                        //    {
                        //        if (nxtlink != cell.Ynd.NodeDictionary.LinksCount)
                        //        { }
                        //    }

                        //    switch (node.Flags0)
                        //    {
                        //        case 0:
                        //        case 1:
                        //        case 2:
                        //        case 8:
                        //        case 10:
                        //        case 32:
                        //        case 34:
                        //        case 35:
                        //        case 40:
                        //        case 42:
                        //        case 66:
                        //        case 98:
                        //        case 129:
                        //        case 130:
                        //        case 162:
                        //        case 194:
                        //        case 226:
                        //            break;
                        //        default:
                        //            break;
                        //    }
                        //    switch (node.Flags1)
                        //    {
                        //        case 0:
                        //        case 1:
                        //        case 2:
                        //        case 3:
                        //        case 4:
                        //        case 16:
                        //        case 80:
                        //        case 112:
                        //        case 120:
                        //        case 121:
                        //        case 122:
                        //        case 128:
                        //        case 129:
                        //        case 136:
                        //        case 144:
                        //        case 152:
                        //        case 160:
                        //            break;
                        //        default:
                        //            break;
                        //    }

                        //}
                        #endregion

                    }
                }
            }

            //join the dots....
            //StringBuilder sb = new StringBuilder();
            List<EditorVertex> tverts = new List<EditorVertex>();
            List<YndLink> tlinks = new List<YndLink>();
            List<YndLink> nlinks = new List<YndLink>();
            foreach (var ynd in AllYnds.Values)
            {
                BuildYndData(ynd, tverts, tlinks, nlinks);

                //sb.Append(ynd.nodestr);
            }

            //string str = sb.ToString();

        }

        private void AddRpfYnds(RpfFile rpffile, Dictionary<uint, RpfFileEntry> yndentries)
        {
            if (rpffile.AllEntries == null) return;
            foreach (var entry in rpffile.AllEntries)
            {
                if (entry is RpfFileEntry)
                {
                    RpfFileEntry fentry = entry as RpfFileEntry;
                    if (entry.NameLower.EndsWith(".ynd"))
                    {
                        if (yndentries.ContainsKey(entry.NameHash))
                        { }
                        yndentries[entry.NameHash] = fentry;
                    }
                }
            }
        }

        public void BuildYndLinks(YndFile ynd, List<YndLink> tlinks = null, List<YndLink> nlinks = null)
        {
            var ynodes = ynd.Nodes;
            var nodes = ynd.NodeDictionary?.Nodes;
            var links = ynd.NodeDictionary?.Links;
            if ((ynodes == null) || (nodes == null) || (links == null)) return;

            int nodecount = ynodes.Length;


            //build the links arrays.
            if(tlinks==null) tlinks = new List<YndLink>();
            if(nlinks==null) nlinks = new List<YndLink>();
            tlinks.Clear();
            for (int i = 0; i < nodecount; i++)
            {
                nlinks.Clear();
                var node = ynodes[i];

                var linkid = node.LinkID;
                for (int l = 0; l < node.LinkCount; l++)
                {
                    var llid = linkid + l;
                    if (llid >= links.Length) continue;
                    var link = links[llid];
                    YndNode tnode;
                    if (link.AreaID == node.AreaID)
                    {
                        if (link.NodeID >= ynodes.Length)
                        { continue; }
                        tnode = ynodes[link.NodeID];
                    }
                    else
                    {
                        tnode = NodeGrid.GetYndNode(link.AreaID, link.NodeID);
                        if (tnode == null)
                        { continue; }
                        if ((Math.Abs(tnode.Ynd.CellX - ynd.CellX) > 1) || (Math.Abs(tnode.Ynd.CellY - ynd.CellY) > 1))
                        { /*continue;*/ } //non-adjacent cell? seems to be the carrier problem...
                    }

                    YndLink yl = new YndLink();
                    yl.Init(ynd, node, tnode, link);
                    tlinks.Add(yl);
                    nlinks.Add(yl);
                }
                node.Links = nlinks.ToArray();
            }
            ynd.Links = tlinks.ToArray();

        }
        public void BuildYndVerts(YndFile ynd, List<EditorVertex> tverts = null)
        {
            var ynodes = ynd.Nodes;
            if (ynodes == null) return;

            int nodecount = ynodes.Length;

            //build the main linked vertex array (used by the renderable to draw the lines).
            if(tverts==null) tverts = new List<EditorVertex>();
            tverts.Clear();
            for (int i = 0; i < nodecount; i++)
            {
                var node = ynodes[i];
                if (node.Links == null) continue;


                var nvert = new EditorVertex();
                nvert.Position = node.Position;
                nvert.Colour = (uint)node.Colour.ToRgba();


                for (int l = 0; l < node.Links.Length; l++)
                {
                    YndLink yl = node.Links[l];
                    var tnode = yl.Node2;
                    if (tnode == null) continue; //invalid links could hit here
                    var tvert = new EditorVertex();
                    tvert.Position = tnode.Position;
                    tvert.Colour = (uint)tnode.Colour.ToRgba();

                    tverts.Add(nvert);
                    tverts.Add(tvert);
                }
            }
            ynd.LinkedVerts = tverts.ToArray();


            ynd.UpdateTriangleVertices();
        }
        public void BuildYndJuncs(YndFile ynd)
        {
            //attach the junctions to the nodes.
            var yjuncs = ynd.Junctions;
            if (yjuncs != null)
            {
                var junccount = yjuncs.Length;
                for (int i = 0; i < junccount; i++)
                {
                    var junc = yjuncs[i];
                    var cell = NodeGrid.GetCell(junc.RefData.AreaID);
                    if ((cell == null) || (cell.Ynd == null) || (cell.Ynd.Nodes == null))
                    { continue; }

                    var jynd = cell.Ynd;
                    if (cell.Ynd != ynd) //junc in different ynd..? no hits here, except ynds in project..
                    {
                        if (cell.Ynd.AreaID == ynd.AreaID)
                        {
                            jynd = ynd;
                        }
                        else
                        { }
                    }

                    if (junc.RefData.NodeID >= jynd.Nodes.Length)
                    { continue; }

                    var jnode = jynd.Nodes[junc.RefData.NodeID];
                    jnode.Junction = junc;
                    jnode.HasJunction = true;
                }
            }

        }
        public void BuildYndData(YndFile ynd, List<EditorVertex> tverts = null, List<YndLink> tlinks = null, List<YndLink> nlinks = null)
        {

            BuildYndLinks(ynd, tlinks, nlinks);

            BuildYndJuncs(ynd);

            BuildYndVerts(ynd, tverts);

        }


        private void InitNavGrid()
        {
            NavGrid = new SpaceNavGrid();

            var rpfman = GameFileCache.RpfMan;
            Dictionary<uint, RpfFileEntry> ynventries = new Dictionary<uint, RpfFileEntry>();
            foreach (var rpffile in GameFileCache.BaseRpfs) //load navmeshes from base rpfs
            {
                AddRpfYnvs(rpffile, ynventries);
            }
            if (GameFileCache.EnableDlc)
            {
                var updrpf = rpfman.FindRpfFile("update\\update.rpf"); //load navmeshes from patch area...
                if (updrpf != null)
                {
                    foreach (var rpffile in updrpf.Children)
                    {
                        AddRpfYnvs(rpffile, ynventries);
                    }
                }
                foreach (var dlcrpf in GameFileCache.DlcActiveRpfs) //load navmeshes from current dlc rpfs
                {
                    foreach (var rpffile in dlcrpf.Children)
                    {
                        AddRpfYnvs(rpffile, ynventries);
                    }
                }
            }


            for (int x = 0; x < NavGrid.CellCountX; x++)
            {
                for (int y = 0; y < NavGrid.CellCountY; y++)
                {
                    var cell = NavGrid.Cells[x, y];
                    string fname = "navmesh[" + cell.FileX.ToString() + "][" + cell.FileY.ToString() + "].ynv";
                    uint fnhash = JenkHash.GenHash(fname);
                    RpfFileEntry fentry = null;
                    if (ynventries.TryGetValue(fnhash, out fentry))
                    {
                        cell.YnvEntry = fentry as RpfResourceFileEntry;
                        //cell.Ynv = rpfman.GetFile<YnvFile>(fentry);
                    }
                }
            }

        }

        private void AddRpfYnvs(RpfFile rpffile, Dictionary<uint, RpfFileEntry> ynventries)
        {
            if (rpffile.AllEntries == null) return;
            foreach (var entry in rpffile.AllEntries)
            {
                if (entry is RpfFileEntry)
                {
                    RpfFileEntry fentry = entry as RpfFileEntry;
                    if (entry.NameLower.EndsWith(".ynv"))
                    {
                        if (ynventries.ContainsKey(entry.NameHash))
                        { }
                        ynventries[entry.NameHash] = fentry;
                    }
                }
            }
        }



        public void Update(float elapsed)
        {
            if (!Inited) return;
            if (BoundsStore == null) return;

            if (elapsed > 0.1f) elapsed = 0.1f;


            Collisions.Clear();


            EnabledEntities.Clear();
            foreach (var e in PersistentEntities)
            {
                if (e.Enabled) EnabledEntities.Add(e);
            }
            foreach (var e in TemporaryEntities)
            {
                if (e.Enabled) EnabledEntities.Add(e);
            }



            float gravamt = -9.8f;
            Vector3 dvgrav = new Vector3(0, 0, gravamt * elapsed); //gravity acceleration vector
            dvgrav += (0.5f * dvgrav * elapsed); //v = ut+0.5at^2 !
            float minvel = 0.5f; // stop bouncing when slow...

            foreach (var e in EnabledEntities)
            {
                if (!e.Enabled) continue;

                e.Velocity += dvgrav; //apply gravity
                e.Momentum = e.Velocity * e.Mass;
                e.Age += elapsed;

                e.PreUpdate(elapsed);

                if (e.EnableCollisions)
                {
                    var coll = FindFirstCollision(e, elapsed);

                    if (coll.Hit)
                    {
                        Collisions.Add(coll);

                        float argvel = Math.Abs((e.Velocity - dvgrav).Length());

                        if (e.WasColliding && (argvel < minvel))
                        {
                            e.Velocity = Vector3.Zero;
                            e.Momentum = Vector3.Zero;
                        }
                        else
                        {
                            e.Position = coll.PrePos; //move to the last known position before collision

                            //bounce...
                            int maxbounce = 5;
                            int curbounce = 0;
                            float trem = 1.0f - coll.PreT;
                            while (trem > 0)
                            {
                                float vl = e.Velocity.Length();
                                float erem = elapsed * trem;
                                float drem = vl * erem;
                                Vector3 hitn = coll.SphereHit.Normal;
                                Vector3 bdir = Vector3.Reflect(coll.HitVelDir, hitn);
                                Vector3 newvel = bdir * (vl * 0.5f); //restitution/bouncyness
                                e.Velocity = newvel;

                                coll = FindFirstCollision(e, erem);

                                if (!coll.Hit)
                                {
                                    e.Position = coll.HitPos;//no hit, all done
                                    break;
                                }

                                Collisions.Add(coll);

                                e.Position = coll.PrePos;

                                trem = Math.Max(trem * (1.0f - coll.PreT), 0);

                                curbounce++;
                                if (curbounce >= maxbounce)
                                {
                                    e.Position = coll.HitPos;
                                    break;
                                }


                                //if ((coll.PreT <= 0))// || (coll.SphereHit.Normal == hitn))
                                //{
                                //    //ae.Velocity = Vector3.Zero; //same collision twice? abort?
                                //    break;
                                //}
                            }

                            e.Momentum = e.Velocity * e.Mass;
                        }
                        e.WasColliding = true;
                    }
                    else
                    {
                        e.Position = coll.HitPos; //hit pos is the end pos if no hit
                        e.WasColliding = false;
                    }
                }

                if (e.EntityDef != null)
                {
                    e.EntityDef.Position = e.Position;
                }


                if ((e.Lifetime > 0.0f) && (e.Age > e.Lifetime))
                {
                    TemporaryEntities.Remove(e);
                }

            }


        }


        public SpaceEntityCollision FindFirstCollision(Entity e, float elapsed)
        {
            SpaceEntityCollision r = new SpaceEntityCollision();
            r.Entity = e;

            Vector3 pos = e.Position;
            Vector3 sphpos = pos + e.Center;
            Vector3 disp = e.Velocity * elapsed;
            float absdisp = disp.Length();

            r.HitVelDir = Vector3.Normalize(disp);
            r.HitPos = pos + disp;
            r.HitVel = e.Velocity;
            r.HitT = 1.0f;
            r.PreT = 0.0f;
            r.PrePos = pos;

            BoundingSphere sph = new BoundingSphere(r.HitPos + e.Center, e.Radius);

            r.SphereHit = SphereIntersect(sph, CollisionLayers);

            if (!r.SphereHit.Hit)
            {
                if (absdisp > e.Radius) //fast-moving... do a ray test to make sure it's not tunnelling
                {
                    Ray rayt = new Ray(sphpos, r.HitVelDir);
                    float rayl = absdisp + e.Radius * 4.0f; //include some extra incase of glancing hit
                    var rayhit = RayIntersect(rayt, rayl);
                    if (rayhit.Hit) //looks like it is tunnelling... need to find the sphere hit point
                    {
                        sph.Center = rayhit.Position - (r.HitVelDir*Math.Min(e.Radius*0.5f, rayhit.HitDist));
                        float hitd = rayhit.HitDist;
                        r.HitT = hitd / absdisp;
                        if (r.HitT > 1.0f)
                        {
                            r.HitT = 1.0f;
                            sph.Center = r.HitPos + e.Center; //this really shouldn't happen... but just in case of glancing hit..
                        }

                        r.SphereHit = SphereIntersect(sph, CollisionLayers); //this really should be a hit!
                    }
                }
            }
            
            if (r.SphereHit.Hit)
            {
                int maxiter = 6;//(would be better to iterate until error within tolerance..)
                int curiter = 0;
                float curt = r.HitT * 0.5f;
                float step = curt * 0.5f;
                float minstep = 0.05f;
                while (curiter < maxiter) //iterate to find a closer hit time... improve this!
                {
                    sph.Center = sphpos + disp * curt;
                    var tcollres = SphereIntersect(sph, CollisionLayers);
                    if (tcollres.Hit)
                    {
                        r.HitT = curt;
                        r.HitPos = sph.Center - e.Center;
                        r.SphereHit = tcollres; //only use the best hit (ignore misses)
                        r.HitNumber = curiter;
                    }
                    else
                    {
                        r.PreT = curt;
                        r.PrePos = sph.Center - e.Center;
                    }
                    curiter++;
                    if (curiter < maxiter)
                    {
                        curt += step * (tcollres.Hit ? -1.0f : 1.0f);
                        step *= 0.5f;
                    }
                    if (absdisp * step < minstep)
                    {
                        break;
                    }
                }
            }

            r.Hit = r.SphereHit.Hit;

            return r;
        }


        public void AddTemporaryEntity(Entity e)
        {
            e.Space = this;
            while (TemporaryEntities.Count > 100)
            {
                TemporaryEntities.RemoveFirst();//don't be too laggy
            }
            TemporaryEntities.AddLast(e);
        }

        public void AddPersistentEntity(Entity e)
        {
            e.Space = this;
            PersistentEntities.AddLast(e);
        }

        public void RemovePersistentEntity(Entity e)
        {
            PersistentEntities.Remove(e);
        }



        private bool IsYmapAvailable(uint ymaphash, int hour, MetaHash weather)
        {
            MetaHash ymapname = new MetaHash(ymaphash);
            uint ymaptime;
            MetaHash[] weathers;
            if ((hour >= 0) && (hour <= 23))
            {
                if (ymaptimes.TryGetValue(ymapname, out ymaptime))
                {
                    uint mask = 1u << hour;
                    if ((ymaptime & mask) == 0) return false;
                }
            }
            if (weather.Hash != 0)
            {
                if (ymapweathertypes.TryGetValue(ymapname, out weathers))
                {
                    for (int i = 0; i < weathers.Length; i++)
                    {
                        if (weathers[i] == weather) return true;
                    }
                    return false;
                }
            }
            return true;
        }

        public void GetVisibleYmaps(Camera cam, int hour, MetaHash weather, Dictionary<MetaHash, YmapFile> ymaps)
        {
            if (!Inited) return;
            if (MapDataStore == null) return;
            CurrentHour = hour;
            CurrentWeather = weather;
            var items = MapDataStore.GetItems(ref cam.Position);
            for (int i = 0; i < items.Count; i++)
            {
                var item = items[i];
                var hash = item.Name;
                if (!ymaps.ContainsKey(hash))
                {
                    var ymap = (hash > 0) ? GameFileCache.GetYmap(hash) : null;
                    while ((ymap != null) && (ymap.Loaded))
                    {
                        if (!IsYmapAvailable(hash, hour, weather)) break;
                        ymaps[hash] = ymap;
                        hash = ymap._CMapData.parent;
                        if (ymaps.ContainsKey(hash)) break;
                        ymap = (hash > 0) ? GameFileCache.GetYmap(hash) : null;
                    }
                }
            }
        }


        public void GetVisibleBounds(Camera cam, int gridrange, bool[] layers, List<BoundsStoreItem> boundslist)
        {
            if (!Inited) return;

            if (BoundsStore == null) return;
            float dist = 50.0f * gridrange;
            var pos = cam.Position;
            var min = pos - dist;
            var max = pos + dist;
            var items = BoundsStore.GetItems(ref min, ref max, layers);
            boundslist.AddRange(items);
        }


        public void GetVisibleYnds(Camera cam, List<YndFile> ynds)
        {
            if (!Inited) return;
            if (NodeGrid == null) return;

            //int x = 9;
            //int y = 15; //== nodes489.ynd

            //ynds.Add(NodeGrid.Cells[x, y].Ynd);

            ynds.AddRange(AllYnds.Values);

        }


        public void GetVisibleYnvs(Camera cam, int gridrange, List<YnvFile> ynvs)
        {
            if (!Inited) return;
            if (NavGrid == null) return;


            ynvs.Clear();


            var pos = NavGrid.GetCellPos(cam.Position);
            int minx = Math.Min(Math.Max(pos.X - gridrange, 0), NavGrid.CellCountX-1);
            int maxx = Math.Min(Math.Max(pos.X + gridrange, 0), NavGrid.CellCountX-1);
            int miny = Math.Min(Math.Max(pos.Y - gridrange, 0), NavGrid.CellCountY-1);
            int maxy = Math.Min(Math.Max(pos.Y + gridrange, 0), NavGrid.CellCountY-1);
            for (int x = minx; x <= maxx; x++)
            {
                for (int y = miny; y <= maxy; y++)
                {
                    var cell = NavGrid.GetCell(new Vector2I(x, y));
                    if ((cell != null) && (cell.YnvEntry != null))
                    {
                        var hash = cell.YnvEntry.ShortNameHash;
                        var ynv = (hash > 0) ? GameFileCache.GetYnv(hash) : null;
                        if ((ynv != null) && (ynv.Loaded))
                        {
                            ynvs.Add(ynv);
                        }
                    }
                }
            }


        }


        public SpaceRayIntersectResult RayIntersect(Ray ray, float maxdist = float.MaxValue, bool[] layers = null)
        {
            var res = new SpaceRayIntersectResult();
            if (GameFileCache == null) return res;
            bool testcomplete = true;
            res.HitDist = maxdist;
            var box = new BoundingBox();
            float boxhitdisttest;

            if ((BoundsStore == null) || (MapDataStore == null)) return res;

            var boundslist = BoundsStore.GetItems(ref ray, layers);
            var mapdatalist = MapDataStore.GetItems(ref ray);

            for (int i = 0; i < boundslist.Count; i++)
            {
                var bound = boundslist[i];
                box.Minimum = bound.Min;
                box.Maximum = bound.Max;
                if (ray.Intersects(ref box, out boxhitdisttest))
                {
                    if (boxhitdisttest > res.HitDist)
                    { continue; } //already a closer hit

                    YbnFile ybn = GameFileCache.GetYbn(bound.Name);
                    if (ybn == null)
                    { continue; } //ybn not found?
                    if (!ybn.Loaded)
                    { testcomplete = false; continue; } //ybn not loaded yet...

                    var b = ybn.Bounds;
                    if (b == null)
                    { continue; }

                    var bhit = b.RayIntersect(ref ray, res.HitDist);
                    if (bhit.Hit)
                    {
                        bhit.HitYbn = ybn;
                    }
                    res.TryUpdate(ref bhit);
                }
            }

            for (int i = 0; i < mapdatalist.Count; i++)
            {
                var mapdata = mapdatalist[i];
                if ((mapdata.ContentFlags & 1) == 0)
                { continue; } //only test HD ymaps

                box.Minimum = mapdata.entitiesExtentsMin;
                box.Maximum = mapdata.entitiesExtentsMax;
                if (ray.Intersects(ref box, out boxhitdisttest))
                {
                    if (boxhitdisttest > res.HitDist)
                    { continue; } //already a closer hit

                    var hash = mapdata.Name;
                    var ymap = (hash > 0) ? GameFileCache.GetYmap(hash) : null;
                    if ((ymap != null) && (ymap.Loaded) && (ymap.AllEntities != null))
                    {
                        if (!IsYmapAvailable(hash, CurrentHour, CurrentWeather))
                        { continue; }

                        for (int e = 0; e < ymap.AllEntities.Length; e++)
                        {
                            var ent = ymap.AllEntities[e];

                            if (!EntityCollisionsEnabled(ent))
                            { continue; }

                            box.Minimum = ent.BBMin;
                            box.Maximum = ent.BBMax;
                            if (ray.Intersects(ref box, out boxhitdisttest))
                            {
                                if (boxhitdisttest > res.HitDist)
                                { continue; } //already a closer hit

                                if (ent.IsMlo)
                                {
                                    var ihit = RayIntersectInterior(ref ray, ent, res.HitDist);
                                    res.TryUpdate(ref ihit);
                                }
                                else
                                {
                                    var ehit = RayIntersectEntity(ref ray, ent, res.HitDist);
                                    res.TryUpdate(ref ehit);
                                }
                            }
                        }
                    }
                    else if ((ymap != null) && (!ymap.Loaded))
                    {
                        testcomplete = false;
                    }
                }
            }




            if (res.Hit)
            {
                res.Position = ray.Position + ray.Direction * res.HitDist;
            }

            res.TestComplete = testcomplete;

            return res;
        }
        public SpaceRayIntersectResult RayIntersectEntity(ref Ray ray, YmapEntityDef ent, float maxdist = float.MaxValue)
        {
            var res = new SpaceRayIntersectResult();
            res.HitDist = maxdist;

            var drawable = GameFileCache.TryGetDrawable(ent.Archetype);
            if (drawable != null)
            {
                var eori = ent.Orientation;
                var eorinv = Quaternion.Invert(ent.Orientation);
                var eray = new Ray();
                eray.Position = eorinv.Multiply(ray.Position - ent.Position);
                eray.Direction = eorinv.Multiply(ray.Direction);

                if ((drawable is Drawable sdrawable) && (sdrawable.Bound != null))
                {
                    var dhit = sdrawable.Bound.RayIntersect(ref eray, res.HitDist);
                    if (dhit.Hit)
                    {
                        dhit.Position = eori.Multiply(dhit.Position) + ent.Position;
                        dhit.Normal = eori.Multiply(dhit.Normal);
                    }
                    res.TryUpdate(ref dhit);
                }
                else if (drawable is FragDrawable fdrawable)
                {
                    if (fdrawable.Bound != null)
                    {
                        var fhit = fdrawable.Bound.RayIntersect(ref eray, res.HitDist);
                        if (fhit.Hit)
                        {
                            fhit.Position = eori.Multiply(fhit.Position) + ent.Position;
                            fhit.Normal = eori.Multiply(fhit.Normal);
                        }
                        res.TryUpdate(ref fhit);
                    }
                    var fbound = fdrawable.OwnerFragment?.PhysicsLODGroup?.PhysicsLOD1?.Bound;
                    if (fbound != null)
                    {
                        var fhit = fbound.RayIntersect(ref eray, res.HitDist);//TODO: these probably have extra transforms..!
                        if (fhit.Hit)
                        {
                            fhit.Position = eori.Multiply(fhit.Position) + ent.Position;
                            fhit.Normal = eori.Multiply(fhit.Normal);
                        }
                        res.TryUpdate(ref fhit);
                    }
                }
            }
            if (res.Hit)
            {
                res.HitEntity = ent;
            }

            return res;
        }
        public SpaceRayIntersectResult RayIntersectInterior(ref Ray ray, YmapEntityDef mlo, float maxdist = float.MaxValue)
        {
            var res = new SpaceRayIntersectResult();
            res.HitDist = maxdist;

            if (mlo.Archetype == null)
            { return res; }

            var iori = mlo.Orientation;
            var iorinv = Quaternion.Invert(mlo.Orientation);
            var iray = new Ray();
            iray.Position = iorinv.Multiply(ray.Position - mlo.Position);
            iray.Direction = iorinv.Multiply(ray.Direction);

            var hash = mlo.Archetype.Hash;
            var ybn = GameFileCache.GetYbn(hash);
            if ((ybn != null) && (ybn.Loaded))
            {
                var ihit = ybn.Bounds.RayIntersect(ref iray, res.HitDist);
                if (ihit.Hit)
                {
                    ihit.HitYbn = ybn;
                    ihit.HitEntity = mlo;
                    ihit.Position = iori.Multiply(ihit.Position) + mlo.Position;
                    ihit.Normal = iori.Multiply(ihit.Normal);
                }
                res.TryUpdate(ref ihit);
            }

            var mlodat = mlo.MloInstance;
            if (mlodat == null)
            { return res; }

            var box = new BoundingBox();
            float boxhitdisttest;

            if (mlodat.Entities != null)
            {
                for (int j = 0; j < mlodat.Entities.Length; j++) //should really improve this by using rooms!
                {
                    var intent = mlodat.Entities[j];
                    if (intent.Archetype == null) continue; //missing archetype...

                    if (!EntityCollisionsEnabled(intent))
                    { continue; }

                    box.Minimum = intent.BBMin;
                    box.Maximum = intent.BBMax;
                    if (ray.Intersects(ref box, out boxhitdisttest))
                    {
                        if (boxhitdisttest > res.HitDist)
                        { continue; } //already a closer hit

                        var ehit = RayIntersectEntity(ref ray, intent, res.HitDist);
                        res.TryUpdate(ref ehit);
                    }
                }
            }
            if (mlodat.EntitySets != null)
            {
                for (int e = 0; e < mlodat.EntitySets.Length; e++)
                {
                    var entityset = mlodat.EntitySets[e];
                    if (!entityset.Visible) continue;
                    var entities = entityset.Entities;
                    if (entities == null) continue;
                    for (int i = 0; i < entities.Count; i++) //should really improve this by using rooms!
                    {
                        var intent = entities[i];
                        if (intent.Archetype == null) continue; //missing archetype...

                        if (!EntityCollisionsEnabled(intent))
                        { continue; }

                        box.Minimum = intent.BBMin;
                        box.Maximum = intent.BBMax;
                        if (ray.Intersects(ref box, out boxhitdisttest))
                        {
                            if (boxhitdisttest > res.HitDist)
                            { continue; } //already a closer hit

                            var ehit = RayIntersectEntity(ref ray, intent, res.HitDist);
                            res.TryUpdate(ref ehit);
                        }
                    }
                }
            }

            return res;
        }

        public SpaceSphereIntersectResult SphereIntersect(BoundingSphere sph, bool[] layers = null)
        {
            var res = new SpaceSphereIntersectResult();
            if (GameFileCache == null) return res;
            bool testcomplete = true;
            Vector3 sphmin = sph.Center - sph.Radius;
            Vector3 sphmax = sph.Center + sph.Radius;
            var box = new BoundingBox();

            if ((BoundsStore == null) || (MapDataStore == null)) return res;

            var boundslist = BoundsStore.GetItems(ref sphmin, ref sphmax, layers);
            var mapdatalist = MapDataStore.GetItems(ref sphmin, ref sphmax);

            for (int i = 0; i < boundslist.Count; i++)
            {
                var bound = boundslist[i];
                box.Minimum = bound.Min;
                box.Maximum = bound.Max;
                if (sph.Intersects(ref box))
                {
                    YbnFile ybn = GameFileCache.GetYbn(bound.Name);
                    if (ybn == null)
                    { continue; } //ybn not found?
                    if (!ybn.Loaded)
                    { testcomplete = false; continue; } //ybn not loaded yet...

                    var b = ybn.Bounds;
                    if (b == null)
                    { continue; }

                    var bhit = b.SphereIntersect(ref sph);
                    res.TryUpdate(ref bhit);
                }
            }

            for (int i = 0; i < mapdatalist.Count; i++)
            {
                var mapdata = mapdatalist[i];
                if ((mapdata.ContentFlags & 1) == 0)
                { continue; } //only test HD ymaps

                box.Minimum = mapdata.entitiesExtentsMin;
                box.Maximum = mapdata.entitiesExtentsMax;
                if (sph.Intersects(ref box))
                {
                    var hash = mapdata.Name;
                    var ymap = (hash > 0) ? GameFileCache.GetYmap(hash) : null;
                    if ((ymap != null) && (ymap.Loaded) && (ymap.AllEntities != null))
                    {
                        if (!IsYmapAvailable(hash, CurrentHour, CurrentWeather))
                        { continue; }

                        for (int e = 0; e < ymap.AllEntities.Length; e++)
                        {
                            var ent = ymap.AllEntities[e];

                            if (!EntityCollisionsEnabled(ent))
                            { continue; }

                            box.Minimum = ent.BBMin;
                            box.Maximum = ent.BBMax;
                            if (sph.Intersects(ref box))
                            {
                                if (ent.IsMlo)
                                {
                                    var ihit = SphereIntersectInterior(ref sph, ent);
                                    res.TryUpdate(ref ihit);
                                }
                                else
                                {
                                    var ehit = SphereIntersectEntity(ref sph, ent);
                                    res.TryUpdate(ref ehit);
                                }
                            }
                        }
                    }
                    else if ((ymap != null) && (!ymap.Loaded))
                    {
                        testcomplete = false;
                    }
                }
            }


            //if (hit)
            //{
            //    hitpos = ray.Position + ray.Direction * itemhitdist;
            //}

            res.TestComplete = testcomplete;

            return res;
        }
        public SpaceSphereIntersectResult SphereIntersectEntity(ref BoundingSphere sph, YmapEntityDef ent)
        {
            var res = new SpaceSphereIntersectResult();

            var drawable = GameFileCache.TryGetDrawable(ent.Archetype);
            if (drawable != null)
            {
                var eori = ent.Orientation;
                var eorinv = Quaternion.Invert(ent.Orientation);
                var esph = sph;
                esph.Center = eorinv.Multiply(sph.Center - ent.Position);

                if ((drawable is Drawable sdrawable) && (sdrawable.Bound != null))
                {
                    var dhit = sdrawable.Bound.SphereIntersect(ref esph);
                    if (dhit.Hit)
                    {
                        dhit.Position = eori.Multiply(dhit.Position) + ent.Position;
                        dhit.Normal = eori.Multiply(dhit.Normal);
                    }
                    res.TryUpdate(ref dhit);
                }
                else if (drawable is FragDrawable fdrawable)
                {
                    if (fdrawable.Bound != null)
                    {
                        var fhit = fdrawable.Bound.SphereIntersect(ref esph);
                        if (fhit.Hit)
                        {
                            fhit.Position = eori.Multiply(fhit.Position) + ent.Position;
                            fhit.Normal = eori.Multiply(fhit.Normal);
                        }
                        res.TryUpdate(ref fhit);
                    }
                    var fbound = fdrawable.OwnerFragment?.PhysicsLODGroup?.PhysicsLOD1?.Bound;
                    if (fbound != null)
                    {
                        var fhit = fbound.SphereIntersect(ref esph);//TODO: these probably have extra transforms..!
                        if (fhit.Hit)
                        {
                            fhit.Position = eori.Multiply(fhit.Position) + ent.Position;
                            fhit.Normal = eori.Multiply(fhit.Normal);
                        }
                        res.TryUpdate(ref fhit);
                    }
                }
            }

            return res;
        }
        public SpaceSphereIntersectResult SphereIntersectInterior(ref BoundingSphere sph, YmapEntityDef mlo)
        {
            var res = new SpaceSphereIntersectResult();

            if (mlo.Archetype == null)
            { return res; }

            var iori = mlo.Orientation;
            var iorinv = Quaternion.Invert(mlo.Orientation);
            var isph = sph;
            isph.Center = iorinv.Multiply(sph.Center - mlo.Position);

            var hash = mlo.Archetype.Hash;
            var ybn = GameFileCache.GetYbn(hash);
            if ((ybn != null) && (ybn.Loaded))
            {
                var ihit = ybn.Bounds.SphereIntersect(ref isph);
                if (ihit.Hit)
                {
                    ihit.Position = iori.Multiply(ihit.Position) + mlo.Position;
                    ihit.Normal = iori.Multiply(ihit.Normal);
                }
                res.TryUpdate(ref ihit);
            }

            var mlodat = mlo.MloInstance;
            if (mlodat == null)
            { return res; }

            var box = new BoundingBox();

            if (mlodat.Entities != null)
            {
                for (int j = 0; j < mlodat.Entities.Length; j++) //should really improve this by using rooms!
                {
                    var intent = mlodat.Entities[j];
                    if (intent.Archetype == null) continue; //missing archetype...

                    if (!EntityCollisionsEnabled(intent))
                    { continue; }

                    box.Minimum = intent.BBMin;
                    box.Maximum = intent.BBMax;
                    if (sph.Intersects(ref box))
                    {
                        var ehit = SphereIntersectEntity(ref sph, intent);
                        res.TryUpdate(ref ehit);
                    }
                }
            }
            if (mlodat.EntitySets != null)
            {
                for (int e = 0; e < mlodat.EntitySets.Length; e++)
                {
                    var entityset = mlodat.EntitySets[e];
                    if (!entityset.Visible) continue;
                    var entities = entityset.Entities;
                    if (entities == null) continue;
                    for (int i = 0; i < entities.Count; i++) //should really improve this by using rooms!
                    {
                        var intent = entities[i];
                        if (intent.Archetype == null) continue; //missing archetype...

                        if (!EntityCollisionsEnabled(intent))
                        { continue; }

                        box.Minimum = intent.BBMin;
                        box.Maximum = intent.BBMax;
                        if (sph.Intersects(ref box))
                        {
                            var ehit = SphereIntersectEntity(ref sph, intent);
                            res.TryUpdate(ref ehit);
                        }
                    }
                }
            }

            return res;
        }

        private bool EntityCollisionsEnabled(YmapEntityDef ent)
        {
            if ((ent._CEntityDef.lodLevel != rage__eLodType.LODTYPES_DEPTH_ORPHANHD) && (ent._CEntityDef.lodLevel != rage__eLodType.LODTYPES_DEPTH_HD))
            { return false; } //only test HD entities

            if ((ent._CEntityDef.flags & 4) > 0)
            { return false; } //embedded collisions disabled

            return true;
        }

    }



    public struct SpaceBoundsKey
    {
        public MetaHash Name { get; set; }
        public Vector3 Position { get; set; }
        public SpaceBoundsKey(MetaHash name, Vector3 position)
        {
            Name = name;
            Position = position;
        }
    }


    public class SpaceMapDataStore
    {
        public SpaceMapDataStoreNode RootNode;
        public int SplitThreshold = 10;

        public List<MapDataStoreNode> VisibleItems = new List<MapDataStoreNode>();

        public void Init(List<MapDataStoreNode> rootnodes)
        {
            RootNode = new SpaceMapDataStoreNode();
            RootNode.Owner = this;
            foreach (var item in rootnodes)
            {
                RootNode.Add(item);
            }
            RootNode.TrySplit(SplitThreshold);
        }

        public List<MapDataStoreNode> GetItems(ref Vector3 p) //get items at a point, using the streaming extents
        {
            VisibleItems.Clear();

            if (RootNode != null)
            {
                RootNode.GetItems(ref p, VisibleItems);
            }

            return VisibleItems;
        }
        public List<MapDataStoreNode> GetItems(ref Vector3 min, ref Vector3 max) //get items intersecting a box, using the entities extents
        {
            VisibleItems.Clear();

            if (RootNode != null)
            {
                RootNode.GetItems(ref min, ref max, VisibleItems);
            }

            return VisibleItems;
        }
        public List<MapDataStoreNode> GetItems(ref Ray ray) //get items intersecting a ray, using the entities extents
        {
            VisibleItems.Clear();

            if (RootNode != null)
            {
                RootNode.GetItems(ref ray, VisibleItems);
            }

            return VisibleItems;
        }
    }
    public class SpaceMapDataStoreNode
    {
        public SpaceMapDataStore Owner = null;
        public SpaceMapDataStoreNode[] Children = null;
        public List<MapDataStoreNode> Items = null;
        public Vector3 BBMin = new Vector3(float.MaxValue);
        public Vector3 BBMax = new Vector3(float.MinValue);
        public int Depth = 0;

        public void Add(MapDataStoreNode item)
        {
            if (Items == null)
            {
                Items = new List<MapDataStoreNode>();
            }
            BBMin = Vector3.Min(BBMin, item.streamingExtentsMin);
            BBMax = Vector3.Max(BBMax, item.streamingExtentsMax);
            Items.Add(item);
        }

        public void TrySplit(int threshold)
        {
            if ((Items == null) || (Items.Count <= threshold))
            { return; }

            Children = new SpaceMapDataStoreNode[4];

            var newItems = new List<MapDataStoreNode>();

            var ncen = (BBMax + BBMin) * 0.5f;
            var next = (BBMax - BBMin) * 0.5f;
            var nsiz = Math.Max(next.X, next.Y);
            var nsizh = nsiz * 0.5f;

            foreach (var item in Items)
            {
                var imin = item.streamingExtentsMin;
                var imax = item.streamingExtentsMax;
                var icen = (imax + imin) * 0.5f;
                var iext = (imax - imin) * 0.5f;
                var isiz = Math.Max(iext.X, iext.Y);

                if (isiz >= nsizh)
                {
                    newItems.Add(item);
                }
                else
                {
                    var cind = ((icen.X > ncen.X) ? 1 : 0) + ((icen.Y > ncen.Y) ? 2 : 0);
                    var c = Children[cind];
                    if (c == null)
                    {
                        c = new SpaceMapDataStoreNode();
                        c.Owner = Owner;
                        c.Depth = Depth + 1;
                        Children[cind] = c;
                    }
                    c.Add(item);
                }
            }

            for (int i = 0; i < 4; i++)
            {
                var c = Children[i];
                if (c != null)
                {
                    c.TrySplit(threshold);
                }
            }

            Items = newItems;
        }

        public void GetItems(ref Vector3 p, List<MapDataStoreNode> items) //get items at a point, using the streaming extents
        {
            if ((p.X >= BBMin.X) && (p.X <= BBMax.X) && (p.Y >= BBMin.Y) && (p.Y <= BBMax.Y))
            {
                if (Items != null)
                {
                    for (int i = 0; i < Items.Count; i++)
                    {
                        var item = Items[i];
                        var imin = item.streamingExtentsMin;
                        var imax = item.streamingExtentsMax;
                        if ((p.X >= imin.X) && (p.X <= imax.X) && (p.Y >= imin.Y) && (p.Y <= imax.Y))
                        {
                            items.Add(item);
                        }
                    }
                }
                if (Children != null)
                {
                    for (int i = 0; i < 4; i++)
                    {
                        var c = Children[i];
                        if (c != null)
                        {
                            c.GetItems(ref p, items);
                        }
                    }
                }
            }
        }
        public void GetItems(ref Vector3 min, ref Vector3 max, List<MapDataStoreNode> items) //get items intersecting a box, using the entities extents
        {
            if ((max.X >= BBMin.X) && (min.X <= BBMax.X) && (max.Y >= BBMin.Y) && (min.Y <= BBMax.Y))
            {
                if (Items != null)
                {
                    for (int i = 0; i < Items.Count; i++)
                    {
                        var item = Items[i];
                        var imin = item.entitiesExtentsMin;
                        var imax = item.entitiesExtentsMax;
                        if ((max.X >= imin.X) && (min.X <= imax.X) && (max.Y >= imin.Y) && (min.Y <= imax.Y))
                        {
                            items.Add(item);
                        }
                    }
                }
                if (Children != null)
                {
                    for (int i = 0; i < 4; i++)
                    {
                        var c = Children[i];
                        if (c != null)
                        {
                            c.GetItems(ref min, ref max, items);
                        }
                    }
                }
            }
        }
        public void GetItems(ref Ray ray, List<MapDataStoreNode> items) //get items intersecting a ray, using the entities extents
        {
            var bb = new BoundingBox(BBMin, BBMax);
            if (ray.Intersects(ref bb))
            {
                if (Items != null)
                {
                    for (int i = 0; i < Items.Count; i++)
                    {
                        var item = Items[i];
                        bb.Minimum = item.entitiesExtentsMin;
                        bb.Maximum = item.entitiesExtentsMax;
                        if (ray.Intersects(ref bb))
                        {
                            items.Add(item);
                        }
                    }
                }
                if (Children != null)
                {
                    for (int i = 0; i < 4; i++)
                    {
                        var c = Children[i];
                        if (c != null)
                        {
                            c.GetItems(ref ray, items);
                        }
                    }
                }
            }
        }
    }


    public class SpaceBoundsStore
    {
        public SpaceBoundsStoreNode RootNode;
        public int SplitThreshold = 10;

        public List<BoundsStoreItem> VisibleItems = new List<BoundsStoreItem>();

        public void Init(List<BoundsStoreItem> items)
        {
            RootNode = new SpaceBoundsStoreNode();
            RootNode.Owner = this;
            foreach (var item in items)
            {
                RootNode.Add(item);
            }
            RootNode.TrySplit(SplitThreshold);
        }

        public List<BoundsStoreItem> GetItems(ref Vector3 min, ref Vector3 max, bool[] layers = null)
        {
            VisibleItems.Clear();

            if (RootNode != null)
            {
                RootNode.GetItems(ref min, ref max, VisibleItems, layers);
            }

            return VisibleItems;
        }
        public List<BoundsStoreItem> GetItems(ref Ray ray, bool[] layers = null)
        {
            VisibleItems.Clear();

            if (RootNode != null)
            {
                RootNode.GetItems(ref ray, VisibleItems, layers);
            }

            return VisibleItems;
        }
    }
    public class SpaceBoundsStoreNode
    {
        public SpaceBoundsStore Owner = null;
        public SpaceBoundsStoreNode[] Children = null;
        public List<BoundsStoreItem> Items = null;
        public Vector3 BBMin = new Vector3(float.MaxValue);
        public Vector3 BBMax = new Vector3(float.MinValue);
        public int Depth = 0;

        public void Add(BoundsStoreItem item)
        {
            if (Items == null)
            {
                Items = new List<BoundsStoreItem>();
            }
            BBMin = Vector3.Min(BBMin, item.Min);
            BBMax = Vector3.Max(BBMax, item.Max);
            Items.Add(item);
        }

        public void TrySplit(int threshold)
        {
            if ((Items == null) || (Items.Count <= threshold))
            { return; }

            Children = new SpaceBoundsStoreNode[4];

            var newItems = new List<BoundsStoreItem>();

            var ncen = (BBMax + BBMin) * 0.5f;
            var next = (BBMax - BBMin) * 0.5f;
            var nsiz = Math.Max(next.X, next.Y);
            var nsizh = nsiz * 0.5f;

            foreach (var item in Items)
            {
                var imin = item.Min;
                var imax = item.Max;
                var icen = (imax + imin) * 0.5f;
                var iext = (imax - imin) * 0.5f;
                var isiz = Math.Max(iext.X, iext.Y);

                if (isiz >= nsizh)
                {
                    newItems.Add(item);
                }
                else
                {
                    var cind = ((icen.X > ncen.X) ? 1 : 0) + ((icen.Y > ncen.Y) ? 2 : 0);
                    var c = Children[cind];
                    if (c == null)
                    {
                        c = new SpaceBoundsStoreNode();
                        c.Owner = Owner;
                        c.Depth = Depth + 1;
                        Children[cind] = c;
                    }
                    c.Add(item);
                }
            }

            for (int i = 0; i < 4; i++)
            {
                var c = Children[i];
                if (c != null)
                {
                    c.TrySplit(threshold);
                }
            }

            Items = newItems;
        }

        public void GetItems(ref Vector3 min, ref Vector3 max, List<BoundsStoreItem> items, bool[] layers = null)
        {
            if ((max.X >= BBMin.X) && (min.X <= BBMax.X) && (max.Y >= BBMin.Y) && (min.Y <= BBMax.Y))
            {
                if (Items != null)
                {
                    for (int i = 0; i < Items.Count; i++)
                    {
                        var item = Items[i];

                        if ((layers != null) && (item.Layer < 3) && (!layers[item.Layer]))
                        { continue; }

                        if ((max.X >= item.Min.X) && (min.X <= item.Max.X) && (max.Y >= item.Min.Y) && (min.Y <= item.Max.Y))
                        {
                            items.Add(item);
                        }
                    }
                }
                if (Children != null)
                {
                    for (int i = 0; i < 4; i++)
                    {
                        var c = Children[i];
                        if (c != null)
                        {
                            c.GetItems(ref min, ref max, items, layers);
                        }
                    }
                }
            }
        }
        public void GetItems(ref Ray ray, List<BoundsStoreItem> items, bool[] layers = null)
        {
            var box = new BoundingBox(BBMin, BBMax);
            if (ray.Intersects(ref box))
            {
                if (Items != null)
                {
                    for (int i = 0; i < Items.Count; i++)
                    {
                        var item = Items[i];

                        if ((layers != null) && (item.Layer < 3) && (!layers[item.Layer]))
                        { continue; }

                        box = new BoundingBox(item.Min, item.Max);
                        if (ray.Intersects(box))
                        {
                            items.Add(item);
                        }
                    }
                }
                if (Children != null)
                {
                    for (int i = 0; i < 4; i++)
                    {
                        var c = Children[i];
                        if (c != null)
                        {
                            c.GetItems(ref ray, items, layers);
                        }
                    }
                }
            }
        }
    }


    public class SpaceNodeGrid
    {
        //node grid for V paths
        public SpaceNodeGridCell[,] Cells { get; set; }
        public float CellSize = 512.0f;
        public float CellSizeInv; //inverse of the cell size.
        public int CellCountX = 32;
        public int CellCountY = 32;
        public float CornerX = -8192.0f;
        public float CornerY = -8192.0f;

        public SpaceNodeGrid()
        {
            CellSizeInv = 1.0f / CellSize;

            Cells = new SpaceNodeGridCell[CellCountX, CellCountY];

            for (int x = 0; x < CellCountX; x++)
            {
                for (int y = 0; y < CellCountY; y++)
                {
                    Cells[x, y] = new SpaceNodeGridCell(x, y);
                }
            }
        }

        public SpaceNodeGridCell GetCell(int id)
        {
            int x = id % CellCountX;
            int y = id / CellCountX;
            if ((x >= 0) && (x < CellCountX) && (y >= 0) && (y < CellCountY))
            {
                return Cells[x, y];
            }
            return null;
        }


        public YndNode GetYndNode(ushort areaid, ushort nodeid)
        {
            var cell = GetCell(areaid);
            if ((cell == null) || (cell.Ynd == null) || (cell.Ynd.Nodes == null))
            { return null; }
            if (nodeid >= cell.Ynd.Nodes.Length)
            { return null; }
            return cell.Ynd.Nodes[nodeid];
        }

    }
    public class SpaceNodeGridCell
    {
        public int X;
        public int Y;
        public int ID;

        public YndFile Ynd;

        public SpaceNodeGridCell(int x, int y)
        {
            X = x;
            Y = y;
            ID = y * 32 + x;
        }

    }


    public class SpaceNavGrid
    {
        //grid for V navmeshes
        public SpaceNavGridCell[,] Cells { get; set; }
        public float CellSize = 150.0f;
        public float CellSizeInv; //inverse of the cell size.
        public int CellCountX = 100;
        public int CellCountY = 100;
        public float CornerX = -6000.0f;//max = -6000+(100*150) = 9000
        public float CornerY = -6000.0f;

        public SpaceNavGrid()
        {
            CellSizeInv = 1.0f / CellSize;

            Cells = new SpaceNavGridCell[CellCountX, CellCountY];

            for (int x = 0; x < CellCountX; x++)
            {
                for (int y = 0; y < CellCountY; y++)
                {
                    Cells[x, y] = new SpaceNavGridCell(x, y);
                }
            }
        }

        public SpaceNavGridCell GetCell(int id)
        {
            int x = id % CellCountX;
            int y = id / CellCountX;
            if ((x >= 0) && (x < CellCountX) && (y >= 0) && (y < CellCountY))
            {
                return Cells[x, y];
            }
            return null;
        }


        public Vector3 GetCellRel(Vector3 p)//float value in cell coords
        {
            return (p - new Vector3(CornerX, CornerY, 0)) * CellSizeInv;
        }
        public Vector2I GetCellPos(Vector3 p)
        {
            Vector3 ind = (p - new Vector3(CornerX, CornerY, 0)) * CellSizeInv;
            int x = (int)ind.X;
            int y = (int)ind.Y;
            x = (x < 0) ? 0 : (x >= CellCountX) ? CellCountX-1 : x;
            y = (y < 0) ? 0 : (y >= CellCountY) ? CellCountY-1 : y;
            return new Vector2I(x, y);
        }
        public SpaceNavGridCell GetCell(Vector2I g)
        {
            var cell = Cells[g.X, g.Y];
            if (cell == null)
            {
                //cell = new SpaceNavGridCell(g.X, g.Y);
                //Cells[g.X, g.Y] = cell;
            }
            return cell;
        }
        public SpaceNavGridCell GetCell(Vector3 p)
        {
            return GetCell(GetCellPos(p));
        }


        public Vector3 GetCellMin(SpaceNavGridCell cell)
        {
            Vector3 c = new Vector3(cell.X, cell.Y, 0);
            return new Vector3(CornerX, CornerY, 0) + (c * CellSize);
        }
        public Vector3 GetCellMax(SpaceNavGridCell cell)
        {
            return GetCellMin(cell) + new Vector3(CellSize, CellSize, 0.0f);
        }

    }
    public class SpaceNavGridCell
    {
        public int X;
        public int Y;
        public int ID;
        public int FileX;
        public int FileY;

        public RpfResourceFileEntry YnvEntry;
        public YnvFile Ynv;

        public SpaceNavGridCell(int x, int y)
        {
            X = x;
            Y = y;
            ID = y * 100 + x;
            FileX = x * 3;
            FileY = y * 3;
        }

    }



    public struct SpaceRayIntersectResult
    {
        public bool Hit;
        public float HitDist;
        public BoundVertexRef HitVertex;
        public BoundPolygon HitPolygon;
        public Bounds HitBounds;
        public YbnFile HitYbn;
        public YmapEntityDef HitEntity;
        public Vector3 Position;
        public Vector3 Normal;
        public int TestedNodeCount;
        public int TestedPolyCount;
        public bool TestComplete;
        public BoundMaterial_s Material;

        public void TryUpdate(ref SpaceRayIntersectResult r)
        {
            if (r.Hit)
            {
                Hit = true;
                HitDist = r.HitDist;
                HitVertex = r.HitVertex;
                HitPolygon = r.HitPolygon;
                HitBounds = r.HitBounds;
                HitYbn = r.HitYbn;
                HitEntity = r.HitEntity;
                Material = r.Material;
                Position = r.Position;
                Normal = r.Normal;
            }
            TestedNodeCount += r.TestedNodeCount;
            TestedPolyCount += r.TestedPolyCount;
        }
    }
    public struct SpaceSphereIntersectResult
    {
        public bool Hit;
        public float HitDist;
        public BoundPolygon HitPolygon;
        public Vector3 Position;
        public Vector3 Normal;
        public int TestedNodeCount;
        public int TestedPolyCount;
        public bool TestComplete;

        public void TryUpdate(ref SpaceSphereIntersectResult r)
        {
            if (r.Hit)
            {
                Hit = true;
                HitPolygon = r.HitPolygon;
                Normal = r.Normal;
            }
            TestedPolyCount += r.TestedPolyCount;
            TestedNodeCount += r.TestedNodeCount;
        }
    }

    public struct SpaceEntityCollision
    {
        public Entity Entity; //the entity owning this collision
        public Entity Entity2; //second entity, if this is a collision between two entities
        public SpaceSphereIntersectResult SphereHit; //details of the sphere intersection point
        public Vector3 PrePos; //last known position before hit
        public float PreT; //last known T before hit
        public float HitT; //fraction of the frame (0-1)
        public Vector3 HitPos; //position of the sphere center at hit point
        public Quaternion HitRot; //rotation of the entity at hit point
        public Vector3 HitVel; //velocity of the entity for this hit
        public Vector3 HitAngVel; //angular velocity of the entity for this hit
        public int HitNumber; //count of previous iterations
        public bool Hit;
        public Vector3 HitVelDir;
    }

}
