using NAudio.Wave;
using System;
using System.Collections.Concurrent;
using System.IO;
using System.Linq;
using System.Net;
using System.Net.Sockets;
using System.Text;
using System.Threading;
using System.Threading.Tasks;

// ReSharper disable AccessToModifiedClosure

namespace AudioNTR
{
    internal class Program
    {
        private static void Main(string[] args)
        {
            var op = args.FirstOrDefault() ?? "";
            var argsToOp = args.Skip(1).ToArray();

            Console.WriteLine("Audio Net Transmitter Receiver");
            switch (op.ToLower())
            {
                default:
                    Console.WriteLine("Usage: AudioNTR <op> [args1[ args2[...]]]");
                    Console.WriteLine(" capture");
                    Console.WriteLine("     Test run WasapiLoopbackCapture");
                    Console.WriteLine(" playback");
                    Console.WriteLine("     Test run WasapiOut (sin wave)");
                    Console.WriteLine(" playback-echo");
                    Console.WriteLine("     Test run WasapiOut (WasapiLoopbackCapture)");
                    Console.WriteLine(" playback-echo-2");
                    Console.WriteLine("     Test run WasapiOut (WasapiLoopbackCapture)");
                    Console.WriteLine(" receiver");
                    Console.WriteLine("     <port> [bind-address]   Run as Receiver");
                    Console.WriteLine(" transmitter");
                    Console.WriteLine("     <target-address> <port> Run as Transmitter Captures default audio output");
                    Console.WriteLine(" transmitter-sine-wave");
                    Console.WriteLine("     <target-address> <port> Run as Transmitter Sine wave");
                    Console.WriteLine(" receiver-as-client");
                    Console.WriteLine("     <target-address> <port>   Run receiver as client");
                    Console.WriteLine(" transmitter-as-server");
                    Console.WriteLine("     <target-address> <port> Run transmitter as server Captures default audio output");
                    break;

                case "capture": TestRunCapture(argsToOp); break;
                case "playback": TestRunPlayBack(argsToOp); break;
                case "playback-echo": TestRunPlayBackEcho(argsToOp); break;
                case "playback-echo-2": TestRunPlayBackEcho2(argsToOp); break;
                case "receiver": RunReceiver(argsToOp); break;
                case "transmitter": RunTransmitter(argsToOp); break;
                case "transmitter-sine-wave": RunTransmitterSineWave(argsToOp); break;
                case "receiver-as-client": RunReceiverAsClient(argsToOp); break;
                case "transmitter-as-server": RunTransmitterAsServer(argsToOp); break;
            }
        }

        private static void RunTransmitterAsServer(string[] argsToOp)
        {
            if (argsToOp.Length < 1) throw new ArgumentException("least 1 args for listen port");
            if (false == int.TryParse(argsToOp[0], out var port)) throw new ArgumentException("invalid port");
            IPAddress address = null;
            if (argsToOp.Length > 1 && false == IPAddress.TryParse(argsToOp[1], out address)) throw new ArgumentException("invalid address");

            Console.WriteLine("Creating TcpListener...");
            var listener = new TcpListener(address ?? IPAddress.Any, port);
            Console.WriteLine("Start Listening...");
            listener.Start();
            Console.WriteLine($"Listening on {listener.LocalEndpoint}");

            var running = true;

            var task = Task.Run(() =>
            {
                while (running)
                {
                    Console.WriteLine("Waiting connection...");
                    var client = listener.AcceptTcpClient();
                    client.NoDelay = true;
                    Console.WriteLine($"Accept connection from {client.Client.RemoteEndPoint}...");
                    var stream = client.GetStream();
                    Console.WriteLine("Creating WasapiLoopbackCapture Instance...");
                    var provider = new WasapiLoopbackCapture();

                    Console.WriteLine($"Encoding: {provider.WaveFormat.Encoding}");
                    Console.WriteLine($"Channels: {provider.WaveFormat.Channels}");
                    Console.WriteLine($"SampleRate: {provider.WaveFormat.SampleRate}");
                    Console.WriteLine($"BitsPerSample: {provider.WaveFormat.BitsPerSample}");

                    Console.WriteLine("Sending wave format ...");
                    var writer = new BinaryWriter(stream, Encoding.UTF8, true);
                    provider.WaveFormat.Serialize(writer);
                    writer.Close();
                    Console.WriteLine("Begin sending chunks...");
                    long totalSent = 0;
                    var up = DateTime.Now;
                    var connectAlive = true;
                    provider.DataAvailable += (sender, args) =>
                    {
                        Console.Title = $"Sent bytes: {totalSent:N0}, UP: {DateTime.Now - up}";
                        try
                        {
                            stream.Write(args.Buffer, 0, args.BytesRecorded);
                            stream.Flush();
                        }
                        catch (Exception e)
                        {
                            Console.WriteLine($"connection lost:{e.Message}");
                            connectAlive = false;
                            return;
                        }

                        totalSent += args.BytesRecorded;
                    };
                    provider.StartRecording();
                    while (running && connectAlive) Thread.Sleep(100);
                    provider.StopRecording();
                }
            });

            Console.WriteLine("Press ENTER to Exit.");
            Console.ReadLine();
            running = false;
            task.Wait();
            Console.WriteLine("Finished.");
        }

