using HavenSoft.HexManiac.Core.Models.Runs;
using System.Collections.Generic;
using System.Diagnostics;
using System.Linq;


namespace HavenSoft.HexManiac.Core.Models {
   /// <summary>
   /// An alternative to the AutoSearchModel.
   /// Instead of using a 'smart' search algorithm to find all the data,
   /// follow hard-coded expected pointers to the known data.
   /// This should still be somewhat robust: the data may move, but the pointers to the data are more likely to be stable.
   ///
   /// Lengths of some tables are still calculated dynamically based on best-fit, so operations like adding pokemon from a separate tool should still be picked up correctly.
   /// </summary>
   public class HardcodeTablesModel : PokemonModel {
      public const string
         FlySpawns = "data.maps.fly.spawn",
         TradeTable = "data.pokemon.trades",
         MapNameTable = "data.maps.names",
         MapBankTable = "data.maps.banks",
         RematchTable = "data.trainers.vsseeker",
         RematchTableRSE = "data.trainers.rematches",
         WildTableName = "data.pokemon.wild",
         SpecialsTable = "scripts.commands.events.specials",
         MoveDataTable = "data.pokemon.moves.stats.battle",
         ItemsTableName = "data.items.stats",
         MapLayoutTable = "data.maps.layouts",
         BerryTableName = "data.items.berry.stats",
         TypesTableName = "data.pokemon.type.names",
         MoveNamesTable = "data.pokemon.moves.names",
         FlyConnections = "data.maps.fly.connections",
         PokeIconsTable = "graphics.pokemon.icons.sprites",
         FontWidthTable = "graphics.text.font.black.width",
         DexInfoTableName = "data.pokedex.stats",
         PokemonNameTable = "data.pokemon.names",
         TrainerTableName = "data.trainers.stats",
         MoveEffectsTable = "scripts.moves.effects",
         NaturesTableName = "data.pokemon.natures.names",
         OverworldSprites = "graphics.overworld.sprites",
         BallSpritesTable = "graphics.items.ball.sprites",
         BackSpritesTable = "graphics.pokemon.sprites.back",
         PokemonStatsTable = "data.pokemon.stats",
         AbilityNamesTable = "data.abilities.names",
         EggMovesTableName = "data.pokemon.moves.egg",
         OverworldPalettes = "graphics.overworld.palettes",
         BallPalettesTable = "graphics.items.ball.palettes",
         FrontSpritesTable = "graphics.pokemon.sprites.front",
         ContestTypesTable = "data.pokemon.type.contest.names",
         PokePalettesTable = "graphics.pokemon.palettes.normal",
         EvolutionTableName = "data.pokemon.evolutions",
         TypeChartTableName = "data.pokemon.type.chart",
         ShinyPalettesTable = "graphics.pokemon.palettes.shiny",
         TrainerSpritesName = "graphics.trainers.sprites.front",
         ItemImagesTableName = "graphics.items.sprites",
         LevelMovesTableName = "data.pokemon.moves.levelup",
         ItemEffectsTableName = "data.items.effects",
         RegionalDexTableName = "data.pokedex.regional",
         NationalDexTableName = "data.pokedex.national",
         DecorationsTableName = "data.decorations.stats",
         MultichoiceTableName = "scripts.text.multichoice",
         MoveDescriptionsName = "data.pokemon.moves.descriptions",
         BackupFontWidthTable = "graphics.text.font.default.width",
         PokeIconPalettesTable = "graphics.pokemon.icons.palettes",
         DefaultSpriteNamespace = "graphics.new.sprite",
         TrainerClassNamesTable = "data.trainers.classes.names",
         ConversionDexTableName = "data.pokedex.hoennToNational",
         DefaultTilemapNamespace = "graphics.new.tilemap",
         DefaultPaletteNamespace = "graphics.new.palette",
         AbilityDescriptionsTable = "data.abilities.descriptions",
         PokeIconPaletteIndexTable = "graphics.pokemon.icons.index",
         BattleParticleSpriteTable = "graphics.moves.particles.sprites",
         BattleParticlePaletteTable = "graphics.moves.particles.palettes";

