﻿using System;
using System.Collections.Generic;
using System.Text;

namespace NLayer.Decoder
{
    class MpegFrame : FrameBase, IMpegFrame
    {
        static readonly int[][][] _bitRateTable =
        {
            new int[][]
            {
                new int[] { 0, 32, 64, 96, 128, 160, 192, 224, 256, 288, 320, 352, 384, 416, 448 },
                new int[] { 0, 32, 48, 56, 64, 80, 96, 112, 128, 160, 192, 224, 256, 320, 384 },
                new int[] { 0, 32, 40, 48, 56, 64, 80, 96, 112, 128, 160, 192, 224, 256, 320 }
            },
            new int[][]
            {
                new int[] { 0, 32, 48, 56, 64, 80, 96, 112, 128, 144, 160, 176, 192, 224, 256 },
                new int[] { 0, 8, 16, 24, 32, 40, 48, 56, 64, 80, 96, 112, 128, 144, 160 },
                new int[] { 0, 8, 16, 24, 32, 40, 48, 56, 64, 80, 96, 112, 128, 144, 160 }
            },
        };

        internal static MpegFrame TrySync(uint syncMark)
        {
            if ((syncMark & 0xFFE00000) == 0xFFE00000   // frame sync
             && (syncMark & 0x00180000) != 0x00080000   // MPEG version != reserved
             && (syncMark & 0x00060000) != 0x00000000   // layer version != reserved
             && (syncMark & 0x0000F000) != 0x0000F000   // bitrate != bad
             && (syncMark & 0x00000C00) != 0x00000C00   // sample rate != reserved
               )
            {
                // now check the stereo modes
                switch ((syncMark >> 4) & 0xF)
                {
                    case 0x0:   // stereo
                    case 0x4:   // joint stereo
                    case 0x5:   // "
                    case 0x6:   // "
                    case 0x7:   // "
                    case 0x8:   // dual channel
                    case 0xC:   // mono
                        return new MpegFrame { _syncBits = (int)syncMark };
                }
            }

            return null;
        }

        // base: 2xIntPtr, 1x8, 1x4
        // total: 3xIntPtr, 4x8, 4x4
        // Long Mode: 72 bytes per instance
        // Prot Mode: 60 bytes per instance

        // IntPtr
        internal MpegFrame Next;
        // 4
        internal int Number;

        // 4
        int _syncBits;

        // 8
        int _readOffset, _bitsRead;
        // 8
        ulong _bitBucket = 0UL;
        // 8
        long _offset;
        // 4
        bool _isMuted;

        MpegFrame()
        {

        }

        protected override int Validate()
        {
            // TrySync has already validated version, layer, bitrate, and samplerate

            // if layer2, we have to verify the channel mode is valid for the bitrate selected
            if (Layer == MpegLayer.LayerII)
            {
                switch (BitRate)
                {
                    case 32000:
                    case 48000:
                    case 56000:
                    case 80000:
                        // don't allow anything except mono
                        if (ChannelMode != MpegChannelMode.Mono) return -1;
                        break;
                    case 224000:
                    case 256000:
                    case 320000:
                    case 384000:
                        // don't allow mono
                        if (ChannelMode == MpegChannelMode.Mono) return -1;
                        break;
                }
            }

            // calculate the frame's length
            int frameSize;
            if (BitRateIndex > 0)
            {
                if (Layer == MpegLayer.LayerI)
                {
                    frameSize = (12 * BitRate / SampleRate + Padding) * 4;
                }
                else
                {
                    frameSize = 144 * BitRate / SampleRate + Padding;
                }
            }
            else
            {
                // "free" frame...  we have to calculate it later
                frameSize = _readOffset + GetSideDataSize() + Padding; // we know the frame will be at least this big...
            }

            // now check the crc if one is present
            if (HasCrc)
            {
                // prep for CRC reading
                _readOffset = 4 + (HasCrc ? 2 : 0);

                // check the CRC
                if (!ValidateCRC())
                {
                    // mute this frame
                    _isMuted = true;
                    return 6;   // header + crc...  force the reader to re-sync
                }
            }

            // prep for reading
            Reset();

            // finally, let our caller know how big this frame is (including the sync header)
            return frameSize;
        }

