// OG Sound Remaster - clip cache, background decoding and memory governor
// Author: StixsworldHD (StixsmasterHD4k)
//
// Decoding happens on worker threads; every single il2cpp interaction happens on the main thread.
// That split is enforced structurally, not by convention: worker threads only ever touch managed
// byte and float buffers, and any callback that could reach a Unity object is routed through
// RunOnMain so it lands in the frame pump instead of executing on the decoder.
//
// This matters more than it looks. Calling into il2cpp from a thread the runtime has not attached
// corrupts process state, and the resulting AccessViolationException is a Corrupted State
// Exception which .NET Core will not let a catch block intercept - it terminates the game outright.
// Defensive try/catch is no protection here, so the unsafe call is simply never made.

using System;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.Diagnostics;
using System.IO;
using System.Threading;
using Il2CppInterop.Runtime.InteropTypes.Arrays;
using UnityEngine;

namespace OGSoundRemaster.Audio
{
    public sealed class ClipStore
    {
        private enum State : int { Cold = 0, Queued = 1, Decoding = 2, Decoded = 3, Live = 4, Failed = 5 }

        private sealed class Slot
        {
            public BankFile File;
            public int State;
            public AudioClip Clip;
            public PcmData Pcm;
            public float LastUse;
            public int Bindings;
            public bool Pinned;
            public List<Action<AudioClip>> Waiters;
        }

        private readonly AudioBank _bank;
        private readonly Slot[] _slots;
        private readonly Action<string> _warn;

        private readonly BlockingCollection<Slot> _decodeQueue = new BlockingCollection<Slot>(new ConcurrentQueue<Slot>());
        private readonly ConcurrentQueue<Slot> _createQueue = new ConcurrentQueue<Slot>();
        private readonly ConcurrentQueue<Action> _mainQueue = new ConcurrentQueue<Action>();
        private readonly List<Thread> _workers = new List<Thread>();
        private volatile bool _shutdown;

        private readonly int _mainThreadId;

        private long _residentBytes;
        private readonly long _budgetBytes;
        private int _decodeFailures;

        private const float SyncDecodeMaxSeconds = 2.5f;
        private const float PinMaxSeconds = 3.5f;
        private const float EvictIdleSeconds = 25f;
        private const double CreateBudgetMs = 2.0;
        private const double MainQueueBudgetMs = 1.0;

        public int LiveClips { get; private set; }
        public int DecodeFailures { get; private set; }
        public long ResidentBytes => Interlocked.Read(ref _residentBytes);
        public long BudgetBytes => _budgetBytes;
        public int Evictions { get; private set; }
        public int SyncDecodes { get; private set; }
        public int CrossThreadDeferrals { get; private set; }

        /// <summary>True only on the thread that constructed the store, which is Unity's main thread.</summary>
        public bool OnMainThread => Environment.CurrentManagedThreadId == _mainThreadId;

        public ClipStore(AudioBank bank, long budgetBytes, Action<string> warn)
        {
            _bank = bank;
            _warn = warn;
            _budgetBytes = budgetBytes;
            _mainThreadId = Environment.CurrentManagedThreadId;

            _slots = new Slot[bank.Files.Count];
            for (int i = 0; i < _slots.Length; i++)
            {
                var f = bank.Files[i];
                _slots[i] = new Slot { File = f, State = (int)State.Cold, Pinned = f.Duration <= PinMaxSeconds };
            }

            int workerCount = Math.Max(1, Math.Min(3, Environment.ProcessorCount - 1));
            for (int i = 0; i < workerCount; i++)
            {
                var t = new Thread(WorkerLoop) { IsBackground = true, Name = "OGSR-Decode-" + i, Priority = ThreadPriority.BelowNormal };
                t.Start();
                _workers.Add(t);
            }
        }

        public void Shutdown()
        {
            _shutdown = true;
            try { _decodeQueue.CompleteAdding(); } catch { }
        }

