using System;
using System.Collections.Generic;
using System.IO;
using OpenTK.Audio;
using OpenTK.Audio.OpenAL;

public class AudioEngine : IDisposable
{
    public class Voice
    {
        public string Path;
        public int    Buffer;
        public int    Source;
        public float  Gain;
    }

    private AudioContext _context;
    private readonly Dictionary<string, Voice> _voices =
        new Dictionary<string, Voice>(StringComparer.OrdinalIgnoreCase);
    private readonly object _lock = new object();

    public event Action<string> Log;

    public bool Init()
    {
        try
        {
            _context = new AudioContext();      // opens default device + context
            _context.MakeCurrent();
            Emit("OpenAL ready: " + AudioContext.CurrentContext.CurrentDevice);
            return true;
        }
        catch (Exception ex)
        {
            Emit("OpenAL init failed: " + ex.Message);
            return false;
        }
    }

    /// Load + start looping at gain 0. Idempotent.
    public Voice Warm(string wavPath)
    {
        if (string.IsNullOrEmpty(wavPath) || !File.Exists(wavPath))
        {
            Emit("Sound file not found: " + wavPath);
            return null;
        }

        lock (_lock)
        {
            Voice v;
            if (_voices.TryGetValue(wavPath, out v)) return v;

            try
            {
                WavFile w = WavFile.Load(wavPath);

                int buf = AL.GenBuffer();
                AL.BufferData(buf, w.Format, w.Data, w.Data.Length, w.SampleRate);

                int src = AL.GenSource();
                AL.Source(src, ALSourcei.Buffer, buf);
                AL.Source(src, ALSourceb.Looping, true);
                AL.Source(src, ALSourcef.Gain, 0.0f);
                AL.SourcePlay(src);                     // runs continuously, silent

                CheckError("Warm " + Path.GetFileName(wavPath));

                v = new Voice { Path = wavPath, Buffer = buf, Source = src, Gain = 0f };
                _voices[wavPath] = v;
                Emit("Warmed " + Path.GetFileName(wavPath));
                return v;
            }
            catch (Exception ex)
            {
                Emit("Load failed " + wavPath + ": " + ex.Message);
                return null;
            }
        }
    }

    /// Replaces SourceVoice.SetVolume. Linear 0..1.
    public void SetGain(Voice v, float gain)
    {
        if (v == null) return;
        if (gain < 0f) gain = 0f;
        if (gain > 1f) gain = 1f;
        if (Math.Abs(gain - v.Gain) < 0.001f) return;   // skip redundant AL calls

        lock (_lock)
        {
            v.Gain = gain;
            AL.Source(v.Source, ALSourcef.Gain, gain);

            if (gain > 0f && AL.GetSourceState(v.Source) != ALSourceState.Playing)
                AL.SourcePlay(v.Source);
        }
    }

    /// Form2's PlayColorFromStart — restart with no leading gap.
    public void PlayFromStart(Voice v, float gain)
    {
        if (v == null) return;
        lock (_lock)
        {
            AL.SourceRewind(v.Source);
            v.Gain = gain;
            AL.Source(v.Source, ALSourcef.Gain, gain);
            AL.SourcePlay(v.Source);
        }
    }

    public void MuteAll()
    {
        lock (_lock)
            foreach (var v in _voices.Values)
            {
                v.Gain = 0f;
                AL.Source(v.Source, ALSourcef.Gain, 0f);
            }
    }

    public void StopAll()
    {
        lock (_lock)
            foreach (var v in _voices.Values)
            {
                AL.SourceStop(v.Source);
                v.Gain = 0f;
            }
    }

    /// Form2's Volume_Control_XAudio2, unchanged — keeps Linux loudness
    /// matched to the Windows build.
    public static float VolumeCurve(int range)
    {
        const double CURVE = 9.5;
        double y = Math.Pow(range / 255.0, 1.0 / CURVE);
        int db = (int)(10000.0 * y - 10000);
        if (db > 0) db = 0;
        if (db < -10000) db = -10000;
        float lin = (float)Math.Pow(10.0, db / 2000.0);
        return Math.Max(0f, Math.Min(1f, lin));
    }

    // ── shims so MapWindow's existing calls still compile ──

    private Voice _main;

    public void Start(string wavPath)
    {
        _main = Warm(wavPath);
        SetGain(_main, 0f);
    }

    public void Stop() { MuteAll(); }

    public Voice MainVoice { get { return _main; } }

    public void Dispose()
    {
        lock (_lock)
        {
            foreach (var v in _voices.Values)
            {
                AL.SourceStop(v.Source);
                AL.DeleteSource(v.Source);
                AL.DeleteBuffer(v.Buffer);
            }
            _voices.Clear();
        }
        if (_context != null) { _context.Dispose(); _context = null; }
    }

    private void CheckError(string where)
    {
        ALError e = AL.GetError();
        if (e != ALError.NoError) Emit(where + ": " + AL.GetErrorString(e));
    }

    private void Emit(string m)
    {
        var h = Log;
        if (h != null) h(m); else Console.WriteLine("[audio] " + m);
    }
}