//! \file       AudioMIO.cs
//! \date       Thu May 28 13:33:07 2015
//! \brief      Entis audio format implementation.
//
// Copyright (C) 2015 by morkt
//
// Permission is hereby granted, free of charge, to any person obtaining a copy
// of this software and associated documentation files (the "Software"), to
// deal in the Software without restriction, including without limitation the
// rights to use, copy, modify, merge, publish, distribute, sublicense, and/or
// sell copies of the Software, and to permit persons to whom the Software is
// furnished to do so, subject to the following conditions:
//
// The above copyright notice and this permission notice shall be included in
// all copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS
// IN THE SOFTWARE.
//

using System;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.ComponentModel;
using System.ComponentModel.Composition;
using System.Diagnostics;
using System.IO;
using System.Threading;
using GameRes.Utility;

namespace GameRes.Formats.Entis
{
    [Export(typeof(AudioFormat))]
    public class MioAudio : AudioFormat
    {
        public override string         Tag { get { return "MIO"; } }
        public override string Description { get { return "Entis engine compressed audio format"; } }
        public override uint     Signature { get { return 0x69746e45u; } } // 'Enti'

        public override SoundInput TryOpen (IBinaryStream file)
        {
            var header = file.ReadHeader (0x40);
            if (0x03000100 != header.ToUInt32 (8))
                return null;
            if (!header.AsciiEqual (0x10, "Music Interleaved"))
                return null;

            return new MioInput (file.AsStream);
        }
    }

    public class MioInput : SoundInput
    {
        MioInfoHeader           m_info;
        long                    m_stream_pos;
        int                     m_bitrate;
        uint                    m_total_samples;
        ERISADecodeContext      m_pmioc;
        MioDecoder              m_pmiod;
        Stream                  m_decoded_stream;

        public int   ChannelCount { get { return m_info.ChannelCount; } }
        public uint BitsPerSample { get { return m_info.BitsPerSample; } }

        public override int   SourceBitrate { get { return m_bitrate; } }
        public override string SourceFormat { get { return "mio"; } }

        #region Stream Members
        public override bool        CanSeek { get { return m_decoded_stream.CanSeek; } }

        public override long Position
        {
            get { return m_decoded_stream.Position; }
            set { m_decoded_stream.Position = value; }
        }

        public override int Read (byte[] buffer, int offset, int count)
        {
            return Read_Threaded (buffer, offset, count);
        }
        #endregion

        public MioInput (Stream file) : base (file)
        {
            file.Position = 0x40;
            using (var erif = new EriFile (file))
            {
                var section = erif.ReadSection();
                if (section.Id != "Header  " || section.Length <= 0 || section.Length > int.MaxValue)
                    throw new InvalidFormatException();
                m_stream_pos = 0x50 + section.Length;
                int header_size = (int)section.Length;
                while (header_size > 0x10)
                {
                    section = erif.ReadSection();
                    header_size -= 0x10;
                    if (section.Length <= 0 || section.Length > header_size)
                        break;
                    if ("SoundInf" == section.Id)
                    {
                        m_info = new MioInfoHeader();
                        m_info.Version        = erif.ReadInt32();
                        m_info.Transformation = (CvType)erif.ReadInt32();
                        m_info.Architecture   = (EriCode)erif.ReadInt32();
                        m_info.ChannelCount   = erif.ReadInt32();
                        m_info.SamplesPerSec  = erif.ReadUInt32();
                        m_info.BlocksetCount  = erif.ReadUInt32();
                        m_info.SubbandDegree  = erif.ReadInt32();
                        m_info.AllSampleCount = erif.ReadUInt32();
                        m_info.LappedDegree   = erif.ReadUInt32();
                        m_info.BitsPerSample  = erif.ReadUInt32();
                        break;
                    }
                    header_size -= (int)section.Length;
                    erif.BaseStream.Seek (section.Length, SeekOrigin.Current);
                }
                if (null == m_info)
                    throw new InvalidFormatException ("MIO sound header not found");

                erif.BaseStream.Position = m_stream_pos;
                var stream_size = erif.FindSection ("Stream  ");
                m_stream_pos = erif.BaseStream.Position;

                m_pmiod = new MioDecoder (m_info);
                if (EriCode.Nemesis != m_info.Architecture)
                    m_pmioc = new HuffmanDecodeContext (0x10000);
                else
                    throw new NotImplementedException ("MIO Nemesis encoding not implemented");

                int pcm_bitrate = (int)(m_info.SamplesPerSec * BitsPerSample * ChannelCount);
                var format = new GameRes.WaveFormat();
                format.FormatTag                = 1;
                format.Channels                 = (ushort)ChannelCount;
                format.SamplesPerSecond         = m_info.SamplesPerSec;
                format.BitsPerSample            = (ushort)BitsPerSample;
                format.BlockAlign               = (ushort)(BitsPerSample/8*format.Channels);
                format.AverageBytesPerSecond    = (uint)pcm_bitrate/8;
                this.Format = format;
                m_decoded_stream = LoadChunks (erif);

                if (0 != m_total_samples)
                    m_bitrate = (int)(stream_size * 8 * m_info.SamplesPerSec / m_total_samples);
                this.PcmSize = m_total_samples * ChannelCount * BitsPerSample / 8;
                m_decoded_stream.Position = 0;
            }
        }