        /// <summary>Run now if already on the main thread, otherwise hand it to the frame pump.</summary>
        public void RunOnMain(Action action)
        {
            if (action == null) return;
            if (OnMainThread) { SafeInvoke(action); return; }

            CrossThreadDeferrals++;
            _mainQueue.Enqueue(action);
        }

        private void SafeInvoke(Action action)
        {
            try { action(); }
            catch (Exception e) { _warn?.Invoke("deferred action threw: " + e.Message); }
        }

        // ---- request paths --------------------------------------------------

        /// <summary>
        /// Main thread only. Returns the clip if it is already live, decodes short sounds inline,
        /// and otherwise queues the work and returns null so the caller keeps the stock sound for
        /// this one playback. Called off the main thread it will only ever queue.
        /// </summary>
        public AudioClip GetImmediate(BankFile file)
        {
            if (file == null) return null;
            var s = _slots[file.Id];
            s.LastUse = NowSeconds();

            int st = Volatile.Read(ref s.State);
            if (st == (int)State.Live) return s.Clip;
            if (st == (int)State.Failed) return null;

            if (!OnMainThread) { Enqueue(s); return null; }

            if (st == (int)State.Decoded)
            {
                MaterialiseNow(s);
                return s.Clip;
            }

            if (file.Duration <= SyncDecodeMaxSeconds)
            {
                if (Interlocked.CompareExchange(ref s.State, (int)State.Decoding, st) == st)
                {
                    if (DecodeInto(s))
                    {
                        MaterialiseNow(s);
                        SyncDecodes++;
                        return s.Clip;
                    }
                    return null;
                }
                return null;
            }

            Enqueue(s);
            return null;
        }

        /// <summary>
        /// Queue a load and invoke <paramref name="onReady"/> on the main thread once the clip is
        /// live. The callback is guaranteed never to run on a decode worker.
        /// </summary>
        public void Request(BankFile file, Action<AudioClip> onReady)
        {
            if (file == null)
            {
                if (onReady != null) RunOnMain(() => onReady(null));
                return;
            }

            var s = _slots[file.Id];
            s.LastUse = NowSeconds();

            int st = Volatile.Read(ref s.State);

            if (st == (int)State.Live)
            {
                if (onReady != null) { var c = s.Clip; RunOnMain(() => onReady(c)); }
                return;
            }
            if (st == (int)State.Failed)
            {
                if (onReady != null) RunOnMain(() => onReady(null));
                return;
            }

            if (onReady != null)
            {
                bool alreadyLive = false;
                AudioClip live = null;

                lock (s)
                {
                    if (Volatile.Read(ref s.State) == (int)State.Live) { alreadyLive = true; live = s.Clip; }
                    else (s.Waiters ?? (s.Waiters = new List<Action<AudioClip>>(2))).Add(onReady);
                }

                if (alreadyLive) { RunOnMain(() => onReady(live)); return; }
            }

            Enqueue(s);
        }

        public void Prefetch(IList<BankFile> files)
        {
            if (files == null) return;
            for (int i = 0; i < files.Count; i++)
            {
                var f = files[i];
                if (f == null) continue;
                var s = _slots[f.Id];
                if (Volatile.Read(ref s.State) == (int)State.Cold) Enqueue(s);
            }
        }

        private void Enqueue(Slot s)
        {
            if (Interlocked.CompareExchange(ref s.State, (int)State.Queued, (int)State.Cold) != (int)State.Cold) return;
            try { _decodeQueue.Add(s); }
            catch { Interlocked.Exchange(ref s.State, (int)State.Cold); }
        }

        // ---- reference counting for eviction safety --------------------------

        public void AddBinding(BankFile f) { if (f != null) Interlocked.Increment(ref _slots[f.Id].Bindings); }

        public void ClearSceneBindings()
        {
            for (int i = 0; i < _slots.Length; i++) Volatile.Write(ref _slots[i].Bindings, 0);
        }

        // ---- worker side ----------------------------------------------------
        //
        // Nothing from here to the main-thread section touches il2cpp.