        private static void RunReceiverAsClient(string[] argsToOp)
        {
            const int bufferMs = 500;
            
            if (argsToOp.Length != 2) throw new ArgumentException("required 2 args for address and port");
            if (false == IPAddress.TryParse(argsToOp[0], out var address)) throw new ArgumentException("invalid address");
            if (false == int.TryParse(argsToOp[1], out var port)) throw new ArgumentException("invalid port");

            var running = true;

            var task = Task.Run(() =>
            {
                do
                {
                    TcpClient client = null;
                    try
                    {
                        Console.WriteLine("Creating TcpClient ...");
                        client = new TcpClient { NoDelay = true };
                        var ipEndPoint = new IPEndPoint(address, port);
                        Console.WriteLine($"Connecting to {ipEndPoint} ...");
                        client.Connect(ipEndPoint);
                        var stream = client.GetStream();
                        Console.WriteLine("Parsing wave format and create BufferedWaveProvider...");
                        var reader = new BinaryReader(stream, Encoding.UTF8, true);
                        var provider = new BufferedWaveProvider(new WaveFormat(reader))
                        {
                            BufferDuration = TimeSpan.FromMilliseconds(bufferMs),
                            DiscardOnBufferOverflow = true,
                        };
                        reader.Dispose();
                        Console.WriteLine($"Encoding: {provider.WaveFormat.Encoding}");
                        Console.WriteLine($"Channels: {provider.WaveFormat.Channels}");
                        Console.WriteLine($"SampleRate: {provider.WaveFormat.SampleRate}");
                        Console.WriteLine($"BitsPerSample: {provider.WaveFormat.BitsPerSample}");

                        Console.WriteLine($"Buffer Duration set to {provider.BufferDuration}");

                        Console.WriteLine("Creating WasapiOut Instance...");
                        var output = new WasapiOut();
                        Console.WriteLine("Setup...");
                        output.Init(provider);

                        const int fillSleep = bufferMs * 11 / 20;
                        Console.WriteLine($"Filling buffer, sleep {fillSleep}ms");
                        Thread.Sleep(fillSleep);

                        Console.WriteLine("Starting play...");
                        output.Play();

                        try
                        {
                            var buf = new byte[1024 * 1024];//1MB
                            int readCount;

                            Console.WriteLine("Begin read chunks...");
                            do
                            {
                                Console.Title = $"Buffered bytes: {provider.BufferedBytes:N0}";
                                readCount = stream.Read(buf, 0, buf.Length);
                                provider.AddSamples(buf, 0, readCount);
                            } while (readCount > 0 && running);
                        }
                        catch (Exception e)
                        {
                            Console.WriteLine(e);
                        }

                        Console.WriteLine("Closing connection...");
                        client.Close();
                        provider.ClearBuffer();
                        Console.WriteLine("Stopping Playing...");
                        output.Stop();
                        Console.WriteLine("Free resources");
                        client.Dispose();
                    }
                    catch (Exception e)
                    {
                        Console.WriteLine(e);

                        if (running)
                        {
                            Console.WriteLine("Wait 10 Seconds to re connection");
                            Thread.Sleep(10 * 1000);
                        }
                    }
                    finally
                    {
                        Console.WriteLine("Free resources");
                        client?.Close();
                        client?.Dispose();
                    }
                } while (running);
            });

            Console.WriteLine("Press ENTER to exit");
            Console.ReadLine();
            running = false;
            task.Wait();
            Console.WriteLine("Finished.");
        }

