using System.Collections.Generic;
using System.IO;
using System.Text;
using SabreTools.Data.Models.BSP;
using SabreTools.IO.Extensions;
using SabreTools.Numerics.Extensions;
using static SabreTools.Data.Models.BSP.Constants;

#pragma warning disable IDE0017 // Simplify object initialization
#pragma warning disable IDE0060 // Remove unused parameter
namespace SabreTools.Serialization.Readers
{
    public class BSP : BaseBinaryReader<BspFile>
    {
        /// <inheritdoc/>
        public override BspFile? Deserialize(Stream? data)
        {
            // If the data is invalid
            if (data is null || !data.CanRead)
                return null;

            try
            {
                // Cache the current offset
                long initialOffset = data.Position;

                // Create a new Half-Life Level to fill
                var file = new BspFile();

                #region Header

                // Try to parse the header
                var header = ParseBspHeader(data);
                if (header.Version < 29 || header.Version > 30)
                    return null;

                // Set the level header
                file.Header = header;

                #endregion

                #region Lumps

                for (int l = 0; l < BSP_HEADER_LUMPS; l++)
                {
                    // Get the next lump entry
                    var lumpEntry = header.Lumps![l];
                    if (lumpEntry is null)
                        continue;
                    if (lumpEntry.Offset == 0 || lumpEntry.Length == 0)
                        continue;

                    // Seek to the lump offset
                    data.SeekIfPossible(initialOffset + lumpEntry.Offset, SeekOrigin.Begin);

                    // Read according to the lump type
                    switch ((BspLumpType)l)
                    {
                        case BspLumpType.LUMP_ENTITIES:
                            file.Entities = ParseEntitiesLump(data, lumpEntry.Offset, lumpEntry.Length);
                            break;
                        case BspLumpType.LUMP_PLANES:
                            file.PlanesLump = ParsePlanesLump(data, lumpEntry.Offset, lumpEntry.Length);
                            break;
                        case BspLumpType.LUMP_TEXTURES:
                            file.TextureLump = ParseTextureLump(data, lumpEntry.Offset, lumpEntry.Length);
                            break;
                        case BspLumpType.LUMP_VERTICES:
                            file.VerticesLump = ParseVerticesLump(data, lumpEntry.Offset, lumpEntry.Length);
                            break;
                        case BspLumpType.LUMP_VISIBILITY:
                            var visiblityLump = ParseVisibilityLump(data, lumpEntry.Offset, lumpEntry.Length);
                            if (visiblityLump is not null)
                                file.VisibilityLump = visiblityLump;

                            break;
                        case BspLumpType.LUMP_NODES:
                            file.NodesLump = ParseNodesLump(data, lumpEntry.Offset, lumpEntry.Length);
                            break;
                        case BspLumpType.LUMP_TEXINFO:
                            file.TexinfoLump = ParseTexinfoLump(data, lumpEntry.Offset, lumpEntry.Length);
                            break;
                        case BspLumpType.LUMP_FACES:
                            file.FacesLump = ParseFacesLump(data, lumpEntry.Offset, lumpEntry.Length);
                            break;
                        case BspLumpType.LUMP_LIGHTING:
                            file.LightmapLump = ParseLightmapLump(data, lumpEntry.Offset, lumpEntry.Length);
                            break;
                        case BspLumpType.LUMP_CLIPNODES:
                            file.ClipnodesLump = ParseClipnodesLump(data, lumpEntry.Offset, lumpEntry.Length);
                            break;
                        case BspLumpType.LUMP_LEAVES:
                            file.LeavesLump = ParseLeavesLump(data, lumpEntry.Offset, lumpEntry.Length);
                            break;
                        case BspLumpType.LUMP_MARKSURFACES:
                            file.MarksurfacesLump = ParseMarksurfacesLump(data, lumpEntry.Offset, lumpEntry.Length);
                            break;
                        case BspLumpType.LUMP_EDGES:
                            file.EdgesLump = ParseEdgesLump(data, lumpEntry.Offset, lumpEntry.Length);
                            break;
                        case BspLumpType.LUMP_SURFEDGES:
                            file.SurfedgesLump = ParseSurfedgesLump(data, lumpEntry.Offset, lumpEntry.Length);
                            break;
                        case BspLumpType.LUMP_MODELS:
                            file.ModelsLump = ParseModelsLump(data, lumpEntry.Offset, lumpEntry.Length);
                            break;

                        default:
                            // Unsupported BspLumpType value, ignore
                            break;
                    }
                }

                #endregion

                return file;
            }
            catch
            {
                // Ignore the actual error
                return null;
            }
        }