      public const string
         MoveInfoListName = "moveinfo",
         MoveEffectListName = "moveeffectoptions",
         MoveTargetListName = "movetarget",
         EvolutionMethodListName = "evolutionmethods",
         DecorationsShapeListName = "decorshape",
         DecorationsCategoryListName = "decorcategory",
         DecorationsPermissionListName = "decorpermissions";

      public const string
         Ruby = "AXVE0",
         Sapphire = "AXPE0",
         Emerald = "BPEE0",
         FireRed = "BPRE0",
         LeafGreen = "BPGE0",
         Ruby1_1 = "AXVE1",
         Sapphire1_1 = "AXPE1",
         FireRed1_1 = "BPRE1",
         LeafGreen1_1 = "BPGE1",
         FireRedFr = "BPRF0",
         FireRedIt = "BPRI0",
         EmeraldFr = "BPEF0",
         EmeraldIt = "BPEI0";

      public const string
         TmMoves = "data.pokemon.moves.tms",
         HmMoves = "data.pokemon.moves.hms",
         TmCompatibility = "data.pokemon.moves.tmcompatibility",
         MoveTutors = "data.pokemon.moves.tutors",
         TutorCompatibility = "data.pokemon.moves.tutorcompatibility";

      private readonly string gameCode;

      /// <summary>
      /// The first 0x100 bytes of the GBA rom is always the header.
      /// The next 0x100 bytes contains some tables and some startup code, but nothing interesting to point to.
      /// Choosing 0x200 might prevent us from seeing an actual anchor, but it will also remove a bunch
      ///      of false positives and keep us from getting conflicts with the RomName (see DecodeHeader).
      /// </summary>
      public override int EarliestAllowedAnchor => 0x200;

      public List<string> LoadingMessages { get; } = new();

      public HardcodeTablesModel(Singletons singletons, byte[] data, StoredMetadata metadata = null, bool devMode = false) : base(data, metadata, singletons, devMode) {
         gameCode = this.GetGameCode();
         if (metadata != null && !metadata.IsEmpty) {
            InitializationWorkload = (singletons?.WorkDispatcher ?? InstantDispatch.Instance).RunBackgroundWork(() => Initialize(metadata));
            return;
         }

         InitializationWorkload = (singletons?.WorkDispatcher ?? InstantDispatch.Instance).RunBackgroundWork(() => {
            {
               var noChangeDelta = new NoDataChangeDeltaModel();
               if (singletons.GameReferenceConstants.TryGetValue(gameCode, out var referenceConstants)) {
                  metadata = DecodeConstantsFromReference(this, singletons.MetadataInfo, metadata, referenceConstants);
               }
               Initialize(metadata);
               isCFRU = GetIsCFRU(this);

               // in vanilla emerald, this pointer isn't four-byte aligned
               // it's at the very front of the ROM, so if there's no metadata we can be pretty sure that the pointer is still there
               if (gameCode == Emerald && data.Length > EarliestAllowedAnchor && data[0x1C3] == 0x08) ObserveRunWritten(noChangeDelta, new PointerRun(0x1C0));

               var gamesToDecode = new[] {
                  Ruby,
                  Sapphire,
                  Emerald,
                  FireRed,
                  LeafGreen,
                  Ruby1_1,
                  Sapphire1_1,
                  FireRed1_1,
                  LeafGreen1_1,
                  FireRedFr,
                  FireRedIt,
                  EmeraldFr,
                  EmeraldIt,
                  "ABCD0", // for tests
               };

               foreach (var defaultMetadata in GetDefaultMetadatas(gameCode.PadRight(4).Substring(0, 4).ToLower(), gameCode.ToLower())) {
                  this.LoadMetadata(defaultMetadata);
               }
               if (gamesToDecode.Contains(gameCode)) {
                  if (singletons.GameReferenceTables.TryGetValue(gameCode, out var referenceTables)) {
                     DecodeTablesFromReference(referenceTables);
                  }
               }

               ResolveConflicts();
            }
         });
      }

      [Conditional("DEBUG")]
      private void CheckForEmptyAnchors(int destination, string anchor) {
         var run = GetNextRun(destination);
         Debug.Assert(run.PointerSources == null || run.PointerSources.Count > 0, $"{anchor} refers to {destination:X6}, but has no pointers. So how did we find it?");
      }