        internal int GetSideDataSize()
        {
            switch (Layer)
            {
                case MpegLayer.LayerI:
                    if (ChannelMode == MpegChannelMode.Mono)
                    {
                        // mono
                        return 16;
                    }
                    else if (ChannelMode == MpegChannelMode.Stereo || ChannelMode == MpegChannelMode.DualChannel)
                    {
                        // full stereo / dual channel
                        return 32;
                    }
                    else
                    {
                        // joint stereo...  ugh...
                        switch (ChannelModeExtension)
                        {
                            case 0:
                                return 18;
                            case 1:
                                return 20;
                            case 2:
                                return 22;
                            case 3:
                                return 24;
                        }
                    }
                    break;
                case MpegLayer.LayerII:
                    return 0;
                case MpegLayer.LayerIII:
                    if (ChannelMode == MpegChannelMode.Mono && Version >= MpegVersion.Version2)
                    {
                        return 9;
                    }
                    else if (ChannelMode != MpegChannelMode.Mono && Version < MpegVersion.Version2)
                    {
                        return 32;
                    }
                    else
                    {
                        return 17;
                    }
            }

            return 0;
        }

        bool ValidateCRC()
        {
            var crc = 0xFFFFU;

            // process the common bits...
            UpdateCRC(_syncBits, 16, ref crc);

            var apply = false;
            switch (Layer)
            {
                case MpegLayer.LayerI:
                    apply = LayerIDecoder.GetCRC(this, ref crc);
                    break;
                case MpegLayer.LayerII:
                    apply = LayerIIDecoder.GetCRC(this, ref crc);
                    break;
                case MpegLayer.LayerIII:
                    apply = LayerIIIDecoder.GetCRC(this, ref crc);
                    break;
            }

            if (apply)
            {
                var checkCrc = ReadByte(4) << 8 | ReadByte(5);
                return checkCrc == crc;
            }

            return true;
        }

        static internal void UpdateCRC(int data, int length, ref uint crc)
        {
            var masking = 1U << length;
            while ((masking >>= 1) != 0)
            {
                var carry = crc & 0x8000;
                crc <<= 1;
                if ((carry == 0) ^ ((data & masking) == 0))
                {
                    crc ^= 0x8005;
                }
            }
            crc &= 0xFFFF;
        }

        internal VBRInfo ParseVBR()
        {
            var buf = new byte[4];

            // Xing first
            int offset;
            if (Version == MpegVersion.Version1 && ChannelMode != MpegChannelMode.Mono)
            {
                offset = 32 + 4;
            }
            else if (Version > MpegVersion.Version1 && ChannelMode == MpegChannelMode.Mono)
            {
                offset = 9 + 4;
            }
            else
            {
                offset = 17 + 4;
            }

            if (Read(offset, buf) != 4) return null;
            if (buf[0] == 'X' && buf[1] == 'i' && buf[2] == 'n' && buf[3] == 'g'
             || buf[0] == 'I' && buf[1] == 'n' && buf[2] == 'f' && buf[3] == 'o')
            {
                return ParseXing(offset + 4);
            }

            // then VBRI (kinda rare)
            if (Read(36, buf) != 4) return null;
            if (buf[0] == 'V' && buf[1] == 'B' && buf[2] == 'R' && buf[3] == 'I')
            {
                return ParseVBRI();
            }

            return null;
        }

