using Verse;
using RimWorld;
using Multiplayer.API;
using System.Collections.Generic;
using HugsLib.Utils;
using System.Linq;

using ShowParts = rjw.RJWSettings.ShowParts;

namespace rjw
{
	/// <summary>
	/// Comp for rjw hediff parts
	/// </summary>
	public class HediffComp_SexPart : HediffComp
	{
		public string SizeLabel => parent.CurStage?.label ?? "";

		public string originalOwnerRace = "";      //base race when part created
		public string previousOwner = "";            //base race when part created

		private float? forcedSize;
		public float baseSize;                              	// Absolute base size when part created, someday alter by operation
		public float originalOwnerSize = 0;                         // Body size of this part's first owner. Not used in calculations.
		public float bodySizeOverride = -1;                // For modders, used to overwrite the (displayed) size instead of pawn.body.size

		private SexFluidDef fluidOverride;                   //cummies/milk - insectjelly/honey etc
		public float partFluidFactor;                       //amount of Milk/Ejaculation/Wetness

		public string possibleEggsTipString;                         //for ovi eggs, maybe
		public bool isTransplant = false;                   //transplanted from someone else

		// Whether this part could ever have been seen by the player based on mod settings and apparel.
		public bool discovered;

		private bool initialised;

		public new HediffDef_SexPart Def => (HediffDef_SexPart)base.Def;

		public HediffCompProperties_SexPart Props => (HediffCompProperties_SexPart)props;

		public float FluidAmount => partFluidFactor * Def.fluidMultiplier;

		public SexFluidDef Fluid
		{
			get => fluidOverride ?? Def.fluid;
			set
			{
				if (value == Def.fluid)
				{
					fluidOverride = null;
				}
				else
				{
					fluidOverride = value;
				}
			}
		}
		
		private List<string> DefaultPositionNames
		{
			get
			{
				List<string> result = Def.defaultBodyPartList ?? new();
				string singleDefaultPart = Def.defaultBodyPart;
				if (!singleDefaultPart.NullOrEmpty() && !result.Contains(singleDefaultPart))
				{
					result.Add(singleDefaultPart);
				}
				return result;
			}
		}

		public float Size
		{
			get
			{
				if (forcedSize.HasValue)
				{
					return forcedSize.Value;
				}
				return baseSize * BodySizeFactor;
			}
		}

		// Scales with square of body size so that severity is not constant. May be worth using relBodySize * Growth instead.
		private float BodySizeFactor => Pawn.BodySize * Pawn.BodySize
									/ (Pawn.RaceProps.baseBodySize * Pawn.RaceProps.baseBodySize);

		public bool HasForcedSize => forcedSize.HasValue;

		/// <summary>
		/// save data
		/// </summary>
		public override void CompExposeData()
		{
			base.CompExposeData();

			Scribe_Values.Look(ref baseSize, nameof(baseSize));
			Scribe_Values.Look(ref originalOwnerSize, nameof(originalOwnerSize));
			Scribe_Values.Look(ref originalOwnerRace, nameof(originalOwnerRace));
			Scribe_Values.Look(ref previousOwner, nameof(previousOwner));
			Scribe_Defs.Look(ref fluidOverride, nameof(fluidOverride));
			Scribe_Values.Look(ref partFluidFactor, nameof(partFluidFactor));
			Scribe_Values.Look(ref possibleEggsTipString, "possibleEggs");
			Scribe_Values.Look(ref isTransplant, nameof(isTransplant));
			Scribe_Values.Look(ref forcedSize, nameof(forcedSize));
			Scribe_Values.Look(ref discovered, nameof(discovered));
			Scribe_Values.Look(ref initialised, nameof(initialised), defaultValue: true);
		}

		public override void CompPostPostAdd(DamageInfo? dinfo)
		{
			base.CompPostPostAdd(dinfo);
			try//error at world gen
			{
				Pawn.GetRJWPawnData().genitals = new List<Hediff>();
				Pawn.GetRJWPawnData().breasts = new List<Hediff>();
				Pawn.GetRJWPawnData().anus = new List<Hediff>();
			}
			catch
			{
			}
			if (!initialised)
			{
				Init(reroll: false);
			}
		}

		public override void CompPostMake()
		{
			base.CompPostMake();
			// A null pawn is only possible in vanilla in verb-related debug outputs, but better safe than sorry.
			if (Pawn != null)	
			{
				Init();
			}
		}