      private void DecodeTablesFromReference(GameReferenceTables tables) {
         // add evolution types
         if (isCFRU && TryGetList(EvolutionMethodListName, out var evolutionmethods)) {
            var newList = new List<string>(evolutionmethods);
            newList.Add("Rain Or Fog");
            newList.Add("Move Type");      // 17 type
            newList.Add("Type in Party");  // 18 type
            newList.Add("Map");
            newList.Add("Male");
            newList.Add("Female");
            newList.Add("Level Night");
            newList.Add("Level Day");
            newList.Add("Hold Item Night"); // 24 hold item
            newList.Add("Hold Item Day");   // 25 hold item
            newList.Add("Move Name");       // 26 move name
            newList.Add("Mon in Party");    // 27 species
            newList.Add("Level Time Range");
            newList.Add("Flag Set");
            newList.Add("3 Critical Hits In One Battle");
            newList.Add("Nature High");
            newList.Add("Nature Low");
            newList.Add("Damage Location");
            newList.Add("Item Location");
            while (newList.Count <= 252) newList.Add(null); // skip to gigantamax = 253 mega = 254
            newList.Add("Gigantamax");
            newList.Add("Mega");
            SetList(new NoDataChangeDeltaModel(), EvolutionMethodListName, newList, null, StoredList.GenerateHash(newList));
         }

         // add move effects
         if (isCFRU && TryGetList(MoveEffectListName, out var moveeffectsoptions)) {
            var newList = new List<string>(moveeffectsoptions);
            newList[12] = "RaiseSpeed1Primary";
            newList[14] = "RaiseSpDefese1Primary";
            newList[15] = "RaiseAccuracy1Primary";
            newList[21] = "LowerSpAtk1Primary";
            newList[22] = "LowerSpDef1Primary";
            newList[25] = "RemoveStatChanges";
            newList[55] = "RaiseAccuracy2Primary";
            newList[56] = "RaiseEvasion2Primary";
            newList[61] = "LowerSpAttack2Primary";
            newList[63] = "LowerAccuracy2Primary";
            newList[64] = "LowerEvasion2Primary";
            newList[74] = "LowerEvasion1HitChance";
            newList[77] = "Unused4D";
            newList[78] = "Unused4E";
            newList[96] = "RaiseSpeed1HitChance";
            newList[110] = "RaiseSpAtk1HitChance";
            newList[121] = "Unused79";
            newList[123] = "Unused7B";
            newList[125] = "BurnUp";
            newList[135] = "RaiseDefense2HitChance";
            newList[150] = "Unused96";
            newList[169] = "UnusedA9";
            newList[174] = "BoostNextElectricMoveAndRaiseSpDef";
            newList[185] = "UnusedB9";
            newList[196] = "UnusedC4";
            newList[198] = "RaiseAttack1SpAtk1";
            newList[199] = "RaiseAttack1Accuracy1";
            newList[200] = "UnusedC8";
            newList[202] = "UnusedCA";
            newList[203] = "UnusedCB";
            newList[207] = "RaiseAllStatsPrimary";
            newList[213] = "Stat Swap or Split";
            newList.AddRange(new string[] {null, null, null, null, "Me First", "Eat Berry", "Natural Gift", "Smack Down", "Remove Target Stat Changes", "SleepHitChance", null, null,
            "Set Terrain", "Pledge", "Field Effects", "Fling", "Feint", "Attack Blockers", "Type Changes", "Heal Target", "Topsy Turvy Electrify", "Fairy Lock Happy Hour",
            "Instruct After You Quash", "Sucker Punch", "Ignore Redirection", "Team Effects", "Camouflage", "Flame Burst", "Last Resort Sky Drop", "Damage Set Terrain", "Teatime",
            "RaiseTarget\'sAttackSpAtk2"});
            for (var k = 0; k < 7; k++) newList.Add(null); // 246 to 252 are unused.
            newList.AddRange(new string[] { "Max Move", "Synchronoise" });
            SetList(new NoDataChangeDeltaModel(), MoveEffectListName, newList, null, StoredList.GenerateHash(newList));
         }

         // add item hold effects
         if (isCFRU && TryGetList("holdeffects", out var holdeffects)) {
            var newList = new List<string>(holdeffects);
            newList.AddRange(new[] {
               "UnusedA", "RockyHelmet", "UnusedB", "AssaultVest",
               "Eviolite", "Plate", "MegaStone", "LifeOrb",
               "ToxicOrb", "FlameOrb", "BlackSludge", "SmoothRock",
               "DampRock", "HeatRock", "IcyRock", "LightClay",
               "WideLens", "SafetyGoggles", "WeaknessPolicy", "Drive",
               "Memory", "AdamantOrb", "LustrousOrb", "GriseousOrb",
               "DestinyKnot", "ExpertBelt", "PrimalOrb", "Gem",
               "WeaknessBerry", "CustapBerry", "LaggingTail", "IronBall",
               "BindingBand", "UnusedC", "ProtectivePads", "AbsorbBulb",
               "AirBalloon", "Bigroot", "CellBattery", "EjectButton",
               "FloatStone", "GripClaw", "LuminousMoss", "UnusedD",
               "Metronome", "MuscleBand", "RedCard", "RingTarget",
               "ShedShell", "Snowball", "StickyBarb", "TerrainExtender",
               "WiseGlasses", "Seeds", "JabocaRowapBerry", "KeeBerry",
               "MarangaBerry", "ZoomLens", "AdrenalineOrb", "PowerHerb",
               "MicleBerry", "EnigmaBerry", "TypeBoosters", "ZCrystal",
               "AbilityCapsule", "EjectPack", "RoomService", "BlunderPolicy",
               "HeavyDutyBoots", "UtilityUmbrella", "ThroatSpray",
            });
            SetList(new NoDataChangeDeltaModel(), "holdeffects", newList, null, StoredList.GenerateHash(newList));
         }

         // Update the type chart effectivenesses list
         if (isCFRU && TryGetList("effectiveness", out var multipliers)) {
            var newMultipliers = new string[21]; // 0 to 20
            newMultipliers[0] = "1x";
            newMultipliers[1] = "0x";
            newMultipliers[5] = "0.5x";
            newMultipliers[20] = "2x";
            SetList(new NoDataChangeDeltaModel(), "effectiveness", newMultipliers, null, StoredList.GenerateHash(newMultipliers));
         }

         foreach (var table in tables) {
            // some tables have been removed from CFRU
            if (isCFRU && CfruIgnoreTables.Contains(table.Name)) continue;

            using (ModelCacheScope.CreateScope(this)) {
               var format = table.Format;
               AddTable(table.Address, table.Offset, table.Name, format);
            }
         }

         if (isCFRU) SetupCFRUSpecificTablesAndConstants();
      }

