context: using System;
  using System.Collections.Generic;
  using System.IO;
  using System.Linq;
  using Microsoft.Xna.Framework.Graphics;
  using StardewModdingAPI.Framework.Reflection;
  using StardewValley;
  using StardewValley.BellsAndWhistles;
  using StardewValley.Buildings;
  using StardewValley.Characters;
  using StardewValley.Locations;
  using StardewValley.Menus;
  using StardewValley.Objects;
  using StardewValley.Projectiles;
  using StardewValley.TerrainFeatures;
  using xTile;
  using xTile.Tiles;
  
  namespace StardewModdingAPI.Metadata
  {
      /// <summary>Propagates changes to core assets to the game state.</summary>
      internal class CoreAssetPropagator
      {
          /*********
          ** Fields
          *********/
          /// <summary>Normalizes an asset key to match the cache key and assert that it's valid.</summary>
          private readonly Func<string, string> AssertAndNormalizeAssetName;
  
          /// <summary>Simplifies access to private game code.</summary>
          private readonly Reflector Reflection;
  
          /// <summary>Encapsulates monitoring and logging.</summary>
          private readonly IMonitor Monitor;
  
          /// <summary>Optimized bucket categories for batch reloading assets.</summary>
          private enum AssetBucket
          {
              /// <summary>NPC overworld sprites.</summary>
              Sprite,
  
              /// <summary>Villager dialogue portraits.</summary>
              Portrait,
  
              /// <summary>Any other asset.</summary>
              Other
          };
  
  
          /*********
          ** Public methods
          *********/
          /// <summary>Initialize the core asset data.</summary>
          /// <param name="assertAndNormalizeAssetName">Normalizes an asset key to match the cache key and assert that it's valid.</param>
          /// <param name="reflection">Simplifies access to private code.</param>
          /// <param name="monitor">Encapsulates monitoring and logging.</param>
          public CoreAssetPropagator(Func<string, string> assertAndNormalizeAssetName, Reflector reflection, IMonitor monitor)
          {
              this.AssertAndNormalizeAssetName = assertAndNormalizeAssetName;
              this.Reflection = reflection;
              this.Monitor = monitor;
          }
  
          /// <summary>Reload one of the game's core assets (if applicable).</summary>
          /// <param name="content">The content manager through which to reload the asset.</param>
          /// <param name="assets">The asset keys and types to reload.</param>
          /// <returns>Returns the number of reloaded assets.</returns>
          public int Propagate(LocalizedContentManager content, IDictionary<string, Type> assets)
          {
              // group into optimized lists
              var buckets = assets.GroupBy(p =>
              {
                  if (this.IsInFolder(p.Key, "Characters") || this.IsInFolder(p.Key, "Characters\\Monsters"))
                      return AssetBucket.Sprite;
  
                  if (this.IsInFolder(p.Key, "Portraits"))
                      return AssetBucket.Portrait;
  
                  return AssetBucket.Other;
              });
  
              // reload assets
              int reloaded = 0;
              foreach (var bucket in buckets)
              {
                  switch (bucket.Key)
                  {
                      case AssetBucket.Sprite:
                          reloaded += this.ReloadNpcSprites(content, bucket.Select(p => p.Key));
                          break;
  
                      case AssetBucket.Portrait:
                          reloaded += this.ReloadNpcPortraits(content, bucket.Select(p => p.Key));
                          break;
  
                      default:
                          reloaded += bucket.Count(p => this.PropagateOther(content, p.Key, p.Value));
                          break;
                  }
              }
              return reloaded;
          }
  
  
          /*********
          ** Private methods
          *********/
          /// <summary>Reload one of the game's core assets (if applicable).</summary>
          /// <param name="content">The content manager through which to reload the asset.</param>
          /// <param name="key">The asset key to reload.</param>
          /// <param name="type">The asset type to reload.</param>
          /// <returns>Returns whether an asset was loaded. The return value may be true or false, or a non-null value for true.</returns>
          private bool PropagateOther(LocalizedContentManager content, string key, Type type)
          {
              key = this.AssertAndNormalizeAssetName(key);
  
              /****
              ** Special case: current map tilesheet
              ** We only need to do this for the current location, since tilesheets are reloaded when you enter a location.
              ** Just in case, we should still propagate by key even if a tilesheet is matched.
              ****/
              if (Game1.currentLocation?.map?.TileSheets != null)
              {
                  foreach (TileSheet tilesheet in Game1.currentLocation.map.TileSheets)
                  {
                      if (this.NormalizeAssetNameIgnoringEmpty(tilesheet.ImageSource) == key)
                          Game1.mapDisplayDevice.LoadTileSheet(tilesheet);
                  }
              }
  
              /****
              ** Propagate map changes
              ****/
              if (type == typeof(Map))
              {
                  bool anyChanged = false;
                  foreach (GameLocation location in this.GetLocations())
                  {
                      if (!string.IsNullOrWhiteSpace(location.mapPath.Value) && this.NormalizeAssetNameIgnoringEmpty(location.mapPath.Value) == key)
                      {
                          // general updates
                          location.reloadMap();
                          location.updateSeasonalTileSheets();
                          location.updateWarps();
  
                          // update interior doors
                          location.interiorDoors.Clear();
                          foreach (var entry in new InteriorDoorDictionary(location))
                              location.interiorDoors.Add(entry);
  
                          // update doors
                          location.doors.Clear();
                          location.updateDoors();
  
                          anyChanged = true;
                      }
                  }
                  return anyChanged;
              }
  
              /****
              ** Propagate by key
              ****/
              Reflector reflection = this.Reflection;
              switch (key.ToLower().Replace("/", "\\")) // normalized key so we can compare statically
              {
                  /****
                  ** Animals
                  ****/
                  case "animals\\cat":
                      return this.ReloadPetOrHorseSprites<Cat>(content, key);
                  case "animals\\dog":
                      return this.ReloadPetOrHorseSprites<Dog>(content, key);
                  case "animals\\horse":
                      return this.ReloadPetOrHorseSprites<Horse>(content, key);
  
                  /****
                  ** Buildings
                  ****/
                  case "buildings\\houses": // Farm
                      reflection.GetField<Texture2D>(typeof(Farm), nameof(Farm.houseTextures)).SetValue(content.Load<Texture2D>(key));
                      return true;
  
                  /****
                  ** Content\Characters\Farmer
                  ****/
                  case "characters\\farmer\\accessories": // Game1.LoadContent
                      FarmerRenderer.accessoriesTexture = content.Load<Texture2D>(key);
                      return true;
  
                  case "characters\\farmer\\farmer_base": // Farmer
                      if (Game1.player == null || !Game1.player.IsMale)
                          return false;
                      Game1.player.FarmerRenderer = new FarmerRenderer(key, Game1.player);
                      return true;
  
                  case "characters\\farmer\\farmer_girl_base": // Farmer
                      if (Game1.player == null || Game1.player.IsMale)
                          return false;
                      Game1.player.FarmerRenderer = new FarmerRenderer(key, Game1.player);
                      return true;
  
                  case "characters\\farmer\\hairstyles": // Game1.LoadContent
                      FarmerRenderer.hairStylesTexture = content.Load<Texture2D>(key);
                      return true;
  
                  case "characters\\farmer\\hats": // Game1.LoadContent
                      FarmerRenderer.hatsTexture = content.Load<Texture2D>(key);
                      return true;
  
                  case "characters\\farmer\\shirts": // Game1.LoadContent
                      FarmerRenderer.shirtsTexture = content.Load<Texture2D>(key);
                      return true;
  
                  /****
                  ** Content\Data
                  ****/
                  case "data\\achievements": // Game1.LoadContent
                      Game1.achievements = content.Load<Dictionary<int, string>>(key);
                      return true;
  
                  case "data\\bigcraftablesinformation": // Game1.LoadContent
                      Game1.bigCraftablesInformation = content.Load<Dictionary<int, string>>(key);
                      return true;
  
                  case "data\\cookingrecipes": // CraftingRecipe.InitShared
                      CraftingRecipe.cookingRecipes = content.Load<Dictionary<string, string>>(key);
                      return true;
  
                  case "data\\craftingrecipes": // CraftingRecipe.InitShared
                      CraftingRecipe.craftingRecipes = content.Load<Dictionary<string, string>>(key);
                      return true;
  
                  case "data\\farmanimals": // FarmAnimal constructor
                      return this.ReloadFarmAnimalData();
  
                  case "data\\npcdispositions": // NPC constructor
                      return this.ReloadNpcDispositions(content, key);
  
                  case "data\\npcgifttastes": // Game1.LoadContent
                      Game1.NPCGiftTastes = content.Load<Dictionary<string, string>>(key);
                      return true;
  
                  case "data\\objectinformation": // Game1.LoadContent
                      Game1.objectInformation = content.Load<Dictionary<int, string>>(key);
                      return true;
  
                  /****
                  ** Content\Fonts
                  ****/
                  case "fonts\\spritefont1": // Game1.LoadContent
                      Game1.dialogueFont = content.Load<SpriteFont>(key);
                      return true;
  
                  case "fonts\\smallfont": // Game1.LoadContent
                      Game1.smallFont = content.Load<SpriteFont>(key);
                      return true;
  
                  case "fonts\\tinyfont": // Game1.LoadContent
                      Game1.tinyFont = content.Load<SpriteFont>(key);
                      return true;
  
                  case "fonts\\tinyfontborder": // Game1.LoadContent
                      Game1.tinyFontBorder = content.Load<SpriteFont>(key);
                      return true;
  
                  /****
                  ** Content\LooseSprites\Lighting
                  ****/
                  case "loosesprites\\lighting\\greenlight": // Game1.LoadContent
                      Game1.cauldronLight = content.Load<Texture2D>(key);
                      return true;
  
                  case "loosesprites\\lighting\\indoorwindowlight": // Game1.LoadContent
                      Game1.indoorWindowLight = content.Load<Texture2D>(key);
                      return true;
  
                  case "loosesprites\\lighting\\lantern": // Game1.LoadContent
                      Game1.lantern = content.Load<Texture2D>(key);
                      return true;
  
                  case "loosesprites\\lighting\\sconcelight": // Game1.LoadContent
                      Game1.sconceLight = content.Load<Texture2D>(key);
                      return true;
  
                  case "loosesprites\\lighting\\windowlight": // Game1.LoadContent
                      Game1.windowLight = content.Load<Texture2D>(key);
                      return true;
  
                  /****
                  ** Content\LooseSprites
                  ****/
                  case "loosesprites\\controllermaps": // Game1.LoadContent
                      Game1.controllerMaps = content.Load<Texture2D>(key);
                      return true;
  
                  case "loosesprites\\cursors": // Game1.LoadContent
                      Game1.mouseCursors = content.Load<Texture2D>(key);
                      foreach (DayTimeMoneyBox menu in Game1.onScreenMenus.OfType<DayTimeMoneyBox>())
                      {
                          foreach (ClickableTextureComponent button in new[] { menu.questButton, menu.zoomInButton, menu.zoomOutButton })
                              button.texture = Game1.mouseCursors;
                      }
                      return true;
  
                  case "loosesprites\\daybg": // Game1.LoadContent
                      Game1.daybg = content.Load<Texture2D>(key);
                      return true;
  
                  case "loosesprites\\font_bold": // Game1.LoadContent
                      SpriteText.spriteTexture = content.Load<Texture2D>(key);
                      return true;
  
                  case "loosesprites\\font_colored": // Game1.LoadContent
                      SpriteText.coloredTexture = content.Load<Texture2D>(key);
                      return true;
  
                  case "loosesprites\\nightbg": // Game1.LoadContent
                      Game1.nightbg = content.Load<Texture2D>(key);
                      return true;
  
                  case "loosesprites\\shadow": // Game1.LoadContent
                      Game1.shadowTexture = content.Load<Texture2D>(key);
                      return true;
  
                  /****
                  ** Content\TileSheets
                  ****/
                  case "tilesheets\\critters": // Critter constructor
                      this.ReloadCritterTextures(content, key);
                      return true;
  
                  case "tilesheets\\crops": // Game1.LoadContent
                      Game1.cropSpriteSheet = content.Load<Texture2D>(key);
                      return true;
  
                  case "tilesheets\\debris": // Game1.LoadContent
                      Game1.debrisSpriteSheet = content.Load<Texture2D>(key);
                      return true;
  
                  case "tilesheets\\emotes": // Game1.LoadContent
                      Game1.emoteSpriteSheet = content.Load<Texture2D>(key);
                      return true;
  
                  case "tilesheets\\furniture": // Game1.LoadContent
                      Furniture.furnitureTexture = content.Load<Texture2D>(key);
                      return true;
  
                  case "tilesheets\\projectiles": // Game1.LoadContent
                      Projectile.projectileSheet = content.Load<Texture2D>(key);
                      return true;
  
                  case "tilesheets\\rain": // Game1.LoadContent
                      Game1.rainTexture = content.Load<Texture2D>(key);
                      return true;
  
                  case "tilesheets\\tools": // Game1.ResetToolSpriteSheet
                      Game1.ResetToolSpriteSheet();
                      return true;
  
                  case "tilesheets\\weapons": // Game1.LoadContent
                      Tool.weaponsTexture = content.Load<Texture2D>(key);
                      return true;
  
                  /****
                  ** Content\Maps
                  ****/
                  case "maps\\menutiles": // Game1.LoadContent
                      Game1.menuTexture = content.Load<Texture2D>(key);
                      return true;
  
                  case "maps\\springobjects": // Game1.LoadContent
                      Game1.objectSpriteSheet = content.Load<Texture2D>(key);
                      return true;
  
                  case "maps\\walls_and_floors": // Wallpaper
                      Wallpaper.wallpaperTexture = content.Load<Texture2D>(key);
                      return true;
  
                  /****
                  ** Content\Minigames
                  ****/
                  case "minigames\\clouds": // TitleMenu
                      {
                          if (Game1.activeClickableMenu is TitleMenu titleMenu)
                          {
                              titleMenu.cloudsTexture = content.Load<Texture2D>(key);
                              return true;
                          }
                      }
                      return false;
  
                  case "minigames\\titlebuttons": // TitleMenu
                      {
                          if (Game1.activeClickableMenu is TitleMenu titleMenu)
                          {
                              Texture2D texture = content.Load<Texture2D>(key);
                              titleMenu.titleButtonsTexture = texture;
                              foreach (TemporaryAnimatedSprite bird in titleMenu.birds)
                                  bird.texture = texture;
                              return true;
                          }
                      }
                      return false;
  
                  /****
                  ** Content\TileSheets
                  ****/
                  case "tilesheets\\animations": // Game1.LoadContent
                      Game1.animations = content.Load<Texture2D>(key);
                      return true;
  
                  case "tilesheets\\buffsicons": // Game1.LoadContent
                      Game1.buffsIcons = content.Load<Texture2D>(key);
                      return true;
  
                  case "tilesheets\\bushes": // new Bush()
                      Bush.texture = new Lazy<Texture2D>(() => content.Load<Texture2D>(key));
                      return true;
  
                  case "tilesheets\\craftables": // Game1.LoadContent
                      Game1.bigCraftableSpriteSheet = content.Load<Texture2D>(key);
                      return true;
  
                  case "tilesheets\\fruittrees": // FruitTree
                      FruitTree.texture = content.Load<Texture2D>(key);
                      return true;
  
                  /****
                  ** Content\TerrainFeatures
                  ****/
                  case "terrainfeatures\\flooring": // Flooring
                      Flooring.floorsTexture = content.Load<Texture2D>(key);
                      return true;
  
                  case "terrainfeatures\\hoedirt": // from HoeDirt
                      HoeDirt.lightTexture = content.Load<Texture2D>(key);
                      return true;
  
                  case "terrainfeatures\\hoedirtdark": // from HoeDirt
                      HoeDirt.darkTexture = content.Load<Texture2D>(key);
                      return true;
  
                  case "terrainfeatures\\hoedirtsnow": // from HoeDirt
                      HoeDirt.snowTexture = content.Load<Texture2D>(key);
                      return true;
  
                  case "terrainfeatures\\mushroom_tree": // from Tree
                      return this.ReloadTreeTextures(content, key, Tree.mushroomTree);
  
                  case "terrainfeatures\\tree_palm": // from Tree
                      return this.ReloadTreeTextures(content, key, Tree.palmTree);
  
                  case "terrainfeatures\\tree1_fall": // from Tree
                  case "terrainfeatures\\tree1_spring": // from Tree
                  case "terrainfeatures\\tree1_summer": // from Tree
                  case "terrainfeatures\\tree1_winter": // from Tree
                      return this.ReloadTreeTextures(content, key, Tree.bushyTree);
  
                  case "terrainfeatures\\tree2_fall": // from Tree
                  case "terrainfeatures\\tree2_spring": // from Tree
                  case "terrainfeatures\\tree2_summer": // from Tree
                  case "terrainfeatures\\tree2_winter": // from Tree
                      return this.ReloadTreeTextures(content, key, Tree.leafyTree);
  
                  case "terrainfeatures\\tree3_fall": // from Tree
                  case "terrainfeatures\\tree3_spring": // from Tree
                  case "terrainfeatures\\tree3_winter": // from Tree
                      return this.ReloadTreeTextures(content, key, Tree.pineTree);
              }
  
              // dynamic textures
              if (this.IsInFolder(key, "Animals"))
                  return this.ReloadFarmAnimalSprites(content, key);
  
              if (this.IsInFolder(key, "Buildings"))
                  return this.ReloadBuildings(content, key);
  
              if (this.KeyStartsWith(key, "LooseSprites\\Fence"))
                  return this.ReloadFenceTextures(key);
  
              // dynamic data
              if (this.IsInFolder(key, "Characters\\Dialogue"))
                  return this.ReloadNpcDialogue(key);
  
              if (this.IsInFolder(key, "Characters\\schedules"))
                  return this.ReloadNpcSchedules(key);
  
              return false;
          }
  
  
          /*********
          ** Private methods
          *********/
          /****
          ** Reload texture methods
          ****/
          /// <summary>Reload the sprites for matching pets or horses.</summary>
          /// <typeparam name="TAnimal">The animal type.</typeparam>
          /// <param name="content">The content manager through which to reload the asset.</param>
          /// <param name="key">The asset key to reload.</param>
          /// <returns>Returns whether any textures were reloaded.</returns>
          private bool ReloadPetOrHorseSprites<TAnimal>(LocalizedContentManager content, string key)
              where TAnimal : NPC
          {
              // find matches
              TAnimal[] animals = this.GetCharacters()
                  .OfType<TAnimal>()
                  .Where(p => key == this.NormalizeAssetNameIgnoringEmpty(p.Sprite?.Texture?.Name))
                  .ToArray();
              if (!animals.Any())
                  return false;
  
              // update sprites
              Texture2D texture = content.Load<Texture2D>(key);
              foreach (TAnimal animal in animals)
                  this.SetSpriteTexture(animal.Sprite, texture);
              return true;
          }
  
          /// <summary>Reload the sprites for matching farm animals.</summary>
          /// <param name="content">The content manager through which to reload the asset.</param>
          /// <param name="key">The asset key to reload.</param>
          /// <returns>Returns whether any textures were reloaded.</returns>
          /// <remarks>Derived from <see cref="FarmAnimal.reload"/>.</remarks>
          private bool ReloadFarmAnimalSprites(LocalizedContentManager content, string key)
          {
              // find matches
              FarmAnimal[] animals = this.GetFarmAnimals().ToArray();
              if (!animals.Any())
                  return false;
  
              // update sprites
              Lazy<Texture2D> texture = new Lazy<Texture2D>(() => content.Load<Texture2D>(key));
              foreach (FarmAnimal animal in animals)
              {
                  // get expected key
                  string expectedKey = animal.age.Value < animal.ageWhenMature.Value
                      ? $"Baby{(animal.type.Value == "Duck" ? "White Chicken" : animal.type.Value)}"
                      : animal.type.Value;
                  if (animal.showDifferentTextureWhenReadyForHarvest.Value && animal.currentProduce.Value <= 0)
                      expectedKey = $"Sheared{expectedKey}";
                  expectedKey = $"Animals\\{expectedKey}";
  
                  // reload asset
  
gt:                 if (expectedKey == key)