using SharpDX;
using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.IO;
using System.Linq;
using System.Runtime.InteropServices;
using System.Text;
using System.Threading.Tasks;
using CodeWalker.Core.Utils;
using CodeWalker.World;

namespace CodeWalker.GameFiles
{
    [TypeConverter(typeof(ExpandableObjectConverter))]
    public class YmapFile : GameFile, PackedFile
    {

        public Meta Meta { get; set; }
        public PsoFile Pso { get; set; }
        public RbfFile Rbf { get; set; }

        public CMapData _CMapData;

        public CMapData CMapData { get { return _CMapData; } set { _CMapData = value; } }
        public CEntityDef[] CEntityDefs { get; set; }
        public CMloInstanceDef[] CMloInstanceDefs { get; set; }
        public CCarGen[] CCarGens { get; set; }
        public CTimeCycleModifier[] CTimeCycleModifiers { get; set; }
        public MetaHash[] physicsDictionaries { get; set; }

        public BoxOccluder[] CBoxOccluders { get; set; }
        public OccludeModel[] COccludeModels { get; set; }


        public string[] Strings { get; set; }
        public YmapEntityDef[] AllEntities;
        public YmapEntityDef[] RootEntities;
        public YmapEntityDef[] MloEntities;

        public YmapFile Parent { get; set; }
        public YmapFile[] ChildYmaps = null;
        public bool MergedWithParent = false;

        public bool IsScripted { get { return (_CMapData.flags & 1) > 0; } }

        public YmapGrassInstanceBatch[] GrassInstanceBatches { get; set; }
        public YmapPropInstanceBatch[] PropInstanceBatches { get; set; }

        public YmapDistantLODLights DistantLODLights { get; set; }

        public YmapLODLights LODLights { get; set; }

        public YmapTimeCycleModifier[] TimeCycleModifiers { get; set; }

        public YmapCarGen[] CarGenerators { get; set; }

        public YmapBoxOccluder[] BoxOccluders { get; set; }
        public YmapOccludeModel[] OccludeModels { get; set; }


        //fields used by the editor:
        public bool HasChanged { get; set; } = false;
        public List<string> SaveWarnings = null;
        public bool LodManagerUpdate = false; //forces the LOD manager to refresh this ymap when rendering
        public YmapEntityDef[] LodManagerOldEntities = null; //when entities are removed, need the old ones to remove from lod manager


        public YmapFile() : base(null, GameFileType.Ymap)
        {
        }
        public YmapFile(RpfFileEntry entry) : base(entry, GameFileType.Ymap)
        {
            RpfFileEntry = entry;
        }

        public void Load(byte[] data)
        {
            //direct load from a raw, compressed ymap file (openIV-compatible format)

            RpfFile.LoadResourceFile(this, data, 2);

            Loaded = true;
        }

        public void Load(byte[] data, RpfFileEntry entry)
        {
            Name = entry.Name;
            RpfFileEntry = entry;

            RpfResourceFileEntry resentry = entry as RpfResourceFileEntry;
            if (resentry == null)
            {
                NonMetaLoad(data);
                Loaded = true;
                return;
            }

            ResourceDataReader rd = new ResourceDataReader(resentry, data);

            Meta = rd.ReadBlock<Meta>();//maybe null this after load to reduce memory consumption?



            CMapData = MetaTypes.GetTypedData<CMapData>(Meta, MetaName.CMapData);



            Strings = MetaTypes.GetStrings(Meta);
            if (Strings != null)
            {
                foreach (string str in Strings)
                {
                    JenkIndex.Ensure(str); //just shove them in there
                }
            }

            physicsDictionaries = MetaTypes.GetHashArray(Meta, _CMapData.physicsDictionaries);


            EnsureEntities(Meta); //load all the entity data and create the YmapEntityDefs

            EnsureInstances(Meta);

            EnsureLODLights(Meta);

            EnsureDistantLODLights(Meta);

            EnsureTimeCycleModifiers(Meta);

            EnsureCarGens(Meta);

            EnsureBoxOccluders(Meta);

            EnsureOccludeModels(Meta);

            EnsureContainerLods(Meta);


            #region data block test and old code

            //foreach (var block in Meta.DataBlocks)
            //{
            //    switch (block.StructureNameHash)
            //    {
            //        case (MetaName)MetaTypeName.STRING:
            //        case (MetaName)MetaTypeName.POINTER:
            //        case (MetaName)MetaTypeName.HASH:
            //        case (MetaName)MetaTypeName.UINT:
            //        case (MetaName)MetaTypeName.VECTOR3: //distant lod lights uses this
            //        case MetaName.CMapData:
            //        case MetaName.CEntityDef:
            //        case MetaName.CTimeCycleModifier: //these sections are handled already
            //        case MetaName.CCarGen:
            //        case MetaName.CLightAttrDef:
            //        case MetaName.CMloInstanceDef:
            //        case MetaName.CExtensionDefDoor:
            //        case MetaName.CExtensionDefLightEffect:
            //        case MetaName.CExtensionDefSpawnPointOverride:
            //        case MetaName.rage__fwGrassInstanceListDef: //grass instance buffer
            //        case MetaName.rage__fwGrassInstanceListDef__InstanceData: //grass instance buffer data
            //            break;
            //        case MetaName.PhVerletClothCustomBounds: //these sections still todo..
            //        case MetaName.SectionUNKNOWN1:
            //        case MetaName.SectionUNKNOWN5://occlusion vertex data container
            //        case MetaName.SectionUNKNOWN7://occlusion related?
            //            break;
            //        case (MetaName)17: //vertex data - occlusion related - SectionUNKNOWN5
            //            break;
            //        case (MetaName)33: //what is this? maybe lodlights related
            //            break;
            //        default:
            //            break;
            //    }
            //}



            //MetaTypes.ParseMetaData(Meta);

            //string shortname = resentry.Name.Substring(0, resentry.Name.LastIndexOf('.'));
            //uint namehash = JenkHash.GenHash(shortname);





            //CLightAttrDefs = MetaTypes.GetTypedDataArray<CLightAttrDef>(Meta, MetaName.CLightAttrDef);
            //if (CLightAttrDefs != null)
            //{ }


            //var unk5s = MetaTypes.GetTypedDataArray<SectionUNKNOWN5>(Meta, MetaName.SectionUNKNOWN5);
            //if (unk5s != null) //used in occlusion ymaps
            //{
            //    foreach (var unk5 in unk5s)
            //    {
            //        if ((unk5.verts.Ptr > 0) && (unk5.verts.Ptr <= (ulong)Meta.DataBlocks.Length))
            //        {
            //            var indicesoffset = unk5.numVertsInBytes;
            //            var datablock = Meta.DataBlocks[((int)unk5.verts.Ptr) - 1];
            //            if (datablock != null)
            //            { }//vertex data... occlusion mesh?
            //        }
            //    }
            //}

            //var unk7s = MetaTypes.GetTypedDataArray<SectionUNKNOWN7>(Meta, MetaName.SectionUNKNOWN7);
            //if (unk7s != null)
            //{ } //used in occlusion ymaps

            //var unk10s = MetaTypes.GetTypedDataArray<SectionUNKNOWN10>(Meta, MetaName.SectionUNKNOWN10);
            //if (unk10s != null)
            //{ } //entity pointer array.. 

            //CDoors = MetaTypes.GetTypedDataArray<CExtensionDefDoor>(Meta, MetaName.CExtensionDefDoor);
            //if (CDoors != null)
            //{ } //needs work - doors can be different types? not enough bytes for one

            //CExtLightEffects = MetaTypes.GetTypedDataArray<CExtensionDefLightEffect>(Meta, MetaName.CExtensionDefLightEffect);
            //if (CExtLightEffects != null)
            //{ }

            //CSpawnOverrides = MetaTypes.GetTypedDataArray<CExtensionDefSpawnPointOverride>(Meta, MetaName.CExtensionDefSpawnPointOverride);
            //if (CSpawnOverrides != null)
            //{ }

            #endregion

//#if !DEBUG
//            Meta = null; //this object is required for XML conversion! can't just let go of it here
//#endif
        }



        private void NonMetaLoad(byte[] data)
        {
            //non meta not supported yet! but see what's in there...
            MemoryStream ms = new MemoryStream(data);
            if (RbfFile.IsRBF(ms))
            {
                Rbf = new RbfFile();
                Rbf.Load(ms);
            }
            else if (PsoFile.IsPSO(ms))
            {
                Pso = new PsoFile();
                Pso.Load(ms);
                //PsoTypes.EnsurePsoTypes(Pso);
            }
            else
            {
            }

        }



        private void EnsureEntities(Meta Meta)
        {
            //CMloInstanceDefs = MetaTypes.ConvertDataArray<CMloInstanceDef>(Meta, MetaName.CMloInstanceDef, CMapData.entities);
            CMloInstanceDefs = MetaTypes.GetTypedDataArray<CMloInstanceDef>(Meta, MetaName.CMloInstanceDef);
            if (CMloInstanceDefs != null)
            { }

            var eptrs = MetaTypes.GetPointerArray(Meta, _CMapData.entities);
            //CEntityDefs = MetaTypes.ConvertDataArray<CEntityDef>(Meta, MetaName.CEntityDef, CMapData.entities);
            CEntityDefs = MetaTypes.GetTypedDataArray<CEntityDef>(Meta, MetaName.CEntityDef);
            if (CEntityDefs != null)
            { }




            int instcount = 0;
            if (CEntityDefs != null) instcount += CEntityDefs.Length;
            if (CMloInstanceDefs != null) instcount += CMloInstanceDefs.Length;

            if (instcount > 0)
            {

                //build the entity hierarchy.
                List<YmapEntityDef> roots = new List<YmapEntityDef>(instcount);
                List<YmapEntityDef> alldefs = new List<YmapEntityDef>(instcount);
                List<YmapEntityDef> mlodefs = null;

                if (CEntityDefs != null)
                {
                    for (int i = 0; i < CEntityDefs.Length; i++)
                    {
                        YmapEntityDef d = new YmapEntityDef(this, i, ref CEntityDefs[i]);
                        alldefs.Add(d);
                    }
                }
                if (CMloInstanceDefs != null)
                {
                    mlodefs = new List<YmapEntityDef>();
                    for (int i = 0; i < CMloInstanceDefs.Length; i++)
                    {
                        YmapEntityDef d = new YmapEntityDef(this, i, ref CMloInstanceDefs[i]);
                        uint[] defentsets = MetaTypes.GetUintArray(Meta, CMloInstanceDefs[i].defaultEntitySets);
                        if (d.MloInstance != null)
                        {
                            d.MloInstance.defaultEntitySets = defentsets;
                        }
                        alldefs.Add(d);
                        mlodefs.Add(d);
                    }
                }


                for (int i = 0; i < alldefs.Count; i++)
                {
                    YmapEntityDef d = alldefs[i];
                    int pind = d._CEntityDef.parentIndex;
                    bool isroot = false;
                    if ((pind < 0) || d.LodInParentYmap)
                    {
                        isroot = true;
                    }
                    else
                    {
                        YmapEntityDef p = alldefs[pind];
                        if ((p._CEntityDef.lodLevel <= d._CEntityDef.lodLevel) ||
                            ((p._CEntityDef.lodLevel == rage__eLodType.LODTYPES_DEPTH_ORPHANHD) &&
                             (d._CEntityDef.lodLevel != rage__eLodType.LODTYPES_DEPTH_ORPHANHD)))
                        {
                            isroot = true;
                            p = null;
                        }
                    }

                    if (isroot)
                    {
                        roots.Add(d);
                    }
                    else
                    {
                        YmapEntityDef p = alldefs[pind];
                        p.AddChild(d);
                    }
                }
                for (int i = 0; i < alldefs.Count; i++)
                {
                    alldefs[i].ChildListToArray();
                }

                AllEntities = alldefs.ToArray();
                RootEntities = roots.ToArray();
                if (mlodefs != null)
                {
                    MloEntities = mlodefs.ToArray();
                }


                foreach (var ent in AllEntities)
                {
                    ent.Extensions = MetaTypes.GetExtensions(Meta, ent._CEntityDef.extensions);
                }
            }

        }

        private void EnsureInstances(Meta Meta)
        {
            if (_CMapData.instancedData.GrassInstanceList.Count1 != 0)
            {
                rage__fwGrassInstanceListDef[] batches = MetaTypes.ConvertDataArray<rage__fwGrassInstanceListDef>(Meta, MetaName.rage__fwGrassInstanceListDef, _CMapData.instancedData.GrassInstanceList);
                YmapGrassInstanceBatch[] gbatches = new YmapGrassInstanceBatch[batches.Length];
                for (int i = 0; i < batches.Length; i++)
                {
                    var batch = batches[i];
                    rage__fwGrassInstanceListDef__InstanceData[] instdatas = MetaTypes.ConvertDataArray<rage__fwGrassInstanceListDef__InstanceData>(Meta, MetaName.rage__fwGrassInstanceListDef__InstanceData, batch.InstanceList);
                    YmapGrassInstanceBatch gbatch = new YmapGrassInstanceBatch();
                    gbatch.Ymap = this;
                    gbatch.Batch = batch;
                    gbatch.Instances = instdatas;
                    gbatch.Position = (batch.BatchAABB.min.XYZ() + batch.BatchAABB.max.XYZ()) * 0.5f;
                    gbatch.Radius = (batch.BatchAABB.max.XYZ() - gbatch.Position).Length();
                    gbatch.AABBMin = (batch.BatchAABB.min.XYZ());
                    gbatch.AABBMax = (batch.BatchAABB.max.XYZ());
                    gbatches[i] = gbatch;
                }
                GrassInstanceBatches = gbatches;
            }
            if (_CMapData.instancedData.PropInstanceList.Count1 != 0)
            {
            }
        }

        private void EnsureLODLights(Meta Meta)
        {
            if (_CMapData.LODLightsSOA.direction.Count1 != 0)
            {
                var soa = _CMapData.LODLightsSOA;
                LODLights = new YmapLODLights();
                LODLights.Ymap = this;
                LODLights.CLODLight = soa;
                LODLights.direction = MetaTypes.ConvertDataArray<MetaVECTOR3>(Meta, MetaName.FloatXYZ, soa.direction);
                LODLights.falloff = MetaTypes.GetFloatArray(Meta, soa.falloff);
                LODLights.falloffExponent = MetaTypes.GetFloatArray(Meta, soa.falloffExponent);
                LODLights.timeAndStateFlags = MetaTypes.GetUintArray(Meta, soa.timeAndStateFlags);
                LODLights.hash = MetaTypes.GetUintArray(Meta, soa.hash);
                LODLights.coneInnerAngle = MetaTypes.GetByteArray(Meta, soa.coneInnerAngle);
                LODLights.coneOuterAngleOrCapExt = MetaTypes.GetByteArray(Meta, soa.coneOuterAngleOrCapExt);
                LODLights.coronaIntensity = MetaTypes.GetByteArray(Meta, soa.coronaIntensity);
                LODLights.CalcBB();
            }
        }