		public override void CompPostPostRemoved()
		{
			base.CompPostPostRemoved();
			try
			{
				Pawn.GetRJWPawnData().genitals = new List<Hediff>();
				Pawn.GetRJWPawnData().breasts = new List<Hediff>();
				Pawn.GetRJWPawnData().anus = new List<Hediff>();
			}
			catch
			{
			}
		}

		public override bool CompDisallowVisible()
		{
			if (base.CompDisallowVisible())
			{
				return true;
			}

			if (RJWSettings.ShowRjwParts == ShowParts.Hide)
			{
				return true;
			}

			if (discovered)
			{
				return false;
			}

			// Show if required by settings, or at game start if in dev mode
			if (RJWSettings.ShowRjwParts != ShowParts.Known || (Current.ProgramState != ProgramState.Playing && Prefs.DevMode))
			{
				discovered = true;
				return false;
			}

			// Show for hero and apparel-less pawns
			if (Pawn.IsDesignatedHero() && Pawn.IsHeroOwner() || Pawn.apparel == null)
			{
				discovered = true;
			}
			else
			{
				var partGroups = parent.Part.groups;
				if (!partGroups.Any(group => Pawn.apparel.BodyPartGroupIsCovered(group)))
				{
					discovered = true;
				}
			}
			return !discovered;
		}

		/// <summary>
		/// show part info in healthtab
		/// </summary>
		public override string CompTipStringExtra
		{
			get
			{
				if (!initialised)
				{
					ModLog.Warning(" " + xxx.get_pawnname(Pawn) + " part " + parent.def.defName + " is broken, resetting");
					Init();
					UpdateSeverity();
					updatepartposition();
				}
				// In theory the tip should be a condensed form and the longer form should be elsewhere, but where?
				return GetTipString(RJWSettings.ShowRjwParts == RJWSettings.ShowParts.Extended).Join("\n");
			}
		}

		IEnumerable<string> GetTipString(bool extended)
		{
			//ModLog.Message(" CompTipStringExtra " + xxx.get_pawnname(Pawn) + " " + parent.def.defName);
			// TODO: Part type should be a property in the extension.
			//StringBuilder defstringBuilder = new StringBuilder();
			if (parent.Part != null)
				if (Def.genitalFamily == GenitalFamily.Breasts)
				{
					return GetBreastTip(extended);
				}
				else if (Def.genitalFamily == GenitalFamily.Penis)
				{
					return GetPenisTip(extended);
				}
				else if (Def.genitalFamily == GenitalFamily.Anus || Def.genitalFamily == GenitalFamily.Vagina)
				{
					return GetOrificeTip(extended);
				}
				else if (Def.genitalFamily == GenitalFamily.FemaleOvipositor || Def.genitalFamily == GenitalFamily.MaleOvipositor)
				{
					return GetEggTip();
				}

			return Enumerable.Empty<string>();
		}

		IEnumerable<string> GetEggTip()
		{
			if (!possibleEggsTipString.NullOrEmpty())
			{
				yield return "RJW_PartInfo_eggs".Translate(possibleEggsTipString);
			}
		}

		IEnumerable<string> GetPenisTip(bool extended)
		{
			if (PartSizeCalculator.TryGetLength(parent, out float length))
			{
				yield return "RJW_PartInfo_length".Translate(length.ToString("F1"));
			}

			if (!extended)
			{
				yield break;
			}

			if (PartSizeCalculator.TryGetGirth(parent, out float girth))
			{
				yield return "RJW_PartInfo_girth".Translate(girth.ToString("F1"));
			}

			if (PartSizeCalculator.TryGetPenisWeight(parent, out float weight))
			{
				yield return "RJW_PartInfo_weight".Translate(weight.ToString("F1"));
			}

			foreach (var line in GetFluidTip())
			{
				yield return line;
			}


			if (Def.partTags.Any())
			{
				yield return "RJW_PartInfo_properties".Translate(Def.partTags.ToCommaList());
			}
		}

