// OG Sound Remaster - heuristic name matcher
// Author: StixsworldHD (StixsmasterHD4k)
//
// Role binding (see RoleBinder) handles everything the remake exposes as a typed field, which is
// the overwhelming majority of the game's audio and is fully deterministic. This class is the
// safety net for anything else that reaches a Play call: it scores the remake's clip name against
// the original bank's vocabulary and only accepts a match it is actually confident about.
//
// Every result is memoised by the Remapper, so this code runs at most once per distinct clip.

using System;
using System.Collections.Generic;
using System.Text;
using OGSoundRemaster.Audio;

namespace OGSoundRemaster.Mapping
{
    public sealed class NameResolver
    {
        private readonly AudioBank _bank;

        private readonly Dictionary<string, List<BankFile>> _tokenIndex =
            new Dictionary<string, List<BankFile>>(StringComparer.Ordinal);

        private readonly Dictionary<int, string[]> _fileTokens = new Dictionary<int, string[]>();

        /// <summary>Remake vocabulary -> original-bank vocabulary.</summary>
        private static readonly Dictionary<string, string[]> Synonyms = new Dictionary<string, string[]>(StringComparer.Ordinal)
        {
            // weapons and shooting
            { "gunshot",  new[]{ "fire" } },        { "shot",     new[]{ "fire" } },
            { "shoot",    new[]{ "fire" } },        { "firing",   new[]{ "fire" } },
            { "muzzle",   new[]{ "fire" } },        { "reload",   new[]{ "rld" } },
            { "reloading",new[]{ "rld" } },         { "cock",     new[]{ "rld" } },
            { "bolt",     new[]{ "rld" } },         { "empty",    new[]{ "rld" } },
            { "thompson", new[]{ "tmp" } },         { "smg",      new[]{ "tmp", "mp40" } },
            { "garand",   new[]{ "m1_garand" } },   { "m1",       new[]{ "m1_garand" } },
            { "k98",      new[]{ "kar98" } },       { "mauser",   new[]{ "kar98" } },
            { "springfield", new[]{ "m1903a4" } },  { "sniper",   new[]{ "m1903a4" } },
            { "colt",     new[]{ "colt45" } },      { "pistol",   new[]{ "colt45", "waltherp38" } },
            { "p38",      new[]{ "waltherp38" } },  { "walther",  new[]{ "waltherp38" } },
            { "luger",    new[]{ "waltherp38" } },  { "bazooka",  new[]{ "m1a1" } },
            { "panzerfaust", new[]{ "panzerschreck" } },
            { "machinegun", new[]{ "mg42" } },      { "mg",       new[]{ "mg42" } },
            { "rifle",    new[]{ "kar98", "m1_garand" } },
            { "draw",     new[]{ "draw" } },        { "equip",    new[]{ "draw" } },
            { "holster",  new[]{ "draw" } },        { "unholster",new[]{ "draw" } },
            { "knife",    new[]{ "knife" } },       { "melee",    new[]{ "knife" } },
            { "stab",     new[]{ "knife" } },       { "slash",    new[]{ "knife" } },
            { "tracer",   new[]{ "fire" } },        { "casing",   new[]{ "rld" } },

            // impacts
            { "impact",   new[]{ "bullet_hit" } },  { "hit",      new[]{ "bullet_hit" } },
            { "ricochet", new[]{ "bullet_hit" } },  { "bullet",   new[]{ "bullet_hit" } },
            { "flesh",    new[]{ "body" } },        { "blood",    new[]{ "body" } },
            { "gore",     new[]{ "body" } },        { "meat",     new[]{ "body" } },
            { "ground",   new[]{ "dirt" } },        { "soil",     new[]{ "dirt" } },
            { "sand",     new[]{ "dirt" } },        { "gravel",   new[]{ "dirt", "rock" } },
            { "stone",    new[]{ "rock" } },        { "concrete", new[]{ "rock" } },
            { "brick",    new[]{ "rock" } },        { "steel",    new[]{ "metal" } },
            { "iron",     new[]{ "metal" } },       { "plank",    new[]{ "wood" } },
            { "crate",    new[]{ "wood" } },        { "barrel",   new[]{ "metal" } },
            { "window",   new[]{ "glass" } },       { "shatter",  new[]{ "glass" } },

            // explosives
            { "explosion",new[]{ "expl", "boom" } },{ "explode",  new[]{ "expl", "boom" } },
            { "blast",    new[]{ "expl", "boom" } },{ "detonate", new[]{ "expl" } },
            { "boom",     new[]{ "boom", "expl" } },{ "dynamite", new[]{ "tnt" } },
            { "c4",       new[]{ "tnt" } },         { "charge",   new[]{ "tnt" } },
            { "beep",     new[]{ "tnt_beepshort", "controlbeep" } },
            { "timer",    new[]{ "tic_toc" } },     { "tick",     new[]{ "tic_toc" } },
            { "mortar",   new[]{ "motars" } },      { "artillery",new[]{ "motars", "boombg" } },
            { "bomb",     new[]{ "bombdrop" } },    { "shell",    new[]{ "boombg" } },
            { "rocket",   new[]{ "rocket" } },      { "missile",  new[]{ "rocket" } },
            { "throw",    new[]{ "grenade_throw" } },
            { "bounce",   new[]{ "grenade_bounce" } },

            // movement
            { "footstep", new[]{ "walkl", "walkr" } },
            { "step",     new[]{ "walkl", "walkr" } },
            { "foot",     new[]{ "walkl", "walkr" } },
            { "walk",     new[]{ "walkl", "walkr" } },
            { "run",      new[]{ "walkl", "walkr" } },
            { "grass",    new[]{ "foliage" } },     { "bush",     new[]{ "foliage" } },
            { "leaves",   new[]{ "foliage" } },     { "vegetation", new[]{ "foliage" } },
            { "brush",    new[]{ "foliage" } },     { "wade",     new[]{ "wadinglp" } },
            { "wading",   new[]{ "wadinglp" } },    { "swim",     new[]{ "swimminglp" } },
            { "land",     new[]{ "hitground" } },   { "landing",  new[]{ "hitground" } },
            { "fall",     new[]{ "hitground" } },

            // ambience and weather
            { "ambience", new[]{ "wind" } },        { "ambient",  new[]{ "wind" } },
            { "breeze",   new[]{ "wind" } },        { "gust",     new[]{ "windgust" } },
            { "storm",    new[]{ "thunder" } },     { "lightning",new[]{ "thunder" } },
            { "rain",     new[]{ "rain1lp" } },     { "insect",   new[]{ "cricket" } },
            { "bug",      new[]{ "cricket" } },     { "bird",     new[]{ "crow_caw", "seagull", "pigeon" } },
            { "birds",    new[]{ "birds_forest_day" } },
            { "crow",     new[]{ "crow_caw" } },    { "raven",    new[]{ "braven" } },
            { "gull",     new[]{ "seagull" } },     { "wave",     new[]{ "wave_rock", "oceanwaves" } },
            { "surf",     new[]{ "oceanwaves" } },  { "ocean",    new[]{ "oceanwaves" } },
            { "sea",      new[]{ "oceanwaves" } },  { "stream",   new[]{ "river" } },
            { "creek",    new[]{ "river" } },       { "drip",     new[]{ "water_dripslp" } },
            { "flame",    new[]{ "firemedlp" } },   { "burning",  new[]{ "firemedlp" } },
            { "campfire", new[]{ "firemedlp" } },

            // vehicles
            { "engine",   new[]{ "idle" } },        { "motor",    new[]{ "idle" } },
            { "kubelwagen", new[]{ "jeep" } },      { "kubel",    new[]{ "jeep" } },
            { "car",      new[]{ "jeep" } },        { "opel",     new[]{ "truck" } },
            { "lorry",    new[]{ "truck" } },       { "motorcycle", new[]{ "bike" } },
            { "motorbike",new[]{ "bike" } },        { "uboat",    new[]{ "uboat" } },
            { "submarine",new[]{ "uboat" } },       { "boat",     new[]{ "uboat" } },
            { "panzer",   new[]{ "panzer4", "tank" } },
            { "tiger",    new[]{ "panzer4", "tank" } },
            { "panther",  new[]{ "panzer4", "tank" } },
            { "sherman",  new[]{ "shermanidlelp", "shermanfire" } },
            { "tread",    new[]{ "tank" } },        { "track",    new[]{ "tank" } },
            { "gear",     new[]{ "gearshift" } },   { "shift",    new[]{ "gearshift" } },
            { "transmission", new[]{ "gearshift" } },
            { "brake",    new[]{ "brake_squeek" } },{ "skid",     new[]{ "vehicle_skid" } },
            { "tire",     new[]{ "vehicle_skid" } },{ "crash",    new[]{ "car_crash" } },
            { "collision",new[]{ "car_crash" } },   { "horn",     new[]{ "buzzer" } },
            { "turret",   new[]{ "turret" } },
            { "plane",    new[]{ "propplane", "planefly" } },
            { "aircraft", new[]{ "propplane", "planefly" } },
            { "airplane", new[]{ "propplane" } },   { "bomber",   new[]{ "bomber2" } },
            { "flyby",    new[]{ "planefly" } },

            // creatures and people
            { "dog",      new[]{ "dog" } },         { "bark",     new[]{ "dog_attack" } },
            { "growl",    new[]{ "dog_attack" } },  { "shepherd", new[]{ "dog" } },
            { "rat",      new[]{ "rat" } },         { "rodent",   new[]{ "rat" } },
            { "owl",      new[]{ "owl_hoot" } },    { "frog",     new[]{ "frog" } },
            { "chatter",  new[]{ "chatter" } },     { "talk",     new[]{ "chatter" } },
            { "idle",     new[]{ "idle" } },

            // objects and interface
            { "door",     new[]{ "door" } },        { "gate",     new[]{ "door" } },
            { "creak",    new[]{ "woodcreak", "metalcreak", "creakdock" } },
            { "click",    new[]{ "clicksound" } },  { "button",   new[]{ "clicksound" } },
            { "select",   new[]{ "clicksound" } },  { "confirm",  new[]{ "controlbeep" } },
            { "cancel",   new[]{ "controlbeep" } }, { "menu",     new[]{ "clicksound" } },
            { "ui",       new[]{ "clicksound" } },  { "hover",    new[]{ "category" } },
            { "pickup",   new[]{ "pickupitem", "pickupgun" } },
            { "pick",     new[]{ "pickupitem" } },  { "loot",     new[]{ "pickupitem" } },
            { "drop",     new[]{ "dropitem" } },    { "coin",     new[]{ "coin" } },
            { "medkit",   new[]{ "firstaid" } },    { "heal",     new[]{ "firstaid" } },
            { "bandage",  new[]{ "firstaid" } },    { "health",   new[]{ "firstaid" } },
            { "map",      new[]{ "map" } },         { "binoculars", new[]{ "camera" } },
            { "zoom",     new[]{ "camera" } },      { "scope",    new[]{ "camera" } },
            { "photo",    new[]{ "camera" } },      { "flare",    new[]{ "flare" } },
            { "radio",    new[]{ "loudspeaker" } }, { "speaker",  new[]{ "loudspeaker" } },
            { "announce", new[]{ "loudspeaker" } }, { "siren",    new[]{ "airraid" } },
            { "alarm",    new[]{ "airraid" } },     { "whistle",  new[]{ "whistle01" } },
            { "phone",    new[]{ "phone_ring" } },  { "telephone",new[]{ "phone_ring" } },
            { "clock",    new[]{ "grandclock" } },  { "heartbeat",new[]{ "heartbeat" } },
            { "heart",    new[]{ "heartbeat" } },   { "electric", new[]{ "electrichum7lp" } },
            { "hum",      new[]{ "electrichum7lp" } },
            { "windmill", new[]{ "windmilllp" } },  { "lighthouse", new[]{ "lighthouselp" } },
            { "foghorn",  new[]{ "foghorn" } },     { "buoy",     new[]{ "buoy1" } },
            { "power",    new[]{ "power" } },       { "generator",new[]{ "power" } },
            { "flicker",  new[]{ "bnflicker01" } }, { "camera",   new[]{ "camera" } },
        };

