﻿using System;
using System.Collections.Generic;
using System.IO;
//using System.Runtime.Remoting.Metadata.W3cXsd2001;

namespace Oni.Sound
{
    internal class WavFile
    {
        #region Private data
        private const int fcc_RIFF = 0x46464952;
        private const int fcc_WAVE = 0x45564157;
        private const int fcc_fmt  = 0x20746d66;
        private const int fcc_fact = 0x74636166;
        private const int fcc_data = 0x61746164;

        private WavFormat format;
        private int channelCount;
        private int sampleRate;
        private int averageBytesPerSecond;
        private int blockAlign;
        private int bitsPerSample;
        private int sampleCount;
        private byte[] extraData;
        private byte[] soundData;
        #endregion

        public static WavFile FromFile(string filePath)
        {
            using (var reader = new BinaryReader(filePath))
            {
                if (reader.ReadInt32() != fcc_RIFF)
                    throw new InvalidDataException("Not a WAV file");

                int size = reader.ReadInt32();

                if (reader.ReadInt32() != fcc_WAVE)
                    throw new InvalidDataException("Not a WAV file");

                var header = new WavFile()
                {
                    sampleCount = -1
                };

                for (int chunkType, chunkSize, chunkStart; reader.Position < size; reader.Position = chunkStart + chunkSize)
                {
                    chunkType = reader.ReadInt32();
                    chunkSize = reader.ReadInt32();
                    chunkStart = reader.Position;

                    if (chunkType == fcc_fmt)
                        header.ReadFormatChunk(reader, chunkSize);
                    if (chunkType == fcc_fact)
                        header.ReadFactChunk(reader, chunkSize);
                    if (chunkType == fcc_data)
                        header.ReadDataChunk(reader, chunkSize);
                }
                header.TruncatePerFact();
                return header;
            }
        }

        private void ReadFormatChunk(BinaryReader reader, int chunkSize)
        {
            format = (WavFormat)reader.ReadInt16();
            channelCount = reader.ReadInt16();
            sampleRate = reader.ReadInt32();
            averageBytesPerSecond = reader.ReadInt32();
            blockAlign = reader.ReadInt16();
            bitsPerSample = reader.ReadInt16();

            if (chunkSize > 16)
                extraData = reader.ReadBytes(reader.ReadInt16());
            else
                extraData = new byte[0];
        }

        private void ReadFactChunk(BinaryReader reader, int chunkSize)
        {
            sampleCount = reader.ReadInt32();
        }

        private void ReadDataChunk(BinaryReader reader, int chunkSize)
        {
            soundData = reader.ReadBytes(chunkSize);
        }
        private void TruncatePerFact() // TODO: MORE THOROUGH VALIDATION?
        {
            if(sampleCount == -1) // not explicitly set (no fact chunk present)
            {
                Console.WriteLine("The imported WAV file has no FACT chunk.");
            }
            else if (format == WavFormat.Adpcm) // calculate truncated data size
            {
                var blockSizeADPCM = blockAlign;
                var samplesPerBlock = 2 + (blockSizeADPCM - channelCount * 7) * 8 / channelCount / bitsPerSample;
                int wholeBlocks = sampleCount / samplesPerBlock;
                if (wholeBlocks * blockAlign > soundData.Length)
                    Console.Error.WriteLine("Sample count exceeds the range of sound data.");
                int leftoverSamples = sampleCount - wholeBlocks * samplesPerBlock;
                if (leftoverSamples < 2) // a block always starts with at least two samples?
                    Console.Error.WriteLine("Improper trailing bytes/samples!");
                if (bitsPerSample != 4) // are MS ADPCM nibbles always 4-bit-sized?
                    Console.Error.WriteLine("Nibble size is expected to be 4 bits!");
                int leftoverNibbles = (leftoverSamples - 2) * channelCount;
                int leftoverBytes = 7 * channelCount
                    + (int)Math.Ceiling((leftoverNibbles * bitsPerSample) * 0.125);
                Array.Resize(ref soundData, wholeBlocks * blockAlign + leftoverBytes);
            }
        }

        public WavFormat Format => format;
        public int ChannelCount => channelCount;
        public int SampleRate => sampleRate;
        public int AverageBytesPerSecond => averageBytesPerSecond;
        public int BlockAlign => blockAlign;
        public int BitsPerSample => bitsPerSample;
        public int SampleCount => sampleCount;
        public byte[] ExtraData => extraData;
        public byte[] SoundData => soundData;
    }
}