        /// <summary>
        /// Parse a Stream into BspFace
        /// </summary>
        /// <param name="data">Stream to parse</param>
        /// <returns>Filled BspFace on success, null on error</returns>
        public static BspFace ParseBspFace(Stream data)
        {
            var obj = new BspFace();

            obj.PlaneIndex = data.ReadUInt16LittleEndian();
            obj.PlaneSideCount = data.ReadUInt16LittleEndian();
            obj.FirstEdgeIndex = data.ReadUInt32LittleEndian();
            obj.NumberOfEdges = data.ReadUInt16LittleEndian();
            obj.TextureInfoIndex = data.ReadUInt16LittleEndian();
            obj.LightingStyles = data.ReadBytes(4);
            obj.LightmapOffset = data.ReadInt32LittleEndian();

            return obj;
        }

        /// <summary>
        /// Parse a Stream into BspHeader
        /// </summary>
        /// <param name="data">Stream to parse</param>
        /// <returns>Filled BspHeader on success, null on error</returns>
        public static BspHeader ParseBspHeader(Stream data)
        {
            var obj = new BspHeader();

            obj.Version = data.ReadInt32LittleEndian();
            obj.Lumps = new BspLumpEntry[BSP_HEADER_LUMPS];
            for (int i = 0; i < BSP_HEADER_LUMPS; i++)
            {
                obj.Lumps[i] = ParseBspLumpEntry(data);
            }

            return obj;
        }

        /// <summary>
        /// Parse a Stream into BspLeaf
        /// </summary>
        /// <param name="data">Stream to parse</param>
        /// <returns>Filled BspLeaf on success, null on error</returns>
        public static BspLeaf ParseBspLeaf(Stream data)
        {
            var obj = new BspLeaf();

            obj.Contents = (BspContents)data.ReadInt32LittleEndian();
            obj.VisOffset = data.ReadInt32LittleEndian();
            obj.Mins = new short[3];
            for (int i = 0; i < 3; i++)
            {
                obj.Mins[i] = data.ReadInt16LittleEndian();
            }

            obj.Maxs = new short[3];
            for (int i = 0; i < 3; i++)
            {
                obj.Maxs[i] = data.ReadInt16LittleEndian();
            }

            obj.FirstMarkSurfaceIndex = data.ReadUInt16LittleEndian();
            obj.MarkSurfacesCount = data.ReadUInt16LittleEndian();
            obj.AmbientLevels = data.ReadBytes(4);

            return obj;
        }

        /// <summary>
        /// Parse a Stream into BspLumpEntry
        /// </summary>
        /// <param name="data">Stream to parse</param>
        /// <returns>Filled BspLumpEntry on success, null on error</returns>
        public static BspLumpEntry ParseBspLumpEntry(Stream data)
        {
            var obj = new BspLumpEntry();

            obj.Offset = data.ReadInt32LittleEndian();
            obj.Length = data.ReadInt32LittleEndian();

            return obj;
        }

        /// <summary>
        /// Parse a Stream into BspModel
        /// </summary>
        /// <param name="data">Stream to parse</param>
        /// <returns>Filled BspModel on success, null on error</returns>
        public static BspModel ParseBspModel(Stream data)
        {
            var obj = new BspModel();

            obj.Mins = ParseVector3D(data);
            obj.Maxs = ParseVector3D(data);
            obj.OriginVector = ParseVector3D(data);
            obj.HeadnodesIndex = new int[MAX_MAP_HULLS];
            for (int i = 0; i < MAX_MAP_HULLS; i++)
            {
                obj.HeadnodesIndex[i] = data.ReadInt32LittleEndian();
            }

            obj.VisLeafsCount = data.ReadInt32LittleEndian();
            obj.FirstFaceIndex = data.ReadInt32LittleEndian();
            obj.FacesCount = data.ReadInt32LittleEndian();

            return obj;
        }

        /// <summary>
        /// Parse a Stream into BspNode
        /// </summary>
        /// <param name="data">Stream to parse</param>
        /// <returns>Filled BspNode on success, null on error</returns>
        public static BspNode ParseBspNode(Stream data)
        {
            var obj = new BspNode();

            obj.PlaneIndex = data.ReadUInt32LittleEndian();
            obj.Children = new ushort[2];
            for (int i = 0; i < 2; i++)
            {
                obj.Children[i] = data.ReadUInt16LittleEndian();
            }

            obj.Mins = new ushort[3];
            for (int i = 0; i < 3; i++)
            {
                obj.Mins[i] = data.ReadUInt16LittleEndian();
            }

            obj.Maxs = new ushort[3];
            for (int i = 0; i < 3; i++)
            {
                obj.Maxs[i] = data.ReadUInt16LittleEndian();
            }

            obj.FirstFace = data.ReadUInt16LittleEndian();
            obj.FaceCount = data.ReadUInt16LittleEndian();

            return obj;
        }