        VBRInfo ParseXing(int offset)
        {
            VBRInfo info = new VBRInfo();
            info.Channels = Channels;
            info.SampleRate = SampleRate;
            info.SampleCount = SampleCount;

            var buf = new byte[100];
            if (Read(offset, buf, 0, 4) != 4) return null;
            offset += 4;

            var flags = buf[0] << 24 | buf[1] << 16 | buf[2] << 8 | buf[3];

            // frame count
            if ((flags & 0x1) != 0)
            {
                if (Read(offset, buf, 0, 4) != 4) return null;
                offset += 4;
                info.VBRFrames = buf[0] << 24 | buf[1] << 16 | buf[2] << 8 | buf[3];
            }

            // byte count
            if ((flags & 0x2) != 0)
            {
                if (Read(offset, buf, 0, 4) != 4) return null;
                offset += 4;
                info.VBRBytes = buf[0] << 24 | buf[1] << 16 | buf[2] << 8 | buf[3];
            }

            // TOC
            if ((flags & 0x4) != 0)
            {
                // we're not using the TOC, so just discard it
                if (Read(offset, buf) != 100) return null;
                offset += 100;
            }

            // scale
            if ((flags & 0x8) != 0)
            {
                if (Read(offset, buf, 0, 4) != 4) return null;
                offset += 4;
                info.VBRQuality = buf[0] << 24 | buf[1] << 16 | buf[2] << 8 | buf[3];
            }

            //// now look for a LAME header (note: if it isn't found, it doesn't fail the VBR parse)
            //do
            //{
            //    if (Read(offset, buf, 0, 20) != 20) break;
            //    offset += 20;

            //    // LAME tag revision: only 0 and 1 are valid
            //    if ((buf[9] & 0xF0) > 0x10) break;

            //    // VBR mode: 0-6, 8 & 9 are valid
            //    var mode = buf[9] & 0xF;
            //    if (mode == 7 || mode > 9) break;

            //    // Lowpass filter value
            //    var lowpass = buf[10] / 100.0;

            //    // Replay Gain
            //    var rgPeak = BitConverter.ToSingle(buf, 11);
            //    var rgGainRadio = buf[15] << 8 | buf[16];
            //    var rgGain = buf[17] << 8 | buf[18];
            //} while (false);

            return info;
        }

        VBRInfo ParseVBRI()
        {
            VBRInfo info = new VBRInfo();
            info.Channels = Channels;
            info.SampleRate = SampleRate;
            info.SampleCount = SampleCount;

            // VBRI is "fixed" size...  Yay. :)
            var buf = new byte[26];
            if (Read(36, buf) != 26) return null;

            // Version
            var version = buf[4] << 8 | buf[5];

            // Delay
            info.VBRDelay = buf[6] << 8 | buf[7];

            // Quality
            info.VBRQuality = buf[8] << 8 | buf[9];

            // Bytes
            info.VBRBytes = buf[10] << 24 | buf[11] << 16 | buf[12] << 8 | buf[13];

            // Frames
            info.VBRFrames = buf[14] << 24 | buf[15] << 16 | buf[16] << 8 | buf[17];

            // TOC
            // entries
            var tocEntries = buf[18] << 8 | buf[19];
            var tocScale = buf[20] << 8 | buf[21];
            var tocEntrySize = buf[22] << 8 | buf[23];
            var tocFramesPerEntry = buf[24] << 8 | buf[25];
            var tocSize = tocEntries * tocEntrySize;

            var toc = new byte[tocSize];
            if (Read(62, toc) != tocSize) return null;

            return info;
        }

        public int FrameLength
        {
            get { return base.Length; }
        }

        public MpegVersion Version
        {
            get
            {
                switch ((_syncBits >> 19) & 3)
                {
                    case 0:
                        return MpegVersion.Version25;
                    case 2:
                        return MpegVersion.Version2;
                    case 3:
                        return MpegVersion.Version1;
                }
                return MpegVersion.Unknown;
            }
        }
        public MpegLayer Layer
        {
            get
            {
                // the order is backwards, and "0" is invalid
                return (MpegLayer)((4 - ((_syncBits >> 17) & 3)) % 4);
            }
        }
        public bool HasCrc
        {
            get { return (_syncBits & 0x10000) == 0; }
        }
        public int BitRate
        {
            get
            {
                if (BitRateIndex > 0)
                {
                    return _bitRateTable[(int)Version / 10 - 1][(int)Layer - 1][BitRateIndex] * 1000;
                }
                else
                {
                    // bitrate is always an even multiple of 1000, so round
                    return ((((FrameLength * 8) * SampleRate) / SampleCount + 499) + 500) / 1000 * 1000;
                }
            }
        }
        public int BitRateIndex
        {
            get { return (_syncBits >> 12) & 0xF; }
        }
        public int SampleRate
        {
            get
            {
                int sr;
                switch (SampleRateIndex)
                {
                    case 0: sr = 44100; break;
                    case 1: sr = 48000; break;
                    case 2: sr = 32000; break;
                    default: sr = 0; break;
                }
                if (Version > MpegVersion.Version1)
                {
                    if (Version == MpegVersion.Version25)
                    {
                        sr /= 4;
                    }
                    else
                    {
                        sr /= 2;
                    }
                }
                return sr;
            }
        }
        public int SampleRateIndex
        {
            get { return (_syncBits >> 10) & 0x3; }
        }
        private int Padding
        {
            get { return (_syncBits >> 9) & 0x1; }
        }
        public MpegChannelMode ChannelMode
        {
            get { return (MpegChannelMode)((_syncBits >> 6) & 0x3); }
        }
        public int ChannelModeExtension
        {
            get { return (_syncBits >> 4) & 0x3; }
        }
        internal int Channels
        {
            get { return (ChannelMode == MpegChannelMode.Mono ? 1 : 2); }
        }
        public bool IsCopyrighted
        {
            get { return (_syncBits & 0x8) == 0x8; }
        }
        internal bool IsOriginal
        {
            get { return (_syncBits & 0x4) == 0x4; }
        }
        internal int EmphasisMode
        {
            get { return (_syncBits & 0x3); }
        }
        public bool IsCorrupted
        {
            get { return _isMuted; }
        }

