#region License
//
// The Open Toolkit Library License
//
// Copyright (c) 2006 - 2009 the Open Toolkit library.
//
// 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.
//
#endregion

using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Runtime.InteropServices;

using OpenTK.Audio.OpenAL;

namespace OpenTK.Audio
{

    /// <summary>
    /// Provides methods to instantiate, use and destroy an audio device for recording.
    /// Static methods are provided to list available devices known by the driver.
    /// </summary>
    public sealed class AudioCapture : IDisposable
    {
        #region Fields

        // This must stay private info so the end-user cannot call any Alc commands for the recording device.
        IntPtr Handle;

        // Alc.CaptureStop should be called prior to device shutdown, this keeps track of Alc.CaptureStart/Stop calls.
        bool _isrecording = false;

        ALFormat sample_format;
        int sample_frequency;
        
        #endregion 

        #region Constructors

        static AudioCapture()
        {
            if (AudioDeviceEnumerator.IsOpenALSupported) // forces enumeration
            {
            }
        }

        /// <summary>
        /// Opens the default device for audio recording.
        /// Implicitly set parameters are: 22050Hz, 16Bit Mono, 4096 samples ringbuffer.
        /// </summary>
        public AudioCapture()
            : this(AudioCapture.DefaultDevice, 22050, ALFormat.Mono16, 4096)
        {
        }

        /// <summary>Opens a device for audio recording.</summary>
        /// <param name="deviceName">The device name.</param>
        /// <param name="frequency">The frequency that the data should be captured at.</param>
        /// <param name="sampleFormat">The requested capture buffer format.</param>
        /// <param name="bufferSize">The size of OpenAL's capture internal ring-buffer. This value expects number of samples, not bytes.</param>
        public AudioCapture(string deviceName, int frequency, ALFormat sampleFormat, int bufferSize)
        {
            if (!AudioDeviceEnumerator.IsOpenALSupported)
                throw new DllNotFoundException("openal32.dll");
            if (frequency <= 0)
                throw new ArgumentOutOfRangeException("frequency");
            if (bufferSize <= 0)
                throw new ArgumentOutOfRangeException("bufferSize");

            // Try to open specified device. If it fails, try to open default device.
            device_name = deviceName;
            Handle = Alc.CaptureOpenDevice(deviceName, frequency, sampleFormat, bufferSize);

            if (Handle == IntPtr.Zero)
            {
                Debug.WriteLine(ErrorMessage(deviceName, frequency, sampleFormat, bufferSize));
                device_name = "IntPtr.Zero";
                Handle = Alc.CaptureOpenDevice(null, frequency, sampleFormat, bufferSize);
            }

            if (Handle == IntPtr.Zero)
            {
                Debug.WriteLine(ErrorMessage("IntPtr.Zero", frequency, sampleFormat, bufferSize));
                device_name = AudioDeviceEnumerator.DefaultRecordingDevice;
                Handle = Alc.CaptureOpenDevice(AudioDeviceEnumerator.DefaultRecordingDevice, frequency, sampleFormat, bufferSize);
            }

            if (Handle == IntPtr.Zero)
            {
                // Everything we tried failed. Capture may not be supported, bail out.
                Debug.WriteLine(ErrorMessage(AudioDeviceEnumerator.DefaultRecordingDevice, frequency, sampleFormat, bufferSize));
                device_name = "None";

                throw new AudioDeviceException("All attempts to open capture devices returned IntPtr.Zero. See debug log for verbose list.");
            }

            // handle is not null, check for some Alc Error
            CheckErrors();

            SampleFormat = sampleFormat;
            SampleFrequency = frequency;
        }

        #endregion Constructor

        #region Public Members

        #region CurrentDevice

        private string device_name;

        /// <summary>
        /// The name of the device associated with this instance.
        /// </summary>
        public string CurrentDevice
        {
            get
            {
                return device_name;
            }
        }

        #endregion

        #region AvailableDevices