        class MioChunk : MioDataHeader
        {
            public uint FirstSample;
            public long Position;
            public uint Size;
        }

        class ChunkStream : InputProxyStream
        {
            MioChunk    m_chunk;

            public ChunkStream (Stream source, MioChunk chunk) : base (source, true)
            {
                m_chunk = chunk;
                BaseStream.Position = m_chunk.Position;
            }

            public override bool  CanRead { get { return true; } }
            public override long   Length { get { return m_chunk.Size; } }

            public override long Position
            {
                get { return BaseStream.Position-m_chunk.Position; }
                set { Seek (value, SeekOrigin.Begin); }
            }

            public override long Seek (long offset, SeekOrigin origin)
            {
                if (origin == SeekOrigin.Begin)
                    offset += m_chunk.Position;
                else if (origin == SeekOrigin.Current)
                    offset += BaseStream.Position;
                else
                    offset += m_chunk.Position + m_chunk.Size;
                if (offset < m_chunk.Position)
                    offset = m_chunk.Position;
                BaseStream.Position = offset;
                return offset - m_chunk.Position;
            }

            public override int Read (byte[] buf, int index, int count)
            {
                long remaining = (m_chunk.Position + m_chunk.Size) - BaseStream.Position;
                if (count > remaining)
                    count = (int)remaining;
                if (count <= 0)
                    return 0;
                return BaseStream.Read (buf, index, count);
            }
        }

        private Stream LoadChunks (EriFile erif)
        {
            uint current_sample = 0;
            List<MioChunk> chunks = new List<MioChunk>();
            try
            {
                erif.BaseStream.Position = m_stream_pos;
                for (;;)
                {
                    long chunk_length = erif.FindSection ("SoundStm");
                    if (chunk_length > int.MaxValue)
                        throw new FileSizeException();
                    var chunk = new MioChunk();
                    chunk.FirstSample = current_sample;
                    chunk.Version     = erif.ReadByte();
                    chunk.Flags       = erif.ReadByte();
                    erif.ReadInt16();
                    chunk.SampleCount = erif.ReadUInt32();
                    chunk.Position    = erif.BaseStream.Position;
                    chunk.Size        = (uint)(chunk_length - 8);
                    current_sample += chunk.SampleCount;
                    chunks.Add (chunk);
                    erif.BaseStream.Seek (chunk.Size, SeekOrigin.Current);
                }
            }
            catch (EndOfStreamException) { /* ignore EOF errors */ }
            m_total_samples = current_sample;
            if (0 == m_total_samples)
            {
                m_decode_finished = true;
                return Stream.Null;
            }
            uint sample_bytes = (uint)ChannelCount * BitsPerSample / 8;
            var total_bytes = m_total_samples * sample_bytes;

            m_wait_handles = new WaitHandle[2] { m_available_chunk, m_decode_complete };
            m_chunk_queue = new ConcurrentQueue<byte[]>();
            m_worker = new BackgroundWorker();
            m_worker.WorkerSupportsCancellation = true;
            m_worker.DoWork += DoWork_Decode;
            m_worker.RunWorkerAsync (chunks);
            return new MemoryStream ((int)total_bytes);
        }