        private void WorkerLoop()
        {
            var buffer = new byte[1 << 20];
            while (!_shutdown)
            {
                Slot s;
                try { s = _decodeQueue.Take(); }
                catch { return; }
                if (s == null) continue;
                if (Interlocked.CompareExchange(ref s.State, (int)State.Decoding, (int)State.Queued) != (int)State.Queued) continue;
                DecodeInto(s, ref buffer);
            }
        }

        private bool DecodeInto(Slot s)
        {
            byte[] scratch = null;
            return DecodeInto(s, ref scratch);
        }

        private bool DecodeInto(Slot s, ref byte[] scratch)
        {
            try
            {
                var f = s.File;
                int len = (int)f.FileBytes;

                byte[] buf;
                if (scratch != null && scratch.Length >= len) buf = scratch;
                else { buf = new byte[Math.Max(len, 1 << 16)]; if (scratch != null) scratch = buf; }

                using (var fs = new FileStream(f.FullPath, FileMode.Open, FileAccess.Read, FileShare.Read, 1 << 16, FileOptions.SequentialScan))
                {
                    int off = 0;
                    while (off < len)
                    {
                        int n = fs.Read(buf, off, len - off);
                        if (n <= 0) break;
                        off += n;
                    }
                    len = off;
                }

                PcmData pcm;
                string err;
                if (!WavDecoder.TryDecode(buf, len, out pcm, out err))
                {
                    _warn?.Invoke("decode failed for " + f.RelPath + ": " + err);
                    Fail(s);
                    return false;
                }

                s.Pcm = pcm;
                Volatile.Write(ref s.State, (int)State.Decoded);
                _createQueue.Enqueue(s);
                return true;
            }
            catch (Exception e)
            {
                _warn?.Invoke("decode exception for " + s.File.RelPath + ": " + e.Message);
                Fail(s);
                return false;
            }
        }

        /// <summary>
        /// Mark a slot failed and hand it to the pump. Waiters are deliberately not flushed here:
        /// this can run on a decode worker, and the callbacks assign Unity objects.
        /// </summary>
        private void Fail(Slot s)
        {
            s.Pcm = null;
            Volatile.Write(ref s.State, (int)State.Failed);
            Interlocked.Increment(ref _decodeFailures);
            _createQueue.Enqueue(s);
        }

        // ---- main thread side -----------------------------------------------

        /// <summary>Called once per frame from the pump. Bounded so it never causes a hitch.</summary>
        public void PumpMainThread()
        {
            DecodeFailures = Volatile.Read(ref _decodeFailures);

            var sw = Stopwatch.StartNew();

            // Cross-thread callbacks first: they are cheap and they unblock role bindings.
            Action action;
            while (sw.Elapsed.TotalMilliseconds < MainQueueBudgetMs && _mainQueue.TryDequeue(out action))
                SafeInvoke(action);

            sw.Restart();
            bool first = true;

            Slot s;
            while ((first || sw.Elapsed.TotalMilliseconds < CreateBudgetMs) && _createQueue.TryDequeue(out s))
            {
                first = false;

                int st = Volatile.Read(ref s.State);
                if (st == (int)State.Decoded) MaterialiseNow(s);
                else if (st == (int)State.Failed) FlushWaiters(s, null);
            }

            if (Interlocked.Read(ref _residentBytes) > _budgetBytes) Evict();
        }

        private void MaterialiseNow(Slot s)
        {
            var pcm = s.Pcm;
            if (pcm == null)
            {
                if (Volatile.Read(ref s.State) != (int)State.Live)
                {
                    Volatile.Write(ref s.State, (int)State.Failed);
                    FlushWaiters(s, null);
                }
                return;
            }

            AudioClip clip = null;
            try
            {
                clip = AudioClip.Create("OGSR:" + s.File.RelPath, pcm.Frames, pcm.Channels, pcm.SampleRate, false);
                if (clip == null) throw new Exception("AudioClip.Create returned null");

                var arr = MakeIl2CppArray(pcm.Samples);
                if (!clip.SetData(arr, 0)) throw new Exception("AudioClip.SetData rejected the buffer");
            }
            catch (Exception e)
            {
                _warn?.Invoke("clip creation failed for " + s.File.RelPath + ": " + e.Message);
                if (clip != null) { try { UnityEngine.Object.Destroy(clip); } catch { } }
                s.Pcm = null;
                Volatile.Write(ref s.State, (int)State.Failed);
                Interlocked.Increment(ref _decodeFailures);
                FlushWaiters(s, null);
                return;
            }

            s.Clip = clip;
            s.Pcm = null;
            s.LastUse = NowSeconds();
            Volatile.Write(ref s.State, (int)State.Live);
            Interlocked.Add(ref _residentBytes, s.File.DecodedBytes);
            LiveClips++;

            ClipRegistry.Register(clip);
            FlushWaiters(s, clip);
        }

