using System.Runtime.InteropServices;
using PortAudioSharp;
using RecordPlayProbe;
using Stream = PortAudioSharp.Stream;

const int SampleRate = 16_000;
const int Channels = 1;
const int DurationS = 5;
const uint BlockFrames = 1024;
const string OutWav = "out.wav";

// Tell PortAudio's ALSA host API to wrap devices in `plughw:` so it does
// rate/format conversion (USB Speaker Phone does not natively offer 16 kHz).
// Environment.SetEnvironmentVariable updates the .NET table but the libc
// getenv() view PortAudio actually reads can lag, so call setenv directly too.
Environment.SetEnvironmentVariable("PA_ALSA_PLUGHW", "1");
Libc.setenv("PA_ALSA_PLUGHW", "1", 1);

PortAudio.Initialize();
try
{
    int device = FindUsbDevice();
    Console.WriteLine(
        $"Recording {DurationS}s from device {device} " +
        $"('{PortAudio.GetDeviceInfo(device).name}')");

    short[] buf = new short[SampleRate * DurationS];
    int writeOffset = 0;
    object gate = new();

    var inParams = new StreamParameters
    {
        device = device,
        channelCount = Channels,
        sampleFormat = SampleFormat.Int16,
        suggestedLatency = PortAudio.GetDeviceInfo(device).defaultLowInputLatency,
        hostApiSpecificStreamInfo = IntPtr.Zero,
    };

    Stream.Callback callback = (IntPtr input, IntPtr _, uint frameCount,
        ref StreamCallbackTimeInfo _2, StreamCallbackFlags status, IntPtr _3) =>
    {
        if (status.HasFlag(StreamCallbackFlags.InputOverflow))
            Console.Error.WriteLine("[status] input overflow");

        lock (gate)
        {
            int remaining = buf.Length - writeOffset;
            int take = (int)Math.Min(frameCount, (uint)remaining);
            if (take > 0)
            {
                unsafe
                {
                    short* src = (short*)input.ToPointer();
                    fixed (short* dst = &buf[writeOffset])
                    {
                        Buffer.MemoryCopy(src, dst, take * sizeof(short), take * sizeof(short));
                    }
                }
                writeOffset += take;
            }
        }
        return StreamCallbackResult.Continue;
    };

    using var stream = new Stream(
        inParams, null, SampleRate, BlockFrames, StreamFlags.NoFlag, callback, IntPtr.Zero);
    stream.Start();

    var start = DateTime.UtcNow;
    while (true)
    {
        int written;
        lock (gate) { written = writeOffset; }
        if (written >= buf.Length) break;

        double level = Rms(buf, Math.Max(0, written - 1600), written);
        int bars = (int)(level * 60);
        double elapsed = (DateTime.UtcNow - start).TotalSeconds;
        Console.Write($"\r{elapsed,4:0.0}s |{new string('#', bars).PadRight(60)}|");
        Thread.Sleep(50);
    }
    Console.WriteLine();
    stream.Stop();

    WavWriter.Write(OutWav, buf, SampleRate);
    Console.WriteLine($"Wrote {OutWav} ({buf.Length} frames @ {SampleRate} Hz)");

    Console.WriteLine($"Playing back through device {device}");

    int playOffset = 0;
    object playGate = new();
    var doneEvent = new ManualResetEventSlim(false);

    var outParams = new StreamParameters
    {
        device = device,
        channelCount = Channels,
        sampleFormat = SampleFormat.Int16,
        suggestedLatency = PortAudio.GetDeviceInfo(device).defaultLowOutputLatency,
        hostApiSpecificStreamInfo = IntPtr.Zero,
    };

    Stream.Callback playCb = (IntPtr _, IntPtr output, uint frameCount,
        ref StreamCallbackTimeInfo _2, StreamCallbackFlags _3, IntPtr _4) =>
    {
        lock (playGate)
        {
            int remaining = buf.Length - playOffset;
            int take = (int)Math.Min(frameCount, (uint)remaining);
            int silence = (int)frameCount - take;

            unsafe
            {
                short* dst = (short*)output.ToPointer();
                if (take > 0)
                {
                    fixed (short* src = &buf[playOffset])
                        Buffer.MemoryCopy(src, dst, take * sizeof(short), take * sizeof(short));
                    playOffset += take;
                }
                if (silence > 0)
                {
                    for (int i = take; i < frameCount; i++) dst[i] = 0;
                }
            }

            if (playOffset >= buf.Length)
            {
                doneEvent.Set();
                return StreamCallbackResult.Complete;
            }
        }
        return StreamCallbackResult.Continue;
    };

    using var playStream = new Stream(
        null, outParams, SampleRate, BlockFrames, StreamFlags.NoFlag, playCb, IntPtr.Zero);
    playStream.Start();
    doneEvent.Wait();
    // Give PortAudio a moment to drain the device buffer before Stop().
    Thread.Sleep(200);
    playStream.Stop();
}
finally
{
    PortAudio.Terminate();
}

static int FindUsbDevice()
{
    int count = PortAudio.DeviceCount;
    for (int i = 0; i < count; i++)
    {
        var info = PortAudio.GetDeviceInfo(i);
        if (info.name.ToLowerInvariant().Contains("usb") && info.maxInputChannels >= 1)
            return i;
    }
    Console.Error.WriteLine("USB audio device not found. Devices:");
    for (int i = 0; i < count; i++)
    {
        var info = PortAudio.GetDeviceInfo(i);
        Console.Error.WriteLine(
            $"  [{i}] {info.name} in={info.maxInputChannels} out={info.maxOutputChannels}");
    }
    Environment.Exit(1);
    return -1; // unreachable
}

static double Rms(short[] data, int from, int to)
{
    if (to <= from) return 0.0;
    double sumSq = 0.0;
    for (int i = from; i < to; i++)
    {
        double x = data[i] / 32768.0;
        sumSq += x * x;
    }
    return Math.Sqrt(sumSq / (to - from) + 1e-12);
}

static class Libc
{
    [DllImport("libc", EntryPoint = "setenv")]
    public static extern int setenv(string name, string value, int overwrite);
}