      public static readonly IReadOnlyList<string> CfruIgnoreTables = new List<string>() {
         "graphics.pokemon.sprites.coordinates.front",
         "data.pokedex.hoennToNational",        // causes problems with pokename count
         "graphics.pokemon.sprites.anchor",     // causes problems with pokename count
         "data.pokedex.search.alpha"            // causes problems with shiny palettes
      };

      public void SetupCFRUSpecificTablesAndConstants() {
         ShowRawIVByteForTrainer = true;

         // class-based pokeballs
         var balls = new List<string> {
            "Master Ball", "Ultra Ball", "Great Ball", "Poke Ball",
            "Safari Ball", "Net Ball", "Dive Ball", "Nest Ball",
            "Repeat Ball", "Timer Ball", "Luxury Ball", "Premier Ball",
            "Dusk Ball", "Heal Ball", "Quick Ball", "Cherish Ball",
            "Park Ball", "Fast Ball", "Level Ball", "Lure Ball",
            "Heavy Ball", "Love Ball", "Friend Ball", "Moon Ball",
            "Sport Ball", "Beast Ball", "Dream Ball",
         };
         while (balls.Count < 0xFE) balls.Add(null);
         balls.Add("Class Based");
         balls.Add("Random");
         SetList(new NoDataChangeDeltaModel(), "trainerballs", balls, null, StoredList.GenerateHash(balls));
         AddTable(0x1456790, 0, "data.trainers.classes.balls", "[ball.trainerballs]data.trainers.classes.names");

         // randomizer restrictions
         AddTable(0x1453828, 0, "data.randomizer.species.banlist", $"[species:{PokemonNameTable}]!FEFE");
         AddTable(0x1454730, 0, "data.randomizer.ability.banlist", $"[ability.{AbilityNamesTable}]!FF");

         // physical/special/split list
         var pss = new[] { "Physical", "Special", "Status" };
         SetList(new NoDataChangeDeltaModel(), "movecategory", pss, null, StoredList.GenerateHash(pss));

         // trainers-with-EVs table
         var trainerabilities = new List<string> { "Hidden", "Ability1", "Ability2", "RandomNormal", "RandomAny" };
         SetList(new NoDataChangeDeltaModel(), "trainerabilities", trainerabilities, null, StoredList.GenerateHash(trainerabilities));
         AddTable(0x1456798, 0, "data.trainers.evs", "[nature.data.pokemon.natures.names ivs. hpEv. atkEv. defEv. spdEv. spAtkEv. spDefEv. ball.data.trainers.classes.names ability.trainerabilities]121");

         // trainer class-based encounter music
         AddTable(0x144C110, 0, "data.trainers.classes.music", "[song:songnames]data.trainers.classes.names");

         // trainer sprite-based mugshots
         AddTable(0x144FC94, 0, "data.trainers.sprites.mugshots", "[sprite<`lzs4x8x8|data.trainers.sprites.mugshots`> pal<`lzp4`> size: x:|z y:|z unused:]graphics.trainers.sprites.front-0-1");

         // kanto dex
         // first hword is actually the length of the table - 1
         // every hword after that is what pokemon appears in that slot of the dex (slot 1, slot 2, etc)
         AddTable(0x160A52C, 0, "data.pokedex.kanto", "[mon:data.pokemon.names]152");

         // PSS icons/palette
         if (0x143B0EC < Count - 4) {
            var spriteStart = ReadPointer(0x143B0EC);
            AddTableDirect(spriteStart, "graphics.pokemon.moves.category.icons", "`ucs4x3x18|graphics.pokemon.moves.category.palette`");
            AddTableDirect(spriteStart + 0x6C0, "graphics.pokemon.moves.category.palette", "`ucp4`");
         }

         // z-effects
         var newList = new List<string>();
         newList.Add("None");
         newList.Add("Reset Stats");
         newList.Add("All Stats Up 1");
         newList.Add("Boost Crits");
         newList.Add("Follow Me");
         newList.Add("Curse");
         newList.Add("Recover Hp");
         newList.Add("Restore Replacement Hp");
         newList.Add("Atk Up 1");
         newList.Add("Def Up 1");
         newList.Add("Spd Up 1");
         newList.Add("Spatk Up 1");
         newList.Add("Spdef Up 1");
         newList.Add("Acc Up 1");
         newList.Add("Evsn Up 1");
         newList.Add("Atk Up 2");
         newList.Add("Def Up 2");
         newList.Add("Spd Up 2");
         newList.Add("Spatk Up 2");
         newList.Add("Spdef Up 2");
         newList.Add("Acc Up 2");
         newList.Add("Evsn Up 2");
         newList.Add("Atk Up 3");
         newList.Add("Def Up 3");
         newList.Add("Spd Up 3");
         newList.Add("Spatk Up 3");
         newList.Add("Spdef Up 3");
         newList.Add("Acc Up 3");
         newList.Add("Evsn Up 3");
         SetList(new NoDataChangeDeltaModel(), "zeffects", newList, null, StoredList.GenerateHash(newList));

      }

