// OG Sound Remaster - Harmony interception
// Author: StixsworldHD (StixsmasterHD4k)
//
// Two layers:
//
//  1. Three patches on UnityEngine.AudioSource form a universal safety net. Every managed
//     playback in the engine funnels through Play() or one of the two PlayOneShot overloads,
//     including AudioSource.PlayClipAtPoint which internally assigns a clip and calls Play().
//     PlayOneShot is redirected straight to PlayOneShotHelper rather than relying on Harmony ref
//     parameter write-back, so no assumptions are made about il2cpp argument marshalling.
//
//  2. Postfix hooks on the game's own Awake/Start methods drive deterministic role binding at the
//     exact moment each component initialises. Nothing polls, nothing scans per frame.
//
// Steady-state cost of layer 1 is a single dictionary probe on a pointer. No strings, no
// allocation, no il2cpp metadata access.

using System;
using System.Collections.Generic;
using System.Reflection;
using HarmonyLib;
using OGSoundRemaster.Audio;
using OGSoundRemaster.Binding;
using OGSoundRemaster.Mapping;
using UnityEngine;

namespace OGSoundRemaster.Patches
{
    public static class Hooks
    {
        internal static RoleBinder Binder;
        internal static NameResolver Resolver;
        internal static Action<string> Warn;

        public static int PatchesApplied;
        public static int PatchesFailed;
        public static readonly List<string> FailedPatches = new List<string>();

        private static Harmony _harmony;

        public static void Apply(Harmony harmony, RoleBinder binder, NameResolver resolver, Action<string> warn)
        {
            _harmony = harmony;
            Binder = binder;
            Resolver = resolver;
            Warn = warn;

            // ---- layer 1: universal playback interception ----
            Patch(typeof(AudioSource), "Play", Type.EmptyTypes, nameof(AudioSource_Play_Prefix), null);
            Patch(typeof(AudioSource), "PlayOneShot", new[] { typeof(AudioClip) }, nameof(AudioSource_PlayOneShot1_Prefix), null);
            Patch(typeof(AudioSource), "PlayOneShot", new[] { typeof(AudioClip), typeof(float) }, nameof(AudioSource_PlayOneShot2_Prefix), null);

            // ---- layer 2: role binding at component initialisation ----
            Patch(typeof(GameManager), "Awake", Type.EmptyTypes, null, nameof(GameManager_Awake_Postfix));
            Patch(typeof(SoundManager), "Start", Type.EmptyTypes, null, nameof(SoundManager_Start_Postfix));
            Patch(typeof(MissionManager), "Start", Type.EmptyTypes, null, nameof(MissionManager_Start_Postfix));
            Patch(typeof(HUD), "Start", Type.EmptyTypes, null, nameof(HUD_Start_Postfix));
            Patch(typeof(MainMenuScreen), "Awake", Type.EmptyTypes, null, nameof(MainMenuScreen_Awake_Postfix));
            Patch(typeof(BriefingScreen), "Start", Type.EmptyTypes, null, nameof(BriefingScreen_Start_Postfix));
            Patch(typeof(DebriefingScreen), "Start", Type.EmptyTypes, null, nameof(DebriefingScreen_Start_Postfix));

            Patch(typeof(WeaponBase), "Start", Type.EmptyTypes, null, nameof(WeaponBase_Start_Postfix));
            Patch(typeof(WeaponMG42), "Start", Type.EmptyTypes, null, nameof(WeaponMG42_Start_Postfix));
            Patch(typeof(EquipmentBase), "Awake", Type.EmptyTypes, null, nameof(EquipmentBase_Awake_Postfix));
            Patch(typeof(EquipmentAPMine), "Awake", Type.EmptyTypes, null, nameof(EquipmentBase_Awake_Postfix));
            Patch(typeof(EquipmentDynamite), "Awake", Type.EmptyTypes, null, nameof(EquipmentBase_Awake_Postfix));
            Patch(typeof(EquipmentGrenade), "Awake", Type.EmptyTypes, null, nameof(EquipmentBase_Awake_Postfix));
            Patch(typeof(EquipmentCoin), "Awake", Type.EmptyTypes, null, nameof(EquipmentBase_Awake_Postfix));
            Patch(typeof(EquipmentMedkit), "Awake", Type.EmptyTypes, null, nameof(EquipmentBase_Awake_Postfix));
            Patch(typeof(EquipmentBinoculars), "Awake", Type.EmptyTypes, null, nameof(EquipmentBase_Awake_Postfix));

            Patch(typeof(PlayerController), "Start", Type.EmptyTypes, null, nameof(PlayerController_Start_Postfix));
            Patch(typeof(AIController), "Start", Type.EmptyTypes, null, nameof(Controller_Start_Postfix));
            Patch(typeof(NPC), "Start", Type.EmptyTypes, null, nameof(NPC_Start_Postfix));
            Patch(typeof(SquadMember), "Start", Type.EmptyTypes, null, nameof(Controller_Start_Postfix));
            Patch(typeof(GermanShepherd), "Awake", Type.EmptyTypes, null, nameof(GermanShepherd_Awake_Postfix));

            Patch(typeof(Tank), "Start", Type.EmptyTypes, null, nameof(Tank_Start_Postfix));
            Patch(typeof(FootStepSoundPlayer), "Start", Type.EmptyTypes, null, nameof(FootStep_Start_Postfix));
            Patch(typeof(ProjectileRocket), "Start", Type.EmptyTypes, null, nameof(Rocket_Start_Postfix));
            Patch(typeof(AttachmentHandler), "Start", Type.EmptyTypes, null, nameof(Attachment_Start_Postfix));
            Patch(typeof(ExplodeWhenDamaged), "Awake", Type.EmptyTypes, null, nameof(ExplodeWhenDamaged_Awake_Postfix));
            Patch(typeof(MineField), "Awake", Type.EmptyTypes, null, nameof(MineField_Awake_Postfix));

            Patch(typeof(RandomSoundPlayer), "Awake", Type.EmptyTypes, null, nameof(RandomSoundPlayer_Awake_Postfix));
            Patch(typeof(RandomLoopingSoundPlayer), "Awake", Type.EmptyTypes, null, nameof(RandomLoopingSoundPlayer_Awake_Postfix));
        }