		IEnumerable<string> GetOrificeTip(bool extended)
		{
			if (!extended)
			{
				yield break;
			}
			//defstringBuilder.Append(this.Def.description);
			//defstringBuilder.AppendLine("Cum: " + FluidType);
			//defstringBuilder.AppendLine("Ejaculation: " + (FluidAmmount * FluidModifier).ToString("F2") + "ml");

			// TODO: Should be scaled bigger than penis size either in xml or here.
			if (PartSizeCalculator.TryGetLength(parent, out float length))
			{
				yield return "RJW_PartInfo_maxLength".Translate(length.ToString("F1"));
			}

			if (PartSizeCalculator.TryGetGirth(parent, out float girth))
			{
				yield return "RJW_PartInfo_maxGirth".Translate(girth.ToString("F1"));
			}

			foreach (var line in GetFluidTip())
			{
				yield return line;
			}

			if (Def.partTags.Any())
			{
				yield return "Properties: " + Def.partTags.ToCommaList();
			}
		}

		IEnumerable<string> GetBreastTip(bool extended)
		{
			if (parent.Part.def == xxx.breastsDef)
			{
				foreach (var line in GetBraCupTip())
				{
					yield return line;
				}

				if (!extended)
				{
					yield break;
				}

				if (PartSizeCalculator.TryGetCupSize(parent, out float size))
				{
					var cupSize = (int)size;

					if (cupSize > 0)
					{
						var bandSize = PartSizeCalculator.GetBandSize(parent);
						yield return "RJW_PartInfo_braSize".Translate(
							bandSize,
							cupSize);

						if (PartSizeCalculator.TryGetOverbustSize(parent, out float overbust))
						{
							var underbust = PartSizeCalculator.GetUnderbustSize(parent);
							yield return "RJW_PartInfo_underbust".Translate(underbust.ToString("F1"));
							yield return "RJW_PartInfo_overbust".Translate(overbust.ToString("F1"));
						}

						if (PartSizeCalculator.TryGetBreastWeight(parent, out float weight))
						{
							yield return "RJW_PartInfo_weight".Translate(weight.ToString("F1"));
						}
					}
					else
					{
						yield return "RJW_PartInfo_braSizeNone".Translate();
					}
				}
			}

			foreach (var line in GetFluidTip())
			{
				yield return line;
			}

			if (Def.partTags.Any())
			{
				yield return "Properties: " + Def.partTags.ToCommaList();
			}
		}

		IEnumerable<string> GetBraCupTip()
		{
			if (PartSizeCalculator.TryGetCupSize(parent, out float size))
			{
				var cupSize = (int)size;

				if (cupSize > 0)
				{
					yield return "RJW_PartInfo_braCup".Translate(
						BraSizeConfigDef.GetCupSizeLabel(cupSize));
				}
			}
		}

		IEnumerable<string> GetFluidTip()
		{
			if (partFluidFactor != 0 && Fluid != null)
			{
				yield return "RJW_PartInfo_fluidTypeFluidAmount".Translate(
					Fluid.LabelCap,
					partFluidFactor.ToString("F1"));
			}
		}

		//TODO: someday part(SizeOwner) change operations
		public void UpdateSeverity(float newSeverity = -1f)
		{
			if (newSeverity != -1f)
			{
				baseSize = newSeverity * Pawn.BodySize / BodySizeFactor;
			}
			parent.Severity = Size / Pawn.BodySize;
		}

		public void ForceSize(float size)
		{
			forcedSize = size;
			UpdateSeverity();
		}

		public void UnforceSize()
		{
			forcedSize = null;
			UpdateSeverity();
		}