        private static void TestRunCapture(string[] argsToOp)
        {
            Console.WriteLine("Creating WasapiLoopbackCapture Instance...");
            var capture = new WasapiLoopbackCapture();

            Console.WriteLine($"Encoding:     {capture.WaveFormat.Encoding}");
            Console.WriteLine($"Channels:     {capture.WaveFormat.Channels}");
            Console.WriteLine($"SampleRate:   {capture.WaveFormat.SampleRate:N0}");
            Console.WriteLine($"BitsPerSample:{capture.WaveFormat.BitsPerSample:N0}");

            var bytes = 0;
            capture.DataAvailable += (_, e) =>
            {
                bytes += e.BytesRecorded;
                Console.Title = $"Buffer: {e.Buffer.Length}   Bytes: {e.BytesRecorded:N0}   Total: {bytes:N0}";
            };
            Console.WriteLine("Starting capture...");
            capture.StartRecording();
            Console.WriteLine("Capturing, Press ENTER to exit");
            Console.ReadLine();
            Console.WriteLine("Stopping capture...");
            capture.StopRecording();
            Console.WriteLine("Stopped.");
        }

        private static void TestRunPlayBack(string[] argsToOp)
        {
            Console.WriteLine("Creating WasapiOut Instance...");
            var output = new WasapiOut();
            Console.WriteLine("Creating SineWaveSampleProvider Instance...");
            var provider = new SineWaveSampleProvider();

            Console.WriteLine("Init...");
            output.Init(provider);
            Console.WriteLine("Starting play...");
            output.Play();
            Console.WriteLine("Playing, Press ENTER to exit");
            Console.ReadLine();
            Console.WriteLine("Stopping Playing...");
            output.Stop();
            Console.WriteLine("Stopped.");
        }

        private static void TestRunPlayBackEcho(string[] argsToOp)
        {
            Console.WriteLine("Creating WasapiOut Instance...");
            var output = new WasapiOut();
            Console.WriteLine("Creating WaveInProvider Instance by WasapiLoopbackCapture...");
            var provider = new WaveInProvider(new WasapiLoopbackCapture());
            Console.WriteLine("Init...");
            output.Init(provider);
            //output.Volume = 0.5f;
            Console.WriteLine("Starting Capture...");
            provider.StartRecording();
            Console.WriteLine("Starting play...");
            output.Play();
            Console.WriteLine("Playing, Press ENTER to exit");
            Console.ReadLine();
            Console.WriteLine("Stopping Capture...");
            provider.StopRecording();
            Console.WriteLine("Stopping Playing...");
            output.Stop();
            Console.WriteLine("Stopped.");
        }