        bool                    m_decode_finished = false;
        AutoResetEvent          m_decode_complete = new AutoResetEvent (false);
        AutoResetEvent          m_available_chunk = new AutoResetEvent (false);
        WaitHandle[]            m_wait_handles;

        ConcurrentQueue<byte[]> m_chunk_queue;
        BackgroundWorker        m_worker;
        Exception               m_decode_error = null;

        private void DoWork_Decode (object sender, DoWorkEventArgs e)
        {
            try
            {
                var worker = sender as BackgroundWorker;
                var chunks = e.Argument as IEnumerable<MioChunk>;
                uint sample_bytes = (uint)ChannelCount * BitsPerSample / 8;
                foreach (var chunk in chunks)
                {
                    if (worker.CancellationPending)
                    {
                        e.Cancel = true;
                        break;
                    }
                    using (var input = new ChunkStream (Source, chunk))
                    {
                        var wave_buf = new byte[chunk.SampleCount * sample_bytes];
                        m_pmioc.AttachInputFile (input);
                        if (!m_pmiod.DecodeSound (m_pmioc, chunk, wave_buf, 0))
                            throw new InvalidFormatException();
                        m_chunk_queue.Enqueue (wave_buf);
                        m_available_chunk.Set();
                    }
                }
            }
            catch (Exception X)
            {
                Trace.WriteLine (X.Message, "[MIO]");
                m_decode_error = X;
            }
            finally
            {
                m_decode_complete.Set();
            }
        }

        private int Read_Threaded (byte[] buf, int idx, int count)
        {
            var current_pos = Position;
            int total_read = 0;
            if (current_pos < m_decoded_stream.Length)
            {
                int available_bytes = (int)(m_decoded_stream.Length - current_pos);
                int read = m_decoded_stream.Read (buf, idx, Math.Min (count, available_bytes));
                idx += read;
                count -= read;
                total_read += read;
            }
            if (count > 0 && (!m_decode_finished || m_chunk_queue.Count > 0))
            {
                current_pos = Position;
                m_decoded_stream.Seek (0, SeekOrigin.End);
                for (;;)
                {
                    byte[] wave_buf = null;
                    while (m_chunk_queue.TryDequeue (out wave_buf))
                    {
                        m_decoded_stream.Write (wave_buf, 0, wave_buf.Length);
                        if (current_pos + count <= m_decoded_stream.Length)
                            break;
                    }
                    if (m_decode_finished || (current_pos + count <= m_decoded_stream.Length))
                        break;
                    int evt = WaitHandle.WaitAny (m_wait_handles);
                    if (1 == evt)
                    {
                        m_decode_finished = true;
                        if (m_decode_error != null)
                        {
                            m_decoded_stream.Position = current_pos;
                            throw m_decode_error;
                        }
                    }
                }
                m_decoded_stream.Position = current_pos;
                total_read += m_decoded_stream.Read (buf, idx, count);
            }
            return total_read;
        }

        #region IDisposable Members
        bool _mio_disposed = false;
        protected override void Dispose (bool disposing)
        {
            if (!_mio_disposed)
            {
                if (disposing)
                {
                    if (!m_decode_finished)
                    {
                        m_worker.CancelAsync();
                        m_decode_complete.WaitOne();
                    }
                    if (m_decoded_stream != null)
                        m_decoded_stream.Dispose();
                    m_decode_complete.Dispose();
                    m_available_chunk.Dispose();
                    m_worker.Dispose();
                }
                _mio_disposed = true;
                base.Dispose (disposing);
            }
        }
        #endregion
    }
}
