using System;
using System.IO;
using System.Text;

/// A WAV file decoded to mono floats, plus a per-pixel min/max cache for drawing.
public class WaveData
{
    public float[] Samples;
    public int SampleRate;

    private int _cacheWidth = -1;
    private float[] _mins, _maxs;

    public double Duration
    {
        get { return (Samples == null || SampleRate <= 0) ? 0 : (double)Samples.Length / SampleRate; }
    }

    public static WaveData Load(string path, out string error)
    {
        float[] s; int rate;
        if (!ReadWav(path, out s, out rate, out error)) return null;
        return new WaveData { Samples = s, SampleRate = rate };
    }

    /// Min/max of the samples that fall into each of `width` pixel columns.
    public void GetPeaks(int width, out float[] mins, out float[] maxs)
    {
        if (width != _cacheWidth)
        {
            _cacheWidth = width;
            _mins = new float[width];
            _maxs = new float[width];
            int n = Samples.Length;

            for (int x = 0; x < width; x++)
            {
                int s0 = (int)((long)x * n / width);
                int s1 = (int)((long)(x + 1) * n / width);
                if (s1 <= s0) s1 = s0 + 1;
                if (s1 > n) s1 = n;

                float mn = 1f, mx = -1f;
                for (int i = s0; i < s1; i++)
                {
                    float v = Samples[i];
                    if (v < mn) mn = v;
                    if (v > mx) mx = v;
                }
                _mins[x] = mn;
                _maxs[x] = mx;
            }
        }
        mins = _mins;
        maxs = _maxs;
    }

    // PCM 8/16/24/32-bit or float32, mono or stereo (left channel), normalized to [-1, 1]
    public static bool ReadWav(string path, out float[] samples, out int sampleRate, out string error)
    {
        samples = null; sampleRate = 0; error = null;
        try
        {
            if (!File.Exists(path)) { error = "not found: " + path; return false; }

            using (var br = new BinaryReader(File.OpenRead(path)))
            {
                long len = br.BaseStream.Length;
                if (Id(br) != "RIFF") { error = "not RIFF"; return false; }
                br.ReadInt32();
                if (Id(br) != "WAVE") { error = "not WAVE"; return false; }

                int fmtTag = 0, channels = 0, bits = 0;
                byte[] data = null;

                while (br.BaseStream.Position + 8 <= len)
                {
                    string id = Id(br);
                    long size = (uint)br.ReadInt32();
                    long start = br.BaseStream.Position;

                    if (id == "fmt ")
                    {
                        fmtTag = (ushort)br.ReadInt16();
                        channels = br.ReadInt16();
                        sampleRate = br.ReadInt32();
                        br.ReadInt32();                         // byte rate
                        br.ReadInt16();                         // block align
                        bits = br.ReadInt16();
                        if (fmtTag == 0xFFFE && size >= 40)     // WAVE_FORMAT_EXTENSIBLE
                        {
                            br.ReadInt16(); br.ReadInt16(); br.ReadInt32();
                            fmtTag = (ushort)br.ReadInt16();
                        }
                    }
                    else if (id == "data")
                    {
                        data = br.ReadBytes((int)Math.Min(size, len - start));
                        break;
                    }

                    long next = start + size + (size & 1);
                    if (next > len) break;
                    br.BaseStream.Position = next;
                }

                if (data == null || channels <= 0 || sampleRate <= 0 || bits <= 0)
                { error = "missing fmt/data chunk"; return false; }

                int frame = (bits / 8) * channels;
                int frames = data.Length / frame;
                if (frames == 0) { error = "empty data"; return false; }

                var s = new float[frames];
                float peak = 0f;
                for (int i = 0; i < frames; i++)
                {
                    int o = i * frame;
                    float v;
                    switch (bits)
                    {
                        case 8:  v = (data[o] - 128) / 128f; break;
                        case 16: v = BitConverter.ToInt16(data, o) / 32768f; break;
                        case 24: v = (data[o] | (data[o + 1] << 8) | ((sbyte)data[o + 2] << 16)) / 8388608f; break;
                        case 32: v = fmtTag == 3 ? BitConverter.ToSingle(data, o)
                                                 : BitConverter.ToInt32(data, o) / 2147483648f; break;
                        default: error = "unsupported bits: " + bits; return false;
                    }
                    s[i] = v;
                    if (Math.Abs(v) > peak) peak = Math.Abs(v);
                }

                if (peak > 0f)
                    for (int i = 0; i < frames; i++) s[i] /= peak;

                samples = s;
                return true;
            }
        }
        catch (Exception ex) { error = ex.Message; return false; }
    }

    private static string Id(BinaryReader br)
    {
        return Encoding.ASCII.GetString(br.ReadBytes(4));
    }
}