context: using System;
  using System.Collections.Generic;
  using System.ComponentModel;
  using System.Linq;
  using System.Text;
  using System.Threading.Tasks;
  using RomUtilities;
  using static System.Math;
  
  namespace FF1Lib
  {
  	public enum ProgressiveScaleMode
  	{
  		[Description("Disabled")]
  		Disabled,
  		[Description("150% @ 12 Items")]
  		FiftyPercentAt12,
  		[Description("150% @ 15 Items")]
  		FiftyPercentAt15,
  		[Description("200% @ 12 Items")]
  		DoubledAt12,
  		[Description("200% @ 15 Items")]
  		DoubledAt15,
  		[Description("+ 5% Per Item")]
  		Progressive5Percent,
  		[Description("+10% Per Item")]
  		Progressive10Percent,
  		[Description("+20% Per Item")]
  		Progressive20Percent,
  		[Description("+12.5% Per Orb or 8 Shards")]
  		OrbProgressiveSlow,
  		[Description("+25% Per Orb or 8 Shards")]
  		OrbProgressiveMedium,
  		[Description("+50% Per Orb or 8 Shards")]
  		OrbProgressiveFast,
  		[Description("+100% Per Orb or 8 Shards")]
  		OrbProgressiveVFast,
  	}
  
  	public enum EvadeCapValues
  	{
  		[Description("Very low, 225")]
  		veryLow,
  		[Description("low, 230")]
  		low,
  		[Description("medium low, 235")]
  		medLow,
  		[Description("medium, 240")]
  		medium,
  		[Description("medium high, 245")]
  		medHigh,
  		[Description("high, 250")]
  		high,
  		[Description("extreme, 253")]
  		extreme,
  		[Description("insane, 255")]
  		insane,
  	}
  
  	public partial class FF1Rom : NesRom
  	{
  		public static readonly List<int> Bosses = new List<int> { Enemy.Garland, Enemy.Astos, Enemy.Pirate, Enemy.WarMech,
  			Enemy.Lich, Enemy.Lich2, Enemy.Kary, Enemy.Kary2, Enemy.Kraken, Enemy.Kraken2, Enemy.Tiamat, Enemy.Tiamat2, Enemy.Chaos };
  
  		public static readonly List<int> NonBossEnemies = Enumerable.Range(0, EnemyCount).Where(x => !Bosses.Contains(x)).ToList();
  
  		public const int PriceOffset = 0x37C00;
  		public const int PriceSize = 2;
  		public const int PriceCount = 240;
  
  		public const int ThreatLevelsOffset = 0x2CC00;
  		public const int ThreatLevelsSize = 64;
  		public const int OverworldThreatLevelOffset = 0x7C4FE;
  		public const int OceanThreatLevelOffset = 0x7C506;
  
  		// Scale is the geometric scale factor used with RNG.  Multiplier is where we make everything cheaper
  		// instead of enemies giving more gold, so we don't overflow.
  		public void ScalePrices(IScaleFlags flags, string[] text, MT19337 rng, bool increaseOnly, ItemShopSlot shopItemLocation)
  		{
  			IEnumerable<Item> tmpExcludedItems = Array.Empty<Item>() ;
  			if (flags.ExcludeGoldFromScaling ?? false) tmpExcludedItems = tmpExcludedItems.Concat(ItemLists.AllGoldTreasure);
  			if ((flags.ExcludeGoldFromScaling ?? false) && flags.CheapVendorItem) tmpExcludedItems = tmpExcludedItems.Concat(ItemLists.AllQuestItems);
  
  			HashSet<Item> excludedItems = new HashSet<Item>(tmpExcludedItems);
  			HashSet<Item> questItems = new HashSet<Item>(ItemLists.AllQuestItems);
  
  			int rawScaleLow = increaseOnly ? 100 : flags.PriceScaleFactorLow;
  			int rawScaleHigh = increaseOnly ? Math.Max(100, flags.PriceScaleFactorHigh) : flags.PriceScaleFactorHigh;
  
  			double scaleLow = (double)rawScaleLow / 100.0;
  			double scaleHigh = (double)rawScaleHigh / 100.0;
  
  			var multiplier = flags.ExcludeGoldFromScaling ?? false ? 1.0 : flags.ExpMultiplier;
  			var prices = Get(PriceOffset, PriceSize * PriceCount).ToUShorts();
  			for (int i = 0; i < prices.Length; i++)
  			{
  				if (excludedItems.Contains((Item)i))
  				{
  					var price = (int)prices[i];
  
  					if (flags.CheapVendorItem && questItems.Contains((Item)i)) price = 20000;
  
  					var newPrice = price + rng.Between(-price / 10, price / 10);
  					prices[i] = (ushort)Math.Min(Math.Max(newPrice, 1), 65535);
  				}
  				else
  				{
  					var price = ExtConsumables.ExtConsumablePriceFix((Item)i, prices[i], flags);
  					var newPrice = RangeScaleWithZero(price / multiplier, scaleLow, scaleHigh, 1e-5 * multiplier, 1, rng);
  					prices[i] = (ushort)(flags.WrapPriceOverflow ? ((newPrice - 1) % 0xFFFF) + 1 : Min(newPrice, 0xFFFF));
  				}
  			}
  			var questItemPrice = prices[(int)Item.Bottle];
  			// If we don't do this before checking for the item shop location factor, Ribbons and Shirts will end up being really cheap
  			// This realistically doesn't matter without Shop Wares shuffle on because nobody wants to sell Ribbons/Shirts, but if it is...
  			prices[(int)Item.WhiteShirt] = (ushort)(questItemPrice / 2);
  			prices[(int)Item.BlackShirt] = (ushort)(questItemPrice / 2);
  			prices[(int)Item.Ribbon] = questItemPrice;
  			var itemShopFactor = new Dictionary<MapLocation, int>() {
  				{ MapLocation.Coneria, 8 },
  				{ MapLocation.Pravoka, 2 }
  			};
  			if (itemShopFactor.TryGetValue(shopItemLocation.MapLocation, out int divisor))
  			{
  				questItemPrice = (ushort)(prices[(int)Item.Bottle] / divisor);
  			}
  			for (var i = 0; i < (int)Item.Tent; i++)
  			{
  				prices[i] = questItemPrice;
  			}
  			Put(PriceOffset, Blob.FromUShorts(prices));
  
  			for (int i = GoldItemOffset; i < GoldItemOffset + GoldItemCount; i++)
  			{
  				text[i] = prices[i].ToString() + " G";
  			}
  
  			var pointers = Get(ShopPointerOffset, ShopPointerCount * ShopPointerSize).ToUShorts();
  			RepackShops(pointers);
  
  			for (int i = (int)ShopType.Clinic; i < (int)ShopType.Inn + ShopSectionSize; i++)
  			{
  				if (pointers[i] != ShopNullPointer)
  				{
  					var priceBytes = Get(ShopPointerBase + pointers[i], 2);
  					var priceValue = BitConverter.ToUInt16(priceBytes, 0);
  
  					priceValue = (ushort)RangeScale(priceValue / multiplier, scaleLow, scaleHigh, 1, rng);
  					priceBytes = BitConverter.GetBytes(priceValue);
  					Put(ShopPointerBase + pointers[i], priceBytes);
  				}
  			}
  			if (flags.StartingGold)
  			{
  				var startingGold = BitConverter.ToUInt16(Get(StartingGoldOffset, 2), 0);
  
  				startingGold = (ushort)Min(RangeScale(startingGold / multiplier, scaleLow, scaleHigh, 1, rng), 0xFFFF);
  
  				Put(StartingGoldOffset, BitConverter.GetBytes(startingGold));
  			}
  
  		}
  
  		public int GetEvadeIntFromFlag(EvadeCapValues evadeCapFlag)
  		{
  			int evadeCap;
  			switch (evadeCapFlag)
  			{
  				case EvadeCapValues.veryLow:
  					evadeCap = 0xE1;
  					break;
  				case EvadeCapValues.low:
  					evadeCap = 0xE6;
  					break;
  				case EvadeCapValues.medLow:
  					evadeCap = 0xEB;
  					break;
  				case EvadeCapValues.medium:
  					evadeCap = 0xF0;
  					break;
  				case EvadeCapValues.medHigh:
  					evadeCap = 0xF5;
  					break;
  				case EvadeCapValues.high:
  					evadeCap = 0xFA;
  					break;
  				case EvadeCapValues.extreme:
  					evadeCap = 0xFC;
  					break;
  				case EvadeCapValues.insane:
  					evadeCap = 0xFF;
  					break;
  				default:
  					evadeCap = 0xF0;
  					break;
  			}
  
  			return evadeCap;
  
  		}
  
  		public void ScaleEnemyStats(MT19337 rng, Flags flags)
  		{
  			int minStats = (bool)flags.ClampMinimumStatScale ? 100 : flags.EnemyScaleStatsLow;
  			int highStats = (bool)flags.ClampMinimumStatScale ? Math.Max(100, flags.EnemyScaleStatsHigh) : flags.EnemyScaleStatsHigh;
  			int minHp = (bool)flags.ClampEnemyHpScaling ? 100 : flags.EnemyScaleHpLow;
  			int highHp = (bool)flags.ClampEnemyHpScaling ? Math.Max(100, flags.EnemyScaleHpHigh) : flags.EnemyScaleHpHigh;
  
  			NonBossEnemies.ForEach(index => ScaleSingleEnemyStats(index, minStats, highStats, flags.WrapStatOverflow, flags.IncludeMorale, rng,
  				(bool)flags.SeparateEnemyHPScaling, minHp, highHp, GetEvadeIntFromFlag(flags.EvadeCap)));
  		}
  
  
  		public void ScaleBossStats(MT19337 rng, Flags flags)
  		{
  			int minStats = (bool)flags.ClampMinimumBossStatScale ? 100 : flags.BossScaleStatsLow;
  			int highStats = (bool)flags.ClampMinimumBossStatScale ? Math.Max(100, flags.BossScaleStatsHigh) : flags.BossScaleStatsHigh;
  			int minHp = (bool)flags.ClampBossHPScaling ? 100 : flags.BossScaleHpLow;
  			int highHp = (bool)flags.ClampBossHPScaling ? Math.Max(100, flags.BossScaleHpHigh) : flags.BossScaleHpHigh;
  			Bosses.ForEach(index => ScaleSingleEnemyStats(index, minStats, highStats, flags.WrapStatOverflow, flags.IncludeMorale, rng,
  				(bool)flags.SeparateBossHPScaling, minHp, highHp, GetEvadeIntFromFlag(flags.EvadeCap)));
  		}
  
  		public abstract class EnemyStat
  		{
  		    public const int HP = 4;
  		    public const int Morale = 6;
  		    public const int Scripts = 7;
  		    public const int Evade = 8;
  		    public const int Defense = 9;
  		    public const int Hits = 10;
  		    public const int HitPercent = 11;
  		    public const int Strength = 12;
  		    public const int CriticalPercent = 13;
  		}
  
  		public void ScaleSingleEnemyStats(int index, int lowPercentStats, int highPercentStats, bool wrapOverflow, bool includeMorale, MT19337 rng,
  			bool separateHPScale, int lowPercentHp, int highPercentHp, int evadeClamp)
  		{
  			double lowDecimalStats = (double)lowPercentStats / 100.0;
  			double highDecimalStats = (double)highPercentStats / 100.0;
  			double lowDecimalHp = (double)lowPercentHp / 100.0;
  			double highDecimalHp = (double)highPercentHp / 100.0;
  
  			var enemy = Get(EnemyOffset + index * EnemySize, EnemySize);
  
  			var hp = BitConverter.ToUInt16(enemy, EnemyStat.HP);
  			if(separateHPScale)
  			{
  				hp = (ushort)Min(RangeScale(hp, lowDecimalHp, highDecimalHp, 1.0, rng), 0x7FFF);
  			} else
  			{
  				hp = (ushort)Min(RangeScale(hp, lowDecimalStats, highDecimalStats, 1.0, rng), 0x7FFF);
  			}
  			var hpBytes = BitConverter.GetBytes(hp);
  			Array.Copy(hpBytes, 0, enemy, EnemyStat.HP, 2);
  
  			var newMorale = includeMorale ? RangeScale(enemy[EnemyStat.Morale], lowDecimalStats, highDecimalStats, 0.25, rng) : enemy[6];
  			var newEvade = RangeScale(enemy[EnemyStat.Evade], lowDecimalStats, highDecimalStats, 1.0, rng);
  			var newDefense = RangeScale(enemy[EnemyStat.Defense], lowDecimalStats, highDecimalStats, 0.5, rng);
  			var newHits = RangeScale(enemy[EnemyStat.Hits], lowDecimalStats, highDecimalStats, 0.5, rng);
  			var newHitPercent = RangeScale(enemy[EnemyStat.HitPercent], lowDecimalStats, highDecimalStats, 1.0, rng);
  			var newStrength = RangeScale(enemy[EnemyStat.Strength], lowDecimalStats, highDecimalStats, 0.25, rng);
  			var newCrit = RangeScale(enemy[EnemyStat.CriticalPercent], lowDecimalStats, highDecimalStats, 0.5, rng);
  			if (wrapOverflow)
  			{
  				newEvade = ((newEvade - 1) % 0xFF) + 1;
  				newDefense = ((newDefense - 1) % 0xFF) + 1;
  				newHits = ((newHits - 1) % 0xFF) + 1;
  				newHitPercent = ((newHitPercent - 1) % 0xFF) + 1;
  				newStrength = ((newStrength - 1) % 0xFF) + 1;
  				newCrit = ((newCrit - 1) % 0xFF) + 1;
  			}
  			enemy[EnemyStat.Morale] = (byte)Min(newMorale, 0xFF); // morale
  			enemy[EnemyStat.Evade] = (byte)Min(newEvade, evadeClamp); // evade
  			enemy[EnemyStat.Defense] = (byte)Min(newDefense, 0xFF); // defense
  			enemy[EnemyStat.Hits] = (byte)Max(Min(newHits, 0xFF), 1); // hits
  			enemy[EnemyStat.HitPercent] = (byte)Min(newHitPercent, 0xFF); // hit%
  			enemy[EnemyStat.Strength] = (byte)Min(newStrength, 0xFF); // strength
  			enemy[EnemyStat.CriticalPercent] = (byte)Min(newCrit, 0xFF); // critical%
  
  			Put(EnemyOffset + index * EnemySize, enemy);
  		}
  
  		private int RangeScaleWithZero(double value, double lowPercent, double highPercent, double lowScalelowPercent, double adjustment, MT19337 rng)
  		{
  			var internalLowPercent = lowPercent;
  			var lowScaleThreshold = 0.0;
  
  			if (lowPercent == 0 && highPercent == 0) return 0;
  
  			if (lowPercent == 0 && highPercent >= 0.2)
  			{
  				internalLowPercent = 0.1;
  				lowScaleThreshold = 0.14;
  			}
  			else if (lowPercent == 0 && highPercent == 0.1)
  			{
  				var ret = RangeScale(value, lowScalelowPercent, highPercent, adjustment, rng);
  				return ret > 3 ? ret : 0;
  			}
  
  			double exponent = (rng != null) ? (double)rng.Next() / uint.MaxValue : 1.0; // A number from 0 - 1
  			double logLowPercent = Log(internalLowPercent);
  			double logDifference = Log(highPercent) - logLowPercent;
  			exponent = exponent * logDifference + logLowPercent; // For example for 50-200% a number from -0.69 to 0.69, for 200-400% a number from 0.69 to 1.38
  
  			double scaleValue = Exp(exponent); // A number from 0.5 to 2, or 2 to 4
  
  
gt: 			if (lowScaleThreshold > 0 && scaleValue < lowScaleThreshold)