// Copyright (c) Microsoft Corporation. All rights reserved.
// Licensed under the MIT License. See LICENSE in the project root for license information.

using System;
using System.Globalization;
using System.Threading;
using UnityEngine;
using HoloToolkit.Unity;
using HoloToolkit.Unity.InputModule;

namespace HoloToolkit.Sharing.VoiceChat
{
    /// <summary>
    /// Transmits data from your microphone to other clients connected to a SessionServer. Requires any receiving client to be running the MicrophoneReceiver script.
    /// </summary>
    [RequireComponent(typeof(AudioSource))]
    public class MicrophoneTransmitter : MonoBehaviour
    {
        /// <summary>
        /// Which type of microphone/quality to access
        /// </summary>
        public MicStream.StreamCategory Streamtype = MicStream.StreamCategory.HIGH_QUALITY_VOICE;

        /// <summary>
        /// You can boost volume here as desired. 1 is default but probably too quiet. You can change during operation. 
        /// </summary>
        public float InputGain = 2;

        /// <summary>
        /// Whether or not to send the microphone data across the network
        /// </summary>
        public bool ShouldTransmitAudio = true;

        /// <summary>
        /// Whether other users should be able to hear the transmitted audio
        /// </summary>
        public bool Mute;

        public Transform GlobalAnchorTransform;

        public bool ShowInterPacketTime;

        private DateTime timeOfLastPacketSend;
        private float worstTimeBetweenPackets;

        private int sequenceNumber;

        private int sampleRateType = 3; // 48000Hz

        private AudioSource audioSource;

        private bool hasServerConnection;
        private bool micStarted;

        public const int AudioPacketSize = 960;
        private CircularBuffer micBuffer = new CircularBuffer(AudioPacketSize * 10 * 2 * 4, true);
        private byte[] packetSamples = new byte[AudioPacketSize * 4];

        // bit packers
        private readonly BitManipulator versionPacker = new BitManipulator(0x7, 0);           // 3 bits, 0 shift
        private readonly BitManipulator audioStreamCountPacker = new BitManipulator(0x38, 3); // 3 bits, 3 shift
        private readonly BitManipulator channelCountPacker = new BitManipulator(0x1c0, 6);    // 3 bits, 6 shift
        private readonly BitManipulator sampleRatePacker = new BitManipulator(0x600, 9);      // 2 bits, 9 shift
        private readonly BitManipulator sampleTypePacker = new BitManipulator(0x1800, 11);    // 2 bits, 11 shift
        private readonly BitManipulator sampleCountPacker = new BitManipulator(0x7fe000, 13); // 10 bits, 13 shift
        private readonly BitManipulator codecTypePacker = new BitManipulator(0x1800000, 23);  // 2 bits, 23 shift
        private readonly BitManipulator mutePacker = new BitManipulator(0x2000000, 25);  // 1 bits, 25 shift
        private readonly BitManipulator sequenceNumberPacker = new BitManipulator(0x7C000000, 26);  // 6 bits, 26 shift

        private readonly Mutex audioDataMutex = new Mutex();

        #region DebugVariables
        public bool HearSelf;

        private readonly CircularBuffer testCircularBuffer = new CircularBuffer(48000 * 2 * 4 * 3, true);
        private AudioSource testSource;
        public AudioClip TestClip;
        public bool SaveTestClip;
        #endregion

        private NetworkConnection GetActiveConnection()
        {
            NetworkConnection connection = null;
            var stage = SharingStage.Instance;
            if (stage && stage.Manager != null)
            {
                connection = stage.Manager.GetServerConnection();
            }
            if (connection == null || !connection.IsConnected())
            {
                return null;
            }
            return connection;
        }

        private void Awake()
        {
            audioSource = GetComponent<AudioSource>();

            int errorCode = MicStream.MicInitializeCustomRate((int)Streamtype, AudioSettings.outputSampleRate);
            CheckForErrorOnCall(errorCode);
            if (errorCode == 0 || errorCode == (int)MicStream.ErrorCodes.ALREADY_RUNNING)
            {
                if (CheckForErrorOnCall(MicStream.MicSetGain(InputGain)))
                {
                    audioSource.volume = HearSelf ? 1.0f : 0.0f;
                    micStarted = CheckForErrorOnCall(MicStream.MicStartStream(false, false));
                }
            }
        }

        private void OnAudioFilterRead(float[] buffer, int numChannels)
        {
            try
            {
                audioDataMutex.WaitOne();

                if (micStarted && hasServerConnection)
                {
                    if (CheckForErrorOnCall(MicStream.MicGetFrame(buffer, buffer.Length, numChannels)))
                    {
                        int dataSize = buffer.Length * 4;
                        if (micBuffer.Write(buffer, 0, dataSize) != dataSize)
                        {
                            Debug.LogError("Send buffer filled up. Some audio will be lost.");
                        }
                    }
                }
            }
            catch (Exception e)
            {
                Debug.LogError(e.Message);
            }
            finally
            {
                audioDataMutex.ReleaseMutex();
            }
        }