        /// <summary>
        /// Returns a list of strings containing all known recording devices.
        /// </summary>
        public static IList<string> AvailableDevices
        {
            get
            {
                return AudioDeviceEnumerator.AvailableRecordingDevices;
            }
        }

        #endregion

        #region DefaultDevice

        /// <summary>
        /// Returns the name of the device that will be used as recording default.
        /// </summary>
        public static string DefaultDevice
        {
            get
            {
                return AudioDeviceEnumerator.DefaultRecordingDevice;
            }
        }

        #endregion

        #region CheckErrors

        /// <summary>
        /// Checks for ALC error conditions.
        /// </summary>
        /// <exception cref="OutOfMemoryException">Raised when an out of memory error is detected.</exception>
        /// <exception cref="AudioValueException">Raised when an invalid value is detected.</exception>
        /// <exception cref="AudioDeviceException">Raised when an invalid device is detected.</exception>
        /// <exception cref="AudioContextException">Raised when an invalid context is detected.</exception>
        public void CheckErrors()
        {
            new AudioDeviceErrorChecker(Handle).Dispose();
        }

        #endregion

        #region CurrentError

        /// <summary>Returns the ALC error code for this device.</summary>
        public AlcError CurrentError
        {
            get
            {
                return Alc.GetError(Handle);
            }
        }

        #endregion

        #region Start & Stop

        /// <summary>
        /// Start recording samples.
        /// The number of available samples can be obtained through the <see cref="AvailableSamples"/> property.
        /// The data can be queried with any <see cref="ReadSamples"/> method.
        /// </summary>
        public void Start()
        {
            Alc.CaptureStart(Handle);
            _isrecording = true;
        }

        /// <summary>Stop recording samples. This will not clear previously recorded samples.</summary>
        public void Stop()
        {
            Alc.CaptureStop(Handle);
            _isrecording = false;
        }

        #endregion Start & Stop Capture

        #region AvailableSamples

        /// <summary>Returns the number of available samples for capture.</summary>
        public int AvailableSamples
        {
            get
            {
                // TODO: Investigate inconsistency between documentation and actual usage.
                // Doc claims the 3rd param is Number-of-Bytes, but it appears to be Number-of-Int32s
                int result;
                Alc.GetInteger(Handle, AlcGetInteger.CaptureSamples, 1, out result);
                return result;
            }
        }

        #endregion Available samples property

        #region ReadSamples

        /// <summary>Fills the specified buffer with samples from the internal capture ring-buffer. This method does not block: it is an error to specify a sampleCount larger than AvailableSamples.</summary>
        /// <param name="buffer">A pointer to a previously initialized and pinned array.</param>
        /// <param name="sampleCount">The number of samples to be written to the buffer.</param>
        public void ReadSamples(IntPtr buffer, int sampleCount)
        {
            Alc.CaptureSamples(Handle, buffer, sampleCount);
        }

        /// <summary>Fills the specified buffer with samples from the internal capture ring-buffer. This method does not block: it is an error to specify a sampleCount larger than AvailableSamples.</summary>
        /// <param name="buffer">The buffer to fill.</param>
        /// <param name="sampleCount">The number of samples to be written to the buffer.</param>
        /// <exception cref="System.ArgumentNullException">Raised when buffer is null.</exception>
        /// <exception cref="System.ArgumentOutOfRangeException">Raised when sampleCount is larger than the buffer.</exception>
        public void ReadSamples<TBuffer>(TBuffer[] buffer, int sampleCount)
            where TBuffer : struct
        {
            if (buffer == null)
                throw new ArgumentNullException("buffer");

            int buffer_size = BlittableValueType<TBuffer>.Stride * buffer.Length;
            // This is more of a heuristic than a 100% valid check. However, it will work
            // correctly for 99.9% of all use cases.
            // This should never produce a false positive, but a false negative might
            // be produced with compressed sample formats (which are very rare).
            // Still, this is better than no check at all.
            if (sampleCount * GetSampleSize(SampleFormat) > buffer_size)
                throw new ArgumentOutOfRangeException("sampleCount");

            GCHandle buffer_ptr = GCHandle.Alloc(buffer, GCHandleType.Pinned);
            try { ReadSamples(buffer_ptr.AddrOfPinnedObject(), sampleCount); }
            finally { buffer_ptr.Free(); }
        }