      public static StoredMetadata DecodeConstantsFromReference(IReadOnlyList<byte> model, IMetadataInfo info, StoredMetadata metadata, GameReferenceConstants constants) {
         if (metadata == null) return metadata;
         var words = metadata.MatchedWords.ToList();
         var constantSet = new Dictionary<string, IList<StoredMatchedWord>>();
         foreach (var constant in constants.SelectMany(c => c.ToStoredMatchedWords())) {
            if (!constantSet.ContainsKey(constant.Name)) constantSet[constant.Name] = new List<StoredMatchedWord>();
            constantSet[constant.Name].Add(constant);
         }
         foreach (var constant in constantSet.Values) {
            if (constant.Any(c => c.Address + c.Length > model.Count)) continue;
            var virtualValues = constant.Select(c => (model.ReadMultiByteValue(c.Address, c.Length) - c.AddOffset) / c.MultOffset).ToList();
            var match = virtualValues.All(vv => vv == virtualValues[0]);
            if (match) words.AddRange(constant);
         }
         return new StoredMetadata(metadata.NamedAnchors, metadata.UnmappedPointers, words, metadata.OffsetPointers, metadata.Lists, metadata.UnmappedConstants, metadata.GotoShortcuts, info,
            new StoredMetadataFields {
               FreeSpaceSearch = metadata.FreeSpaceSearch,
               FreeSpaceBuffer = metadata.FreeSpaceBuffer,
               NextExportID = metadata.NextExportID,
               ShowRawIVByteForTrainer = metadata.ShowRawIVByteForTrainer
            });
      }