        private void EnsureDistantLODLights(Meta Meta)
        {
            if (_CMapData.DistantLODLightsSOA.position.Count1 != 0)
            {
                var soa = _CMapData.DistantLODLightsSOA;
                DistantLODLights = new YmapDistantLODLights();
                DistantLODLights.Ymap = this;
                DistantLODLights.CDistantLODLight = soa;
                DistantLODLights.colours = MetaTypes.GetUintArray(Meta, soa.RGBI);
                DistantLODLights.positions = MetaTypes.ConvertDataArray<MetaVECTOR3>(Meta, MetaName.FloatXYZ, soa.position);
                DistantLODLights.CalcBB();
            }
        }

        private void EnsureTimeCycleModifiers(Meta Meta)
        {
            CTimeCycleModifiers = MetaTypes.ConvertDataArray<CTimeCycleModifier>(Meta, MetaName.CTimeCycleModifier, _CMapData.timeCycleModifiers);
            if (CTimeCycleModifiers != null)
            {
                TimeCycleModifiers = new YmapTimeCycleModifier[CTimeCycleModifiers.Length];
                for (int i = 0; i < CTimeCycleModifiers.Length; i++)
                {
                    YmapTimeCycleModifier tcm = new YmapTimeCycleModifier();
                    tcm.Ymap = this;
                    tcm.CTimeCycleModifier = CTimeCycleModifiers[i];
                    tcm.BBMin = tcm.CTimeCycleModifier.minExtents;
                    tcm.BBMax = tcm.CTimeCycleModifier.maxExtents;
                    TimeCycleModifiers[i] = tcm;
                }
            }
        }

        private void EnsureCarGens(Meta Meta)
        {

            CCarGens = MetaTypes.ConvertDataArray<CCarGen>(Meta, MetaName.CCarGen, _CMapData.carGenerators);
            if (CCarGens != null)
            {
                //string str = MetaTypes.GetTypesInitString(resentry, Meta); //to generate structinfos and enuminfos
                CarGenerators = new YmapCarGen[CCarGens.Length];
                for (int i = 0; i < CCarGens.Length; i++)
                {
                    CarGenerators[i] = new YmapCarGen(this, CCarGens[i]);
                }
            }
        }

        private void EnsureBoxOccluders(Meta meta)
        {
            CBoxOccluders = MetaTypes.ConvertDataArray<BoxOccluder>(Meta, MetaName.BoxOccluder, _CMapData.boxOccluders);
            if (CBoxOccluders != null)
            {
                BoxOccluders = new YmapBoxOccluder[CBoxOccluders.Length];
                for (int i = 0; i < CBoxOccluders.Length; i++)
                {
                    BoxOccluders[i] = new YmapBoxOccluder(this, CBoxOccluders[i]);
                    BoxOccluders[i].Index = i;
                }
            }
        }

        private void EnsureOccludeModels(Meta meta)
        {
            COccludeModels = MetaTypes.ConvertDataArray<OccludeModel>(Meta, MetaName.OccludeModel, _CMapData.occludeModels);
            if (COccludeModels != null)
            {
                OccludeModels = new YmapOccludeModel[COccludeModels.Length];
                for (int i = 0; i < COccludeModels.Length; i++)
                {
                    OccludeModels[i] = new YmapOccludeModel(this, COccludeModels[i]);
                    OccludeModels[i].Index = i;
                    OccludeModels[i].Load(Meta);

                }
            }
        }

        private void EnsureContainerLods(Meta meta)
        {

            //TODO: containerLods
            if (_CMapData.containerLods.Count1 > 0)
            {
                //string str = MetaTypes.GetTypesInitString(Meta); //to generate structinfos and enuminfos


            }

        }


        public void BuildCEntityDefs()
        {
            //recreates the CEntityDefs and CMloInstanceDefs arrays from AllEntities.
            //TODO: save entity extensions!!?

            CEntityDefs = null;
            CMloInstanceDefs = null;
            if (AllEntities == null)
            {
                return;
            }


            List<CEntityDef> centdefs = new List<CEntityDef>();
            List<CMloInstanceDef> cmlodefs = new List<CMloInstanceDef>();

            for (int i = 0; i < AllEntities.Length; i++)
            {
                var ent = AllEntities[i];
                if (ent.MloInstance != null)
                {
                    cmlodefs.Add(ent.MloInstance.Instance);
                }
                else
                {
                    centdefs.Add(ent._CEntityDef);
                }
            }

            if (centdefs.Count > 0)
            {
                CEntityDefs = centdefs.ToArray();
            }
            if (cmlodefs.Count > 0)
            {
                CMloInstanceDefs = cmlodefs.ToArray();
            }
        }
        public void BuildCCarGens()
        {
            //recreates the CCarGens array from CarGenerators.
            if (CarGenerators == null)
            {
                CCarGens = null;
                return;
            }

            int count = CarGenerators.Length;
            CCarGens = new CCarGen[count];
            for (int i = 0; i < count; i++)
            {
                CCarGens[i] = CarGenerators[i].CCarGen;
            }
        }
        public void BuildInstances()
        {
            if (GrassInstanceBatches == null)
            {
                return;
            }

            if (PropInstanceBatches == null)
            { }

            int count = GrassInstanceBatches.Length;
            for (int i = 0; i < count; i++)
            {
                var g = GrassInstanceBatches[i];
                var b = g.Batch;

                var aabb = new rage__spdAABB();
                aabb.min = new Vector4(g.AABBMin, 0);
                aabb.max = new Vector4(g.AABBMax, 0);

                b.BatchAABB = aabb;

                GrassInstanceBatches[i].Batch = b;
            }
        }
        public void BuildLodLights()
        {
            if (LODLights == null) return;
            LODLights.RebuildFromLodLights();
        }
        public void BuildDistantLodLights()
        {
            //how to rebuild these here? the LODlights array is on the child ymap...
            //for now, they are being updated as they are edited in project window
        }
        public void BuildBoxOccluders()
        {
            if (BoxOccluders == null) return;
            if (BoxOccluders.Length == 0) return;

            var boxes = new BoxOccluder[BoxOccluders.Length];
            for (int i = 0; i < BoxOccluders.Length; i++)
            {
                var box = BoxOccluders[i];
                box.UpdateBoxStruct();
                boxes[i] = box._Box;
            }

            CBoxOccluders = boxes;

        }
        public void BuildOccludeModels()
        {
            if (OccludeModels == null) return;
            if (OccludeModels.Length == 0) return;
            //nothing to do here, has to be done later due to embedded data
        }

        public byte[] Save()
        {
            //direct save to a raw, compressed ymap file (openIV-compatible format)


            //since Ymap object contents have been modified, need to recreate the arrays which are what is saved.
            BuildCEntityDefs(); //technically this isn't required anymore since the CEntityDefs is no longer used for saving.
            BuildCCarGens();
            BuildInstances();
            BuildLodLights();
            BuildDistantLodLights();
            BuildBoxOccluders();
            BuildOccludeModels();

            //TODO:
            //BuildTimecycleModifiers(); //already being saved - update them..
            //BuildContainerLods();



            MetaBuilder mb = new MetaBuilder();


            var mdb = mb.EnsureBlock(MetaName.CMapData);

            CMapData mapdata = CMapData;



            if ((AllEntities != null) && (AllEntities.Length > 0))
            {
                for (int i = 0; i < AllEntities.Length; i++)
                {
                    var ent = AllEntities[i]; //save the extensions first..
                    ent._CEntityDef.extensions = mb.AddWrapperArrayPtr(ent.Extensions);
                }

                MetaPOINTER[] ptrs = new MetaPOINTER[AllEntities.Length];
                for (int i = 0; i < AllEntities.Length; i++)
                {
                    var ent = AllEntities[i];
                    if (ent.MloInstance != null)
                    {
                        ent.MloInstance.UpdateDefaultEntitySets();

                        ent.MloInstance._Instance.CEntityDef = ent.CEntityDef; //overwrite with all the updated values..
                        ent.MloInstance._Instance.defaultEntitySets = mb.AddUintArrayPtr(ent.MloInstance.defaultEntitySets);

                        ptrs[i] = mb.AddItemPtr(MetaName.CMloInstanceDef, ent.MloInstance.Instance);
                    }
                    else
                    {
                        ptrs[i] = mb.AddItemPtr(MetaName.CEntityDef, ent.CEntityDef);
                    }
                }
                mapdata.entities = mb.AddPointerArray(ptrs);
            }
            else
            {
                mapdata.entities = new Array_StructurePointer();
            }

            mapdata.timeCycleModifiers = mb.AddItemArrayPtr(MetaName.CTimeCycleModifier, CTimeCycleModifiers);

            mapdata.physicsDictionaries = mb.AddHashArrayPtr(physicsDictionaries);

            mapdata.carGenerators = mb.AddItemArrayPtr(MetaName.CCarGen, CCarGens);



            //clear everything else out for now - TODO: fix
            if (mapdata.containerLods.Count1 != 0) LogSaveWarning("containerLods were not saved. (TODO!)");
            if (mapdata.instancedData.PropInstanceList.Count1 != 0) LogSaveWarning("instancedData.PropInstanceList was not saved. (TODO!)");
            mapdata.containerLods = new Array_Structure();

            if ((GrassInstanceBatches != null) && (GrassInstanceBatches.Length > 0))
            {
                var instancedData = new rage__fwInstancedMapData();
                rage__fwGrassInstanceListDef[] batches = new rage__fwGrassInstanceListDef[GrassInstanceBatches.Length];
                for (int i = 0; i < GrassInstanceBatches.Length; i++)
                {
                    var batch = GrassInstanceBatches[i];

                    if (batch != null)
                    {
                        var b = batch.Batch;
                        b.InstanceList = mb.AddItemArrayPtr(MetaName.rage__fwGrassInstanceListDef__InstanceData, batch.Instances);
                        batches[i] = b;
                    }
                }

                instancedData.GrassInstanceList = mb.AddItemArrayPtr(MetaName.rage__fwGrassInstanceListDef, batches);
                mapdata.instancedData = instancedData;
            }
            else
            {
                mapdata.instancedData = new rage__fwInstancedMapData();
            }

            if ((LODLights != null) && (LODLights.direction != null))
            {
                var soa = new CLODLight();
                soa.direction = mb.AddItemArrayPtr(MetaName.FloatXYZ, LODLights.direction);
                soa.falloff = mb.AddFloatArrayPtr(LODLights.falloff);
                soa.falloffExponent = mb.AddFloatArrayPtr(LODLights.falloffExponent);
                soa.timeAndStateFlags = mb.AddUintArrayPtr(LODLights.timeAndStateFlags);
                soa.hash = mb.AddUintArrayPtr(LODLights.hash);
                soa.coneInnerAngle = mb.AddByteArrayPtr(LODLights.coneInnerAngle);
                soa.coneOuterAngleOrCapExt = mb.AddByteArrayPtr(LODLights.coneOuterAngleOrCapExt);
                soa.coronaIntensity = mb.AddByteArrayPtr(LODLights.coronaIntensity);
                mapdata.LODLightsSOA = soa;
            }
            else
            {
                mapdata.LODLightsSOA = new CLODLight();
            }
            if ((DistantLODLights != null) && (DistantLODLights.positions != null))
            {
                var soa = DistantLODLights.CDistantLODLight;//to copy base vars
                soa.position = mb.AddItemArrayPtr(MetaName.FloatXYZ, DistantLODLights.positions);
                soa.RGBI = mb.AddUintArrayPtr(DistantLODLights.colours);
                mapdata.DistantLODLightsSOA = soa;
            }
            else
            {
                mapdata.DistantLODLightsSOA = new CDistantLODLight();
            }

            if ((CBoxOccluders != null) && (CBoxOccluders.Length > 0))
            {
                mapdata.boxOccluders = mb.AddItemArrayPtr(MetaName.BoxOccluder, CBoxOccluders);
            }
            else
            {
                mapdata.boxOccluders = new Array_Structure();
            }
            if ((OccludeModels != null) && (OccludeModels.Length > 0))
            {
                COccludeModels = new OccludeModel[OccludeModels.Length];
                for (int i = 0; i < OccludeModels.Length; i++)
                {
                    var model = OccludeModels[i];
                    model.BuildVertices();
                    model.BuildData();
                    var cocc = model._OccludeModel;
                    cocc.verts = mb.AddDataBlockPtr(model.Data, (MetaName)MetaStructureEntryDataType.UnsignedByte);//17
                    COccludeModels[i] = cocc;
                }
                mapdata.occludeModels = mb.AddItemArrayPtr(MetaName.OccludeModel, COccludeModels);
            }
            else
            {
                mapdata.occludeModels = new Array_Structure();
            }


            var block = new CBlockDesc();
            block.name = mb.AddStringPtr(Path.GetFileNameWithoutExtension(Name));
            block.exportedBy = mb.AddStringPtr("CodeWalker");
            block.time = mb.AddStringPtr(DateTime.UtcNow.ToString("dd MMMM yyyy HH:mm"));

            mapdata.block = block;


            string name = Path.GetFileNameWithoutExtension(Name);
            uint nameHash = JenkHash.GenHash(name);
            mapdata.name = new MetaHash(nameHash);//make sure name is upto date...


            mb.AddItem(MetaName.CMapData, mapdata);



            //make sure all the relevant structure and enum infos are present.
            if ((GrassInstanceBatches != null) && (GrassInstanceBatches.Length > 0))
            {
                mb.AddStructureInfo(MetaName.rage__spdAABB);
                mb.AddStructureInfo(MetaName.rage__fwGrassInstanceListDef__InstanceData);
                mb.AddStructureInfo(MetaName.rage__fwGrassInstanceListDef);
            }
            mb.AddStructureInfo(MetaName.rage__fwInstancedMapData);
            mb.AddStructureInfo(MetaName.CLODLight);
            mb.AddStructureInfo(MetaName.CDistantLODLight);
            mb.AddStructureInfo(MetaName.CBlockDesc);
            mb.AddStructureInfo(MetaName.CMapData);
            if ((AllEntities != null) && (AllEntities.Length > 0))
            {
                mb.AddStructureInfo(MetaName.CEntityDef);
                mb.AddStructureInfo(MetaName.CMloInstanceDef);
                mb.AddEnumInfo(MetaName.rage__eLodType); //LODTYPES_
                mb.AddEnumInfo(MetaName.rage__ePriorityLevel);  //PRI_
            }
            if ((CTimeCycleModifiers != null) && (CTimeCycleModifiers.Length > 0))
            {
                mb.AddStructureInfo(MetaName.CTimeCycleModifier);
            }
            if ((CCarGens != null) && (CCarGens.Length > 0))
            {
                mb.AddStructureInfo(MetaName.CCarGen);
            }
            if ((LODLights != null) && (LODLights.direction != null))
            {
                mb.AddStructureInfo(MetaName.FloatXYZ);
            }
            if ((DistantLODLights != null) && (DistantLODLights.positions != null))
            {
                mb.AddStructureInfo(MetaName.FloatXYZ);
            }
            if ((CBoxOccluders != null) && (CBoxOccluders.Length > 0))
            {
                mb.AddStructureInfo(MetaName.BoxOccluder);
            }
            if ((COccludeModels != null) && (COccludeModels.Length > 0))
            {
                mb.AddStructureInfo(MetaName.OccludeModel);
            }


            Meta meta = mb.GetMeta();

            byte[] data = ResourceBuilder.Build(meta, 2); //ymap is version 2...


            return data;
        }