        #endregion

        #region SampleFormat & SampleFrequency

        /// <summary>
        /// Gets the OpenTK.Audio.ALFormat for this instance.
        /// </summary>
        public ALFormat SampleFormat
        {
            get { return sample_format; }
            private set { sample_format = value; }
        }

        /// <summary>
        /// Gets the sampling rate for this instance.
        /// </summary>
        public int SampleFrequency
        {
            get { return sample_frequency; }
            private set { sample_frequency = value; }
        }

        #endregion

        #region IsRunning

        /// <summary>
        /// Gets a value indicating whether this instance is currently capturing samples.
        /// </summary>
        public bool IsRunning
        {
            get { return _isrecording; }
        }

        #endregion

        #endregion

        #region Private Members

        // Retrieves the sample size in bytes for various ALFormats.
        // Compressed formats always return 1.
        static int GetSampleSize(ALFormat format)
        {
            switch (format)
            {
                case ALFormat.Mono8: return 1;
                case ALFormat.Mono16: return 2;
                case ALFormat.Stereo8: return 2;
                case ALFormat.Stereo16: return 4;
                case ALFormat.MonoFloat32Ext: return 4;
                case ALFormat.MonoDoubleExt: return 8;
                case ALFormat.StereoFloat32Ext: return 8;
                case ALFormat.StereoDoubleExt: return 16;

                case ALFormat.MultiQuad8Ext: return 4;
                case ALFormat.MultiQuad16Ext: return 8;
                case ALFormat.MultiQuad32Ext: return 16;

                case ALFormat.Multi51Chn8Ext: return 6;
                case ALFormat.Multi51Chn16Ext: return 12;
                case ALFormat.Multi51Chn32Ext: return 24;

                case ALFormat.Multi61Chn8Ext: return 7;
                case ALFormat.Multi71Chn16Ext: return 14;
                case ALFormat.Multi71Chn32Ext: return 28;

                case ALFormat.MultiRear8Ext: return 1;
                case ALFormat.MultiRear16Ext: return 2;
                case ALFormat.MultiRear32Ext: return 4;

                default: return 1; // Unknown sample size.
            }
        }

        // Converts an error code to an error string with additional information.
        string ErrorMessage(string devicename, int frequency, ALFormat bufferformat, int buffersize)
        {
            string alcerrmsg;
            AlcError alcerrcode = CurrentError;
            switch (alcerrcode)
            {
                case AlcError.OutOfMemory:
                    alcerrmsg = alcerrcode.ToString() + ": The specified device is invalid, or can not capture audio.";
                    break;
                case AlcError.InvalidValue:
                    alcerrmsg = alcerrcode.ToString() + ": One of the parameters has an invalid value.";
                    break;
                default:
                    alcerrmsg = alcerrcode.ToString();
                    break;
            }
            return "The handle returned by Alc.CaptureOpenDevice is null." +
                   "\nAlc Error: " + alcerrmsg +
                   "\nDevice Name: " + devicename +
                   "\nCapture frequency: " + frequency +
                   "\nBuffer format: " + bufferformat +
                   "\nBuffer Size: " + buffersize;
        }

        #endregion

        #region IDisposable Members

        /// <summary>
        /// Finalizes this instance.
        /// </summary>
        ~AudioCapture()
        {
            Dispose();
        }

        private bool IsDisposed;

        /// <summary>Closes the device and disposes the instance.</summary>
        public void Dispose()
        {
            this.Dispose(true);
            GC.SuppressFinalize(this);
        }

        private void Dispose(bool manual)
        {
            if (!this.IsDisposed)
            {
                if (this.Handle != IntPtr.Zero)
                {
                    if (this._isrecording)
                        this.Stop();

                    Alc.CaptureCloseDevice(this.Handle);
                }
                this.IsDisposed = true;
            }
        }

        #endregion Destructor
    }
}
