using HarmonyLib;
using System;
using System.Collections;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Reflection;
using UnityEngine;
using UnityEngine.Networking;

namespace MuzikaGromche;

internal static class AudioClipsCacheManager
{
    // Cache of file names to loaded AudioClips.
    // Cache is cleared at the end of each round.
    static readonly Dictionary<string, AudioClip> Cache = [];

    // In-flight requests
    static readonly Dictionary<string, (UnityWebRequest Request, List<Action<AudioClip>> Setters)> Requests = [];

    // Not just isDone status, but also whether all requests have been processed.
    public static bool AllDone => Requests.Count == 0;

    public static void LoadAudioTrack(IAudioTrack track)
    {
        GlobalBehaviour.Instance.StartCoroutine(LoadAudioTrackCoroutine(track));
    }

    static IEnumerator LoadAudioTrackCoroutine(IAudioTrack track)
    {
        List<UnityWebRequest> requests = [];
        requests.Capacity = 2;

        LoadAudioClip(requests, track.AudioType, track.FileNameIntro, clip => track.LoadedIntro = clip);
        LoadAudioClip(requests, track.AudioType, track.FileNameLoop, clip => track.LoadedLoop = clip);

        yield return new WaitUntil(() => requests.All(request => request.isDone));

        if (requests.All(request => request.result == UnityWebRequest.Result.Success))
        {
            foreach (var request in requests)
            {
                foreach (var (fileName, (Request, Setters)) in Requests)
                {
                    if (request == Request)
                    {
                        Plugin.Log.LogDebug($"Audio clip loaded successfully: {fileName}");
                        var clip = DownloadHandlerAudioClip.GetContent(request);
                        Cache[fileName] = clip;
                        foreach (var setter in Setters)
                        {
                            setter(clip);
                        }
                    }
                }
            }
        }
        else
        {
            var failed = Requests.Values.Where(tuple => tuple.Request.result != UnityWebRequest.Result.Success).Select(tuple => tuple.Request.GetUrl());
            Plugin.Log.LogError("Could not load audio file " + string.Join(", ", failed));
        }

        // cleanup
        foreach (var request in requests)
        {
            // collect matching keys first to avoid mutating Requests while iterating it
            var fileNames = Requests
                .Where(kv => kv.Value.Request == request)
                .Select(kv => kv.Key)
                .ToArray();

            foreach (var fileName in fileNames)
            {
                if (Requests.TryGetValue(fileName, out var tuple) && tuple.Request != null)
                {
                    tuple.Request.Dispose();
                }

                Requests.Remove(fileName);
            }
        }
    }

    static readonly string dir = Path.GetDirectoryName(Assembly.GetExecutingAssembly().Location);

    static void LoadAudioClip(List<UnityWebRequest> requests, AudioType audioType, string fileName, Action<AudioClip> setter)
    {
        if (Cache.TryGetValue(fileName, out var cachedClip))
        {
            Plugin.Log.LogDebug($"Found cached audio clip: {fileName}");
            setter(cachedClip);
        }
        else if (Requests.TryGetValue(fileName, out var tuple))
        {
            Plugin.Log.LogDebug($"Found existing in-flight request for audio clip: {fileName}");
            tuple.Setters.Add(setter);
        }
        else
        {
            Plugin.Log.LogDebug($"Sending request to load audio clip: {fileName}");
            var request = UnityWebRequestMultimedia.GetAudioClip($"file://{dir}/{fileName}", audioType);
            request.SendWebRequest();
            Requests[fileName] = (request, [setter]);
            requests.Add(request);
        }
    }

    public static void Clear()
    {
        // Iterate over LoadedClipsCache keys and values, cross join with Plugin.Tracks list,
        // find AudioTracks that reference the key (file name), and null their corresponding loaded tracks;
        // then destroy tracks and clear the cache.

        Plugin.Log.LogDebug($"Clearing {Cache.Count} cached audio clips and {Requests.Count} pending requests");

        if (Cache.Count > 0)
        {
            var allTracks = Plugin.Tracks.SelectMany(t => t.GetTracks()).ToArray();

            foreach (var (fileName, clip) in Cache)
            {
                foreach (var track in allTracks)
                {
                    // Null out any references to this clip on matching file names.
                    if (track.FileNameIntro == fileName)
                    {
                        track.LoadedIntro = null;
                    }
                    if (track.FileNameLoop == fileName)
                    {
                        track.LoadedLoop = null;
                    }
                }

                if (clip != null)
                {
                    UnityEngine.Object.Destroy(clip);
                }
            }

            Cache.Clear();
        }

        foreach (var (fileName, (Request, Setters)) in Requests)
        {
            if (Request != null)
            {
                Request.Abort();
                Request.Dispose();
            }
        }

        Requests.Clear();
    }
}

[HarmonyPatch(typeof(RoundManager))]
static class ClearAudioClipCachePatch
{
    [HarmonyPatch(nameof(RoundManager.DespawnPropsAtEndOfRound))]
    [HarmonyPatch(nameof(RoundManager.OnDestroy))]
    [HarmonyPrefix]
    static void OnDestroy(RoundManager __instance)
    {
        var _ = __instance;
        AudioClipsCacheManager.Clear();
    }
}