      /// <summary>
      /// Find a table given a pointer to that table
      /// The pointer at the source may not point directly to the table: it may point to an offset from the start of the table.
      /// </summary>
      private void AddTable(int source, int offset, string name, string format) {
         var noChangeDelta = new NoDataChangeDeltaModel();
         format = AdjustFormatForCFRU(name, format, ref source);
         if (source < 0 || source > RawData.Length) return;
         var destination = ReadPointer(source) - offset;
         if (destination < 0 || destination > RawData.Length) return;

         var interruptingSourceRun = GetNextRun(source);
         if (interruptingSourceRun.Start < source && interruptingSourceRun.Start + interruptingSourceRun.Length > source) {
            if (interruptingSourceRun is ITableRun tableRun) {
               var tableOffset = tableRun.ConvertByteOffsetToArrayOffset(source);
               if (tableOffset.SegmentOffset != 0) {
                  LoadingMessages.Add($"Failed to add table {name}: it conflicted with an existing table found at {tableRun.Start.ToAddress()}.");
                  return;
               }
               if (tableRun.ElementContent[tableOffset.SegmentIndex].Type != ElementContentType.Pointer) {
                  LoadingMessages.Add($"Failed to add table {name} at {destination.ToAddress()}: data in the table didn't match pointers in the file.");
                  return;
               }
            } else {
               // the source isn't actually a pointer, we shouldn't write anything
               return;
            }
         }

         var interruptingRun = GetNextRun(destination);
         if (interruptingRun.Start < destination && interruptingRun is ArrayRun array) {
            var elementCount = (destination - array.Start) / array.ElementLength;
            var desiredChange = elementCount - array.ElementCount;
            while (!string.IsNullOrEmpty(array.LengthFromAnchor)) {
               if (GetNextRun(GetAddressFromAnchor(noChangeDelta, -1, array.LengthFromAnchor)) is ArrayRun nextArray) {
                  array = nextArray;
               } else {
                  break;
               }
            }
            if (array.ElementCount + desiredChange <= 0) {
               // erase the entire run
               LoadingMessages.Add($"When trying to add table {name}, had to clear existing format from {array.Start.ToAddress()} to {(array.Start + array.Length).ToAddress()} because a formatting conflict was found.");
               ClearFormat(noChangeDelta, array.Start, array.Length);
            } else {
               var arrayName = GetAnchorFromAddress(-1, array.Start);
               array = array.Append(noChangeDelta, desiredChange); // if append is negative, the name might get erased. Store it.
               ObserveAnchorWritten(noChangeDelta, arrayName, array);
            }
         }

         AddTableDirect(destination, name, format, validatePointerFound: offset == 0);
         if (offset != 0) ObserveRunWritten(noChangeDelta, new OffsetPointerRun(source, offset));
      }

      /// <summary>
      /// Find a table given an address for that table
      /// </summary>
      private void AddTableDirect(int destination, string name, string format, bool validatePointerFound = false) {
         var noChangeDelta = new NoDataChangeDeltaModel();
         using (ModelCacheScope.CreateScope(this)) {
            var errorInfo = ApplyAnchor(this, noChangeDelta, destination, "^" + name + format, allowAnchorOverwrite: true);
            validatePointerFound &= !errorInfo.HasError;
         }

         if (validatePointerFound) {
            CheckForEmptyAnchors(destination, name);
         }
      }