        private static void Patch(Type declaring, string method, Type[] args, string prefix, string postfix)
        {
            string label = declaring.Name + "." + method;
            try
            {
                MethodInfo target = AccessTools.Method(declaring, method, args);
                if (target == null)
                {
                    PatchesFailed++; FailedPatches.Add(label + " (not found)");
                    return;
                }

                var pre = prefix == null ? null : new HarmonyMethod(AccessTools.Method(typeof(Hooks), prefix));
                var post = postfix == null ? null : new HarmonyMethod(AccessTools.Method(typeof(Hooks), postfix));

                _harmony.Patch(target, prefix: pre, postfix: post);
                PatchesApplied++;
            }
            catch (Exception e)
            {
                PatchesFailed++;
                FailedPatches.Add(label + " (" + e.GetType().Name + ": " + e.Message + ")");
                Warn?.Invoke("could not patch " + label + ": " + e.Message);
            }
        }

        // =====================================================================
        // Layer 1 - universal playback interception
        // =====================================================================

        public static void AudioSource_Play_Prefix(AudioSource __instance)
        {
            try
            {
                if (__instance == null) return;
                var clip = __instance.clip;
                if (clip == null) return;

                var replacement = Remapper.Resolve(clip);
                if (replacement != null)
                {
                    __instance.clip = replacement;
                    ClipRegistry.NotePlayed(replacement);
                }
                else
                {
                    ClipRegistry.NotePlayed(clip);
                }
            }
            catch { }
        }

        public static bool AudioSource_PlayOneShot1_Prefix(AudioSource __instance, AudioClip clip)
        {
            try
            {
                if (__instance == null || clip == null) return true;

                var replacement = Remapper.Resolve(clip);
                if (replacement == null) { ClipRegistry.NotePlayed(clip); return true; }

                ClipRegistry.NotePlayed(replacement);
                AudioSource.PlayOneShotHelper(__instance, replacement, 1f);
                return false;
            }
            catch { return true; }
        }

