//! \file       AudioADX.cs
//! \date       Wed Mar 09 11:21:57 2016
//! \brief      CRI MiddleWare ADPCM audio.
//
// Copyright (C) 2016 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.ComponentModel.Composition;
using System.IO;
using System.Threading;
using GameRes.Utility;

namespace GameRes.Formats.Cri
{
    [Export(typeof(AudioFormat))]
    public class AdxAudio : AudioFormat
    {
        public override string         Tag { get { return "ADX"; } }
        public override string Description { get { return "CRI MiddleWare ADPCM audio"; } }
        public override uint     Signature { get { return 0; } }

        public override SoundInput TryOpen (IBinaryStream file)
        {
            uint signature = file.Signature;
            if (0x80 != (signature & 0xFFFF))
                return null;
            int header_size = (int)Binary.BigEndian (signature & 0xFFFF0000);
            if (header_size < 0x10 || header_size >= file.Length)
                return null;
            var header = file.ReadBytes (header_size);
            if (header.Length != header_size)
                return null;
            if (!Binary.AsciiEqual (header, header.Length-6, "(c)CRI"))
                return null;

            return new AdxInput (file.AsStream, header);
        }
    }

    internal class AdxInput : SoundInput
    {
        AdxReader       m_reader;
        int             m_data_offset;
        int             m_bitrate;
        long            m_position;
        int             m_buffered_sample;
        int             m_buffered_count;
        int             m_bytes_per_frame;
        ThreadLocal<short[]> m_frame_buffer;

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

        public AdxInput (Stream file, byte[] header) : base (file)
        {
            m_reader = new AdxReader (file, header);
            m_data_offset = 4 + header.Length;
            this.Format = m_reader.Format;
            this.PcmSize = m_reader.SampleCount * Format.BlockAlign;
            m_bitrate = (int)(Format.SamplesPerSecond * (file.Length-m_data_offset) * 8 / m_reader.SampleCount);
            int frame_buffer_length = m_reader.SamplesPerFrame * m_reader.Format.Channels;
            m_frame_buffer = new ThreadLocal<short[]> (() => new short[frame_buffer_length]);
            m_bytes_per_frame = frame_buffer_length * Format.BitsPerSample / 8;
        }

        public override int Read (byte[] buffer, int offset, int count)
        {
            int total_read = 0;
            int current_sample = (int)(m_position / Format.BlockAlign);
            bool need_refill = !(current_sample >= m_buffered_sample && current_sample < m_buffered_sample + m_buffered_count);
            int src_offset = (int)(m_position % m_bytes_per_frame);
            while (count > 0 && m_position < PcmSize)
            {
                if (need_refill)
                    FillBuffer();
                int available = Math.Min (count, m_buffered_count * Format.BlockAlign - src_offset);
                Buffer.BlockCopy (m_frame_buffer.Value, src_offset, buffer, offset, available);
                offset += available;
                count -= available;
                total_read += available;
                m_position += available;
                src_offset = 0;
                need_refill = true;
            }
            return total_read;
        }

        void FillBuffer ()
        {
            int frame_number = (int)(m_position / m_bytes_per_frame);
            m_reader.SetPosition (m_data_offset + frame_number * m_reader.FrameSize * Format.Channels);
            for (int i = 0; i < Format.Channels; ++i)
            {
                m_reader.DecodeFrame (i, m_frame_buffer.Value);
            }
            m_buffered_sample = frame_number * m_reader.SamplesPerFrame;
            m_buffered_count = Math.Min (m_reader.SampleCount - m_buffered_sample, m_reader.SamplesPerFrame);
        }

        // FIXME
        // such implementation of seek is broken since it doesn't take into account ADPCM decoder 'history',
        // therefore CanSeek returns false.

        public override bool CanSeek { get { return false; } }
        public override long Position
        {
            get { return m_position; }
            set { m_position = Math.Max (value, 0); }
        }

        #region IDisposable Members
        bool _adx_disposed = false;
        protected override void Dispose (bool disposing)
        {
            if (!_adx_disposed)
            {
                if (disposing)
                {
                    m_reader.Dispose();
                    m_frame_buffer.Dispose();
                }
                _adx_disposed = true;
                base.Dispose (disposing);
            }
        }
        #endregion
    }