        private void LogSaveWarning(string w)
        {
            if (SaveWarnings == null) SaveWarnings = new List<string>();
            SaveWarnings.Add(w);
        }




        public void EnsureChildYmaps(GameFileCache gfc)
        {
            if (ChildYmaps == null)
            {
                //no children here... look for child ymap....
                var node = gfc.GetMapNode(RpfFileEntry.ShortNameHash);
                if ((node != null) && (node.Children != null) && (node.Children.Length > 0))
                {
                    ChildYmaps = new YmapFile[node.Children.Length];
                    for (int i = 0; i < ChildYmaps.Length; i++)
                    {
                        var chash = node.Children[i].Name;
                        ChildYmaps[i] = gfc.GetYmap(chash);
                        if (ChildYmaps[i] == null)
                        {
                            //couldn't find child ymap..
                        }
                    }
                }
            }


            bool needupd = false;
            if (ChildYmaps != null)
            {
                for (int i = 0; i < ChildYmaps.Length; i++)
                {
                    var cmap = ChildYmaps[i];
                    if (cmap == null) continue; //nothing here..
                    if (!cmap.Loaded)
                    {
                        //ChildYmaps[i] = gfc.GetYmap(cmap.Hash); //incase no load was requested.
                        cmap = gfc.GetYmap(cmap.Key.Hash);
                        ChildYmaps[i] = cmap;
                    }
                    if ((cmap.Loaded) && (!cmap.MergedWithParent))
                    {
                        needupd = true;
                    }
                }
            }

            if ((ChildYmaps != null) && needupd)
            {
                List<YmapEntityDef> newroots = new List<YmapEntityDef>(RootEntities);
                for (int i = 0; i < ChildYmaps.Length; i++)
                {
                    var cmap = ChildYmaps[i];
                    if (cmap == null) continue; //nothing here..
                    //cmap.EnsureChildYmaps();
                    if ((cmap.Loaded) && (!cmap.MergedWithParent))
                    {
                        cmap.MergedWithParent = true;
                        if (cmap.RootEntities != null)
                        {
                            foreach (var rcent in cmap.RootEntities)
                            {
                                int pind = rcent._CEntityDef.parentIndex;
                                if (pind < 0)
                                {
                                    if (rcent._CEntityDef.lodLevel != rage__eLodType.LODTYPES_DEPTH_ORPHANHD)
                                    {
                                    }
                                    //pind = 0;
                                }
                                if ((pind >= 0) && (pind < AllEntities.Length) && rcent.LodInParentYmap)
                                {
                                    var pentity = AllEntities[pind];
                                    pentity.AddChild(rcent);
                                }
                                else
                                {
                                    //TODO: fix this!!
                                    //newroots.Add(rcent); //not sure this is the right approach.
                                    //////rcent.Owner = this;
                                }
                            }
                        }
                    }
                }
                if (AllEntities != null)
                {
                    for (int i = 0; i < AllEntities.Length; i++)
                    {
                        AllEntities[i].ChildListToMergedArray();
                    }
                }

                RootEntities = newroots.ToArray();
            }


        }


        public void ConnectToParent(YmapFile pymap)
        {
            Parent = pymap;
            if (RootEntities != null) //parent changed or first set, make sure to link entities hierarchy
            {
                for (int i = 0; i < RootEntities.Length; i++)
                {
                    var ent = RootEntities[i];
                    int pind = ent._CEntityDef.parentIndex;
                    if (pind >= 0) //connect root entities to parents if they have them..
                    {
                        YmapEntityDef p = null;
                        if ((pymap != null) && (pymap.AllEntities != null))
                        {
                            if ((pind < pymap.AllEntities.Length))
                            {
                                p = pymap.AllEntities[pind];
                                ent.Parent = p;
                                ent.ParentName = p._CEntityDef.archetypeName;
                            }
                        }
                        else
                        { }//should only happen if parent ymap not loaded yet...
                    }
                }
            }
            if (LODLights != null)
            {
                if (Parent?.DistantLODLights != null)
                {
                    LODLights.Init(Parent.DistantLODLights);
                }
                else
                { }
            }
        }





        public void AddEntity(YmapEntityDef ent)
        {
            //used by the editor to add to the ymap.

            List<YmapEntityDef> allents = new List<YmapEntityDef>();
            if (AllEntities != null) allents.AddRange(AllEntities);
            ent.Index = allents.Count;
            ent.Ymap = this;
            allents.Add(ent);
            AllEntities = allents.ToArray();


            if ((ent.Parent == null) || (ent.Parent.Ymap != this))
            {
                //root entity, add to roots.

                List<YmapEntityDef> rootents = new List<YmapEntityDef>();
                if (RootEntities != null) rootents.AddRange(RootEntities);
                rootents.Add(ent);
                RootEntities = rootents.ToArray();
            }

            HasChanged = true;
            LodManagerUpdate = true;
        }

        public bool RemoveEntity(YmapEntityDef ent)
        {
            //used by the editor to remove from the ymap.
            if (ent == null) return false;

            var res = true;

            int idx = ent.Index;
            List<YmapEntityDef> newAllEntities = new List<YmapEntityDef>();
            List<YmapEntityDef> newRootEntities = new List<YmapEntityDef>();

            for (int i = 0; i < AllEntities.Length; i++)
            {
                var oent = AllEntities[i];
                oent.Index = newAllEntities.Count;
                if (oent != ent) newAllEntities.Add(oent);
                else if (i != idx)
                {
                    res = false; //indexes didn't match.. this shouldn't happen!
                }
            }
            for (int i = 0; i < RootEntities.Length; i++)
            {
                var oent = RootEntities[i];
                if (oent != ent) newRootEntities.Add(oent);
            }

            if ((AllEntities.Length == newAllEntities.Count) || (RootEntities.Length == newRootEntities.Count))
            {
                res = false;
            }

            LodManagerOldEntities = AllEntities;
            AllEntities = newAllEntities.ToArray();
            RootEntities = newRootEntities.ToArray();

            HasChanged = true;
            LodManagerUpdate = true;

            return res;
        }


        public void AddCarGen(YmapCarGen cargen)
        {
            List<YmapCarGen> cargens = new List<YmapCarGen>();
            if (CarGenerators != null) cargens.AddRange(CarGenerators);
            cargen.Ymap = this;
            cargens.Add(cargen);
            CarGenerators = cargens.ToArray();

            HasChanged = true;
        }

        public bool RemoveCarGen(YmapCarGen cargen)
        {
            if (cargen == null) return false;

            List<YmapCarGen> newcargens = new List<YmapCarGen>();

            if (CarGenerators != null)
            {
                for (int i = 0; i < CarGenerators.Length; i++)
                {
                    var cg = CarGenerators[i];
                    if (cg != cargen)
                    {
                        newcargens.Add(cg);
                    }
                }
                if (newcargens.Count == CarGenerators.Length)
                {
                    return false; //nothing removed... wasn't present?
                }
            }


            CarGenerators = newcargens.ToArray();

            HasChanged = true;

            return true;
        }


        public void AddLodLight(YmapLODLight lodlight)
        {
            if (LODLights == null)
            {
                LODLights = new YmapLODLights();
                LODLights.Ymap = this;
            }
            List<YmapLODLight> lodlights = new List<YmapLODLight>();
            if (LODLights?.LodLights != null) lodlights.AddRange(LODLights.LodLights);
            lodlight.LodLights = this.LODLights;
            lodlight.Index = lodlights.Count;
            lodlights.Add(lodlight);
            LODLights.LodLights = lodlights.ToArray();

            HasChanged = true;

            if (Parent?.DistantLODLights != null)
            {
                Parent.DistantLODLights.RebuildFromLodLights(LODLights.LodLights);
                Parent.HasChanged = true;
            }
        }

        public bool RemoveLodLight(YmapLODLight lodlight)
        {
            if (lodlight == null) return false;

            List<YmapLODLight> newlodlights = new List<YmapLODLight>();

            var lodlights = LODLights?.LodLights;
            if (lodlights != null)
            {
                for (int i = 0; i < lodlights.Length; i++)
                {
                    var ll = lodlights[i];
                    if (ll != lodlight)
                    {
                        newlodlights.Add(ll);
                    }
                }
                if (newlodlights.Count == lodlights.Length)
                {
                    return false; //nothing removed... wasn't present?
                }
            }

            for (int i = 0; i < newlodlights.Count; i++)
            {
                newlodlights[i].Index = i;
            }

            LODLights.LodLights = newlodlights.ToArray();

            HasChanged = true;

            if (Parent?.DistantLODLights != null)
            {
                Parent.DistantLODLights.RebuildFromLodLights(LODLights.LodLights);
                Parent.HasChanged = true;
            }

            return true;
        }


        public void AddBoxOccluder(YmapBoxOccluder box)
        {
            if (box == null) return;
            var boxes = new List<YmapBoxOccluder>();
            if (BoxOccluders != null) boxes.AddRange(BoxOccluders);
            box.Ymap = this;
            box.Index = boxes.Count;
            boxes.Add(box);
            BoxOccluders = boxes.ToArray();

            HasChanged = true;
        }

        public bool RemoveBoxOccluder(YmapBoxOccluder box)
        {
            if (box == null) return false;
            var newboxes = new List<YmapBoxOccluder>();
            if (BoxOccluders != null)
            {
                foreach (var oldbox in BoxOccluders)
                {
                    if (oldbox != box)
                    {
                        oldbox.Index = newboxes.Count;
                        newboxes.Add(oldbox);
                    }
                }
                if (newboxes.Count == BoxOccluders.Length)
                {
                    return false;//nothing removed... wasn't present?
                }
            }

            BoxOccluders = newboxes.ToArray();

            HasChanged = true;

            return true;
        }


        public void AddOccludeModel(YmapOccludeModel model)
        {
            if (model == null) return;
            var models = new List<YmapOccludeModel>();
            if (OccludeModels != null) models.AddRange(OccludeModels);
            model.Ymap = this;
            models.Add(model);
            OccludeModels = models.ToArray();

            HasChanged = true;
        }

        public bool RemoveOccludeModel(YmapOccludeModel model)
        {
            if (model == null) return false;
            var newmodels = new List<YmapOccludeModel>();
            if (OccludeModels != null)
            {
                foreach (var oldmodel in OccludeModels)
                {
                    if (oldmodel != model)
                    {
                        oldmodel.Index = newmodels.Count;
                        newmodels.Add(oldmodel);
                    }
                }
                if (newmodels.Count == OccludeModels.Length)
                {
                    return false;//nothing removed... wasn't present?
                }
            }

            OccludeModels = newmodels.ToArray();

            HasChanged = true;

            return true;
        }


        public void AddOccludeModelTriangle(YmapOccludeModelTriangle tri)
        {
            if (tri == null) return;
            if (tri.Model == null) return;

            var tris = tri.Model.Triangles.ToList();
            tri.Index = tris.Count;
            tris.Add(tri);
            tri.Model.Triangles = tris.ToArray();

            //tri.Model.BuildBVH();
            //...

            HasChanged = true;
        }

        public bool RemoveOccludeModelTriangle(YmapOccludeModelTriangle tri)
        {
            if (tri == null) return false;
            if (tri.Model == null) return false;

            var newtris = new List<YmapOccludeModelTriangle>();
            if (tri.Model.Triangles != null)
            {
                foreach (var oldtri in tri.Model.Triangles)
                {
                    if (oldtri != tri)
                    {
                        oldtri.Index = newtris.Count;
                        newtris.Add(oldtri);
                    }
                }
            }
            tri.Model.Triangles = newtris.ToArray();
            //tri.Model.BuildBVH();
            //...

            HasChanged = true;

            return true;
        }


        public void AddGrassBatch(YmapGrassInstanceBatch newbatch)
        {
            List<YmapGrassInstanceBatch> batches = new List<YmapGrassInstanceBatch>();
            if (GrassInstanceBatches != null) batches.AddRange(GrassInstanceBatches);
            newbatch.Ymap = this;
            batches.Add(newbatch);
            GrassInstanceBatches = batches.ToArray();

            HasChanged = true;
            UpdateGrassPhysDict(true);
        }

        public bool RemoveGrassBatch(YmapGrassInstanceBatch batch)
        {
            if (batch == null) return false;

            List<YmapGrassInstanceBatch> batches = new List<YmapGrassInstanceBatch>();

            if (GrassInstanceBatches != null)
            {
                for (int i = 0; i < GrassInstanceBatches.Length; i++)
                {
                    var gb = GrassInstanceBatches[i];
                    if (gb != batch)
                    {
                        batches.Add(gb);
                    }
                }
                if (batches.Count == GrassInstanceBatches.Length)
                {
                    return false; //nothing removed... wasn't present?
                }
            }

            if (batches.Count <= 0)
            {
                UpdateGrassPhysDict(false);
            }

            GrassInstanceBatches = batches.ToArray();

            HasChanged = true;

            return true;
        }




        public void SetName(string newname)
        {
            var newnamel = newname.ToLowerInvariant();
            var newnamex = newname + ".ymap";
            var newnamexl = newname.ToLowerInvariant();
            var newhash = JenkHash.GenHash(newnamel);
            JenkIndex.Ensure(newnamel);
            if (RpfFileEntry != null)
            {
                RpfFileEntry.Name = newnamex;
                RpfFileEntry.NameLower = newnamexl;
                RpfFileEntry.NameHash = JenkHash.GenHash(newnamexl);
                RpfFileEntry.ShortNameHash = newhash;
            }
            Name = newnamex;
            _CMapData.name = newhash;
        }
        public void SetFilePath(string filepath)
        {
            FilePath = filepath.ToLowerInvariant();
            var newname = Path.GetFileNameWithoutExtension(filepath);
            SetName(newname);
        }