        /// <summary>
        /// Parse a Stream into BspTexinfo
        /// </summary>
        /// <param name="data">Stream to parse</param>
        /// <returns>Filled BspTexinfo on success, null on error</returns>
        public static BspTexinfo ParseBspTexinfo(Stream data)
        {
            var obj = new BspTexinfo();

            obj.SVector = ParseVector3D(data);
            obj.TextureSShift = data.ReadSingle();
            obj.TVector = ParseVector3D(data);
            obj.TextureTShift = data.ReadSingle();
            obj.MiptexIndex = data.ReadUInt32LittleEndian();
            obj.Flags = (TextureFlag)data.ReadUInt32LittleEndian();

            return obj;
        }

        /// <summary>
        /// Parse a Stream into Clipnode
        /// </summary>
        /// <param name="data">Stream to parse</param>
        /// <returns>Filled Clipnode on success, null on error</returns>
        public static Clipnode ParseClipnode(Stream data)
        {
            var obj = new Clipnode();

            obj.PlaneIndex = data.ReadInt32LittleEndian();
            obj.ChildrenIndices = new short[2];
            for (int i = 0; i < 2; i++)
            {
                obj.ChildrenIndices[i] = data.ReadInt16LittleEndian();
            }

            return obj;
        }

        /// <summary>
        /// Parse a Stream into Edge
        /// </summary>
        /// <param name="data">Stream to parse</param>
        /// <returns>Filled Edge on success, null on error</returns>
        public static Edge ParseEdge(Stream data)
        {
            var obj = new Edge();

            obj.VertexIndices = new ushort[2];
            for (int i = 0; i < 2; i++)
            {
                obj.VertexIndices[i] = data.ReadUInt16LittleEndian();
            }

            return obj;
        }

        /// <summary>
        /// Parse a Stream into MipTexture
        /// </summary>
        /// <param name="data">Stream to parse</param>
        /// <returns>Filled MipTexture on success, null on error</returns>
        public static MipTexture ParseMipTexture(Stream data)
        {
            var obj = new MipTexture();

            byte[] name = data.ReadBytes(MAXTEXTURENAME);
            obj.Name = Encoding.ASCII.GetString(name).TrimEnd('\0');
            obj.Width = data.ReadUInt32LittleEndian();
            obj.Height = data.ReadUInt32LittleEndian();
            obj.Offsets = new uint[MIPLEVELS];
            for (int i = 0; i < MIPLEVELS; i++)
            {
                obj.Offsets[i] = data.ReadUInt32LittleEndian();
            }

            return obj;
        }

        /// <summary>
        /// Parse a Stream into Plane
        /// </summary>
        /// <param name="data">Stream to parse</param>
        /// <returns>Filled Plane on success, null on error</returns>
        public static Plane ParsePlane(Stream data)
        {
            var obj = new Plane();

            obj.NormalVector = ParseVector3D(data);
            obj.Distance = data.ReadSingle();
            obj.PlaneType = (PlaneType)data.ReadInt32LittleEndian();

            return obj;
        }

        /// <summary>
        /// Parse a Stream into a TextureHeader
        /// </summary>
        /// <param name="data">Stream to parse</param>
        /// <returns>Filled TextureHeader on success, null on error</returns>
        public static TextureHeader ParseTextureHeader(Stream data)
        {
            var obj = new TextureHeader();

            obj.MipTextureCount = data.ReadUInt32LittleEndian();
            obj.Offsets = new int[obj.MipTextureCount];
            for (int i = 0; i < obj.Offsets.Length; i++)
            {
                obj.Offsets[i] = data.ReadInt16LittleEndian();
            }

            return obj;
        }

        /// <summary>
        /// Parse a Stream into Vector3D
        /// </summary>
        /// <param name="data">Stream to parse</param>
        /// <returns>Filled Vector3D on success, null on error</returns>
        public static Vector3D ParseVector3D(Stream data)
        {
            var obj = new Vector3D();

            obj.X = data.ReadSingle();
            obj.Y = data.ReadSingle();
            obj.Z = data.ReadSingle();

            return obj;
        }