    internal sealed class AdxReader : IDisposable
    {
        MsbBitStream    m_input;
        WaveFormat      m_format;
        int             m_samples_per_frame;
        int             m_prev_scale0;
        int             m_prev_scale1;
        int[][]         m_prev_samples;

        public WaveFormat   Format { get { return m_format; } }
        public int SamplesPerFrame { get { return m_samples_per_frame; } }
        public int     SampleCount { get; private set; }
        public int       FrameSize { get; private set; }
        public int   BitsPerSample { get; private set; }

        public AdxReader (Stream file, byte[] header)
        {
            FrameSize = header[1];
            BitsPerSample = header[2];
            if (header[0] != 3 || FrameSize != 0x12 || BitsPerSample != 4)
                throw new InvalidFormatException();
            m_samples_per_frame = (FrameSize - 2) * 8 / BitsPerSample;
            m_format.Channels = header[3];
            if (0 == m_format.Channels || m_format.Channels > 16)
                throw new InvalidFormatException();
            int encoding = BigEndian.ToInt16 (header, 0x0E);
            if (encoding != 0x0400)
                throw new NotSupportedException ("Not supported ADX encoding");

            m_format.FormatTag              = 1;
            m_format.SamplesPerSecond       = BigEndian.ToUInt32 (header, 4);
            m_format.BitsPerSample          = 16;
            m_format.BlockAlign             = (ushort)(m_format.BitsPerSample * m_format.Channels / 8);
            m_format.AverageBytesPerSecond  = m_format.SamplesPerSecond * m_format.BlockAlign;
            SampleCount = BigEndian.ToInt32 (header, 8);

            int lowest_freq = BigEndian.ToUInt16 (header, 12);
            var sqrt2 = Math.Sqrt (2.0);
            var x = sqrt2 - Math.Cos (2 * Math.PI * lowest_freq / m_format.SamplesPerSecond);
            var y = sqrt2 - 1;
            var z = (x - Math.Sqrt ((x + y) * (x - y))) / y;

            m_prev_scale0 = (int)Math.Floor (z * 8192);
            m_prev_scale1 = (int)Math.Floor (z * z * -4096);
            m_prev_samples = new int[m_format.Channels][];
            for (int i = 0; i < m_format.Channels; ++i)
            {
                m_prev_samples[i] = new int[2];
            }

            file.Position = 4+header.Length;
            m_input = new MsbBitStream (file, true);
        }

        /*
        public void Decode (Stream output)
        {
            int block_align = (int)m_format.BlockAlign;
            var frame = new short[m_samples_per_frame * Format.Channels];
            int sample = 0;
            while (sample < SampleCount)
            {
                int channel_offset = 0;
                for (int i = 0; i < m_format.Channels; ++i)
                {
                    DecodeFrame (i, frame);
                }
                int samples_in_frame = Math.Min (SampleCount - sample, m_samples_per_frame);
                output.Write (frame, 0, samples_in_frame * block_align); // XXX convert to byte array
                sample += samples_in_frame;
            }
        }
        */

        public void SetPosition (long position)
        {
            m_input.Input.Position = position;
            m_input.Reset();
        }

        public void DecodeFrame (int channel, short[] output)
        {
            int offset = channel;
            var prev_samples = m_prev_samples[channel];
            int scale = (short)m_input.GetBits (16) + 1;
            for (int i = 0; i < m_samples_per_frame; ++i)
            {
                int sample = NibbleToSigned (m_input.GetBits (BitsPerSample));
                int adjust = (m_prev_scale0 * prev_samples[0] + m_prev_scale1 * prev_samples[1]) >> 12;
                sample = Clamp16 (sample * scale + adjust);
                output[offset] = (short)sample;
                offset += m_format.Channels;
                prev_samples[1] = prev_samples[0];
                prev_samples[0] = sample;
            }
        }

        static int NibbleToSigned (int n)
        {
            return (n & 7) - (n & 8);
        }

        static int Clamp16 (int sample)
        {
            if (sample > 0x7FFF)
                sample = 0x7FFF;
            else if (sample < -0x8000)
                sample = -0x8000;
            return sample;
        }

        #region IDisposable Members
        bool _disposed = false;
        public void Dispose ()
        {
            if (!_disposed)
            {
                m_input.Dispose();
                _disposed = true;
            }
        }
        #endregion
    }
}