        /// <summary>Noise words that carry no matching signal.</summary>
        private static readonly HashSet<string> Stop = new HashSet<string>(StringComparer.Ordinal)
        {
            "sfx","sound","sounds","audio","clip","fx","the","new","old","a","an","of","and",
            "loop","looping","lp","oneshot","one","shot","var","variant","alt","copy","final",
            "wav","mp3","ogg","asset","default","temp","test","main","player","character","obj",
            "prefab","mix","master","stereo","mono","v","ver","version"
        };

        public NameResolver(AudioBank bank)
        {
            _bank = bank;

            for (int i = 0; i < bank.Files.Count; i++)
            {
                var f = bank.Files[i];
                var toks = Tokenize(f.Name);
                _fileTokens[f.Id] = toks;

                for (int t = 0; t < toks.Length; t++)
                {
                    List<BankFile> list;
                    if (!_tokenIndex.TryGetValue(toks[t], out list)) { list = new List<BankFile>(4); _tokenIndex[toks[t]] = list; }
                    list.Add(f);
                }
            }
        }

        /// <summary>Split a name into lowercase tokens on separators and camelCase boundaries.</summary>
        public static string[] Tokenize(string name)
        {
            if (string.IsNullOrEmpty(name)) return Array.Empty<string>();

            var result = new List<string>(6);
            var sb = new StringBuilder(24);

            for (int i = 0; i < name.Length; i++)
            {
                char c = name[i];

                bool sep = c == '_' || c == '-' || c == ' ' || c == '.' || c == '/' || c == '\\' ||
                           c == '(' || c == ')' || c == '[' || c == ']' || c == '#' || c == '+';

                if (sep)
                {
                    if (sb.Length > 0) { result.Add(sb.ToString()); sb.Clear(); }
                    continue;
                }

                // camelCase / PascalCase boundary
                if (sb.Length > 0 && char.IsUpper(c) && !char.IsUpper(name[i - 1]))
                {
                    result.Add(sb.ToString()); sb.Clear();
                }

                sb.Append(char.ToLowerInvariant(c));
            }
            if (sb.Length > 0) result.Add(sb.ToString());

            // Drop pure digits and stop words, but keep alphanumeric compounds like "mp40".
            var final = new List<string>(result.Count);
            for (int i = 0; i < result.Count; i++)
            {
                string t = result[i];
                if (t.Length == 0) continue;

                bool allDigits = true;
                for (int k = 0; k < t.Length; k++) if (t[k] < '0' || t[k] > '9') { allDigits = false; break; }
                if (allDigits) continue;

                if (Stop.Contains(t)) continue;

                // "walk1" -> also emit "walk"
                int trimmed = t.Length;
                while (trimmed > 0 && t[trimmed - 1] >= '0' && t[trimmed - 1] <= '9') trimmed--;
                if (trimmed >= 3 && trimmed < t.Length)
                {
                    string bare = t.Substring(0, trimmed);
                    if (!Stop.Contains(bare)) final.Add(bare);
                }

                final.Add(t);
            }

            return final.ToArray();
        }