        /// <summary>
        /// Parse a Stream into LUMP_ENTITIES
        /// </summary>
        /// <param name="data">Stream to parse</param>
        /// <returns>Filled LUMP_ENTITIES on success, null on error</returns>
        private static EntitiesLump ParseEntitiesLump(Stream data, int offset, int length)
        {
            var entities = new List<Entity>();

            // Read the entire lump as text
            byte[] lumpData = data.ReadBytes(length);
            string lumpText = Encoding.ASCII.GetString(lumpData);

            // Break the text by ending curly braces
            string[] lumpSections = lumpText.Split('}');

            // Loop through all sections
            for (int i = 0; i < lumpSections.Length; i++)
            {
                // Prepare an attributes list
                var attributes = new List<KeyValuePair<string, string>>();

                // Split the section by newlines
                string section = lumpSections[i].Trim('{', '}');
                string[] lines = section.Split('\n');

                // Convert each line into a key-value pair and add
                for (int j = 0; j < lines.Length; j++)
                {
                    // TODO: Split lines and add
                }

                // Create a new entity and add
                var entity = new Entity { Attributes = attributes };
                entities.Add(entity);
            }

            return new EntitiesLump { Entities = [.. entities] };
        }

        /// <summary>
        /// Parse a Stream into LUMP_PLANES
        /// </summary>
        /// <param name="data">Stream to parse</param>
        /// <returns>Filled LUMP_PLANES on success, null on error</returns>
        private static PlanesLump ParsePlanesLump(Stream data, int offset, int length)
        {
            var planes = new List<Plane>();
            while (data.Position < offset + length)
            {
                var plane = ParsePlane(data);
                planes.Add(plane);
            }

            return new PlanesLump { Planes = [.. planes] };
        }

        /// <summary>
        /// Parse a Stream into LUMP_TEXTURES
        /// </summary>
        /// <param name="data">Stream to parse</param>
        /// <returns>Filled LUMP_TEXTURES on success, null on error</returns>
        private static TextureLump ParseTextureLump(Stream data, int offset, int length)
        {
            var lump = new TextureLump();

            lump.Header = ParseTextureHeader(data);
            var textures = new List<MipTexture>();
            while (data.Position < offset + length)
            {
                var texture = ParseMipTexture(data);
                textures.Add(texture);
            }

            lump.Textures = [.. textures];
            return lump;
        }

        /// <summary>
        /// Parse a Stream into LUMP_VERTICES
        /// </summary>
        /// <param name="data">Stream to parse</param>
        /// <returns>Filled LUMP_VERTICES on success, null on error</returns>
        private static VerticesLump ParseVerticesLump(Stream data, int offset, int length)
        {
            var vertices = new List<Vector3D>();
            while (data.Position < offset + length)
            {
                var vertex = ParseVector3D(data);
                vertices.Add(vertex);
            }

            return new VerticesLump { Vertices = [.. vertices] };
        }

        /// <summary>
        /// Parse a Stream into LUMP_VISIBILITY
        /// </summary>
        /// <param name="data">Stream to parse</param>
        /// <returns>Filled LUMP_VISIBILITY on success, null on error</returns>
        private static VisibilityLump? ParseVisibilityLump(Stream data, int offset, int length)
        {
            var lump = new VisibilityLump();

            lump.NumClusters = data.ReadInt32LittleEndian();

            // BSP29 has an incompatible value here
            int bytesNeeded = lump.NumClusters * 8;
            if (bytesNeeded > length)
                return null;

            lump.ByteOffsets = new int[lump.NumClusters][];
            for (int i = 0; i < lump.NumClusters; i++)
            {
                lump.ByteOffsets[i] = new int[2];
                for (int j = 0; j < 2; j++)
                {
                    lump.ByteOffsets[i][j] = data.ReadInt32LittleEndian();
                }
            }

            return lump;
        }

        /// <summary>
        /// Parse a Stream into LUMP_NODES
        /// </summary>
        /// <param name="data">Stream to parse</param>
        /// <returns>Filled LUMP_NODES on success, null on error</returns>
        private static BspNodesLump ParseNodesLump(Stream data, int offset, int length)
        {
            var nodes = new List<BspNode>();
            while (data.Position < offset + length)
            {
                var node = ParseBspNode(data);
                nodes.Add(node);
            }

            return new BspNodesLump { Nodes = [.. nodes] };
        }

        /// <summary>
        /// Parse a Stream into LUMP_TEXINFO
        /// </summary>
        /// <param name="data">Stream to parse</param>
        /// <returns>Filled LUMP_TEXINFO on success, null on error</returns>
        private static BspTexinfoLump ParseTexinfoLump(Stream data, int offset, int length)
        {
            var texinfos = new List<BspTexinfo>();
            while (data.Position < offset + length)
            {
                var texinfo = ParseBspTexinfo(data);
                texinfos.Add(texinfo);
            }

            return new BspTexinfoLump { Texinfos = [.. texinfos] };
        }