      private bool isCFRU;
      private const int CFRU_Check_Address = 0x00051A, CFRU_Check_Value = 0x46C0, CFRU_ValueRepeateCount = 5;
      public static bool GetIsCFRU(IDataModel model) {
         var gameCode = model.GetGameCode();
         if (gameCode != FireRed) return false;
         if (model.RawData.Length < CFRU_Check_Address + 3) return false;
         for (int i = 0; i < CFRU_ValueRepeateCount; i++) {
            if (model.ReadMultiByteValue(CFRU_Check_Address + i * 2, 2) != CFRU_Check_Value) return false;
         }
         return true;
      }

      private string AdjustFormatForCFRU(string name, string format, ref int source) {
         if (!isCFRU) return format;

         // type names
         if (name == TypesTableName) format = format.Split("]")[0] + "]24";

         // type icons
         if (name == "graphics.pokemon.type.icons") format = format.Replace("ucs4x16x16", "ucs4x16x18");

         // remove the extra +28 from pokemon-related tables
         if (format.EndsWith(PokemonNameTable + "+28")) format = format.Substring(0, format.Length - 3);

         // remove the extra +1 from pokemon-related tables
         if (format.EndsWith(PokemonNameTable + "+1")) format = format.Substring(0, format.Length - 2);

         // hidden abilities stored in pokemon stats
         if (name == PokemonStatsTable) format = format.Replace("padding:", $"hiddenAbility.{AbilityNamesTable} padding.");

         // items: no constant
         if (name == ItemsTableName) format = format.Replace("data.items.count", string.Empty);

         // moves
         if (name == MoveNamesTable) format += "896";
         if (name == MoveDataTable) format = format.Replace("unused. unused:", "zMovePower. category.movecategory zMoveEffect.zeffects");

         // level-up moves uses Jambo format
         if (name == LevelMovesTableName) return $"[movesFromLevel<[move:{MoveNamesTable} level.]!0000FF>]{PokemonNameTable}";

         // tms / tutors
         if (name == MoveTutors) format = format.Replace("]15", "]128");
         if (name == TmMoves) format = format.Replace("]58", "]128");

         // graphics.pokemon.type.map[width. height. xy:|t|:|x:|.|y::]data.pokemon.type.names+1+2
         if (name == "graphics.pokemon.type.map") format = format.Replace("data.pokemon.type.names+1+5", "data.pokemon.type.names+1+2");

         // overworld sprites
         if (name == OverworldSprites) format = format.Replace("graphics.overworld.tablelength", "240");

         // 16 evolutions per pokemon
         if (name == EvolutionTableName) {
            var vars = "|".Join(new[] {
               "6=" + ItemsTableName,
               "7=" + ItemsTableName,
               "17=" + TypesTableName,
               "18=" + TypesTableName,
               "24=" + ItemsTableName,
               "25=" + ItemsTableName,
               "26=" + MoveNamesTable,
               "27=" + PokemonNameTable,
               "254=" + ItemsTableName,
            });
            var methods = " ".Join("0123456789ABCDEF".Select(i => $"method{i}:evolutionmethods arg{i}:|s=method{i}({vars}) species{i}:{PokemonNameTable} value{i}:"));
            return $"[{methods}]{PokemonNameTable}";
         }

         // roaming locations
         if (name == "data.maps.roaming.sets") source = 0x14889B0;

         if (name == ItemEffectsTableName) format = format.Replace("-199", string.Empty); // item effects are as long as the items



         // type chart
         if (name == "data.pokemon.type.chart") {
            source = 0x145BB74;
            format = "Norm Fight Fly Poison Grd Rock Bug Ghost Steel arg9 Fire Water Grass Elec Psych Ice Drag Dark arg18 arg19 arg20 arg21 arg22 Fairy";
            format = " ".Join(format.Split(' ').Select(type => $"{type}.effectiveness"));
            format = $"[{format}]data.pokemon.type.names";
         }

         return format;
      }
   }
}