        public static bool AudioSource_PlayOneShot2_Prefix(AudioSource __instance, AudioClip clip, float volumeScale)
        {
            try
            {
                if (__instance == null || clip == null) return true;

                var replacement = Remapper.Resolve(clip);
                if (replacement == null) { ClipRegistry.NotePlayed(clip); return true; }

                ClipRegistry.NotePlayed(replacement);
                AudioSource.PlayOneShotHelper(__instance, replacement, volumeScale);
                return false;
            }
            catch { return true; }
        }

        // =====================================================================
        // Layer 2 - role binding
        // =====================================================================

        public static void GameManager_Awake_Postfix(GameManager __instance) { Safe(() => Binder.BindGameManager(__instance)); }
        public static void SoundManager_Start_Postfix(SoundManager __instance) { Safe(() => Binder.BindSoundManager(__instance)); }
        public static void MissionManager_Start_Postfix(MissionManager __instance) { Safe(() => Binder.BindMissionManager(__instance)); }
        public static void HUD_Start_Postfix(HUD __instance) { Safe(() => Binder.BindHud(__instance)); }
        public static void MainMenuScreen_Awake_Postfix(MainMenuScreen __instance) { Safe(() => Binder.BindMainMenu(__instance)); }
        public static void BriefingScreen_Start_Postfix(BriefingScreen __instance) { Safe(() => Binder.BindBriefingScreen(__instance)); }
        public static void DebriefingScreen_Start_Postfix(DebriefingScreen __instance) { Safe(() => Binder.BindDebriefingScreen(__instance)); }

        public static void WeaponBase_Start_Postfix(WeaponBase __instance) { Safe(() => Binder.BindWeapon(__instance)); }
        public static void WeaponMG42_Start_Postfix(WeaponMG42 __instance) { Safe(() => Binder.BindMG42(__instance)); }
        public static void EquipmentBase_Awake_Postfix(EquipmentBase __instance) { Safe(() => Binder.BindEquipment(__instance)); }
        public static void PlayerController_Start_Postfix(PlayerController __instance)
        {
            Safe(() => { Binder.BindPlayer(__instance); Binder.BindController(__instance); });
        }
        public static void Controller_Start_Postfix(ControllerBase __instance) { Safe(() => Binder.BindController(__instance)); }
        public static void NPC_Start_Postfix(NPC __instance) { Safe(() => Binder.BindNpc(__instance)); }
        public static void GermanShepherd_Awake_Postfix(GermanShepherd __instance) { Safe(() => Binder.BindDog(__instance)); }

        public static void Tank_Start_Postfix(Tank __instance) { Safe(() => Binder.BindTank(__instance)); }
        public static void FootStep_Start_Postfix(FootStepSoundPlayer __instance) { Safe(() => Binder.BindFootsteps(__instance)); }
        public static void Rocket_Start_Postfix(ProjectileRocket __instance) { Safe(() => Binder.BindRocket(__instance)); }
        public static void Attachment_Start_Postfix(AttachmentHandler __instance) { Safe(() => Binder.BindAttachmentHandler(__instance)); }
        public static void ExplodeWhenDamaged_Awake_Postfix(ExplodeWhenDamaged __instance) { Safe(() => Binder.BindExplodeWhenDamaged(__instance)); }
        public static void MineField_Awake_Postfix(MineField __instance) { Safe(() => Binder.BindMineField(__instance)); }

        // Awake runs before the owning component's Start, so binding here immediately would fight
        // with the role binder and rewrite the clip array twice. The player is parked instead, and
        // the frame pump only falls back to name matching for players nobody claimed.
        public static void RandomSoundPlayer_Awake_Postfix(RandomSoundPlayer __instance)
        {
            Safe(() => Binder.QueueLoose(__instance, CurrentFrame()));
        }

        public static void RandomLoopingSoundPlayer_Awake_Postfix(RandomLoopingSoundPlayer __instance)
        {
            Safe(() => Binder.QueueLooseLooping(__instance, CurrentFrame()));
        }

        private static int CurrentFrame()
        {
            try { return Time.frameCount; } catch { return 0; }
        }

        private static void Safe(Action a)
        {
            try { if (Binder != null) a(); }
            catch (Exception e) { Warn?.Invoke("hook: " + e.Message); }
        }
    }
}