        /// <summary>
        /// Parse a Stream into LUMP_FACES
        /// </summary>
        /// <param name="data">Stream to parse</param>
        /// <returns>Filled LUMP_FACES on success, null on error</returns>
        private static BspFacesLump ParseFacesLump(Stream data, int offset, int length)
        {
            var faces = new List<BspFace>();
            while (data.Position < offset + length)
            {
                var face = ParseBspFace(data);
                faces.Add(face);
            }

            return new BspFacesLump { Faces = [.. faces] };
        }

        /// <summary>
        /// Parse a Stream into LUMP_LIGHTING
        /// </summary>
        /// <param name="data">Stream to parse</param>
        /// <returns>Filled LUMP_LIGHTING on success, null on error</returns>
        private static LightmapLump ParseLightmapLump(Stream data, int offset, int length)
        {
            var lump = new LightmapLump();
            lump.Lightmap = new byte[length / 3][];

            for (int i = 0; i < length / 3; i++)
            {
                lump.Lightmap[i] = data.ReadBytes(3);
            }

            return lump;
        }

        /// <summary>
        /// Parse a Stream into LUMP_CLIPNODES
        /// </summary>
        /// <param name="data">Stream to parse</param>
        /// <returns>Filled LUMP_CLIPNODES on success, null on error</returns>
        private static ClipnodesLump ParseClipnodesLump(Stream data, int offset, int length)
        {
            var clipnodes = new List<Clipnode>();
            while (data.Position < offset + length)
            {
                var clipnode = ParseClipnode(data);
                clipnodes.Add(clipnode);
            }

            return new ClipnodesLump { Clipnodes = [.. clipnodes] };
        }

        /// <summary>
        /// Parse a Stream into LUMP_LEAVES
        /// </summary>
        /// <param name="data">Stream to parse</param>
        /// <returns>Filled LUMP_LEAVES on success, null on error</returns>
        private static BspLeavesLump ParseLeavesLump(Stream data, int offset, int length)
        {
            var leaves = new List<BspLeaf>();
            while (data.Position < offset + length)
            {
                var leaf = ParseBspLeaf(data);
                leaves.Add(leaf);
            }

            return new BspLeavesLump { Leaves = [.. leaves] };
        }

        /// <summary>
        /// Parse a Stream into LUMP_MARKSURFACES
        /// </summary>
        /// <param name="data">Stream to parse</param>
        /// <returns>Filled LUMP_MARKSURFACES on success, null on error</returns>
        private static MarksurfacesLump ParseMarksurfacesLump(Stream data, int offset, int length)
        {
            var marksurfaces = new List<ushort>();
            while (data.Position < offset + length)
            {
                marksurfaces.Add(data.ReadUInt16LittleEndian());
            }

            return new MarksurfacesLump { Marksurfaces = [.. marksurfaces] };
        }

        /// <summary>
        /// Parse a Stream into LUMP_EDGES
        /// </summary>
        /// <param name="data">Stream to parse</param>
        /// <returns>Filled LUMP_EDGES on success, null on error</returns>
        private static EdgesLump ParseEdgesLump(Stream data, int offset, int length)
        {
            var edges = new List<Edge>();
            while (data.Position < offset + length)
            {
                var edge = ParseEdge(data);
                edges.Add(edge);
            }

            return new EdgesLump { Edges = [.. edges] };
        }

        /// <summary>
        /// Parse a Stream into LUMP_SURFEDGES
        /// </summary>
        /// <param name="data">Stream to parse</param>
        /// <returns>Filled LUMP_SURFEDGES on success, null on error</returns>
        private static SurfedgesLump ParseSurfedgesLump(Stream data, int offset, int length)
        {
            var surfedges = new List<int>();
            while (data.Position < offset + length)
            {
                surfedges.Add(data.ReadInt32LittleEndian());
            }

            return new SurfedgesLump { Surfedges = [.. surfedges] };
        }

        /// <summary>
        /// Parse a Stream into LUMP_MODELS
        /// </summary>
        /// <param name="data">Stream to parse</param>
        /// <returns>Filled LUMP_MODELS on success, null on error</returns>
        private static BspModelsLump ParseModelsLump(Stream data, int offset, int length)
        {
            var models = new List<BspModel>();
            while (data.Position < offset + length)
            {
                var model = ParseBspModel(data);
                models.Add(model);
            }

            return new BspModelsLump { Models = [.. models] };
        }
    }
}
