// OG Sound Remaster - curated original-game sound tables
// Author: StixsworldHD (StixsmasterHD4k)
//
// Every entry here is a deliberate editorial decision about which 2002 asset stands in for which
// Reloaded asset. The mission music playlist is not guesswork: it is transcribed verbatim from
// Music/songs.txt shipped inside the original game's sound folder, so missions get exactly the
// tracks they had in 2002.

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

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

        public SoundMap(AudioBank bank) { _bank = bank; }

        private List<BankFile> G(params string[] keys) => _bank.Groups(keys);
        private List<BankFile> One(string key)
        {
            var f = _bank.Get(key);
            return f == null ? AudioBank.Empty : new List<BankFile>(1) { f };
        }

        // ---- weapons ---------------------------------------------------------

        /// <summary>
        /// Fire sound for a weapon component type. The original bank only ships continuous burst
        /// loops for the three automatics, so when the remake fires a one-shot per round we use the
        /// single-shot cuts derived from those loops instead (see Derived/ and the README).
        /// </summary>
        public List<BankFile> WeaponFire(string typeName, bool looping)
        {
            switch (Norm(typeName))
            {
                case "weaponcolt45":        return One("colt45-fire");
                case "weapongarand":        return One("m1_garand-fire");
                case "weaplink98":
                case "weaponk98":           return One("kar98-fire");
                case "weaponspringfield":   return One("m1903a4-fire");
                case "weaponwaltherp38":    return One("waltherp38-fire");
                case "weaponbazooka":       return One("m1a1-fire");
                case "weaponpanzerschreck": return One("panzerschreck-fire");
                case "weaponthompson":      return One(looping ? "tmp-loop"  : "derived/tmp-single");
                case "weaponmp40":          return One(looping ? "mp40-loop" : "derived/mp40-single");
                case "weaponmg42":          return One(looping ? "mg42-loop" : "derived/mg42-single");
                default:                    return One("kar98-fire");
            }
        }

        public List<BankFile> WeaponReload(string typeName)
        {
            switch (Norm(typeName))
            {
                case "weaponcolt45":        return One("colt45-rld");
                case "weapongarand":        return One("m1_garand-rld");
                case "weaponk98":           return One("kar98-rld");
                case "weaponspringfield":   return One("m1903a4-rld");
                case "weaponwaltherp38":    return One("waltherp38-rld");
                case "weaponthompson":      return One("tmp-rld");
                case "weaponmp40":          return One("mp40-rld");
                case "weaponmg42":          return One("mg42-rld");
                case "weaponbazooka":
                case "weaponpanzerschreck": return One("rifle_draw01");
                default:                    return One("kar98-rld");
            }
        }

        /// <summary>Tail-off for the automatics; empty for everything else.</summary>
        public List<BankFile> WeaponFireEnd(string typeName)
        {
            switch (Norm(typeName))
            {
                case "weaponthompson": return One("tmp-end");
                case "weaponmp40":     return One("mp40-end");
                case "weaponmg42":     return One("mg42-end");
                default:               return AudioBank.Empty;
            }
        }

        public List<BankFile> GarandPing => One("m1_garand-rld");
        public List<BankFile> WeaponDraw => One("rifle_draw01");
        public List<BankFile> KnifeDraw  => One("knife_draw01");
        public List<BankFile> KnifeAttack => G("knife_attack");

        // ---- squad voice packs ----------------------------------------------

        /// <summary>The four original American voice packs, in roster order.</summary>
        public static readonly string[] VoicePackFolders = { "avoice1", "avoice2", "avoice3", "avoice5" };

        /// <summary>SquadMemberVoicePack field name -> original line category.</summary>
        private static readonly Dictionary<string, string> SquadRoles = new Dictionary<string, string>(StringComparer.Ordinal)
        {
            { "attackMyTargetSound",     "attack" },
            { "fireAtWillSound",         "fireatwill" },
            { "followSound",             "follow" },
            { "holdFireSound",           "holdfire" },
            { "holdPositionSound",       "holdpos" },
            { "switchToSound",           "switch" },
            { "killSound",               "command" },
            { "objectivesCompleteSound", "command" },
            { "damagedSound",            "damage" },
            { "friendlyFireSound",       "damage" },
            { "deathSound",              "death" },
            { "noTargetSound",           "lowammo" },
        };

        public List<BankFile> SquadVoice(int packIndex, string roleField)
        {
            string cat;
            if (!SquadRoles.TryGetValue(roleField, out cat)) return AudioBank.Empty;

            string folder = VoicePackFolders[((packIndex % VoicePackFolders.Length) + VoicePackFolders.Length) % VoicePackFolders.Length];
            var list = _bank.Group(folder + "/" + cat);
            if (list.Count > 0) return list;

            // Not every pack ships every category - fall back through the other packs.
            for (int i = 0; i < VoicePackFolders.Length; i++)
            {
                list = _bank.Group(VoicePackFolders[i] + "/" + cat);
                if (list.Count > 0) return list;
            }
            return AudioBank.Empty;
        }

        // ---- enemy / neutral voices -----------------------------------------

        public List<BankFile> EnemyAlert   => G("voice/alert", "voice/notice");
        public List<BankFile> EnemyDamaged => G("voice/damage");
        public List<BankFile> EnemyDeath   => G("voice/death");
        public List<BankFile> EnemyKilledPlayer => G("voice/pdead");
        public List<BankFile> EnemyEndSearch    => G("voice/endsearch");

        public List<BankFile> DogAttack  => G("dog_attack");
        public List<BankFile> DogDamaged => G("dog_damage");
        public List<BankFile> DogDeath   => G("dog_death");
        public List<BankFile> DogAlert   => G("dog_alert");

        // ---- impacts ---------------------------------------------------------

        // Rock joins dirt: both are "hard natural ground" in the original set and the remake only
        // exposes four impact categories, so folding them keeps eight distinct variations alive.
        public List<BankFile> HitDirt  => G("hitsounds/bullet_hit_dirt", "hitsounds/bullet_hit_rock");
        public List<BankFile> HitMetal => G("hitsounds/bullet_hit_metal");
        public List<BankFile> HitGlass => G("hitsounds/bullet_hit_glass");
        public List<BankFile> HitFlesh => G("hitsounds/bullet_hit_body");
        public List<BankFile> HitWood  => G("hitsounds/bullet_hit_wood");

        // ---- movement --------------------------------------------------------

        // Mostly hard footfalls with two foliage rustles mixed in, matching how the original
        // alternated boot steps with undergrowth on its outdoor maps.
        public List<BankFile> Footsteps
        {
            get
            {
                var l = new List<BankFile>(8);
                Append(l, _bank.Group("walkl"));
                Append(l, _bank.Group("walkr"));
                var fol = _bank.Group("foliage");
                if (fol.Count > 0) l.Add(fol[0]);
                if (fol.Count > 2) l.Add(fol[2]);
                return l;
            }
        }

        public List<BankFile> FoliageMove => G("foliage", "folmove");
        public List<BankFile> Wading      => One("wadinglp");
        public List<BankFile> Swimming    => One("swimminglp");
        public List<BankFile> HitGround   => G("hitground");

        // ---- explosives and equipment ---------------------------------------

        public List<BankFile> APMinePlace   => One("apmineplace");
        public List<BankFile> APMineExplode => G("apmineexp");
        public List<BankFile> ATMinePlace   => One("atmineplace");
        public List<BankFile> ATMineExplode => G("atmineexp");
        public List<BankFile> MineExplode   => G("mine_explosion", "apmineexp");
        public List<BankFile> TntPlace      => One("atmineplace");
        public List<BankFile> TntBeep       => One("tnt_beepshort01");
        public List<BankFile> TntBeepLong   => One("tnt_beeplong01");
        public List<BankFile> TntTicToc     => One("tic_toc");
        public List<BankFile> TntExplode    => One("tnt_explosion");
        public List<BankFile> GrenadeBounce => G("grenade_bounce");
        public List<BankFile> GrenadeExplode=> One("grenade_explosion");
        public List<BankFile> GrenadeThrow  => One("grenade_throw");
        public List<BankFile> GrenadeDraw   => One("grenade_draw01");
        public List<BankFile> CoinThrow     => One("coin_throw");
        public List<BankFile> CoinBounce    => One("coin_bounce");
        public List<BankFile> FirstAid      => One("firstaid");
        public List<BankFile> Binoculars    => One("camera");
        public List<BankFile> DropItem      => One("dropitem");
        public List<BankFile> PickupGun     => One("pickupgun");
        public List<BankFile> PickupItem    => One("pickupitem");
        public List<BankFile> GenericExplode=> G("expl");
        public List<BankFile> BigExplode    => G("boombg", "bigweapons");
        public List<BankFile> RocketFly     => One("rocket_fly");
        public List<BankFile> RocketExplode => G("rocket_boom");
        public List<BankFile> CarHitBody    => One("carhitbody");
        public List<BankFile> FlareFire     => One("flare_fire");
        public List<BankFile> FlareEnd      => One("flare_end");
        public List<BankFile> MapOpen       => One("map");
        public List<BankFile> Door          => One("door");
        public List<BankFile> WoodBreak     => G("woodbreak");
        public List<BankFile> GlassBreak    => G("glass");

        // ---- armour ----------------------------------------------------------

        public List<BankFile> TankIdle      => G("shermanidlelp", "trckidle");
        public List<BankFile> TankMove      => G("tank", "trckloop");
        public List<BankFile> TankTurretLoop=> One("turret_loop");
        public List<BankFile> TankTurretMove=> G("turretstartstop");
        public List<BankFile> TankFire(string objectName)
        {
            string n = Norm(objectName);
            if (n.Contains("sherman") || n.Contains("allied") || n.Contains("us"))
                return G("shermanfire");
            return G("panzer4-fire");
        }

        // ---- wheeled vehicles -------------------------------------------------

        public enum VehicleFamily { Jeep, Truck, Bike, Boat }

        public static VehicleFamily FamilyOf(string objectName)
        {
            string n = Norm(objectName);
            if (n.Contains("bike") || n.Contains("motor") || n.Contains("bmw") || n.Contains("zundapp")) return VehicleFamily.Bike;
            if (n.Contains("truck") || n.Contains("opel") || n.Contains("lorry") || n.Contains("blitz")) return VehicleFamily.Truck;
            if (n.Contains("boat") || n.Contains("uboat") || n.Contains("sub") || n.Contains("ship")) return VehicleFamily.Boat;
            return VehicleFamily.Jeep;
        }

        public List<BankFile> VehicleIdle(VehicleFamily f)
        {
            switch (f)
            {
                case VehicleFamily.Bike:  return One("bike_idlelp");
                case VehicleFamily.Truck: return G("truck_idlelp", "trckidle");
                case VehicleFamily.Boat:  return One("uboat_idle");
                default:                  return One("jeep_idlelp");
            }
        }

        public List<BankFile> VehicleDriving(VehicleFamily f)
        {
            switch (f)
            {
                case VehicleFamily.Bike:  return One("bike_drivinglp");
                case VehicleFamily.Truck: return G("truck_drivinglp", "trckloop");
                case VehicleFamily.Boat:  return One("uboat_idle");
                default:                  return One("jeep_drivinglp");
            }
        }

        public List<BankFile> VehicleStart(VehicleFamily f)
        {
            switch (f)
            {
                case VehicleFamily.Bike:  return One("bike_start");
                case VehicleFamily.Truck: return One("truck_door_start");
                case VehicleFamily.Boat:  return One("uboat_idle");
                default:                  return One("jeep_door_start");
            }
        }

        public List<BankFile> VehicleStop(VehicleFamily f)
        {
            switch (f)
            {
                case VehicleFamily.Bike:  return One("bike_off");
                case VehicleFamily.Truck: return One("truck_off_door");
                default:                  return One("jeep_off_door");
            }
        }

        public List<BankFile> VehicleCrash      => G("car_crash");
        public List<BankFile> VehicleGearChange => G("gearshift");
        public List<BankFile> VehicleSkid       => One("vehicle_skid");
        public List<BankFile> VehicleBrake      => One("brake_squeek");
        public List<BankFile> VehicleHorn       => One("buzzer");
        public List<BankFile> VehicleSuspension => G("hitground");

        // ---- music -----------------------------------------------------------

        /// <summary>
        /// Verbatim from the original game's Music/songs.txt. Index is zero-based mission index,
        /// so element 0 is the original "mission1:" block.
        /// </summary>
        private static readonly string[][] MissionPlaylist =
        {
            new[]{ "music/sneaky02",  "music/tension01" },   // mission 1
            new[]{ "music/sneaky01",  "music/tension02" },   // mission 2
            new[]{ "music/calm01",    "music/tension03" },   // mission 3
            new[]{ "music/tension04", "music/sneaky01"  },   // mission 4
            new[]{ "music/sneaky03",  "music/calm02"    },   // mission 5
            new[]{ "music/sneaky02",  "music/tension01" },   // mission 6
            new[]{ "music/calm01",    "music/sneaky03"  },   // mission 7
            new[]{ "music/calm02",    "music/calm03"    },   // mission 8
            new[]{ "music/tension03", "music/calm01"    },   // mission 9
            new[]{ "music/action01",  "music/action03"  },   // mission 10
        };

        public List<BankFile> MissionMusic(int missionIndex)
        {
            if (MissionPlaylist.Length == 0) return AudioBank.Empty;

            int i = missionIndex;
            if (i < 0) i = 0;
            if (i >= MissionPlaylist.Length) i %= MissionPlaylist.Length;

            var keys = MissionPlaylist[i];
            var list = new List<BankFile>(keys.Length);
            for (int k = 0; k < keys.Length; k++)
            {
                var f = _bank.Get(keys[k]);
                if (f != null) list.Add(f);
            }
            if (list.Count == 0) list = G("music/calm", "music/sneaky");
            return list;
        }

        public int MissionCount => MissionPlaylist.Length;

        public List<BankFile> MenuMusic       => One("music/menu");
        public List<BankFile> BriefingMusic   => One("music/briefing01");
        public List<BankFile> BattleMusic     => G("music/battle", "music/action");
        public List<BankFile> VictoryCamMusic => G("music/victorycam");
        public List<BankFile> DeathCamMusic   => G("music/deathcam");
        public List<BankFile> VictoryDebrief  => One("music/victorydebrief01");
        public List<BankFile> FailDebrief     => One("music/faildebrief01");

        // ---- mission voice-over ----------------------------------------------

        /// <summary>Briefing/mission{N}{b|s|f}: b = briefing, s = success debrief, f = failure debrief.</summary>
        public BankFile BriefingVO(int missionIndex, char kind)
        {
            int n = missionIndex + 1;
            if (n < 1) n = 1;
            var f = _bank.Get("briefing/mission" + n + kind);
            if (f != null) return f;

            // Reloaded may ship more missions than the original shipped briefings for.
            int wrapped = ((missionIndex % 10) + 10) % 10 + 1;
            return _bank.Get("briefing/mission" + wrapped + kind);
        }

        // ---- ambience ---------------------------------------------------------

        public List<BankFile> AmbientWind     => G("wind_light", "windloop5", "wind2lp");
        public List<BankFile> AmbientWindGust => G("windgust");
        public List<BankFile> AmbientBirds    => G("birds_forest_day", "crow_caw", "seagull");
        public List<BankFile> AmbientNight    => G("cricket1lp", "cricket2lp", "owl_hoot", "frog1lp");
        public List<BankFile> AmbientOcean    => G("oceanwaves", "waves_rocklp");
        public List<BankFile> AmbientRiver    => G("rivermd", "riversm", "river");
        public List<BankFile> AmbientRain     => G("rain1lp");
        public List<BankFile> AmbientThunder  => G("thunder");
        public List<BankFile> AmbientUnderwater => G("swimminglp");
        public List<BankFile> AmbientSubmerge   => G("wadinglp");
        public List<BankFile> DistantWar      => G("distantmachine_gun", "boombg", "motars");
        public List<BankFile> Planes          => G("propplane", "planefly", "bomber2");

        // ---- interface --------------------------------------------------------

        public List<BankFile> UiClick   => G("clicksound");
        public List<BankFile> UiBeep    => G("controlbeep");
        public List<BankFile> UiHover   => G("category");

        // ---- helpers ----------------------------------------------------------

        private static void Append(List<BankFile> dst, List<BankFile> src)
        {
            for (int i = 0; i < src.Count; i++) dst.Add(src[i]);
        }

        private static string Norm(string s)
        {
            if (string.IsNullOrEmpty(s)) return string.Empty;
            var chars = new char[s.Length];
            int n = 0;
            for (int i = 0; i < s.Length; i++)
            {
                char c = s[i];
                if (c == ' ' || c == '_' || c == '-' || c == '(' || c == ')') continue;
                chars[n++] = char.ToLowerInvariant(c);
            }
            return new string(chars, 0, n);
        }
    }
}