        private void Update()
        {
            CheckForErrorOnCall(MicStream.MicSetGain(InputGain));
            audioSource.volume = HearSelf ? 1.0f : 0.0f;

            try
            {
                audioDataMutex.WaitOne();

                var connection = GetActiveConnection();
                hasServerConnection = (connection != null);
                if (hasServerConnection)
                {
                    while (micBuffer.UsedCapacity >= 4 * AudioPacketSize)
                    {
                        TransmitAudio(connection);
                    }
                }
            }
            catch (Exception e)
            {
                Debug.LogError(e.Message);
            }
            finally
            {
                audioDataMutex.ReleaseMutex();
            }

            #region DebugInfo
            if (SaveTestClip && testCircularBuffer.UsedCapacity == testCircularBuffer.TotalCapacity)
            {
                float[] testBuffer = new float[testCircularBuffer.UsedCapacity / 4];
                testCircularBuffer.Read(testBuffer, 0, testBuffer.Length * 4);
                testCircularBuffer.Reset();
                TestClip = AudioClip.Create("testclip", testBuffer.Length / 2, 2, 48000, false);
                TestClip.SetData(testBuffer, 0);
                if (!testSource)
                {
                    GameObject testObj = new GameObject("testclip");
                    testObj.transform.parent = transform;
                    testSource = testObj.AddComponent<AudioSource>();
                }
                testSource.PlayClip(TestClip);
                SaveTestClip = false;
            }
            #endregion
        }

        private void TransmitAudio(NetworkConnection connection)
        {
            micBuffer.Read(packetSamples, 0, 4 * AudioPacketSize);
            SendFixedSizedChunk(connection, packetSamples, packetSamples.Length);

            if (SaveTestClip)
            {
                testCircularBuffer.Write(packetSamples, 0, packetSamples.Length);
            }
        }

        private void SendFixedSizedChunk(NetworkConnection connection, byte[] data, int dataSize)
        {
            DateTime currentTime = DateTime.Now;
            float seconds = (float)(currentTime - timeOfLastPacketSend).TotalSeconds;
            timeOfLastPacketSend = currentTime;
            if (seconds < 10.0)
            {
                if (worstTimeBetweenPackets < seconds)
                {
                    worstTimeBetweenPackets = seconds;
                }

                if (ShowInterPacketTime)
                {
                    Debug.LogFormat("Microphone: Milliseconds since last sent: {0}, Worst: {1}",
                        (seconds * 1000.0).ToString(CultureInfo.InvariantCulture),
                        (worstTimeBetweenPackets * 1000.0).ToString(CultureInfo.InvariantCulture));
                }
            }

            int clientId = SharingStage.Instance.Manager.GetLocalUser().GetID();

            // pack the header
            NetworkOutMessage msg = connection.CreateMessage((byte)MessageID.AudioSamples);

            int dataCountFloats = dataSize / 4;

            msg.Write((byte)5); // 8 byte header size

            Int32 pack = 0;
            versionPacker.SetBits(ref pack, 1);                   // version
            audioStreamCountPacker.SetBits(ref pack, 1);          // AudioStreamCount
            channelCountPacker.SetBits(ref pack, 1);              // ChannelCount
            sampleRatePacker.SetBits(ref pack, sampleRateType);   // SampleRate: 1 = 16000, 3 = 48000
            sampleTypePacker.SetBits(ref pack, 0);                // SampleType
            sampleCountPacker.SetBits(ref pack, dataCountFloats); // SampleCount (data count is in bytes and the actual data is in floats, so div by 4)
            codecTypePacker.SetBits(ref pack, 0);                 // CodecType
            mutePacker.SetBits(ref pack, Mute ? 1 : 0);
            sequenceNumberPacker.SetBits(ref pack, sequenceNumber++);
            sequenceNumber %= 32;

            msg.Write(pack); // the packed bits

            // This is where stream data starts. Write all data for one stream

            msg.Write(0.0f);     // average amplitude.  Not needed in direction from client to server.
            msg.Write(clientId); // non-zero client ID for this client.

            // HRTF position bits

            Vector3 cameraPosRelativeToGlobalAnchor = Vector3.zero;
            Vector3 cameraDirectionRelativeToGlobalAnchor = Vector3.zero;

            if (GlobalAnchorTransform != null)
            {
                cameraPosRelativeToGlobalAnchor = MathUtils.TransformPointFromTo(
                    null,
                    GlobalAnchorTransform,
                    CameraCache.Main.transform.position);

                cameraDirectionRelativeToGlobalAnchor = MathUtils.TransformDirectionFromTo(
                    null,
                    GlobalAnchorTransform,
                    CameraCache.Main.transform.position);
            }

            cameraPosRelativeToGlobalAnchor.Normalize();
            cameraDirectionRelativeToGlobalAnchor.Normalize();

            // Camera position
            msg.Write(cameraPosRelativeToGlobalAnchor.x);
            msg.Write(cameraPosRelativeToGlobalAnchor.y);
            msg.Write(cameraPosRelativeToGlobalAnchor.z);

            // HRTF direction bits
            msg.Write(cameraDirectionRelativeToGlobalAnchor.x);
            msg.Write(cameraDirectionRelativeToGlobalAnchor.y);
            msg.Write(cameraDirectionRelativeToGlobalAnchor.z);

            msg.WriteArray(data, (uint)dataCountFloats * 4);

            connection.Send(msg, MessagePriority.Immediate, MessageReliability.ReliableOrdered, MessageChannel.Audio, true);
        }

        private void OnDestroy()
        {
            CheckForErrorOnCall(MicStream.MicDestroy());
        }

        private bool CheckForErrorOnCall(int returnCode)
        {
            return MicStream.CheckForErrorOnCall(returnCode);
        }

#if DOTNET_FX
        // on device, deal with all the ways that we could suspend our program in as few lines as possible
        private void OnApplicationPause(bool pause)
        {
            if (pause)
            {
                CheckForErrorOnCall(MicStream.MicPause());
            }
            else
            {
                CheckForErrorOnCall(MicStream.MicResume());
            }
        }

        private void OnApplicationFocus(bool focused)
        {
            OnApplicationPause(!focused);
        }

        private void OnDisable()
        {
            OnApplicationPause(true);
        }

        private void OnEnable()
        {
            OnApplicationPause(false);
        }
#endif
    }
}