        public bool CalcFlags()
        {
            uint flags = 0;
            uint contentFlags = 0;

            if (AllEntities != null)
            {
                foreach (var yent in AllEntities)
                {
                    switch (yent._CEntityDef.lodLevel)
                    {
                        case rage__eLodType.LODTYPES_DEPTH_ORPHANHD:
                        case rage__eLodType.LODTYPES_DEPTH_HD:
                            contentFlags = SetBit(contentFlags, 0); //1
                            break;
                        case rage__eLodType.LODTYPES_DEPTH_LOD:
                            contentFlags = SetBit(contentFlags, 1); //2
                            flags = SetBit(flags, 1); //2
                            break;
                        case rage__eLodType.LODTYPES_DEPTH_SLOD1:
                            contentFlags = SetBit(contentFlags, 4); //16
                            flags = SetBit(flags, 1); //2
                            break;
                        case rage__eLodType.LODTYPES_DEPTH_SLOD2:
                        case rage__eLodType.LODTYPES_DEPTH_SLOD3:
                        case rage__eLodType.LODTYPES_DEPTH_SLOD4:
                            contentFlags = SetBit(contentFlags, 2); //4
                            contentFlags = SetBit(contentFlags, 4); //16
                            flags = SetBit(flags, 1); //2
                            break;
                    }
                    if (yent.MloInstance != null)
                    {
                        contentFlags = SetBit(contentFlags, 3); //8  //(interior instance)
                    }
                }
            }

            if ((CMloInstanceDefs != null) && (CMloInstanceDefs.Length > 0))
            {
                contentFlags = SetBit(contentFlags, 3); //8  //(interior instance) //is this still necessary?
            }
            if ((physicsDictionaries != null) && (physicsDictionaries.Length > 0))
            {
                contentFlags = SetBit(contentFlags, 6); //64
            }
            if ((GrassInstanceBatches != null) && (GrassInstanceBatches.Length > 0))
            {
                contentFlags = SetBit(contentFlags, 10); //64
            }
            if ((LODLights != null) && ((LODLights.direction?.Length ?? 0) > 0))
            {
                contentFlags = SetBit(contentFlags, 7); //128
            }
            if ((DistantLODLights != null) && ((DistantLODLights.positions?.Length ?? 0) > 0))
            {
                flags = SetBit(flags, 1); //2
                contentFlags = SetBit(contentFlags, 8); //256
            }
            if ((BoxOccluders != null) || (OccludeModels != null))
            {
                contentFlags = SetBit(contentFlags, 5); //32
            }


            bool change = false;
            if (_CMapData.flags != flags)
            {
                _CMapData.flags = flags;
                change = true;
            }
            if (_CMapData.contentFlags != contentFlags)
            {
                _CMapData.contentFlags = contentFlags;
                change = true;
            }
            return change;
        }

        public bool CalcExtents()
        {
            Vector3 emin = new Vector3(float.MaxValue);
            Vector3 emax = new Vector3(float.MinValue);
            Vector3 smin = new Vector3(float.MaxValue);
            Vector3 smax = new Vector3(float.MinValue);
            Vector3[] c = new Vector3[8];
            Vector3[] s = new Vector3[8];

            if (AllEntities != null)
            {
                for (int i = 0; i < AllEntities.Length; i++)
                {
                    var ent = AllEntities[i];
                    var arch = ent.Archetype;
                    var ori = ent.Orientation;
                    float loddist = ent._CEntityDef.lodDist;

                    Vector3 bbmin = ent.Position - ent.BSRadius; //sphere
                    Vector3 bbmax = ent.Position + ent.BSRadius;
                    Vector3 sbmin = bbmin - loddist;
                    Vector3 sbmax = bbmax + loddist;
                    if (arch != null)
                    {
                        if (loddist <= 0.0f)
                        {
                            loddist = arch.LodDist;
                        }

                        Vector3 abmin = arch.BBMin * ent.Scale; //entity box
                        Vector3 abmax = arch.BBMax * ent.Scale;
                        c[0] = abmin;
                        c[1] = new Vector3(abmin.X, abmin.Y, abmax.Z);
                        c[2] = new Vector3(abmin.X, abmax.Y, abmin.Z);
                        c[3] = new Vector3(abmin.X, abmax.Y, abmax.Z);
                        c[4] = new Vector3(abmax.X, abmin.Y, abmin.Z);
                        c[5] = new Vector3(abmax.X, abmin.Y, abmax.Z);
                        c[6] = new Vector3(abmax.X, abmax.Y, abmin.Z);
                        c[7] = abmax;

                        abmin = arch.BBMin * ent.Scale - loddist; //streaming box
                        abmax = arch.BBMax * ent.Scale + loddist;
                        s[0] = abmin;
                        s[1] = new Vector3(abmin.X, abmin.Y, abmax.Z);
                        s[2] = new Vector3(abmin.X, abmax.Y, abmin.Z);
                        s[3] = new Vector3(abmin.X, abmax.Y, abmax.Z);
                        s[4] = new Vector3(abmax.X, abmin.Y, abmin.Z);
                        s[5] = new Vector3(abmax.X, abmin.Y, abmax.Z);
                        s[6] = new Vector3(abmax.X, abmax.Y, abmin.Z);
                        s[7] = abmax;

                        bbmin = new Vector3(float.MaxValue);
                        bbmax = new Vector3(float.MinValue);
                        sbmin = new Vector3(float.MaxValue);
                        sbmax = new Vector3(float.MinValue);
                        for (int j = 0; j < 8; j++)
                        {
                            Vector3 corn = ori.Multiply(c[j]) + ent.Position;
                            bbmin = Vector3.Min(bbmin, corn);
                            bbmax = Vector3.Max(bbmax, corn);

                            corn = ori.Multiply(s[j]) + ent.Position;
                            sbmin = Vector3.Min(sbmin, corn);
                            sbmax = Vector3.Max(sbmax, corn);
                        }
                    }

                    emin = Vector3.Min(emin, bbmin);
                    emax = Vector3.Max(emax, bbmax);
                    smin = Vector3.Min(smin, sbmin);
                    smax = Vector3.Max(smax, sbmax);
                }
            }

            if (GrassInstanceBatches != null)
            {
                //var lodoffset = Vector3.Zero;// new Vector3(0, 0, 100);  //IDK WHY -neos7  //dexy: i guess it's not completely necessary... //blame neos
                foreach (var batch in GrassInstanceBatches) //thanks to Neos7
                {
                    emin = Vector3.Min(emin, batch.AABBMin);
                    emax = Vector3.Max(emax, batch.AABBMax);
                    smin = Vector3.Min(smin, (batch.AABBMin - batch.Batch.lodDist)); // + lodoffset
                    smax = Vector3.Max(smax, (batch.AABBMax + batch.Batch.lodDist)); // - lodoffset
                }
            }

            if (CarGenerators != null)
            {
                foreach (var cargen in CarGenerators)
                {
                    var len = cargen._CCarGen.perpendicularLength;
                    emin = Vector3.Min(emin, cargen.Position - len);
                    emax = Vector3.Max(emax, cargen.Position + len);
                    smin = Vector3.Min(smin, cargen.Position - len*2.0f); //just a random guess, maybe should be more?
                    smax = Vector3.Max(smax, cargen.Position + len*2.0f);
                }
            }

            if (LODLights != null)
            {
                LODLights.CalcBB();
                emin = Vector3.Min(emin, LODLights.BBMin - 20.0f); //about right
                emax = Vector3.Max(emax, LODLights.BBMax + 20.0f);
                smin = Vector3.Min(smin, (LODLights.BBMin - 950.0f)); //seems correct
                smax = Vector3.Max(smax, (LODLights.BBMax + 950.0f));
            }

            if (DistantLODLights != null)
            {
                DistantLODLights.CalcBB();
                emin = Vector3.Min(emin, DistantLODLights.BBMin - 20.0f); //not exact, but probably close enough
                emax = Vector3.Max(emax, DistantLODLights.BBMax + 20.0f);
                smin = Vector3.Min(smin, (DistantLODLights.BBMin - 3000.0f)); //seems correct
                smax = Vector3.Max(smax, (DistantLODLights.BBMax + 3000.0f));
            }

            if (BoxOccluders != null)
            {
                foreach (var box in BoxOccluders)
                {
                    var siz = box.Size.Length() * 0.5f;//should really use box rotation instead....
                    emin = Vector3.Min(emin, box.Position - siz);
                    emax = Vector3.Max(emax, box.Position + siz);
                    smin = Vector3.Min(smin, box.Position - siz);//check this! for some vanilla ymaps it seems right, others not
                    smax = Vector3.Max(smax, box.Position + siz);//occluders don't seem to have a loddist
                }
            }

            if (OccludeModels != null)
            {
                foreach (var model in OccludeModels)
                {
                    emin = Vector3.Min(emin, (model.BVH?.Box.Minimum ?? model._OccludeModel.bmin));//this needs to be updated!
                    emax = Vector3.Max(emax, (model.BVH?.Box.Maximum ?? model._OccludeModel.bmax));
                    smin = Vector3.Min(smin, (model.BVH?.Box.Minimum ?? model._OccludeModel.bmin));//check this! for some vanilla ymaps it seems right, others not
                    smax = Vector3.Max(smax, (model.BVH?.Box.Maximum ?? model._OccludeModel.bmax));//occluders don't seem to have a loddist
                }
            }

            bool change = false;
            if (_CMapData.entitiesExtentsMin != emin)
            {
                _CMapData.entitiesExtentsMin = emin;
                change = true;
            }
            if (_CMapData.entitiesExtentsMax != emax)
            {
                _CMapData.entitiesExtentsMax = emax;
                change = true;
            }
            if (_CMapData.streamingExtentsMin != smin)
            {
                _CMapData.streamingExtentsMin = smin;
                change = true;
            }
            if (_CMapData.streamingExtentsMax != smax)
            {
                _CMapData.streamingExtentsMax = smax;
                change = true;
            }
            return change;
        }


        private void UpdateGrassPhysDict(bool add)
        {
            var physDict = physicsDictionaries?.ToList() ?? new List<MetaHash>();
            var vproc1 = JenkHash.GenHash("v_proc1");
            var vproc2 = JenkHash.GenHash("v_proc2"); // I think you need vproc2 as well.
            var change = false;
            if (!physDict.Contains(vproc1))
            {
                change = true;
                if (add) physDict.Add(vproc1);
                else physDict.Remove(vproc1);
            }
            if (!physDict.Contains(vproc2))
            {
                change = true;
                if (add) physDict.Add(vproc2);
                else physDict.Remove(vproc2);
            }
            if (change) physicsDictionaries = physDict.ToArray();
        }

        public void InitYmapEntityArchetypes(GameFileCache gfc)
        {
            if (AllEntities != null)
            {
                for (int i = 0; i < AllEntities.Length; i++)
                {
                    var ent = AllEntities[i];
                    var arch = gfc.GetArchetype(ent._CEntityDef.archetypeName);
                    ent.SetArchetype(arch);
                    if (ent.IsMlo) ent.MloInstance.InitYmapEntityArchetypes(gfc);
                }
            }
            if (GrassInstanceBatches != null)
            {
                for (int i = 0; i < GrassInstanceBatches.Length; i++)
                {
                    var batch = GrassInstanceBatches[i];
                    batch.Archetype = gfc.GetArchetype(batch.Batch.archetypeName);
                }
            }

            if (TimeCycleModifiers != null)
            {
                for (int i = 0; i < TimeCycleModifiers.Length; i++)
                {
                    var tcm = TimeCycleModifiers[i];
                    World.TimecycleMod wtcm;
                    if (gfc.TimeCycleModsDict.TryGetValue(tcm.CTimeCycleModifier.name.Hash, out wtcm))
                    {
                        tcm.TimeCycleModData = wtcm;
                    }
                }
            }

        }

        private static uint SetBit(uint value, int bit)
        {
            return (value | (1u << bit));
        }

    }


    [TypeConverter(typeof(ExpandableObjectConverter))]
    public class YmapEntityDef
    {
        public Archetype Archetype { get; set; } //cached by GameFileCache on loading...
        public Vector3 BBMin;//oriented archetype AABBmin
        public Vector3 BBMax;//oriented archetype AABBmax
        public Vector3 BBCenter; //oriented archetype AABB center
        public Vector3 BBExtent; //oriented archetype AABB extent
        public Vector3 BSCenter; //oriented archetype BS center
        public float BSRadius;//cached from archetype
        public float LodDist;
        public float ChildLodDist;

        public CEntityDef _CEntityDef;
        public CEntityDef CEntityDef { get { return _CEntityDef; } set { _CEntityDef = value; } }
        private List<YmapEntityDef> ChildList { get; set; }
        public YmapEntityDef[] Children { get; set; }
        public YmapEntityDef[] ChildrenMerged;// { get; set; }
        public Vector3 Position { get; set; }
        public Quaternion Orientation { get; set; }
        public Vector3 Scale { get; set; }
        public bool IsMlo { get; set; }
        public MloInstanceData MloInstance { get; set; }
        public YmapEntityDef MloParent { get; set; }
        public MloInstanceEntitySet MloEntitySet { get; set; }
        public Vector3 MloRefPosition { get; set; }
        public Quaternion MloRefOrientation { get; set; }
        public MetaWrapper[] Extensions { get; set; }

        public int Index { get; set; }
        public float Distance { get; set; } //used for rendering
        public bool IsVisible; //used for rendering
        public bool ChildrenVisible; //used for rendering
        public bool ChildrenRendered; //used when rendering ymap mode to reduce LOD flashing...
        public YmapEntityDef Parent { get; set; } //for browsing convenience, also used/updated for rendering
        public MetaHash ParentName { get; set; } //just for browsing convenience

        public YmapFile Ymap { get; set; }

        public Vector3 PivotPosition = Vector3.Zero;
        public Quaternion PivotOrientation = Quaternion.Identity;
        public Vector3 WidgetPosition = Vector3.Zero;
        public Quaternion WidgetOrientation = Quaternion.Identity;

        public uint EntityHash { get; set; } = 0; //used by CW as a unique position+name identifier

        public LinkedList<YmapEntityDef> LodManagerChildren = null;
        public object LodManagerRenderable = null;


        public LightInstance[] Lights { get; set; }
        //public uint[] LightHashTest { get; set; }

        public bool LodInParentYmap { get { return ((_CEntityDef.flags >> 3) & 1) > 0; } }


        public string Name
        {
            get
            {
                return _CEntityDef.archetypeName.ToString();
            }
        }


        public YmapEntityDef()
        {
            Scale = Vector3.One;
            Position = Vector3.One;
            Orientation = Quaternion.Identity;
        }
        public YmapEntityDef(YmapFile ymap, int index, ref CEntityDef def)
        {
            Ymap = ymap;
            Index = index;
            CEntityDef = def;
            Scale = new Vector3(new Vector2(_CEntityDef.scaleXY), _CEntityDef.scaleZ);
            Position = _CEntityDef.position;
            Orientation = new Quaternion(_CEntityDef.rotation);
            if (Orientation != Quaternion.Identity)
            {
                Orientation = Quaternion.Invert(Orientation);
            }
            IsMlo = false;

            UpdateWidgetPosition();
            UpdateWidgetOrientation();
            UpdateEntityHash();
        }

        public YmapEntityDef(YmapFile ymap, int index, ref CMloInstanceDef mlo)
        {
            Ymap = ymap;
            Index = index;
            CEntityDef = mlo.CEntityDef;
            Scale = new Vector3(new Vector2(_CEntityDef.scaleXY), _CEntityDef.scaleZ);
            Position = _CEntityDef.position;
            Orientation = new Quaternion(_CEntityDef.rotation);
            //if (Orientation != Quaternion.Identity)
            //{
            //    Orientation = Quaternion.Invert(Orientation);
            //}
            IsMlo = true;

            MloInstance = new MloInstanceData(this, null);//is this necessary..? will get created in SetArchetype..
            MloInstance.Instance = mlo;

            UpdateWidgetPosition();
            UpdateWidgetOrientation();
            UpdateEntityHash();
        }