        private static void TestRunPlayBackEcho2(string[] argsToOp)
        {
            Console.WriteLine("Creating WasapiOut Instance...");
            var output = new WasapiOut();
            Console.WriteLine("Creating WasapiLoopbackCapture Instance...");
            var capture = new WasapiLoopbackCapture();
            Console.WriteLine("Creating WasapiLoopbackCapture Instance...");
            var provider = new BufferedWaveProvider(capture.WaveFormat);
            Console.WriteLine("Setup...");
            capture.DataAvailable += (sender, args) => provider.AddSamples(args.Buffer, 0, args.BytesRecorded);
            output.Init(provider);
            Console.WriteLine("Starting Capture...");
            capture.StartRecording();
            Console.WriteLine("Starting play...");
            output.Play();
            Console.WriteLine("Playing, Press ENTER to exit");
            Console.ReadLine();
            Console.WriteLine("Stopping Capture...");
            capture.StopRecording();
            Console.WriteLine("Free resoures...");
            provider.ClearBuffer();
            Console.WriteLine("Stopping Playing...");
            output.Stop();
            Console.WriteLine("Stopped.");
        }

        private static void RunReceiver(string[] argsToOp)
        {
            const int bufferMs = 500;

            if (argsToOp.Length < 1) throw new ArgumentException("least 1 args for listen port");
            if (false == int.TryParse(argsToOp[0], out var port)) throw new ArgumentException("invalid port");
            IPAddress address = null;
            if (argsToOp.Length > 1 && false == IPAddress.TryParse(argsToOp[1], out address)) throw new ArgumentException("invalid address");

            Console.WriteLine("Creating TcpListener...");
            var listener = new TcpListener(address ?? IPAddress.Any, port);
            Console.WriteLine("Start Listening...");
            listener.Start();
            Console.WriteLine($"Listening on {listener.LocalEndpoint}");

            var running = true;

            var task = Task.Run(() =>
            {
                while (running)
                {
                    Console.WriteLine("Waiting connection...");
                    var client = listener.AcceptTcpClient();
                    client.NoDelay = true;
                    Console.WriteLine($"Accept connection from {client.Client.RemoteEndPoint}...");

                    var stream = client.GetStream();
                    Console.WriteLine("Parsing wave format and create BufferedWaveProvider...");
                    var reader = new BinaryReader(stream, Encoding.UTF8, true);
                    var provider = new BufferedWaveProvider(new WaveFormat(reader))
                    {
                        BufferDuration = TimeSpan.FromMilliseconds(bufferMs),
                        DiscardOnBufferOverflow = true,
                    };
                    reader.Dispose();
                    Console.WriteLine($"Encoding: {provider.WaveFormat.Encoding}");
                    Console.WriteLine($"Channels: {provider.WaveFormat.Channels}");
                    Console.WriteLine($"SampleRate: {provider.WaveFormat.SampleRate}");
                    Console.WriteLine($"BitsPerSample: {provider.WaveFormat.BitsPerSample}");

                    Console.WriteLine($"Buffer Duration set to {provider.BufferDuration}");

                    Console.WriteLine("Creating WasapiOut Instance...");
                    var output = new WasapiOut();
                    Console.WriteLine("Setup...");
                    output.Init(provider);

                    const int fillSleep = bufferMs * 11 / 20;
                    Console.WriteLine($"Filling buffer, sleep {fillSleep}ms");
                    Thread.Sleep(fillSleep);

                    Console.WriteLine("Starting play...");
                    output.Play();

                    try
                    {
                        var buf = new byte[1024 * 1024];//1MB
                        int readCount;

                        //Console.WriteLine("Sending ACK...");
                        //stream.WriteByte(1);

                        Console.WriteLine("Begin read chunks...");
                        do
                        {
                            Console.Title = $"Buffered bytes: {provider.BufferedBytes:N0}";
                            readCount = stream.Read(buf, 0, buf.Length);
                            provider.AddSamples(buf, 0, readCount);
                        } while (readCount > 0 && running);
                    }
                    catch (Exception e)
                    {
                        Console.WriteLine(e);
                    }

                    Console.WriteLine("Closing connection...");
                    client.Close();
                    provider.ClearBuffer();
                    Console.WriteLine("Stopping Playing...");
                    output.Stop();
                    Console.WriteLine("Free resources");
                    client.Dispose();
                }
            });

            Console.WriteLine("Press ENTER to Exit.");
            Console.ReadLine();
            running = false;
            task.Wait();
            Console.WriteLine("Finished.");
        }

