context: using System;
  using System.Collections.Generic;
  using System.Linq;
  using Hospitality.Utilities;
  using JetBrains.Annotations;
  using RimWorld;
  using RimWorld.Planet;
  using UnityEngine;
  using Verse;
  using Verse.AI;
  using Verse.AI.Group;
  using GuestUtility = Hospitality.Utilities.GuestUtility;
  
  namespace Hospitality
  {
      // Note that this implementation is VERY different from vanilla
      public class IncidentWorker_VisitorGroup : IncidentWorker_NeutralGroup
      {
          private static ThingDef[] _items;
  
          // Taken from core
          private static readonly SimpleCurve pointsCurve = new SimpleCurve
          {
              new CurvePoint(45f, 0f), new CurvePoint(50f, 1f), new CurvePoint(100f, 1f), new CurvePoint(200f, 0.25f), new CurvePoint(300f, 0.1f), new CurvePoint(500f, 0f)
          };
  
          public static float MaxPleaseAmount(float current)
          {
              // if current standing is 100, 10 can be gained
              // if current standing is -100, 50 can be gained
              // then clamped.
              return Mathf.Clamp(current + 30 - Offset(current), -100, 100);
          }
  
          public static float MaxAngerAmount(float current)
          {
              return Mathf.Clamp(current - 30, -100, 100);
          }
  
          private static float Offset(float current)
          {
              return Mathf.Lerp(-20, 20, Mathf.InverseLerp(-100, 100, current));
          }
  
          public override bool FactionCanBeGroupSource(Faction f, Map map, bool desperate = false)
          {
              return !f.IsPlayer && !f.defeated && !f.def.hidden && !f.HostileTo(Faction.OfPlayer) 
                     && f.def.pawnGroupMakers != null && f.def.pawnGroupMakers.Any(x => x.kindDef == PawnGroupKindDef);
          }
  
          private static bool CheckCanCome(Map map, Faction faction, out TaggedString reasons)
          {
              var fallout = map.GameConditionManager.ConditionIsActive(GameConditionDefOf.ToxicFallout);
              var potentiallyDangerous = map.mapPawns.AllPawnsSpawned.Where(p => !p.Dead && !p.IsPrisoner && !p.Downed && !IsFogged(p) && !p.InContainerEnclosed).ToArray();
              var hostileFactions = potentiallyDangerous.Where(p => p.Faction != null).Select(p => p.Faction).Where(f => f.HostileTo(Faction.OfPlayer) || f.HostileTo(faction)).ToArray();
              var winter = map.GameConditionManager.ConditionIsActive(GameConditionDefOf.VolcanicWinter);
              var temp = faction.def.allowedArrivalTemperatureRange.Includes(map.mapTemperature.OutdoorTemp) && faction.def.allowedArrivalTemperatureRange.Includes(map.mapTemperature.SeasonalTemp);
              var beds = map.listerBuildings.AllBuildingsColonistOfClass<Building_GuestBed>().Any();
              var manhunters = potentiallyDangerous.Where(p => p.InAggroMentalState);
  
              reasons = null;
  
              if (temp && !fallout && !winter && beds && !hostileFactions.Any() && !manhunters.Any()) return true; // All clear, don't ask
  
              var reasonList = new List<string>(); // string, so we can check for Distinct later
              if (!beds) reasonList.Add("- " + "VisitorsArrivedReasonNoBeds".Translate());
              if (fallout) reasonList.Add("- " + GameConditionDefOf.ToxicFallout.LabelCap);
              if (winter) reasonList.Add("- " + GameConditionDefOf.VolcanicWinter.LabelCap);
              if (!temp) reasonList.Add("- " + "Temperature".Translate());
  
              foreach (var f in hostileFactions)
              {
                  reasonList.Add("- " + f.def.pawnsPlural.CapitalizeFirst());
              }
  
              var manhunterNames = manhunters.GroupBy(p=>p.MentalStateDef);
              foreach (var manhunter in manhunterNames)
              {
                  if (manhunter.Count() > 1)
                      reasonList.Add($"- {manhunter.First().GetKindLabelPlural()} ({manhunter.First().MentalStateDef.label})");
                  else if (manhunter.Count() == 1)
                      reasonList.Add($"- {manhunter.First().LabelShort} ({manhunter.First().MentalStateDef.label})");
              }
  
              reasons = reasonList.Distinct().Aggregate((a, b) => a+"\n"+b);
              return false; // Do ask
      
          }
  
          private static bool IsFogged(Pawn pawn)
          {
              return pawn.MapHeld.fogGrid.IsFogged(pawn.PositionHeld);
          }
  
          private static void ShowAskMayComeDialog(Faction faction, Map map, TaggedString reasons, Direction8Way spawnDirection, Action allow, Action refuse)
          {
              var text = "VisitorsArrivedDesc".Translate(faction, reasons);
  
              DiaNode diaNode = new DiaNode(text);
              DiaOption diaOption = new DiaOption("VisitorsArrivedAccept".Translate()) {action = allow, resolveTree = true};
              diaNode.options.Add(diaOption);
  
              DiaOption diaOption2 = new DiaOption("VisitorsArrivedRefuse".Translate()) {action = refuse, resolveTree = true};
              diaNode.options.Add(diaOption2);
  
              if (!map.listerBuildings.AllBuildingsColonistOfClass<Building_GuestBed>().Any())
              {
                  DiaOption diaOption3 = new DiaOption("VisitorsArrivedRefuseUntilBeds".Translate());
                  diaOption3.resolveTree = true;
                  diaOption3.action = () => {
                      Hospitality_MapComponent.RefuseGuestsUntilWeHaveBeds(map);
                      refuse();
                  };
                  diaNode.options.Add(diaOption3);
              }
  
              string title = "VisitorsArrivedTitle".Translate(new NamedArgument((MapParent)map.ParentHolder, "WORLDOBJECT"), spawnDirection.LabelShort(), new NamedArgument(faction, "FACTION"));
              Find.WindowStack.Add(new Dialog_NodeTree(diaNode, true, true, title));
          }
  
          public override bool TryExecuteWorker(IncidentParms parms)
          {
              if (!TryResolveParms(parms))
              {
                  Log.ErrorOnce($"Trying to spawn visitors, but parms couldn't be resolved.", 84289426);
                  return false;
              }
  
              // Is map not available anymore?
              if (!(parms.target is Map map))
              {
                  Log.ErrorOnce("Trying to spawn visitors, but map does not exist anymore.", 43692862);
                  return true;
              }
  
              if (parms.points < 40)
              {
                  Log.Error("Trying to spawn visitors, but points are too low.");
                  return false;
              }
  
              if (parms.faction == null)
              {
                  Log.ErrorOnce("Trying to spawn visitors, but couldn't find valid faction.", 43638973);
                  return true;
              }
              if (!parms.spawnCenter.IsValid)
              {
                  Log.ErrorOnce("Trying to spawn visitors, but could not find a valid spawn point.", 94839643);
                  return false;
              }
  
              if (parms.faction == Faction.OfPlayer)
              {
                  Log.ErrorOnce("Trying to spawn visitors, but they are of Faction.OfPlayer.", 3464363);
                  return true;
              }
              if (parms.faction.RelationWith(Faction.OfPlayer).kind == FactionRelationKind.Hostile)
              {
                  Log.Message($"Trying to spawn visitors of faction {parms.faction.Name}, but they are hostile to the player (now).");
                  return true;
              }
  
              if (parms.faction.defeated)
              {
                  Log.Message($"Trying to spawn visitors of faction {parms.faction.Name}, but they have been defeated.");
                  return true;
              }
  
              if (Settings.disableGuests || map.mapPawns.ColonistCount == 0 || !map.GetMapComponent().guestsAreWelcome)
              {
                  if(Settings.disableGuests)
                      Log.Message($"Guest group arrived, but guests are disabled in the options.");
                  else if(map.mapPawns.ColonistCount == 0)
                      Log.Message($"Guest group arrived, but there are no remaining colonists on the map.");
                  else if(!map.GetMapComponent().guestsAreWelcome)
                      Log.Message($"Guest group arrived, but the player disabled guests for this map.");
  
                  GenericUtility.PlanNewVisit(map, Rand.Range(5f, 25f), parms.faction);
              }
              else
              {
                  // Did the player refuse guests until beds are made and there are no beds yet?
                  if (!BedCheck(map))
                  {
                      Log.Message("Guest group arrived, but there are no guest beds and the player asked to wait until they are built.");
  
                      MaybeRevisit(parms, map, new FloatRange(5, 10));
                      return true;
                  }
  
                  // We check here instead of CanFireNow, so we can reschedule the visit.
                  // Any reasons not to come?
                  if (CheckCanCome(map, parms.faction, out var reasons))
                  {
                      // No, spawn
                      return SpawnGroup(parms, map);
                  }
  
                  // Yes, ask the player for permission
                  void Allow() => SpawnGroup(parms, map);
                  void Refuse() => MaybeRevisit(parms, map, new FloatRange(2, 5));
  
                  AskForPermission(parms, map, reasons, Allow, Refuse);
              }
              return true;
          }
  
          private static void MaybeRevisit(IncidentParms parms, Map map, FloatRange afterDays)
          {
              var queue = map.GetMapComponent().GetQueuedVisits(afterDays.TrueMax);
              var chance = GenMath.LerpDoubleClamped(0, 5, 1f, 0.3f, queue.Count());
  
              if (Rand.Chance(chance))
              {
                  GenericUtility.PlanNewVisit(map, afterDays.RandomInRange, parms.faction);
              }
          }
  
          protected virtual void AskForPermission(IncidentParms parms, Map map, string reasons, Action allow, Action refuse)
          {
              var spawnDirection = GetSpawnDirection(map, parms.spawnCenter);
              ShowAskMayComeDialog(parms.faction, map, reasons, spawnDirection,
                  // Permission, spawn
                  allow,
                  // No permission, come again later
                  refuse);
          }
  
          private static Direction8Way GetSpawnDirection(Map map, IntVec3 position)
          {
              var offset = map.Center - position;
              var angle = (offset.AngleFlat+180)%360;
  
              const float step = 360/16f;
              if(angle < 1*step) return Direction8Way.North;
              if(angle < 3*step) return Direction8Way.NorthEast;
              if(angle < 5*step) return Direction8Way.East;
              if(angle < 7*step) return Direction8Way.SouthEast;
              if(angle < 9*step) return Direction8Way.South;
              if(angle < 11*step) return Direction8Way.SouthWest;
              if(angle < 13*step) return Direction8Way.West;
              if(angle < 15*step) return Direction8Way.NorthWest;
              return Direction8Way.North;
          }
  
          private bool SpawnGroup(IncidentParms parms, Map map)
          {
              var visitors = new List<Pawn>();
              try
              {
                  //Log.Message(string.Format("Spawning visitors from {0}, at {1}.", parms.faction, parms.spawnCenter));
                  SpawnPawns(parms, visitors);
  
                  SpawnGroupUtility.CheckVisitorsValid(visitors);
  
                  if (visitors.Count == 0) return false;
  
                  var area = visitors.First().GetGuestArea() ?? map.GetMapComponent().defaultAreaRestriction;
                  var spot = GetSpot(map, area, visitors.First());
  
                  if (!spot.IsValid)
                  {
                      throw new Exception($"Visitors from {parms.faction.Name} failed to find a valid travel target from {visitors.First()?.Position} to guest area {area?.Label}.");
                  }
  
                  GiveItems(visitors);
  
                  var stayDuration = (int)(Rand.Range(1f, 2.4f) * GenDate.TicksPerDay);
                  CreateLord(parms.faction, spot, visitors, map, true, true, stayDuration);
  
                  // Update our mapcomponents guests.
                  map.GetMapComponent().RefreshGuestListTotal();                
              }
              catch (Exception e)
              {
                  var faction = parms.faction?.Name;
                  var factionType = parms.faction?.def.label;
                  Log.Error($"Hospitality: Something failed when setting up visitors from faction {faction} ({factionType}):\n{e}");
                  foreach (var visitor in visitors)
                  {
                      if (visitor?.Spawned == true)
                      {
                          visitor.DeSpawn();
                          visitor.DestroyOrPassToWorld();
                      }
                  }
                  GenericUtility.PlanNewVisit(map, Rand.Range(1f, 3f), parms.faction);
              }
              return true; // be gone, event
          }
  
          public override void ResolveParmsPoints(IncidentParms parms)
          {
              if (parms.points < 0f)
              {
                  parms.points = Rand.ByCurve(pointsCurve);
              }
          }
  
          protected void SpawnPawns(IncidentParms parms, List<Pawn> spawned)
          {
              var map = (Map)parms.target;
  
              var selection = GetPawnsToSpawn(parms).Distinct();
  
              foreach (var pawn in selection)
              {
                  try
                  {
                      var visitor = SpawnGroupUtility.SpawnVisitor(spawned, pawn, map, parms.spawnCenter);
                      if (visitor?.needs?.rest != null) visitor.needs.rest.CurLevel = Rand.Range(0.1f, 0.7f);
                  }
                  catch (Exception e)
                  {
                      Log.Error($"Hospitality: Failed to spawn pawn {pawn?.Label}:\n{e}");
                      if (pawn.Spawned)
                      {
                          pawn.DeSpawn();
                          pawn.DestroyOrPassToWorld();
                      }
                  }
              }
          }
  
          [NotNull]
  
gt:         protected IEnumerable<Pawn> GetPawnsToSpawn(IncidentParms parms)