        public void SetArchetype(Archetype arch)
        {
            Archetype = arch;
            if (Archetype != null)
            {
                UpdateBB();

                if (Archetype.Type == MetaName.CMloArchetypeDef)
                {
                    //transform interior entities into world space...
                    var mloa = Archetype as MloArchetype;
                    MloInstance = new MloInstanceData(this, mloa);
                    MloInstance._Instance = new CMloInstanceDef { CEntityDef = _CEntityDef };
                    if (mloa != null)
                    {
                        if (!IsMlo)
                        {
                            IsMlo = true;
                            List<YmapEntityDef> mloEntities = Ymap.MloEntities?.ToList() ?? new List<YmapEntityDef>();
                            mloEntities.Add(this);
                            Ymap.MloEntities = mloEntities.ToArray();
                        }

                        MloInstance.CreateYmapEntities();
                    }

                    if (BSRadius == 0.0f)
                    {
                        BSRadius = LodDist;//need something so it doesn't get culled...
                    }
                    if (BBMin == BBMax)
                    {
                        BBMin = Position - BSRadius;
                        BBMax = Position + BSRadius;//it's not ideal
                        BBCenter = (BBMax + BBMin) * 0.5f;
                        BBExtent = (BBMax - BBMin) * 0.5f;
                    }
                }
                else if (IsMlo) // archetype is no longer an mlo
                {
                    IsMlo = false;
                    MloInstance = null;

                    if (Ymap.MloEntities != null)
                    {
                        List<YmapEntityDef> mloEntities = Ymap.MloEntities.ToList();
                        if (mloEntities.Remove(this))
                        {
                            Ymap.MloEntities = mloEntities.ToArray();
                        }
                    }
                }
            }
        }

        public void SetPosition(Vector3 pos)
        {
            Position = pos;
            if (MloParent != null)
            {
                _CEntityDef.position = Quaternion.Normalize(Quaternion.Invert(MloParent.Orientation)).Multiply(pos - MloParent.Position);
                MloRefPosition = _CEntityDef.position;
                UpdateBB();
                UpdateMloArchetype();
            }
            else
            {
                _CEntityDef.position = pos;
                UpdateBB();
            }


            if (MloInstance != null)
            {
                MloInstance.SetPosition(Position);
                MloInstance.UpdateEntities();
            }

            UpdateEntityHash();
            UpdateWidgetPosition();
        }

        private void UpdateBB()
        {
            if (Archetype != null)
            {
                BSCenter = Orientation.Multiply(Archetype.BSCenter) * Scale;
                BSRadius = Archetype.BSRadius * Math.Max(Scale.X, Scale.Z);
                if (Orientation == Quaternion.Identity)
                {
                    BBMin = (Vector3.Min(Archetype.BBMin, Archetype.BBMax) * Scale) + Position;
                    BBMax = (Vector3.Max(Archetype.BBMin, Archetype.BBMax) * Scale) + Position;
                    BBCenter = (BBMax + BBMin) * 0.5f;
                    BBExtent = (BBMax - BBMin) * 0.5f;
                }
                else
                {
                    var mat = Matrix.Transformation(Vector3.Zero, Quaternion.Identity, Scale, Vector3.Zero, Orientation, Position);
                    var matabs = mat;
                    matabs.Column1 = mat.Column1.Abs();
                    matabs.Column2 = mat.Column2.Abs();
                    matabs.Column3 = mat.Column3.Abs();
                    matabs.Column4 = mat.Column4.Abs();
                    var bbcenter = (Archetype.BBMax + Archetype.BBMin) * 0.5f;
                    var bbextent = (Archetype.BBMax - Archetype.BBMin) * 0.5f;
                    var ncenter = Vector3.TransformCoordinate(bbcenter, mat);
                    var nextent = Vector3.TransformNormal(bbextent, matabs).Abs();
                    BBCenter = ncenter;
                    BBExtent = nextent;
                    BBMin = ncenter - nextent;
                    BBMax = ncenter + nextent;
                }
                LodDist = _CEntityDef.lodDist;
                if (LodDist <= 0)
                {
                    LodDist = Archetype.LodDist;
                }
                ChildLodDist = _CEntityDef.childLodDist;
                if (ChildLodDist < 0)
                {
                    ChildLodDist = LodDist * 0.5f;
                }
            }
        }

        public void UpdateEntityHash()
        {
            uint xhash = (uint)(Position.X * 100);
            uint yhash = (uint)(Position.Y * 100);
            uint zhash = (uint)(Position.Z * 100);
            EntityHash = _CEntityDef.archetypeName.Hash ^ xhash ^ yhash ^ zhash & 0xffffffff;
        }

        public void SetOrientation(Quaternion ori, bool inverse = false)
        {
            if (MloParent != null)
            {
                var mloInv = Quaternion.Normalize(Quaternion.Invert(MloParent.Orientation));
                Quaternion rel = Quaternion.Normalize(Quaternion.Multiply(mloInv, ori));
                Quaternion inv = Quaternion.Normalize(Quaternion.Invert(rel));
                Orientation = ori;
                _CEntityDef.rotation = inv.ToVector4();
            }
            else
            {
                Orientation = inverse ? Quaternion.Normalize(Quaternion.Invert(ori)) : ori;
                if (MloInstance != null)
                {
                    _CEntityDef.rotation = Orientation.ToVector4();
                }
                else
                {
                    Quaternion inv = inverse ? ori : Quaternion.Normalize(Quaternion.Invert(ori));
                    _CEntityDef.rotation = inv.ToVector4();
                }
            }

            if (MloInstance != null)
            {
                MloInstance.SetOrientation(ori);
                MloInstance.UpdateEntities();
            }

            UpdateBB();
            UpdateWidgetPosition();
            UpdateWidgetOrientation();
        }

        public void SetScale(Vector3 s)
        {
            Scale = new Vector3(s.X, s.X, s.Z);
            _CEntityDef.scaleXY = s.X;
            _CEntityDef.scaleZ = s.Z;

            MloInstanceData mloInstance = MloParent?.MloInstance;
            if (mloInstance != null)
            {
                var mcEntity = mloInstance.TryGetArchetypeEntity(this);
                if (mcEntity != null)
                {
                    mcEntity._Data.scaleXY = s.X;
                    mcEntity._Data.scaleZ = s.Z;
                }
            }
            if (Archetype != null)
            {
                float smax = Math.Max(Scale.X, Scale.Z);
                BSRadius = Archetype.BSRadius * smax;
            }

            SetPosition(Position);//update the BB
        }

        private void UpdateMloArchetype()
        {
            if (!(MloParent.Archetype is MloArchetype mloArchetype)) return;

            MCEntityDef entity = null;
            if ((MloEntitySet?.Entities != null) && (MloEntitySet?.EntitySet?.Entities != null))
            {
                var idx = MloEntitySet.Entities.IndexOf(this);
                if ((idx < 0) || (idx >= MloEntitySet.EntitySet.Entities.Length)) return;
                entity = MloEntitySet.EntitySet.Entities[idx];
            }
            else
            {
                if (Index >= mloArchetype.entities.Length) return;
                entity = mloArchetype.entities[Index];
            }

            entity._Data.position = _CEntityDef.position;
            entity._Data.rotation = _CEntityDef.rotation;
        }


        public void SetPivotPosition(Vector3 pos)
        {
            PivotPosition = pos;

            UpdateWidgetPosition();
        }

        public void SetPivotOrientation(Quaternion ori)
        {
            PivotOrientation = ori;

            UpdateWidgetOrientation();
        }


        public void SetPositionFromWidget(Vector3 pos)
        {
            SetPosition(pos - Orientation.Multiply(PivotPosition));
        }
        public void SetOrientationFromWidget(Quaternion ori)
        {
            var newori = Quaternion.Normalize(Quaternion.Multiply(ori, Quaternion.Invert(PivotOrientation)));
            var newpos = WidgetPosition - newori.Multiply(PivotPosition);
            SetOrientation(newori);
            SetPosition(newpos);
        }
        public void SetPivotPositionFromWidget(Vector3 pos)
        {
            var orinv = Quaternion.Invert(Orientation);
            SetPivotPosition(orinv.Multiply(pos - Position));
        }
        public void SetPivotOrientationFromWidget(Quaternion ori)
        {
            var orinv = Quaternion.Invert(Orientation);
            SetPivotOrientation(Quaternion.Multiply(orinv, ori));
        }


        public void UpdateWidgetPosition()
        {
            WidgetPosition = Position + Orientation.Multiply(PivotPosition);
        }
        public void UpdateWidgetOrientation()
        {
            WidgetOrientation = Quaternion.Multiply(Orientation, PivotOrientation);
        }


        public void AddChild(YmapEntityDef c)
        {
            if (ChildList == null)
            {
                ChildList = new List<YmapEntityDef>();
            }
            c.Parent = this;
            c.ParentName = _CEntityDef.archetypeName;

            ChildList.Add(c);
        }

        public void ChildListToArray()
        {
            if (ChildList == null) return;
            //if (Children == null)
            //{
            Children = ChildList.ToArray();
            ChildrenMerged = Children;//include these by default in merged array
            //}
            //else
            //{
            //    List<YmapEntityDef> newc = new List<YmapEntityDef>(Children.Length + ChildList.Count);
            //    newc.AddRange(Children);
            //    newc.AddRange(ChildList);
            //    Children = newc.ToArray();
            //}
            ChildList.Clear();
            ChildList = null;
        }
        public void ChildListToMergedArray()
        {
            if (ChildList == null) return;
            if (ChildrenMerged == null)
            {
                ChildrenMerged = ChildList.ToArray();
            }
            else
            {
                List<YmapEntityDef> newc = new List<YmapEntityDef>(ChildrenMerged.Length + ChildList.Count);
                newc.AddRange(ChildrenMerged);
                newc.AddRange(ChildList);
                ChildrenMerged = newc.ToArray();
            }
            ChildList.Clear();
            ChildList = null;
        }


        public void LodManagerAddChild(YmapEntityDef child)
        {
            if (LodManagerChildren == null)
            {
                LodManagerChildren = new LinkedList<YmapEntityDef>();
            }
            LodManagerChildren.AddLast(child);
        }
        public void LodManagerRemoveChild(YmapEntityDef child)
        {
            LodManagerChildren?.Remove(child);//could improve this by caching the list node....
        }


        public override string ToString()
        {
            return _CEntityDef.ToString() + ((ChildList != null) ? (" (" + ChildList.Count.ToString() + " children) ") : " ") + _CEntityDef.lodLevel.ToString();
        }



        public void EnsureLights(DrawableBase db)
        {
            if (Lights != null) return;
            if (Archetype == null) return;
            if (db == null) return;

            var dd = db as Drawable;
            var fd = db as FragDrawable;
            var skel = db.Skeleton;
            LightAttributes[] lightAttrs = null;
            Bounds b = null;
            if (dd != null)
            {
                lightAttrs = dd.LightAttributes?.data_items;
                b = dd.Bound;
            }
            else if (fd != null)
            {
                var frag = fd?.OwnerFragment;
                skel = skel ?? frag?.Drawable?.Skeleton;
                lightAttrs = frag?.LightAttributes?.data_items;
                b = frag?.PhysicsLODGroup?.PhysicsLOD1?.Bound;
            }
            if (lightAttrs == null) return;

            var abmin = Vector3.Min(Archetype.BBMin, db.BoundingBoxMin);
            var abmax = Vector3.Max(Archetype.BBMax, db.BoundingBoxMax);
            if (b != null)
            {
                abmin = Vector3.Min(abmin, b.BoxMin);
                abmax = Vector3.Max(abmax, b.BoxMax);
            }
            var bb = new BoundingBox(abmin, abmax).Transform(Position, Orientation, Scale);
            var ints = new uint[7];
            ints[0] = (uint)(bb.Minimum.X * 10.0f);
            ints[1] = (uint)(bb.Minimum.Y * 10.0f);
            ints[2] = (uint)(bb.Minimum.Z * 10.0f);
            ints[3] = (uint)(bb.Maximum.X * 10.0f);
            ints[4] = (uint)(bb.Maximum.Y * 10.0f);
            ints[5] = (uint)(bb.Maximum.Z * 10.0f);

            var bones = skel?.BonesMap;
            var exts = (Archetype.Extensions?.Length ?? 0);// + (Extensions?.Length ?? 0);//seems entity extensions aren't included in this
            //todo: create extension light instances

            var lightInsts = new LightInstance[lightAttrs.Length];
            for (int i = 0; i < lightAttrs.Length; i++)
            {
                ints[6] = (uint)(exts + i);
                var la = lightAttrs[i];

                var xform = Matrix.Identity;
                if ((bones != null) && (bones.TryGetValue(la.BoneId, out Bone bone)))
                {
                    xform = bone.AbsTransform;
                }

                var li = new LightInstance();
                li.Attributes = la;
                li.Hash = ComputeLightHash(ints);
                li.Position = Orientation.Multiply(xform.Multiply(la.Position)) + Position;
                li.Direction = Orientation.Multiply(xform.MultiplyRot(la.Direction));
                lightInsts[i] = li;
            }
            Lights = lightInsts;

            //LightHashTest = new uint[25];
            //for (int i = 0; i < 25; i++)
            //{
            //    ints[6] = (uint)(i);
            //    LightHashTest[i] = ComputeLightHash(ints);
            //}

        }


        public static uint ComputeLightHash(uint[] ints, uint seed = 0)
        {
            var a2 = ints.Length;
            var v3 = a2;
            var v5 = (uint)(seed + 0xDEADBEEF + 4 * ints.Length);
            var v6 = v5;
            var v7 = v5;

            var c = 0;
            for (var i = 0; i < (ints.Length - 4) / 3 + 1; i++, v3 -= 3, c += 3)
            {
                var v9 = ints[c + 2] + v5;
                var v10 = ints[c + 1] + v6;
                var v11 = ints[c] - v9;
                var v13 = v10 + v9;
                var v14 = (v7 + v11) ^ BitUtil.RotateLeft(v9, 4);
                var v15 = v10 - v14;
                var v17 = v13 + v14;
                var v18 = v15 ^ BitUtil.RotateLeft(v14, 6);
                var v19 = v13 - v18;
                var v21 = v17 + v18;
                var v22 = v19 ^ BitUtil.RotateLeft(v18, 8);
                var v23 = v17 - v22;
                var v25 = v21 + v22;
                var v26 = v23 ^ BitUtil.RotateLeft(v22, 16);
                var v27 = v21 - v26;
                var v29 = v27 ^ BitUtil.RotateRight(v26, 13);
                var v30 = v25 - v29;
                v7 = v25 + v26;
                v6 = v7 + v29;
                v5 = v30 ^ BitUtil.RotateLeft(v29, 4);
            }

            if (v3 == 3)
            {
                v5 += ints[c + 2];
            }

            if (v3 >= 2)
            {
                v6 += ints[c + 1];
            }

            if (v3 >= 1)
            {
                var v34 = (v6 ^ v5) - BitUtil.RotateLeft(v6, 14);
                var v35 = (v34 ^ (v7 + ints[c])) - BitUtil.RotateLeft(v34, 11);
                var v36 = (v35 ^ v6) - BitUtil.RotateRight(v35, 7);
                var v37 = (v36 ^ v34) - BitUtil.RotateLeft(v36, 16);
                var v38 = BitUtil.RotateLeft(v37, 4);
                var v39 = (((v35 ^ v37) - v38) ^ v36) - BitUtil.RotateLeft((v35 ^ v37) - v38, 14);
                return (v39 ^ v37) - BitUtil.RotateRight(v39, 8);
            }

            return v5;
        }