        private static void RunTransmitterSineWave(string[] argsToOp)
        {
            if (argsToOp.Length != 2) throw new ArgumentException("required 2 args for address and port");
            if (false == IPAddress.TryParse(argsToOp[0], out var address)) throw new ArgumentException("invalid address");
            if (false == int.TryParse(argsToOp[1], out var port)) throw new ArgumentException("invalid port");

            var running = true;

            var task = Task.Run(() =>
            {
                do
                {
                    TcpClient client = null;
                    try
                    {
                        Console.WriteLine("Creating TcpClient ...");
                        client = new TcpClient();
                        var ipEndPoint = new IPEndPoint(address, port);
                        Console.WriteLine($"Connecting to {ipEndPoint} ...");
                        client.Connect(ipEndPoint);
                        var stream = client.GetStream();
                        Console.WriteLine("Connected, creating SineWaveSampleProvider ...");
                        var provider = new SineWaveSampleProvider();

                        Console.WriteLine($"Encoding: {provider.WaveFormat.Encoding}");
                        Console.WriteLine($"Channels: {provider.WaveFormat.Channels}");
                        Console.WriteLine($"SampleRate: {provider.WaveFormat.SampleRate}");
                        Console.WriteLine($"BitsPerSample: {provider.WaveFormat.BitsPerSample}");

                        Console.WriteLine("Sending wave format ...");
                        var writer = new BinaryWriter(stream, Encoding.UTF8, true);
                        provider.WaveFormat.Serialize(writer);
                        writer.Close();

                        Console.WriteLine("Begin sending chunks...");
                        var buffer = new byte[provider.WaveFormat.AverageBytesPerSecond / 4];
                        while (running)
                        {
                            provider.Read(buffer, 0, buffer.Length);
                            stream.Write(buffer, 0, buffer.Length);
                        }
                    }
                    catch (Exception e)
                    {
                        Console.WriteLine(e);

                        if (running)
                        {
                            Console.WriteLine("Wait 10 Seconds to re connection");
                            Thread.Sleep(10 * 1000);
                        }
                    }
                    finally
                    {
                        Console.WriteLine("Free resources");
                        client?.Close();
                        client?.Dispose();
                    }
                } while (running);
            });

            Console.WriteLine("Press ENTER to exit");
            Console.ReadLine();
            running = false;
            task.Wait();
            Console.WriteLine("Finished.");
        }

        private static void RunTransmitter(string[] argsToOp)
        {
            if (argsToOp.Length != 2) throw new ArgumentException("required 2 args for address and port");
            if (false == IPAddress.TryParse(argsToOp[0], out var address)) throw new ArgumentException("invalid address");
            if (false == int.TryParse(argsToOp[1], out var port)) throw new ArgumentException("invalid port");

            var running = true;

            var task = Task.Run(() =>
            {
                do
                {
                    TcpClient client = null;
                    try
                    {
                        Console.WriteLine("Creating TcpClient ...");
                        client = new TcpClient { NoDelay = true };
                        var ipEndPoint = new IPEndPoint(address, port);
                        Console.WriteLine($"Connecting to {ipEndPoint} ...");
                        client.Connect(ipEndPoint);
                        var stream = client.GetStream();
                        Console.WriteLine("Creating WasapiLoopbackCapture Instance...");
                        var provider = new WasapiLoopbackCapture();
                        Console.WriteLine("Sending wave format ...");
                        var writer = new BinaryWriter(stream, Encoding.UTF8, true);
                        provider.WaveFormat.Serialize(writer);
                        writer.Close();

                        //Console.WriteLine("Waiting ACK ...");
                        //var ack = stream.ReadByte();
                        //if (ack != 1) throw new Exception("Unexpected ACK");

                        Console.WriteLine("Begin sending chunks...");
                        long totalSent = 0;
                        var up = DateTime.Now;
                        provider.DataAvailable += (sender, args) =>
                        {
                            Console.Title = $"Sent bytes: {totalSent:N0}, UP: {DateTime.Now - up}";
                            stream.Write(args.Buffer, 0, args.BytesRecorded);
                            stream.Flush();
                            totalSent += args.BytesRecorded;
                        };
                        provider.StartRecording();
                        while (running) Thread.Sleep(100);
                        provider.StopRecording();
                    }
                    catch (Exception e)
                    {
                        Console.WriteLine(e);

                        if (running)
                        {
                            Console.WriteLine("Wait 10 Seconds to re connection");
                            Thread.Sleep(10 * 1000);
                        }
                    }
                    finally
                    {
                        Console.WriteLine("Free resources");
                        client?.Close();
                        client?.Dispose();
                    }
                } while (running);
            });

            Console.WriteLine("Press ENTER to exit");
            Console.ReadLine();
            running = false;
            task.Wait();
            Console.WriteLine("Finished.");
        }
    }