        public int SampleCount
        {
            get
            {
                if (Layer == MpegLayer.LayerI) return 384;
                if (Layer == MpegLayer.LayerIII && Version > MpegVersion.Version1) return 576;
                return 1152;
            }
        }
        internal long SampleOffset
        {
            get { return _offset; }
            set { _offset = value; }
        }


        public void Reset()
        {
            _readOffset = 4 + (HasCrc ? 2 : 0);
            _bitBucket = 0UL;
            _bitsRead = 0;
        }

        public int ReadBits(int bitCount)
        {
            if (bitCount < 1 || bitCount > 32) throw new ArgumentOutOfRangeException("bitCount");
            if (_isMuted) return 0;

            while (_bitsRead < bitCount)
            {
                var b = ReadByte(_readOffset);
                if (b == -1) throw new System.IO.EndOfStreamException();
                
                ++_readOffset;
                
                _bitBucket <<= 8;
                _bitBucket |= (byte)(b & 0xFF);
                _bitsRead += 8;
            }

            var temp = (int)((_bitBucket >> (_bitsRead - bitCount)) & ((1UL << bitCount) - 1));
            _bitsRead -= bitCount;
            return temp;
        }

#if DEBUG
        public override string ToString()
        {
            // version
            var sb = new StringBuilder("MPEG");
            switch (Version)
            {
                case MpegVersion.Version1: sb.Append("1"); break;
                case MpegVersion.Version2: sb.Append("2"); break;
                case MpegVersion.Version25: sb.Append("2.5"); break;
            }

            // layer
            sb.Append(" Layer ");
            switch (Layer)
            {
                case MpegLayer.LayerI: sb.Append("I"); break;
                case MpegLayer.LayerII: sb.Append("II"); break;
                case MpegLayer.LayerIII: sb.Append("III"); break;
            }

            // bitrate
            sb.AppendFormat(" {0} kbps ", BitRate / 1000);

            // channel mode
            switch (ChannelMode)
            {
                case MpegChannelMode.Stereo:
                    sb.Append("Stereo");
                    break;
                case MpegChannelMode.JointStereo:
                    sb.Append("Joint Stereo");
                    switch (ChannelModeExtension)
                    {
                        case 1: sb.Append(" (I)"); break;
                        case 2: sb.Append(" (M/S)"); break;
                        case 3: sb.Append(" (M/S,I)"); break;
                    }
                    break;
                case MpegChannelMode.DualChannel:
                    sb.Append("Dual Channel");
                    break;
                case MpegChannelMode.Mono:
                    sb.Append("Mono");
                    break;
            }

            // sample rate
            sb.AppendFormat(" {0} KHz", (float)SampleRate / 1000);

            var flagList = new List<string>();
            // protection
            if (HasCrc) flagList.Add("CRC");

            // copyright
            if (IsCopyrighted) flagList.Add("Copyright");

            // original
            if (IsOriginal) flagList.Add("Original");

            // emphasis
            switch (EmphasisMode)
            {
                case 1:
                    flagList.Add("50/15 ms");
                    break;
                case 2:
                    flagList.Add("Invalid Emphasis");
                    break;
                case 3:
                    flagList.Add("CCIT J.17");
                    break;
            }

            if (flagList.Count > 0)
            {
                sb.AppendFormat(" ({0})", string.Join(",", flagList.ToArray()));
            }

            return sb.ToString();
        }
#endif
    }
}