        [TypeConverter(typeof(ExpandableObjectConverter))]
        public class LightInstance
        {
            public LightAttributes Attributes { get; set; } //just for display purposes!
            public uint Hash { get; set; }
            public Vector3 Position { get; set; }
            public Vector3 Direction { get; set; }

            public override string ToString()
            {
                return Hash.ToString() + ": " + Attributes.Type.ToString();
            }
        }
    }


    [TypeConverter(typeof(ExpandableObjectConverter))]
    public class YmapGrassInstanceBatch
    {
        private const float BatchVertMultiplier = 0.00001525878f;

        public Archetype Archetype { get; set; } //cached by GameFileCache on loading...
        public rage__fwGrassInstanceListDef Batch { get; set; }
        public rage__fwGrassInstanceListDef__InstanceData[] Instances { get; set; }
        public Vector3 Position { get; set; } //calculated from AABB
        public float Radius { get; set; } //calculated from AABB
        public Vector3 AABBMin { get; set; }
        public Vector3 AABBMax { get; set; }
        public Vector3 CamRel; //used for rendering...
        public float Distance; //used for rendering
        public YmapFile Ymap { get; set; }

        private List<BoundingBox> grassBounds; // for brush
        public bool BrushEnabled; // for brush
        public float BrushRadius = 5f; // for brush
        public bool HasChanged; // for brush and renderer

        // TODO: Make configurable.
        const float BoundingSize = 0.3F;
        static readonly Vector3 GrassMinMax = Vector3.One * BoundingSize;

        public override string ToString()
        {
            return Batch.ToString();
        }

        public void UpdateInstanceCount()
        {
            var b = Batch;
            var ins = b.InstanceList;
            ins.Count1 = (ushort)Instances.Length;
            b.InstanceList = ins;
            Batch = b;
        }

        public bool IsPointBlockedByInstance(Vector3 point)
        {
            return grassBounds.Any(bb => bb.Contains(point) == ContainmentType.Contains);
        }

        private void ReInitializeBoundingCache()
        {
            // cache is already initialized correctly.
            if (grassBounds != null && (grassBounds.Count == Instances.Length))
                return;

            // Clear the current bounding cache.
            if (grassBounds == null)
                grassBounds = new List<BoundingBox>();
            else grassBounds?.Clear();

            foreach (var inst in Instances)
            {
                // create bounding box for this instance.
                var worldPos = GetGrassWorldPos(inst.Position, new BoundingBox(AABBMin, AABBMax));
                var bb = new BoundingBox(worldPos - GrassMinMax, worldPos + GrassMinMax);
                grassBounds.Add(bb);
            }
        }

        public bool EraseInstancesAtMouse(
            YmapGrassInstanceBatch batch,
            SpaceRayIntersectResult mouseRay,
            float radius)
        {
            rage__spdAABB batchAABB = batch.Batch.BatchAABB;
            var oldInstanceBounds = new BoundingBox
            (
                batchAABB.min.XYZ(),
                batchAABB.max.XYZ()
            );
            var deleteSphere = new BoundingSphere(mouseRay.Position, radius);

            // check each instance to see if it's in the delete sphere
            // thankfully we've just avoided an O(n^2) op using this bounds stuff (doesn't mean it's super fast though,
            // but it's not super slow either, even at like 50,000 instances)
            var insList = new List<rage__fwGrassInstanceListDef__InstanceData>();
            foreach (var instance in batch.Instances)
            {
                // get the world pos
                var worldPos = GetGrassWorldPos(instance.Position, oldInstanceBounds);

                // create a boundary around the instance.
                var instanceBounds = new BoundingBox(worldPos - GrassMinMax, worldPos + GrassMinMax);

                // check if the sphere contains the boundary.
                var bb = new BoundingBox(instanceBounds.Minimum, instanceBounds.Maximum);
                var ct = deleteSphere.Contains(ref bb);
                if (ct == ContainmentType.Contains || ct == ContainmentType.Intersects)
                {
                    //delInstances.Add(instance); // Add a copy of this instance
                    continue;
                }
                insList.Add(instance);
            }
            if (insList.Count == Instances.Length)
                return false;

            var newBounds = GetNewGrassBounds(insList, oldInstanceBounds);
            // recalc instances
            var b = RecalcBatch(newBounds, batch);
            batch.Batch = b;
            insList = RecalculateInstances(insList, oldInstanceBounds, newBounds);
            batch.Instances = insList.ToArray();
            return true;
        }

        public void CreateInstancesAtMouse(
            YmapGrassInstanceBatch batch,
            SpaceRayIntersectResult mouseRay,
            float radius,
            int amount,
            Func<Vector3, SpaceRayIntersectResult> spawnRayFunc,
            Color color,
            int ao,
            int scale,
            Vector3 pad,
            bool randomScale)
        {

            ReInitializeBoundingCache();
            var spawnPosition = mouseRay.Position;
            var positions = new List<Vector3>();
            var normals = new List<Vector3>();

            // Get rand positions.
            GetSpawns(spawnPosition, spawnRayFunc, positions, normals, radius, amount);
            if (positions.Count <= 0) return;

            // get the instance list
            var instances =
                batch.Instances?.ToList() ?? new List<rage__fwGrassInstanceListDef__InstanceData>();
            var batchAABB = batch.Batch.BatchAABB;

            // make sure to store the old instance bounds for the original
            // grass instances
            var oldInstanceBounds = new BoundingBox(batchAABB.min.XYZ(), batchAABB.max.XYZ());

            if (positions.Count <= 0)
                return;

            // Begin the spawn bounds.
            var grassBound = new BoundingBox(positions[0] - GrassMinMax, positions[0] + GrassMinMax);
            grassBound = EncapsulatePositions(positions, grassBound);

            // Calculate the new spawn bounds.
            var newInstanceBounds = new BoundingBox(oldInstanceBounds.Minimum, oldInstanceBounds.Maximum);
            newInstanceBounds = instances.Count > 0
                ? newInstanceBounds.Encapsulate(grassBound)
                : new BoundingBox(grassBound.Minimum, grassBound.Maximum);

            // now we need to recalculate the position of each instance
            instances = RecalculateInstances(instances, oldInstanceBounds, newInstanceBounds);

            // Add new instances at each spawn position with
            // the parameters in the brush.
            SpawnInstances(positions, normals, instances, newInstanceBounds, color, ao, scale, pad, randomScale);

            // then recalc the bounds of the grass batch
            var b = RecalcBatch(newInstanceBounds, batch);

            // plug our values back in and refresh the ymap.
            batch.Batch = b;

            // Give back the new intsances
            batch.Instances = instances.ToArray();
            grassBounds.Clear();
        }

        // bhv approach recommended by dexy.
        public YmapGrassInstanceBatch[] OptimizeInstances(YmapGrassInstanceBatch batch, float minRadius)
        {
            // this function will return an array of grass instance batches
            // that are split up into sectors (groups) with a specific size.
            // say for instance we have 30,000 instances spread across a large
            // distance. We will split those instances into a grid-like group
            // and return the groups as an array of batches.
            var oldInstanceBounds = new BoundingBox(batch.Batch.BatchAABB.min.XYZ(), batch.Batch.BatchAABB.max.XYZ());

            if (oldInstanceBounds.Radius() < minRadius)
            {
                return new [] { batch };
            }

            // Get our optimized grassInstances
            var split = SplitGrassRecursive(batch.Instances.ToList(), oldInstanceBounds, minRadius);

            // Initiate a new batch list.
            var newBatches = new List<YmapGrassInstanceBatch>();

            foreach (var grassList in split)
            {
                // Create a new batch
                var newBatch = new YmapGrassInstanceBatch
                {
                    Archetype = batch.Archetype,
                    Ymap = batch.Ymap
                };

                // Get the boundary of the grassInstances
                var newInstanceBounds = GetNewGrassBounds(grassList, oldInstanceBounds);

                // Recalculate the batch boundaries.
                var b = RecalcBatch(newInstanceBounds, newBatch);
                newBatch.Batch = b;

                var ins = RecalculateInstances(grassList, oldInstanceBounds, newInstanceBounds);
                newBatch.Instances = ins.ToArray();
                newBatches.Add(newBatch);
            }

            return newBatches.ToArray();
        }

        private List<List<rage__fwGrassInstanceListDef__InstanceData>> SplitGrassRecursive(
            IReadOnlyList<rage__fwGrassInstanceListDef__InstanceData> grassInstances,
            BoundingBox batchAABB,
            float minRadius = 15F
        )
        {
            var ret = new List<List<rage__fwGrassInstanceListDef__InstanceData>>();
            var oldPoints = SplitGrass(grassInstances, batchAABB);
            while (true)
            {
                var stop = true;
                var newPoints = new List<List<rage__fwGrassInstanceListDef__InstanceData>>();
                foreach (var mb in oldPoints)
                {
                    // for some reason we got a null group?
                    if (mb == null)
                        continue;

                    // Get the bounds of the grassInstances list
                    var radius = GetNewGrassBounds(mb, batchAABB).Radius();

                    // check if the radius of the grassInstances
                    if (radius <= minRadius)
                    {
                        // this point list is within the minimum 
                        // radius.
                        ret.Add(mb);
                        continue; // we don't need to continue.
                    }

                    // since we're here let's keep going
                    stop = false;

                    // split the grassInstances again
                    var s = SplitGrass(mb, batchAABB);

                    // add it into the new grassInstances list.
                    newPoints.AddRange(s);
                }

                // set the old grassInstances to the new grassInstances.
                oldPoints = newPoints.ToArray();

                // if we're done, and all grassInstances are within the desired size
                // then end the loop.
                if (stop) break;
            }
            return ret;
        }

        private List<rage__fwGrassInstanceListDef__InstanceData>[] SplitGrass(
            IReadOnlyList<rage__fwGrassInstanceListDef__InstanceData> points,
            BoundingBox batchAABB)
        {
            var pointGroup = new List<rage__fwGrassInstanceListDef__InstanceData>[2];

            // Calculate the bounds of these grassInstances.
            var m = GetNewGrassBounds(points, batchAABB);

            // Get the center and size
            var mm = new Vector3
            {
                X = Math.Abs(m.Minimum.X - m.Maximum.X),
                Y = Math.Abs(m.Minimum.Y - m.Maximum.Y),
                Z = Math.Abs(m.Minimum.Z - m.Maximum.Z)
            };

            // x is the greatest axis...
            if (mm.X > mm.Y && mm.X > mm.Z)
            {
                // Calculate both boundaries.
                var lhs = new BoundingBox(m.Minimum, m.Maximum - new Vector3(mm.X * 0.5F, 0, 0));
                var rhs = new BoundingBox(m.Minimum + new Vector3(mm.X * 0.5F, 0, 0), m.Maximum);

                // Set the grassInstances accordingly.
                pointGroup[0] = points
                    .Where(p => lhs.Contains(GetGrassWorldPos(p.Position, batchAABB)) == ContainmentType.Contains).ToList();
                pointGroup[1] = points
                    .Where(p => rhs.Contains(GetGrassWorldPos(p.Position, batchAABB)) == ContainmentType.Contains).ToList();
            }
            // y is the greatest axis...
            else if (mm.Y > mm.X && mm.Y > mm.Z)
            {
                // Calculate both boundaries.
                var lhs = new BoundingBox(m.Minimum, m.Maximum - new Vector3(0, mm.Y * 0.5F, 0));
                var rhs = new BoundingBox(m.Minimum + new Vector3(0, mm.Y * 0.5F, 0), m.Maximum);

                // Set the grassInstances accordingly.
                pointGroup[0] = points
                    .Where(p => lhs.Contains(GetGrassWorldPos(p.Position, batchAABB)) == ContainmentType.Contains).ToList();
                pointGroup[1] = points
                    .Where(p => rhs.Contains(GetGrassWorldPos(p.Position, batchAABB)) == ContainmentType.Contains).ToList();
            }
            // z is the greatest axis...
            else if (mm.Z > mm.X && mm.Z > mm.Y)
            {
                // Calculate both boundaries.
                var lhs = new BoundingBox(m.Minimum, m.Maximum - new Vector3(0, 0, mm.Z * 0.5F));
                var rhs = new BoundingBox(m.Minimum + new Vector3(0, 0, mm.Z * 0.5F), m.Maximum);

                // Set the grassInstances accordingly.
                pointGroup[0] = points
                    .Where(p => lhs.Contains(GetGrassWorldPos(p.Position, batchAABB)) == ContainmentType.Contains).ToList();
                pointGroup[1] = points
                    .Where(p => rhs.Contains(GetGrassWorldPos(p.Position, batchAABB)) == ContainmentType.Contains).ToList();
            }
            return pointGroup;
        }

        private static BoundingBox GetNewGrassBounds(IReadOnlyList<rage__fwGrassInstanceListDef__InstanceData> newGrass, BoundingBox oldAABB)
        {
            var grassPositions = newGrass.Select(x => GetGrassWorldPos(x.Position, oldAABB)).ToArray();
            return BoundingBox.FromPoints(grassPositions).Expand(1f);
        }

        private void SpawnInstances(
            IReadOnlyList<Vector3> positions,
            IReadOnlyList<Vector3> normals,
            ICollection<rage__fwGrassInstanceListDef__InstanceData> instanceList,
            BoundingBox instanceBounds,
            Color color,
            int ao,
            int scale,
            Vector3 pad,
            bool randomScale)
        {
            for (var i = 0; i < positions.Count; i++)
            {
                var pos = positions[i];
                // create the new instance.
                var newInstance = CreateNewInstance(normals[i], color, ao, scale, pad, randomScale);

                // get the grass position of the new instance and add it to the 
                // instance list
                var grassPosition = GetGrassPos(pos, instanceBounds);
                newInstance.Position = grassPosition;
                instanceList.Add(newInstance);
            }
        }

        private rage__fwGrassInstanceListDef__InstanceData CreateNewInstance(Vector3 normal, Color color, int ao, int scale, Vector3 pad,
            bool randomScale = false)
        {
            //Vector3 pad = FloatUtil.ParseVector3String(PadTextBox.Text);
            //int scale = (int)ScaleNumericUpDown.Value;
            var rand = new Random();
            if (randomScale)
                scale = rand.Next(scale / 2, scale);
            var newInstance = new rage__fwGrassInstanceListDef__InstanceData
            {
                Ao = (byte)ao,
                Scale = (byte)scale,
                Color = new ArrayOfBytes3 { b0 = color.R, b1 = color.G, b2 = color.B },
                Pad = new ArrayOfBytes3 { b0 = (byte)pad.X, b1 = (byte)pad.Y, b2 = (byte)pad.Z },
                NormalX = (byte)((normal.X + 1) * 0.5F * 255F),
                NormalY = (byte)((normal.Y + 1) * 0.5F * 255F)
            };
            return newInstance;
        }