    internal class WaveInProvider : IWaveProvider
    {
        private readonly IWaveIn _input;
        private readonly BlockingCollection<byte> _buffer;
        private readonly CancellationTokenSource _cancellationTokenSource = new();

        public WaveFormat WaveFormat { get; }

        public WaveInProvider(IWaveIn input, int msBuffer = 1000)
        {
            _input = input;
            WaveFormat = _input.WaveFormat;
            _input.DataAvailable += Input_DataAvailable;

            _buffer = new((int)Math.Ceiling(WaveFormat.SampleRate / 1000f * msBuffer * WaveFormat.Channels * (WaveFormat.BitsPerSample / 8f)));
        }

        private void Input_DataAvailable(object sender, WaveInEventArgs e)
        {
            if (_cancellationTokenSource.IsCancellationRequested) return;
            for (var i = 0; i < e.BytesRecorded; i++) _buffer.Add(e.Buffer[i], _cancellationTokenSource.Token);
        }

        public void StartRecording()
        {
            _input.StartRecording();
        }

        public void StopRecording()
        {
            _input.StopRecording();
            _cancellationTokenSource.Cancel();
        }

        public int Read(byte[] buffer, int offset, int count)
        {
            var readCount = 0;

            var eof = offset + count;

            do
            {
                try
                {
                    buffer[offset] = _buffer.Take(_cancellationTokenSource.Token);
                }
                catch (OperationCanceledException)
                {
                    return readCount;
                }
                readCount++;
                offset++;
            } while (_buffer.Count > 0 && offset < eof && _cancellationTokenSource.IsCancellationRequested == false);

            return readCount;
        }
    }

    internal class SineWaveSampleProvider : IWaveProvider, ISampleProvider
    {
        public int Freq { get; }
        public float Amp { get; }

        public WaveFormat WaveFormat { get; }

        private int _sample;

        public SineWaveSampleProvider(int sampleRate = 441000, int channels = 1, int freq = 1000, float amp = 0.25f)
        {
            Freq = freq;
            Amp = amp;
            WaveFormat = WaveFormat.CreateIeeeFloatWaveFormat(sampleRate, channels);
        }

        public int Read(byte[] buffer, int offset, int count)
        {
            var waveBuffer = new WaveBuffer(buffer);
            var samplesRequired = count / 4;
            var samplesRead = Read(waveBuffer.FloatBuffer, offset / 4, samplesRequired);
            return samplesRead * 4;
        }

        public int Read(float[] buffer, int offset, int sampleCount)
        {
            var sampleRate = WaveFormat.SampleRate;
            for (var n = 0; n < sampleCount; n++)
            {
                buffer[n + offset] = (float)(Amp * Math.Sin((2 * Math.PI * _sample * Freq) / sampleRate));
                _sample++;
                if (_sample >= sampleRate) _sample = 0;
            }
            return sampleCount;
        }
    }
}