		[SyncMethod]
		public void updatepartposition()
		{
			if (parent.def is not HediffDef_SexPart def)
			{
				return;
			}

			//Log.Message("0 " + (parent.Part == null));
			//Log.Message("1 " +parent.Part);
			//Log.Message("2 " +parent.Part.def);
			//Log.Message("3 " +parent.Part.def.defName);
			//Log.Message("4 " +partBase);
			//Log.Message("5 " +partBase.DefaultBodyPart);
			//ModLog.Message("pawn " + xxx.get_pawnname(Pawn) + ", part: " + parent.def.defName);
			if (parent.Part == null)
			{
				var bp = Pawn.RaceProps.body.AllParts.Find(x => DefaultPositionNames.Any(x.def.defName.Contains));
				if (bp != null)
				{
					ModLog.Warning(" " + xxx.get_pawnname(Pawn) + " part is null/wholebody? is in wrong BodyPart position, resetting to default: " + def.defaultBodyPart);
					parent.Part = bp;
					Pawn.GetRJWPawnData().genitals = new List<Hediff>();
					Pawn.GetRJWPawnData().breasts = new List<Hediff>();
					Pawn.GetRJWPawnData().anus = new List<Hediff>();
				}
				else
				{
					ModLog.Message(" " + xxx.get_pawnname(Pawn) + " default bodypart not found: " + def.defaultBodyPart + " -skip.");
				}
			}
			else if (def.defaultBodyPart != "" && !parent.Part.def.defName.Contains(def.defaultBodyPart))
			{
				//Log.Message("f");
				//Log.Message(partBase.DefaultBodyPartList.ToLineList());
				if (def.defaultBodyPartList.Any() && def.defaultBodyPartList.Any(parent.Part.def.defName.Contains))
				{
					return;
				}
				var bp = Pawn.RaceProps.body.AllParts.Find(x => x.def.defName.Contains(def.defaultBodyPart));
				if (bp != null)
				{
					ModLog.Warning(" " + xxx.get_pawnname(Pawn) + " part " + parent.def.defName + " is in wrong BodyPart position, resetting to default: " + def.defaultBodyPart);
					parent.Part = bp;
					Pawn.GetRJWPawnData().genitals = new List<Hediff>();
					Pawn.GetRJWPawnData().breasts = new List<Hediff>();
					Pawn.GetRJWPawnData().anus = new List<Hediff>();
				}
				else
				{
					ModLog.Message(" " + xxx.get_pawnname(Pawn) + " default bodypart not found: " + def.defaultBodyPart + " -skip.");
				}
				//if (pawn.IsColonist)
				//{
				//	Log.Message(xxx.get_pawnname(pawn) + " has broken hediffs, removing " + this.ToString());
				//	Log.Message(Part.ToString());
				//	Log.Message(bp.def.defName);
				//	Log.Message(partBase.DefaultBodyPart.ToString());
				//}
			}
		}

		/// <summary>
		/// fill comp data
		/// </summary>
		[SyncMethod]
		public void Init(Pawn pawn = null, bool reroll = true)
		{
			pawn ??= Pawn;

			if (originalOwnerSize == 0)
			{
				originalOwnerSize = pawn.BodySize;
			}



			if (reroll || baseSize == 0)
			{
				baseSize = CalculateSize(pawn, originalOwnerSize);

				if (Def.fluidMultiplier > 0)
				{
					partFluidFactor = Def.GetRandomFluidAmount(pawn.RaceProps.baseBodySize, baseSize);
				}
			}

			if (Def.genitalTags.Contains(GenitalTag.CanFertilizeEgg) || Def.HasComp(typeof(HediffComp_Ovipositor)))
			{
				var possibleEggNames = HediffComp_Ovipositor.PossibleEggs(pawn.kindDef?.defName)
					.SelectMany(eggDef => eggDef.childrenDefs)
					.Select(defName => DefDatabase<PawnKindDef>.GetNamedSilentFail(defName)?.label)
					.Where(label => label != null);

				if (possibleEggNames.Any())
				{
					possibleEggsTipString = possibleEggNames.ToCommaList();
				}
				else
				{
					possibleEggsTipString = pawn?.kindDef?.label ?? "";
				}
			}

			if (originalOwnerRace.NullOrEmpty())
			{
				originalOwnerRace = pawn?.kindDef.race.LabelCap ?? "RJW_PartInfo_unknownSpecies".Translate();
			}
			previousOwner = pawn?.LabelNoCount ?? "RJW_PartInfo_unknownOwner".Translate();

			UpdateSeverity();
			initialised = true;
		}
		
		public float CalculateSize(Pawn pawn, float bodySize)
		{
			float relativeSize;
			if (Def.genitalFamily == GenitalFamily.Breasts && pawn?.gender == Gender.Male && !Rand.Chance(TrapChance(pawn)))
			{
				relativeSize = .01f;
			}
			else
			{
				relativeSize = Def.GetRandomSize();
			}
			return relativeSize * bodySize;
		}

		private static float TrapChance(Pawn pawn)
		{
			if (RJWSettings.MaleTrap && !xxx.is_animal(pawn))
			{
				if (xxx.is_nympho(pawn))
				{
					return RJWSettings.futa_nymph_chance;
				}
				if (pawn.Faction != null && (int)pawn.Faction.def.techLevel < 5)
				{
					return RJWSettings.futa_natives_chance;
				}
				return RJWSettings.futa_spacers_chance;
			}
			return 0f;
		}
	}
}