        private readonly Dictionary<int, float> _scores = new Dictionary<int, float>(128);

        /// <summary>
        /// Best original-game file for a remake clip name, or null when nothing scores high enough.
        /// Deliberately conservative: a wrong swap is far worse than leaving a sound alone.
        /// </summary>
        public BankFile Resolve(string clipName, out float confidence)
        {
            confidence = 0f;
            if (string.IsNullOrEmpty(clipName)) return null;

            // Direct hits first - the remake may well reuse original file names.
            string lower = clipName.ToLowerInvariant();
            var direct = _bank.Get(lower);
            if (direct != null) { confidence = 10f; return direct; }

            int dot = lower.LastIndexOf('.');
            if (dot > 0)
            {
                direct = _bank.Get(lower.Substring(0, dot));
                if (direct != null) { confidence = 10f; return direct; }
            }

            var tokens = Tokenize(clipName);
            if (tokens.Length == 0) return null;

            _scores.Clear();

            float totalWeight = 0f;
            for (int i = 0; i < tokens.Length; i++)
            {
                string tok = tokens[i];
                float weight = tok.Length >= 5 ? 1.4f : (tok.Length >= 3 ? 1.0f : 0.5f);
                totalWeight += weight;

                Accumulate(tok, weight * 3f);

                string[] syn;
                if (Synonyms.TryGetValue(tok, out syn))
                    for (int k = 0; k < syn.Length; k++) Accumulate(syn[k], weight * 2.2f);
            }

            if (totalWeight <= 0f || _scores.Count == 0) return null;

            BankFile best = null;
            float bestScore = 0f;

            foreach (var kv in _scores)
            {
                var f = _bank.Files[kv.Key];
                string[] ftoks = _fileTokens[f.Id];

                // Normalise by both sides so a 1-token file cannot win on a single weak overlap.
                float denom = totalWeight * 3f + ftoks.Length * 0.8f;
                float score = kv.Value / Math.Max(1f, denom);

                if (score > bestScore) { bestScore = score; best = f; }
            }

            // Threshold tuned so single generic-token overlaps ("fire", "hit") are rejected but a
            // real two-token agreement ("mp40" + "reload") passes.
            if (best == null || bestScore < 0.34f) return null;

            confidence = bestScore;
            return best;
        }

        private void Accumulate(string token, float weight)
        {
            List<BankFile> list;
            if (!_tokenIndex.TryGetValue(token, out list)) return;

            for (int i = 0; i < list.Count; i++)
            {
                int id = list[i].Id;
                float cur;
                _scores.TryGetValue(id, out cur);
                _scores[id] = cur + weight;
            }
        }

        /// <summary>All numbered siblings of a resolved file, so variety is preserved.</summary>
        public List<BankFile> VariantsOf(BankFile f)
        {
            if (f == null) return AudioBank.Empty;
            var g = _bank.Group(f.GroupKey);
            return g.Count > 0 ? g : new List<BankFile>(1) { f };
        }
    }
}