        /// <summary>Main thread only - reached through PumpMainThread or GetImmediate.</summary>
        private void FlushWaiters(Slot s, AudioClip clip)
        {
            List<Action<AudioClip>> waiters;
            lock (s) { waiters = s.Waiters; s.Waiters = null; }
            if (waiters == null) return;

            for (int i = 0; i < waiters.Count; i++)
            {
                try { waiters[i](clip); }
                catch (Exception e) { _warn?.Invoke("continuation threw: " + e.Message); }
            }
        }

        /// <summary>
        /// Copies managed float PCM into an il2cpp float[]. The payload begins four pointer-sized
        /// words past the object header - confirmed against Il2CppArrayBase.ArrayStartPointer,
        /// which is Pointer + sizeof(Il2CppObject) + sizeof(void*) + sizeof(UIntPtr) = Pointer + 32
        /// on x64. The copy is verified through the interop indexer before being trusted, and falls
        /// back to element-wise assignment if the layout is ever anything else.
        /// </summary>
        private static unsafe Il2CppStructArray<float> MakeIl2CppArray(float[] src)
        {
            int n = src.Length;
            var arr = new Il2CppStructArray<float>(n);

            try
            {
                IntPtr baseAddr = arr.Pointer;
                if (baseAddr != IntPtr.Zero)
                {
                    byte* dst = (byte*)baseAddr + 4 * IntPtr.Size;
                    fixed (float* p = src)
                        Buffer.MemoryCopy(p, dst, (long)n * sizeof(float), (long)n * sizeof(float));

                    bool ok = n == 0 || (arr[0] == src[0] && arr[n - 1] == src[n - 1]);
                    if (ok) return arr;
                }
            }
            catch { /* fall through to the safe path */ }

            for (int i = 0; i < n; i++) arr[i] = src[i];
            return arr;
        }

        // ---- eviction -------------------------------------------------------

        private readonly List<Slot> _evictScratch = new List<Slot>(64);

        private void Evict()
        {
            float now = NowSeconds();
            _evictScratch.Clear();

            for (int i = 0; i < _slots.Length; i++)
            {
                var s = _slots[i];
                if (Volatile.Read(ref s.State) != (int)State.Live) continue;
                if (s.Pinned) continue;
                if (Volatile.Read(ref s.Bindings) > 0) continue;
                if (now - s.LastUse < EvictIdleSeconds) continue;
                if (s.Clip == null) continue;
                if (ClipRegistry.IsAudible(s.Clip)) continue;
                _evictScratch.Add(s);
            }

            _evictScratch.Sort(static (a, b) => a.LastUse.CompareTo(b.LastUse));

            for (int i = 0; i < _evictScratch.Count && Interlocked.Read(ref _residentBytes) > _budgetBytes; i++)
            {
                var s = _evictScratch[i];
                try
                {
                    ClipRegistry.Unregister(s.Clip);
                    UnityEngine.Object.Destroy(s.Clip);
                }
                catch { }
                s.Clip = null;
                Volatile.Write(ref s.State, (int)State.Cold);
                Interlocked.Add(ref _residentBytes, -s.File.DecodedBytes);
                LiveClips--;
                Evictions++;
            }
        }

        private static float NowSeconds()
        {
            return (float)Stopwatch.GetTimestamp() / Stopwatch.Frequency;
        }
    }
}