        private rage__fwGrassInstanceListDef RecalcBatch(BoundingBox newInstanceBounds, YmapGrassInstanceBatch batch)
        {
            batch.AABBMax = newInstanceBounds.Maximum;
            batch.AABBMin = newInstanceBounds.Minimum;
            batch.Position = newInstanceBounds.Center();
            batch.Radius = newInstanceBounds.Radius();
            var b = batch.Batch;
            b.BatchAABB = new rage__spdAABB
            {
                min =
                    new Vector4(newInstanceBounds.Minimum,
                        0), // Let's pass the new stuff into the batchabb as well just because.
                max = new Vector4(newInstanceBounds.Maximum, 0)
            };
            return b;
        }

        private void GetSpawns(
            Vector3 origin, Func<Vector3,
                SpaceRayIntersectResult> spawnRayFunc,
            ICollection<Vector3> positions,
            ICollection<Vector3> normals,
            float radius,
            int resolution = 28)
        {
            var rand = new Random();
            for (var i = 0; i < resolution; i++)
            {
                var randX = (float)rand.NextDouble(-radius, radius);
                var randY = (float)rand.NextDouble(-radius, radius);
                if (Math.Abs(randX) > 0 && Math.Abs(randY) > 0)
                {
                    randX *= .7071f;
                    randY *= .7071f;
                }
                var posOffset = origin + new Vector3(randX, randY, 2f);
                var spaceRay = spawnRayFunc.Invoke(posOffset);
                if (!spaceRay.Hit) continue;
                // not truly O(n^2) but may be slow...
                // actually just did some testing, not slow at all.
                if (IsPointBlockedByInstance(spaceRay.Position)) continue;
                normals.Add(spaceRay.Normal);
                positions.Add(spaceRay.Position);
            }
        }

        private static List<rage__fwGrassInstanceListDef__InstanceData> RecalculateInstances(
            List<rage__fwGrassInstanceListDef__InstanceData> instances,
            BoundingBox oldInstanceBounds,
            BoundingBox newInstanceBounds)
        {
            var refreshList = new List<rage__fwGrassInstanceListDef__InstanceData>();
            foreach (var inst in instances)
            {
                // Copy instance
                var copy =
                    new rage__fwGrassInstanceListDef__InstanceData
                    {
                        Position = inst.Position,
                        Ao = inst.Ao,
                        Color = inst.Color,
                        NormalX = inst.NormalX,
                        NormalY = inst.NormalY,
                        Pad = inst.Pad,
                        Scale = inst.Scale
                    };
                // get the position from where we would be in the old bounds, and move it to
                // the position it needs to be in the new bounds.
                var oldPos = GetGrassWorldPos(copy.Position, oldInstanceBounds);
                //var oldPos = oldInstanceBounds.min + oldInstanceBounds.Size * (grassPos * BatchVertMultiplier);
                copy.Position = GetGrassPos(oldPos, newInstanceBounds);
                refreshList.Add(copy);
            }
            instances = refreshList.ToList();
            return instances;
        }

        private static BoundingBox EncapsulatePositions(IEnumerable<Vector3> positions, BoundingBox bounds)
        {
            foreach (var pos in positions)
            {
                var posBounds = new BoundingBox(pos - (GrassMinMax + 0.1f), pos + (GrassMinMax + 0.1f));
                bounds = bounds.Encapsulate(posBounds);
            }
            return bounds;
        }

        private static ArrayOfUshorts3 GetGrassPos(Vector3 worldPos, BoundingBox batchAABB)
        {
            var offset = worldPos - batchAABB.Minimum;
            var size = batchAABB.Size();
            var percentage =
                new Vector3(
                    offset.X / size.X,
                    offset.Y / size.Y,
                    offset.Z / size.Z
                );
            var instancePos = percentage / BatchVertMultiplier;
            return new ArrayOfUshorts3
            {
                u0 = (ushort)instancePos.X,
                u1 = (ushort)instancePos.Y,
                u2 = (ushort)instancePos.Z
            };
        }

        private static Vector3 GetGrassWorldPos(ArrayOfUshorts3 grassPos, BoundingBox batchAABB)
        {
            return batchAABB.Minimum + batchAABB.Size() * (grassPos.XYZ() * BatchVertMultiplier);
        }
    }

    [TypeConverter(typeof(ExpandableObjectConverter))]
    public class YmapPropInstanceBatch
    {
        public YmapFile Ymap { get; set; }

    }

    [TypeConverter(typeof(ExpandableObjectConverter))]
    public class YmapDistantLODLights
    {
        public CDistantLODLight CDistantLODLight { get; set; }
        public uint[] colours { get; set; }
        public MetaVECTOR3[] positions { get; set; }

        public Vector3 BBMin { get; set; }
        public Vector3 BBMax { get; set; }
        public YmapFile Ymap { get; set; }

        public void CalcBB()
        {
            if (positions != null)
            {
                Vector3 min = new Vector3(float.MaxValue);
                Vector3 max = new Vector3(float.MinValue);
                for (int i = 0; i < positions.Length; i++)
                {
                    var p = positions[i];
                    Vector3 pv = p.ToVector3();
                    min = Vector3.Min(min, pv);
                    max = Vector3.Max(max, pv);
                }
                BBMin = min;
                BBMax = max;
            }

        }


        public void RebuildFromLodLights(YmapLODLight[] lodlights)
        {
            var n = lodlights?.Length ?? 0;
            if (n == 0) return;

            colours = new uint[n];
            positions = new MetaVECTOR3[n];
            var nstreetlights = 0;
            for (int i = 0; i < n; i++)
            {
                var ll = lodlights[i];
                colours[i] = (uint)(ll.Colour.ToBgra());
                positions[i] = new MetaVECTOR3(ll.Position);
                if ((ll.StateFlags1 & 1) > 0)
                {
                    nstreetlights++;
                }
            }

            var cdll = CDistantLODLight;
            cdll.numStreetLights = (ushort)nstreetlights;

            CalcBB();

        }


        public override string ToString()
        {
            if (Ymap != null)
            {
                return Ymap.ToString();
            }
            return base.ToString();
        }
    }

    [TypeConverter(typeof(ExpandableObjectConverter))]
    public class YmapLODLights
    {
        public CLODLight CLODLight { get; set; }
        public MetaVECTOR3[] direction { get; set; }
        public float[] falloff { get; set; }
        public float[] falloffExponent { get; set; }
        public uint[] timeAndStateFlags { get; set; }
        public uint[] hash { get; set; }
        public byte[] coneInnerAngle { get; set; }
        public byte[] coneOuterAngleOrCapExt { get; set; }
        public byte[] coronaIntensity { get; set; }

        public Vector3 BBMin { get; set; }
        public Vector3 BBMax { get; set; }
        public YmapFile Ymap { get; set; }

        public YmapLODLight[] LodLights { get; set; }

        public PathBVH BVH { get; set; }

        public void Init(YmapDistantLODLights parent)
        {
            if (parent == null) return;

            BuildLodLights(parent);
            CalcBB();
            BuildBVH();
        }

        public void BuildLodLights(YmapDistantLODLights parent)
        {
            var n = direction?.Length ?? 0;
            n = Math.Min(n, parent.positions?.Length ?? 0);
            n = Math.Min(n, parent.colours?.Length ?? 0);
            n = Math.Min(n, falloff?.Length ?? 0);
            n = Math.Min(n, falloffExponent?.Length ?? 0);
            n = Math.Min(n, timeAndStateFlags?.Length ?? 0);
            n = Math.Min(n, hash?.Length ?? 0);
            n = Math.Min(n, coneInnerAngle?.Length ?? 0);
            n = Math.Min(n, coneOuterAngleOrCapExt?.Length ?? 0);
            n = Math.Min(n, coronaIntensity?.Length ?? 0);
            if (n == 0) return;

            LodLights = new YmapLODLight[n];
            for (int i = 0; i < n; i++)
            {
                var l = new YmapLODLight();
                l.Init(this, parent, i);
                LodLights[i] = l;
            }
        }

        public void BuildBVH()
        {
            BVH = new PathBVH(LodLights, 10, 10);
        }


        public void CalcBB()
        {
            var positions = Ymap?.Parent?.DistantLODLights?.positions;
            if (positions != null)
            {
                Vector3 min = new Vector3(float.MaxValue);
                Vector3 max = new Vector3(float.MinValue);
                for (int i = 0; i < positions.Length; i++)
                {
                    var p = positions[i];
                    Vector3 pv = p.ToVector3();
                    min = Vector3.Min(min, pv);
                    max = Vector3.Max(max, pv);
                }
                BBMin = min;
                BBMax = max;
            }
            else if (Ymap != null)
            {
                BBMin = Ymap._CMapData.entitiesExtentsMin;
                BBMax = Ymap._CMapData.entitiesExtentsMax;
            }
            else
            { }
        }

        public void RebuildFromLodLights()
        {
            var n = LodLights?.Length ?? 0;
            if (n <= 0)
            {
                direction = null;
                falloff = null;
                falloffExponent = null;
                timeAndStateFlags = null;
                hash = null;
                coneInnerAngle = null;
                coneOuterAngleOrCapExt = null;
                coronaIntensity = null;
            }
            else
            {
                direction = new MetaVECTOR3[n];
                falloff = new float[n];
                falloffExponent = new float[n];
                timeAndStateFlags = new uint[n];
                hash = new uint[n];
                coneInnerAngle = new byte[n];
                coneOuterAngleOrCapExt = new byte[n];
                coronaIntensity = new byte[n];

                for (int i = 0; i < n; i++)
                {
                    var ll = LodLights[i];
                    direction[i] = new MetaVECTOR3(ll.Direction);
                    falloff[i] = ll.Falloff;
                    falloffExponent[i] = ll.FalloffExponent;
                    timeAndStateFlags[i] = ll.TimeAndStateFlags;
                    hash[i] = ll.Hash;
                    coneInnerAngle[i] = ll.ConeInnerAngle;
                    coneOuterAngleOrCapExt[i] = ll.ConeOuterAngleOrCapExt;
                    coronaIntensity[i] = ll.CoronaIntensity;
                }
            }

        }


        public override string ToString()
        {
            if (Ymap != null)
            {
                return Ymap.ToString();
            }
            return base.ToString();
        }
    }

    [TypeConverter(typeof(ExpandableObjectConverter))]
    public class YmapLODLight : BasePathNode
    {
        public YmapFile Ymap { get { return LodLights?.Ymap ?? DistLodLights?.Ymap; } }
        public YmapLODLights LodLights { get; set; }
        public YmapDistantLODLights DistLodLights { get; set; }
        public int Index { get; set; }
        public Color Colour { get; set; }
        public Vector3 Position { get; set; }
        public Vector3 Direction { get; set; }
        public float Falloff { get; set; }
        public float FalloffExponent { get; set; }
        public uint TimeAndStateFlags { get; set; }
        public uint Hash { get; set; }
        public byte ConeInnerAngle { get; set; }
        public byte ConeOuterAngleOrCapExt { get; set; }
        public byte CoronaIntensity { get; set; }

        public Quaternion Orientation { get; set; }
        public Vector3 Scale { get; set; }

        public Vector3 TangentX { get; set; }
        public Vector3 TangentY { get; set; }

        public LightType Type
        {
            get
            {
                return (LightType)((TimeAndStateFlags >> 26) & 7);
            }
            set
            {
                TimeAndStateFlags = (TimeAndStateFlags & 0xE3FFFFFF) + (((uint)value & 7) << 26);
            }
        }
        public FlagsUint TimeFlags
        {
            get
            {
                return (TimeAndStateFlags & 0xFFFFFF);
            }
            set
            {
                TimeAndStateFlags = (TimeAndStateFlags & 0xFF000000) + (value & 0xFFFFFF);
            }
        }
        public uint StateFlags1
        {
            get
            {
                return (TimeAndStateFlags >> 24) & 3;
            }
            set
            {
                TimeAndStateFlags = (TimeAndStateFlags & 0xFCFFFFFF) + ((value & 3) << 24);
            }
        }
        public uint StateFlags2
        {
            get
            {
                return (TimeAndStateFlags >> 29) & 7;
            }
            set
            {
                TimeAndStateFlags = (TimeAndStateFlags & 0x1FFFFFFF) + ((value & 7) << 29);
            }
        }

        public bool Enabled { get; set; } = true;

        public void Init(YmapLODLights l, YmapDistantLODLights p, int i)
        {
            LodLights = l;
            DistLodLights = p;
            Index = i;

            if (p.colours == null) return;
            if ((i < 0) || (i >= p.colours.Length)) return;

            Colour = Color.FromBgra(p.colours[i]);
            Position = p.positions[i].ToVector3();
            Direction = l.direction[i].ToVector3();
            Falloff = l.falloff[i];
            FalloffExponent = l.falloffExponent[i];
            TimeAndStateFlags = l.timeAndStateFlags[i];
            Hash = l.hash[i];
            ConeInnerAngle = l.coneInnerAngle[i];
            ConeOuterAngleOrCapExt = l.coneOuterAngleOrCapExt[i];
            CoronaIntensity = l.coronaIntensity[i];

            UpdateTangentsAndOrientation();

            Scale = new Vector3(Falloff);
        }

        public void UpdateTangentsAndOrientation()
        {
            switch (Type)
            {
                default:
                case LightType.Point:
                    TangentX = Vector3.UnitX;
                    TangentY = Vector3.UnitY;
                    Orientation = Quaternion.Identity;
                    break;
                case LightType.Spot:
                    TangentX = Vector3.Normalize(Direction.GetPerpVec());
                    TangentY = Vector3.Normalize(Vector3.Cross(Direction, TangentX));
                    break;
                case LightType.Capsule:
                    TangentX = -Vector3.Normalize(Direction.GetPerpVec());
                    TangentY = Vector3.Normalize(Vector3.Cross(Direction, TangentX));
                    break;
            }

            if (Type == LightType.Point)
            {
                Orientation = Quaternion.Identity;
            }
            else
            {
                var m = new Matrix();
                m.Row1 = new Vector4(TangentX, 0);
                m.Row2 = new Vector4(TangentY, 0);
                m.Row3 = new Vector4(Direction, 0);
                Orientation = Quaternion.RotationMatrix(m);
            }
        }

