using BOTWToolset.Debugging;
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
namespace BOTWToolset.IO.TSCB
{
    /// 
    /// Interacts with .tcsb (terrain scene binary) files.
    /// More info found on the ZeldaMods wiki.
    /// 
    public class TSCB
    {
        public const byte HeaderLength = 48; // Length of the header, in bytes
        public string Signature { get => _signature; set => _signature = value; }
        private string _signature;
        public uint Version { get => _version; set => _version = value; }
        private uint _version;
        public uint FileBaseOffset { get => _fileBaseOffset; set => _fileBaseOffset = value; }
        private uint _fileBaseOffset;
        public float WorldScale { get => _worldScale; set => _worldScale = Math.Clamp(value, 0f, 800.0f); }
        private float _worldScale;
        public float TerrainMaxHeight { get => _terrainMaxHeight; set => _terrainMaxHeight = Math.Clamp(value, 0f, 800.0f); }
        private float _terrainMaxHeight;
        public byte[] MaterialInfoOffsets;
        public uint MaterialInfoLength { get => _materialInfoLength; set => _materialInfoLength = value; }
        private uint _materialInfoLength;
        public byte[] AreaArrayOffsets;
        public uint AreaArrayLength { get => _areaArrayLength; set => _areaArrayLength = value; }
        private uint _areaArrayLength;
        public MaterialInfo[] MaterialInfo;
        public AreaInfo[] AreaInfo;
        public float TileSize;
        public string[] FileNames;
        /// 
        /// Reads a .tscb file and returns a TSCBInfo containing its data.
        /// 
        /// The .tscb file.
        /// 
        public static TSCB FromBytes(byte[] bytes)
        {
            TSCB t = new TSCB();
            // Use big-endian
            using (var r = new BinaryReaderBig(new MemoryStream(bytes)))
            {
                // Set header info from file on the new TSCBInfo
                t.Signature = new string(r.ReadChars(4));
                t.Version = r.ReadByte();
                // Skip the 3 extra version bytes
                r.BaseStream.Seek(3, SeekOrigin.Current);
                // Skip 4 bytes of "00 00 00 01"
                r.BaseStream.Seek(4, SeekOrigin.Current);
                t.FileBaseOffset = r.ReadUInt32();
                t.WorldScale = r.ReadSingle();
                t.TerrainMaxHeight = r.ReadSingle();
                t.MaterialInfoLength = r.ReadUInt32();
                t.AreaArrayLength = r.ReadUInt32();
                // Skip 8 bytes of padding
                r.Advance(8);
                t.TileSize = r.ReadSingle();
                // Skip 4 bytes of "00 00 00 08"
                r.Advance(4);
                // Read mat info offsets
                t.MaterialInfoOffsets = r.ReadBytes((int)((t.MaterialInfoLength * 4) + 4));
                // Initialize mat info array with provided length
                t.MaterialInfo = new MaterialInfo[t.MaterialInfoLength];
                // Initialize every mat info, then add to the array
                for (int i = 0; i < t.MaterialInfoLength; i++)
                {
                    uint index = r.ReadUInt32();
                    float tex_u = r.ReadSingle();
                    float tex_v = r.ReadSingle();
                    float unk_1 = r.ReadSingle();
                    float unk_2 = r.ReadSingle();
                    MaterialInfo matInfo = new MaterialInfo(index, tex_u, tex_v, unk_1, unk_2);
                    t.MaterialInfo[i] = matInfo;
                }
                // Read area offsets
                t.AreaArrayOffsets = r.ReadBytes((int)(t.AreaArrayLength * 4));
                t.AreaInfo = new AreaInfo[t.AreaArrayLength];
                // Read every area info entry
                for (int i = 0; i < t.AreaArrayLength; i++)
                {
                    uint offset = (uint)r.BaseStream.Position;
                    float xpos = r.ReadSingle();
                    float zpos = r.ReadSingle();
                    float area_size = r.ReadSingle();
                    float min_terrain_height = r.ReadSingle();
                    float max_terrain_height = r.ReadSingle();
                    float min_water_height = r.ReadSingle();
                    float max_water_height = r.ReadSingle();
                    uint unk_1 = r.ReadUInt32();
                    if (unk_1 == 0)
                    { // If this unknown is equal to 0, skip the extra byte coming after it
                        uint next_val = r.ReadUInt32();
                        if (next_val != 1) // If the next value isn't extra unneeded info
                        {
                            r.Advance(-4);
                        }
                    }
                    uint file_base = r.ReadUInt32();
                    uint unk_2 = r.ReadUInt32();
                    uint unk_3 = r.ReadUInt32();
                    uint ref_extra = r.ReadUInt32();
                    AreaInfo areaInfo = new AreaInfo
                    {
                        PositionX = xpos,
                        PositionZ = zpos,
                        AreaSize = area_size,
                        MinTerrainHeight = min_terrain_height,
                        MaxTerrainHeight = max_terrain_height,
                        MinWaterHeight = min_water_height,
                        MaxWaterHeight = max_water_height,
                        Unknown1 = unk_1,
                        FileBase = file_base,
                        Unknown2 = unk_2,
                        Unknown3 = unk_3,
                        ReferenceExtra = ref_extra,
                        Offset = offset
                    };
                    areaInfo.ExtraInfoLength = r.ReadUInt32(); //Usually 0, 4, or 8
                    if (ref_extra == 4)
                    {
                        if (areaInfo.ExtraInfoLength == 8)
                        { //Skip the extra "20" after the 8, as well as the extra info
                            areaInfo.HasGrass = true;
                            areaInfo.HasWater = true;
                            r.Advance(36);
                        }
                        else //If the length is 4
                        {
                            var areabytes = r.ReadBytes(16).ToArray();
                            if (areabytes[7] == 0) //If byte 7 equals 0
                                areaInfo.HasGrass = true;
                            else //Else if the 2nd byte should be anything else (should always be 1)
                                areaInfo.HasWater = true;
                        }
                    }
                    else //If the extra info flags aren't set, go back 4
                    {
                        r.Advance(-4);
                    }
                    t.AreaInfo[i] = areaInfo;
                }
                //Get the number of filenames by getting how many bytes they take up out of the entire file size
                var filenames_count = (r.BaseStream.Length - (t.FileBaseOffset + 16)) / 12;
                t.FileNames = new string[filenames_count];
                r.BaseStream.Seek(t.FileBaseOffset + 16, SeekOrigin.Begin); // TODO: change this to 'current' later, or maybe even remove
                for (int i = 0; i < filenames_count; i++)
                {
                    string filename = new string(r.ReadChars(12));
                    t.FileNames[i] = filename;
                }
            }
            return t;
        }
        /// 
        /// Writes TSCB data to a byte array.
        /// 
        ///  that contains data to write.
        /// Byte array containing the TSCB data.
        public static byte[] ToBytes(TSCB tscb)
        {
            List b = new List();
            //TSCB header
            b.AddRange(new byte[] {
                0x54, 0x53, 0x43, 0x42,
                0x0A, 0x00, 0x00, 0x00,
                0x00, 0x00, 0x00, 0x01
            });
            b.AddRange(BitConverter.GetBytes(tscb.FileBaseOffset).Reverse());
            b.AddRange(BitConverter.GetBytes(tscb.WorldScale).Reverse());
            b.AddRange(BitConverter.GetBytes(tscb.TerrainMaxHeight).Reverse());
            b.AddRange(BitConverter.GetBytes(tscb.MaterialInfoLength).Reverse());
            b.AddRange(BitConverter.GetBytes(tscb.AreaArrayLength).Reverse());
            // Padding
            b.AddRange(new byte[] { 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00 });
            b.AddRange(BitConverter.GetBytes(tscb.TileSize).Reverse());
            b.AddRange(new byte[] { 0x00, 0x00, 0x00, 0x08 });
            // Add material info offsets
            b.AddRange(tscb.MaterialInfoOffsets);
            // Write material infos
            foreach (var mat in tscb.MaterialInfo)
            {
                b.AddRange(BitConverter.GetBytes(mat.MaterialIndex).Reverse());
                b.AddRange(BitConverter.GetBytes(mat.TextureU).Reverse());
                b.AddRange(BitConverter.GetBytes(mat.TextureV).Reverse());
                b.AddRange(BitConverter.GetBytes(mat.Unknown1).Reverse());
                b.AddRange(BitConverter.GetBytes(mat.Unknown2).Reverse());
            }
            b.AddRange(tscb.AreaArrayOffsets);
            foreach (var area in tscb.AreaInfo)
            {
                // Add area bytes
                b.AddRange(BitConverter.GetBytes(area.PositionX).Reverse());
                b.AddRange(BitConverter.GetBytes(area.PositionZ).Reverse());
                b.AddRange(BitConverter.GetBytes(area.AreaSize).Reverse());
                b.AddRange(BitConverter.GetBytes(area.MinTerrainHeight).Reverse());
                b.AddRange(BitConverter.GetBytes(area.MaxTerrainHeight).Reverse());
                b.AddRange(BitConverter.GetBytes(area.MinWaterHeight).Reverse());
                b.AddRange(BitConverter.GetBytes(area.MaxWaterHeight).Reverse());
                b.AddRange(BitConverter.GetBytes(area.Unknown1).Reverse());
                b.AddRange(BitConverter.GetBytes(area.FileBase).Reverse());
                b.AddRange(BitConverter.GetBytes(area.Unknown2).Reverse());
                b.AddRange(BitConverter.GetBytes(area.Unknown3).Reverse());
                b.AddRange(BitConverter.GetBytes(area.ReferenceExtra).Reverse());
                if (area.ReferenceExtra == 4)
                {
                    b.AddRange(BitConverter.GetBytes(area.ExtraInfoLength).Reverse());
                    if (area.ExtraInfoLength == 8)
                    {
                        b.AddRange(new byte[] {
                        0x00, 0x00, 0x00, 0x14,
                        0x00, 0x00, 0x00, 0x03,
                        0x00, 0x00, 0x00, 0x00,
                        0x00, 0x00, 0x00, 0x01,
                        0x00, 0x00, 0x00, 0x00,
                        0x00, 0x00, 0x00, 0x03,
                        0x00, 0x00, 0x00, 0x01,
                        0x00, 0x00, 0x00, 0x01,
                        0x00, 0x00, 0x00, 0x00
                    });
                    }
                    else
                    {
                        byte[] grass = new byte[] { 0x00, 0x00, 0x00, 0x00 };
                        byte[] water = new byte[] { 0x00, 0x00, 0x00, 0x01 };
                        b.AddRange(new byte[] { 0x00, 0x00, 0x00, 0x03 });
                        b.AddRange(area.HasGrass == true ? grass : water);
                        b.AddRange(new byte[] { 0x00, 0x00, 0x00, 0x01 });
                        b.AddRange(new byte[] { 0x00, 0x00, 0x00, 0x00 });
                    }
                }
            }
            foreach (string filename in tscb.FileNames)
            {
                b.AddRange(System.Text.Encoding.ASCII.GetBytes(filename));
            }
            return b.ToArray();
        }
    }
}