        public void SetColour(Color c)
        {
            Colour = c;

            if ((DistLodLights?.colours != null) && (DistLodLights.colours.Length >= Index))
            {
                DistLodLights.colours[Index] = (uint)(c.ToBgra());
            }

        }
        public void SetPosition(Vector3 pos)
        {
            Position = pos;

            if ((DistLodLights?.positions != null) && (DistLodLights.positions.Length >= Index))
            {
                DistLodLights.positions[Index] = new MetaVECTOR3(pos);
            }

        }
        public void SetOrientation(Quaternion ori)
        {
            var inv = Quaternion.Invert(Orientation);
            var delta = ori * inv;
            TangentX = Vector3.Normalize(delta.Multiply(TangentX));
            TangentY = Vector3.Normalize(delta.Multiply(TangentY));
            Direction = Vector3.Normalize(delta.Multiply(Direction));
            Orientation = ori;
        }
        public void SetScale(Vector3 scale)
        {
            switch (Type)
            {
                case LightType.Point:
                case LightType.Spot:
                    Falloff = scale.Z;
                    break;
                case LightType.Capsule:
                    Falloff = scale.X;
                    ConeOuterAngleOrCapExt = (byte)Math.Min(255, Math.Max(0, Math.Round(Math.Max((uint)ConeOuterAngleOrCapExt, 2) * Math.Abs(scale.Z / Math.Max(Scale.Z, 0.01f)))));//lols
                    break;
            }
            Scale = scale;
        }



        public void CopyFrom(YmapLODLight l)
        {
            Colour = l.Colour;
            Position = l.Position;
            Direction = l.Direction;
            Falloff = l.Falloff;
            FalloffExponent = l.FalloffExponent;
            TimeAndStateFlags = l.TimeAndStateFlags;
            Hash = l.Hash;
            ConeInnerAngle = l.ConeInnerAngle;
            ConeOuterAngleOrCapExt = l.ConeOuterAngleOrCapExt;
            CoronaIntensity = l.CoronaIntensity;

            Orientation = l.Orientation;
            Scale = l.Scale;
            TangentX = l.TangentX;
            TangentY = l.TangentY;
        }


        public override string ToString()
        {
            return Index.ToString() + ": " + Position.ToString();
        }
    }

    [TypeConverter(typeof(ExpandableObjectConverter))]
    public class YmapTimeCycleModifier
    {
        public CTimeCycleModifier CTimeCycleModifier { get; set; }
        public World.TimecycleMod TimeCycleModData { get; set; }

        public Vector3 BBMin { get; set; }
        public Vector3 BBMax { get; set; }

        public YmapFile Ymap { get; set; }
    }


    [TypeConverter(typeof(ExpandableObjectConverter))]
    public class YmapCarGen
    {
        public CCarGen _CCarGen;
        public CCarGen CCarGen { get { return _CCarGen; } set { _CCarGen = value; } }

        public Vector3 Position { get; set; }
        public Quaternion Orientation { get; set; }
        public Vector3 BBMin { get; set; }
        public Vector3 BBMax { get; set; }

        public YmapFile Ymap { get; set; }


        public YmapCarGen(YmapFile ymap, CCarGen cargen)
        {
            float hlen = cargen.perpendicularLength * 0.5f;
            Ymap = ymap;
            CCarGen = cargen;
            Position = cargen.position;
            CalcOrientation();
            BBMin = new Vector3(-hlen);
            BBMax = new Vector3(hlen);
        }


        public void CalcOrientation()
        {
            float angl = (float)Math.Atan2(_CCarGen.orientY, _CCarGen.orientX);
            Orientation = Quaternion.RotationYawPitchRoll(0.0f, 0.0f, angl);
        }

        public void SetPosition(Vector3 pos)
        {
            Position = pos;
            _CCarGen.position = pos;
        }
        public void SetOrientation(Quaternion ori)
        {
            Orientation = ori;

            float len = Math.Max(_CCarGen.perpendicularLength * 1.5f, 5.0f);
            Vector3 v = new Vector3(len, 0, 0);
            Vector3 t = ori.Multiply(v);

            _CCarGen.orientX = t.X;
            _CCarGen.orientY = t.Y;
        }
        public void SetScale(Vector3 scale)
        {
            float s = scale.X;
            float hlen = s * 0.5f;
            _CCarGen.perpendicularLength = s;

            BBMin = new Vector3(-hlen);
            BBMax = new Vector3(hlen);
        }

        public void SetLength(float length)
        {
            _CCarGen.perpendicularLength = length;
            float hlen = length * 0.5f;

            BBMin = new Vector3(-hlen);
            BBMax = new Vector3(hlen);
        }

        public string NameString()
        {
            MetaHash mh = _CCarGen.carModel;
            if ((mh == 0) && (_CCarGen.popGroup != 0))
            {
                mh = _CCarGen.popGroup;
            }
            return mh.ToString();
        }

        public override string ToString()
        {
            return _CCarGen.carModel.ToString() + ", " + Position.ToString() + ", " + _CCarGen.popGroup.ToString() + ", " + _CCarGen.livery.ToString();
        }
    }


    [TypeConverter(typeof(ExpandableObjectConverter))]
    public class YmapOccludeModel : BasePathData
    {
        public OccludeModel _OccludeModel;
        public OccludeModel OccludeModel { get { return _OccludeModel; } set { _OccludeModel = value; } }

        public YmapFile Ymap { get; set; }

        public byte[] Data { get; set; }
        public Vector3[] Vertices { get; set; }
        public byte[] Indices { get; set; }
        public int Index { get; set; }

        public YmapOccludeModelTriangle[] Triangles { get; set; }
        public TriangleBVH BVH { get; set; }

        public FlagsUint Flags
        {
            get
            {
                return _OccludeModel.flags;
            }
            set
            {
                _OccludeModel.flags = value;
            }
        }

        public YmapOccludeModel(YmapFile ymap, OccludeModel model)
        {
            Ymap = ymap;
            _OccludeModel = model;
        }


        public void Load(Meta meta)
        {
            var vptr = _OccludeModel.verts;
            var dataSize = _OccludeModel.dataSize;
            var indicesOffset = _OccludeModel.numVertsInBytes;
            var vertexCount = indicesOffset / 12;
            var indexCount = (int)(dataSize - indicesOffset);// / 4;
            Data = MetaTypes.GetByteArray(meta, vptr, dataSize);
            Vertices = MetaTypes.ConvertDataArray<Vector3>(Data, 0, vertexCount);
            Indices = new byte[indexCount];
            Buffer.BlockCopy(Data, indicesOffset, Indices, 0, indexCount);
        }


        public void BuildTriangles()
        {
            if ((Vertices == null) || (Indices == null))
            {
                Triangles = null;
                return;
            }
            var tris = new List<YmapOccludeModelTriangle>();
            for (int i = 0; i < Indices.Length; i += 3)
            {
                var tri = new YmapOccludeModelTriangle(this, Vertices[Indices[i]], Vertices[Indices[i+1]], Vertices[Indices[i+2]], tris.Count);
                tris.Add(tri);
            }
            Triangles = tris.ToArray();
        }
        public void BuildBVH()
        {
            if (Triangles == null)
            {
                BVH = null;
                return;
            }

            BVH = new TriangleBVH(Triangles);

        }
        public void BuildVertices()
        {
            //create vertices and indices arrays from Triangles
            if (Triangles == null)
            {
                Vertices = null;
                Indices = null;
                return;
            }
            var vdict = new Dictionary<Vector3, byte>();
            var verts = new List<Vector3>();
            var inds = new List<byte>();
            byte ensureVert(Vector3 v)
            {
                if (vdict.TryGetValue(v, out byte b))
                {
                    return b;
                }
                if (verts.Count > 255)
                {
                    return 0;
                }
                var i = (byte)verts.Count;
                vdict[v] = i;
                verts.Add(v);
                return i;
            }
            for (int i = 0; i < Triangles.Length; i++)
            {
                var tri = Triangles[i];
                inds.Add(ensureVert(tri.Corner1));
                inds.Add(ensureVert(tri.Corner2));
                inds.Add(ensureVert(tri.Corner3));
            }
            Vertices = verts.ToArray();
            Indices = inds.ToArray();
        }
        public void BuildData()
        {
            //create Data from vertices and indices arrays
            if (Vertices == null) return;
            if (Indices == null) return;
            var dlen = (Vertices.Length * 12) + (Indices.Length * 1);
            var d = new byte[dlen];
            var vbytes = MetaTypes.ConvertArrayToBytes(Vertices);
            var ibytes = Indices;
            Buffer.BlockCopy(vbytes, 0, d, 0, vbytes.Length);
            Buffer.BlockCopy(ibytes, 0, d, vbytes.Length, ibytes.Length);
            Data = d;
            var min = new Vector3(float.MaxValue);
            var max = new Vector3(float.MinValue);
            for (int i = 0; i < Vertices.Length; i++)
            {
                min = Vector3.Min(min, Vertices[i]);
                max = Vector3.Max(max, Vertices[i]);
            }
            _OccludeModel.bmin = min;
            _OccludeModel.bmax = max;
            _OccludeModel.Unused0 = min.X;
            _OccludeModel.Unused1 = max.X;
            _OccludeModel.dataSize = (uint)dlen;
            _OccludeModel.numVertsInBytes = (ushort)vbytes.Length;
            _OccludeModel.numTris = (ushort)((ibytes.Length / 3) + 32768);//is this actually a flag lurking..?
            //_OccludeModel.flags = ...
        }

        public YmapOccludeModelTriangle RayIntersect(ref Ray ray, ref float hitdist)
        {
            if (Triangles == null)
            {
                BuildTriangles();
                BuildBVH();
            }
            else if (BVH == null)
            {
                BuildBVH();
            }
            if (BVH == null) return null;
            return BVH.RayIntersect(ref ray, ref hitdist) as YmapOccludeModelTriangle;
        }

        public EditorVertex[] GetTriangleVertices()
        {
            if ((Vertices == null) || (Indices == null)) return null;
            EditorVertex[] res = new EditorVertex[Indices.Length];//changing from indexed to nonindexed triangle list
            var colour = new Color4(1.0f, 0.0f, 0.0f, 0.8f); //todo: colours for occluders?
            var colourval = (uint)colour.ToRgba();
            for (int i = 0; i < Indices.Length; i++)
            {
                res[i].Position = Vertices[Indices[i]];
                res[i].Colour = colourval;
            }
            return res;
        }
        public EditorVertex[] GetPathVertices()
        {
            return null;
        }
        public Vector4[] GetNodePositions()
        {
            return null;
        }

        public override string ToString()
        {
            return Index.ToString() + ": " + (Vertices?.Length ?? 0).ToString() + " vertices, " + (Triangles?.Length ?? 0).ToString() + " triangles";
        }
    }

    [TypeConverter(typeof(ExpandableObjectConverter))]
    public class YmapOccludeModelTriangle : TriangleBVHItem
    {
        public YmapOccludeModel Model { get; set; }
        public YmapFile Ymap { get { return Model?.Ymap; } }
        public int Index { get; set; }

        public YmapOccludeModelTriangle(YmapOccludeModel model, Vector3 v1, Vector3 v2, Vector3 v3, int i)
        {
            Model = model;
            Corner1 = v1;
            Corner2 = v2;
            Corner3 = v3;
            Index = i;
        }
    }

    [TypeConverter(typeof(ExpandableObjectConverter))]
    public class YmapBoxOccluder : BasePathData
    {
        public BoxOccluder _Box;
        public BoxOccluder Box { get { return _Box; } set { _Box = value; } }

        public YmapFile Ymap { get; set; }


        public Vector3 Position { get; set; }
        public Vector3 Size { get; set; }
        public Vector3 BBMin { get; set; }
        public Vector3 BBMax { get; set; }
        public Quaternion Orientation { get; set; }
        public int Index { get; set; }



        public YmapBoxOccluder(YmapFile ymap, BoxOccluder box)
        {
            Ymap = ymap;
            _Box = box;

            Position = new Vector3(box.iCenterX, box.iCenterY, box.iCenterZ) / 4.0f;
            Size = new Vector3(box.iLength, box.iWidth, box.iHeight) / 4.0f;
            BBMin = Size * -0.5f;
            BBMax = Size * 0.5f;

            float cosz = box.iCosZ / 32767.0f;// ((float)short.MaxValue)
            float sinz = box.iSinZ / 32767.0f;

            float angl = (float)Math.Atan2(cosz, sinz);
            Orientation = Quaternion.RotationYawPitchRoll(0.0f, 0.0f, angl);

        }

        public void UpdateBoxStruct()
        {
            _Box.iCenterX = (short)Math.Round(Position.X * 4.0f);
            _Box.iCenterY = (short)Math.Round(Position.Y * 4.0f);
            _Box.iCenterZ = (short)Math.Round(Position.Z * 4.0f);
            _Box.iLength = (short)Math.Round(Size.X * 4.0f);
            _Box.iWidth = (short)Math.Round(Size.Y * 4.0f);
            _Box.iHeight = (short)Math.Round(Size.Z * 4.0f);

            var dir = Orientation.Multiply(Vector3.UnitX) * 0.5f;
            _Box.iSinZ = (short)Math.Round(dir.X * 32767.0f);
            _Box.iCosZ = (short)Math.Round(dir.Y * 32767.0f);
        }


        public void SetSize(Vector3 s)
        {
            Size = s;
            BBMin = Size * -0.5f;
            BBMax = Size * 0.5f;
        }


        public EditorVertex[] GetTriangleVertices()
        {
            Vector3 xform(float x, float y, float z)
            {
                return Orientation.Multiply(new Vector3(x, y, z)) + Position;
            }
            EditorVertex[] res = new EditorVertex[36];
            var colour = new Color4(0.0f, 0.0f, 1.0f, 0.8f); //todo: colours for occluders?
            var c = (uint)colour.ToRgba();
            var s = Size * 0.5f;
            var v0 = new EditorVertex() { Position = xform(-s.X, -s.Y, -s.Z), Colour = c };
            var v1 = new EditorVertex() { Position = xform(-s.X, -s.Y, +s.Z), Colour = c };
            var v2 = new EditorVertex() { Position = xform(-s.X, +s.Y, -s.Z), Colour = c };
            var v3 = new EditorVertex() { Position = xform(-s.X, +s.Y, +s.Z), Colour = c };
            var v4 = new EditorVertex() { Position = xform(+s.X, -s.Y, -s.Z), Colour = c };
            var v5 = new EditorVertex() { Position = xform(+s.X, -s.Y, +s.Z), Colour = c };
            var v6 = new EditorVertex() { Position = xform(+s.X, +s.Y, -s.Z), Colour = c };
            var v7 = new EditorVertex() { Position = xform(+s.X, +s.Y, +s.Z), Colour = c };
            res[00] = v0; res[01] = v1; res[02] = v2; res[03] = v2; res[04] = v1; res[05] = v3;
            res[06] = v2; res[07] = v3; res[08] = v6; res[09] = v6; res[10] = v3; res[11] = v7;
            res[12] = v1; res[13] = v5; res[14] = v3; res[15] = v3; res[16] = v5; res[17] = v7;
            res[18] = v6; res[19] = v7; res[20] = v4; res[21] = v4; res[22] = v7; res[23] = v5;
            res[24] = v4; res[25] = v5; res[26] = v0; res[27] = v0; res[28] = v5; res[29] = v1;
            res[30] = v2; res[31] = v6; res[32] = v0; res[33] = v0; res[34] = v6; res[35] = v4;
            return res;
        }
        public EditorVertex[] GetPathVertices()
        {
            return null;
        }
        public Vector4[] GetNodePositions()
        {
            return null;
        }

        public override string ToString()
        {
            return Index.ToString() + ": " + FloatUtil.GetVector3String(Position);
        }
    }

}
