context: using System;
  using System.Collections.Generic;
  using System.Linq;
  using System.Text;
  using UnityEngine;
  
  namespace ESLDCore
  {
      public class ESLDBeacon : PartModule, ISerializationCallbackReceiver
      {
          // Activation animation
          [KSPField]
          public string animationName = "";
  
          protected Animation anim;
  
          // Display beacon status in right click menu.
          [KSPField(guiName = "Beacon Status", guiActive = true)]
          public string beaconStatus;
  
          // Per-beacon G limit.
          [KSPField(isPersistant=true)]
          public float gLimitEff = 0.5f;
  
          [KSPField]
          public float gLimit = 0.5f;
  
          // Activated state.
          [KSPField(isPersistant = true)]
          public bool activated = false;
  
          // Beacon model (from part config).
          // Can be any string, but overrides cost parameters when equal to one of the original 4 models.
          [KSPField]
          public string beaconModel = string.Empty;
  
          // Cost equation:
          // M=tonnage, D=tripdist, f=massFctr, e=massExp, c=coef, p=distPow, b=baseMult, l=yardstick, d=2 if D>distpenalty
          // COST = M*b + M^(1+f*M^e+d) * (D/sqrt(l))^(1/2^p) * c
  
          // Cost parameter (from part config). Coefficient for total cost. (Does not affect base cost)
          [KSPField]
          public float coef = 0.01f;
  
          // Cost parameter (from part config). Coefficient for mass exponent.
          [KSPField]
          public float massFctr = 0.0002f;
  
          // Cost parameter (from part config). Second exponent of mass. (M^(1+f*M^e))
          [KSPField]
          public float massExp = 1f;
  
          // Cost parameter (from part config). Root order of distance cost. (tripdist^(1/(2^distPow)))
          [KSPField]
          public float distPow = 1;
  
          // Cost parameter (from part config). Coefficient for base cost from mass.
          [KSPField]
          public float baseMult = 0.25f;
  
          // Cost parameter (from part config). Distance beyond which cost becomes prohibitive. (Zero for infinite)
          [KSPField]
          public float distPenalty = 0;
  
          // Cost parameter (from part config). Cost added simply for using the beacon.
          [KSPField]
          public float baseCost = 0;
  
          // Cost multiplier. Applies to entire base jump cost.
          [KSPField]
          public float multiplier = 1;
  
          [KSPField]
          public float jumpPrecision = 10;
  
          [KSPField]
          public bool canJumpSelf = false;
  
          [KSPField]
          public bool builtInHCU = false;
  
          [KSPField]
          public bool jumpTargetable = true;
  
          public static float galaxyScalar = 1;
  
          [KSPField]
          public bool needsResourcesToBoot = false;
          
          // Display beacon operational floor in right click menu.
          [KSPField(guiName = "Lowest Altitude", guiActive = true, guiUnits = "km")]
          public double opFloor;
  
          // Charge to initialize beacon.
          [KSPField(guiName = "Charge to Activate", guiActive = false, guiUnits = " EC")]
          public double neededEC;
  
          // Charge to run beacon.
          [KSPField(guiName = "Electric Use", guiActive = false, guiUnits = " EC/s")]
          public double constantEC;
  
          public List<ESLDJumpResource> JumpResources { get => jumpResources; }
          protected List<ESLDJumpResource> jumpResources = new List<ESLDJumpResource>();
          [SerializeField]
          private List<string> serializedJumpResources = new List<string>();
  
          public bool UnsafeTransfer { get => !hasHCU; }
          public bool CarriesVelocity { get => !hasAMU; }
          public string Description { get => beaconModel; }
          public Vessel Vessel { get => vessel; }
          public float PathGLimit { get => gLimitEff; }
  
          [KSPField(isPersistant = true)]
          public bool hasAMU = false;
          [KSPField(isPersistant = true)]
          public bool hasHCU = false;
          [KSPField(isPersistant = true)]
          public bool hasGMU = false;
          [KSPField(isPersistant = true)]
          public bool hasSCU = false;
          [KSPField(isPersistant = true)]
          public double massBonus = 1;
  
          [KSPField]
          public string infoLine = "";
  
          public const string RnodeName = "RESOURCE";
          Logger log = new Logger("ESLDCore:ESLDBeacons: ");
  
          public static Dictionary<int, ESLDBeacon> SnapshotModules { get; } = new Dictionary<int, ESLDBeacon>();
  
          public static ESLDBeacon InstantiateFromSnapshot(ProtoPartSnapshot partSnapshot, ProtoPartModuleSnapshot moduleSnapshot, int index = 0)
          {
              if (moduleSnapshot.moduleName != "ESLDBeacon")
                  return null;
  
              int instanceID = -1;
              if (moduleSnapshot.moduleValues.TryGetValue("__snapshottedModuleInstanceID", ref instanceID))
              {
                  if (SnapshotModules.ContainsKey(instanceID))
                      return SnapshotModules[instanceID];
                  else
                      moduleSnapshot.moduleValues.RemoveValue("__snapshottedModuleInstanceID");
              }
  
              ESLDBeacon beacon = UnityEngine.Component.Instantiate<ESLDBeacon>(PartLoader.getPartInfoByName(partSnapshot.partName).partPrefab.FindModulesImplementing<ESLDBeacon>()[index]);
              //moduleSnapshot.moduleRef = beacon;    // Can't do this because it breaks game saving...
              instanceID = beacon.GetInstanceID();
              moduleSnapshot.moduleValues.AddValue("__snapshottedModuleInstanceID", instanceID);
              SnapshotModules.Add(instanceID, beacon);
              beacon.part = new Part() { vessel = partSnapshot.pVesselRef.vesselRef };
              beacon.snapshot = moduleSnapshot;
              SerializationHelper.LoadObjectFromConfig(beacon, moduleSnapshot.moduleValues);
              beacon.OnLoad(moduleSnapshot.moduleValues);
              return beacon;
          }
  
          public override void OnStart(StartState state)
          {
              SetFieldsEventsActions(activated);
  
              if (part != null)
              {
                  if (animationName != "")
                  {
                      anim = part.FindModelAnimators(animationName).FirstOrDefault();
                      if (anim == null)
                          log.Warning("Animation not found! " + animationName);
                      else
                      {
                          log.Debug("Animation found: " + animationName);
                          anim[animationName].wrapMode = WrapMode.Once;
                          if (activated)
                          {
                              anim[animationName].normalizedTime = 1;
                              anim.Play(animationName);
                          }
                          else
                          {
                              anim[animationName].normalizedTime = 0;
                              anim[animationName].normalizedSpeed = -10;
                              anim.Play(animationName);
                          }
                      }
                  }
              }
              if (HighLogic.LoadedSceneIsFlight)
              {
                  CheckOwnTechBoxes();
                  opFloor = FindAcceptableAltitude(); // Update tooltip display.
              }
          }
  
          public override void OnLoad(ConfigNode node)
          {
              base.OnLoad(node);
              
              foreach (ConfigNode rNode in node.GetNodes(RnodeName))
              {
                  ESLDJumpResource existingResource = jumpResources.Find(r => String.Equals(r.name, rNode.GetValue("name")));
                  if (existingResource == null)
                  {
                      existingResource = new ESLDJumpResource();
                      existingResource.Load(rNode);
                      jumpResources.Add(existingResource);
                  }
                  else
                  {
                      existingResource.Load(rNode);
                  }
              }
  
              if (!node.HasValue("gLimitEff"))
                  gLimitEff = gLimit;
  
              // Not sure if there should be a default resource...
              if (jumpResources.Count == 0 && (beaconModel == "LB10" || beaconModel == "LB15" || beaconModel == "LB100" || beaconModel == "IB1"))
              {
                  log.Warning("Generating new ESLDJumpResource for legacy beacon savefile");
                  jumpResources.Add(new ESLDJumpResource("Karborundum", ratio: 1));
              }
  
              ConfigNode[] settingsNode = GameDatabase.Instance.GetConfigNodes("ESLDSettings");
              if (settingsNode.Length < 1)
              {
                  ConfigNode save = new ConfigNode();
                  ConfigNode data = new ConfigNode("ESLDSettings");
                  data.AddValue("galaxyScalar", 1);
                  save.AddNode(data);
                  Debug.Log("Creating ESLD Settings file.");
                  save.Save("GameData/ESLDBeacons/ESLDSettings.cfg");
              }
              else
              {
                  if (float.TryParse(settingsNode[0].GetValue("galaxyScalar"), out float loadGalaxyScalar))
                      galaxyScalar = loadGalaxyScalar;
                  else
                      Debug.LogError("Did not load ESLD settings.");
              }
          }
  
          public override void OnSave(ConfigNode node)
          {
              CheckOwnTechBoxes();
              base.OnSave(node);
              for (int i = 0; i < jumpResources.Count; i++)
              {
                  ConfigNode resNode = node.AddNode(RnodeName);
                  jumpResources[i].Save(resNode);
              }
          }
  
          public void FixedUpdate()
          {
              if (!HighLogic.LoadedSceneIsFlight)
                  return;
              for (int i = jumpResources.Count - 1; i >= 0; i--)
                  jumpResources[i].GetFuelOnBoard(part);
              opFloor = FindAcceptableAltitude(); // Keep updating tooltip display. Also, needed EC.
              if (activated)
              {
                  if (FlightGlobals.getGeeForceAtPosition(vessel.GetWorldPos3D()).magnitude > gLimitEff)
                  {
                      ScreenMessages.PostScreenMessage("Warning: Too deep in gravity well.  Beacon has been shut down for safety.", 5.0f, ScreenMessageStyle.UPPER_CENTER);
                      BeaconShutdown();
                  }
                  if (vessel.altitude < (vessel.mainBody.Radius * 0.25f))
                  {
                      string thevar = (vessel.mainBody.name == "Mun" || vessel.mainBody.name == "Sun") ? "the " : string.Empty;
                      ScreenMessages.PostScreenMessage("Warning: Too close to " + thevar + vessel.mainBody.name + ".  Beacon has been shut down for safety.", 5.0f, ScreenMessageStyle.UPPER_CENTER);
                      BeaconShutdown();
                  }
                  double ECgotten = part.RequestResource("ElectricCharge", constantEC * TimeWarp.fixedDeltaTime);
                  if (!Double.IsNaN(constantEC) && (constantEC > 0) &&
                      ECgotten <= constantEC * TimeWarp.fixedDeltaTime * 0.9)
                  {
                      ScreenMessages.PostScreenMessage("Warning: Electric Charge depleted.  Beacon has been shut down.", 5.0f, ScreenMessageStyle.UPPER_CENTER);
                      log.Debug("Requested: " + constantEC + ", got: " + ECgotten / TimeWarp.fixedDeltaTime);
                      BeaconShutdown();
                  }
                  if (needsResourcesToBoot)
                  {
                      foreach (ESLDJumpResource Jresource in jumpResources)
                      {
                          if (Jresource.neededToBoot && !RequireResource(Jresource.resID, double.Epsilon, false))
                          {
                              ScreenMessages.PostScreenMessage("Warning: " + Jresource.name + " depleted.  Beacon has been shut down.", 5.0f, ScreenMessageStyle.UPPER_CENTER);
                              BeaconShutdown();
                              break;
                          }
                      }
                  }
                  //part.AddThermalFlux(TimeWarp.fixedDeltaTime * constantEC * 10);  // Not feasible until the fluctuation with high warp is nailed down.
              }
          }
  
          public void Warp(Vessel target, Vessel destination, float precision, List<Part> unsafeParts = null)
          {
              float tripDist = Vector3.Distance(vessel.GetWorldPos3D(), destination.GetWorldPos3D());
              float tonnage = target.GetTotalMass();
              float cost = GetTripFinalCost(GetTripBaseCost(tripDist / ModuleESLDLens.GetLensingGainRatio(Vessel.GetWorldPos3D(), target.GetWorldPos3D(), HailerGUI.Lenses), tonnage), target, destination, tonnage, unsafeParts);
              HailerGUI.PathCheck pathCheck = new HailerGUI.PathCheck(vessel, destination, gLimitEff);
              if (pathCheck.clear)
              {
                  if (jumpResources.All(res => RequireResource(res.resID, res.ratio * cost, false)))
                  {
                      jumpResources.All(res => RequireResource(res.resID, res.ratio * cost, true));
  
                      HailerButton.Instance.Dazzle();
  
                      Vector3d transferVelOffset = GetJumpVelOffset(target, destination);
                      if (hasAMU) transferVelOffset = destination.orbit.vel;
                      Vector3d spread = ((UnityEngine.Random.onUnitSphere + UnityEngine.Random.insideUnitSphere) / 2) * precision;
                      // Making the spread less likely to throw you outside the SoI of the body.
                      if ((destination.orbit.pos + spread).magnitude > destination.mainBody.sphereOfInfluence)
                          spread = -spread;   // Negative random is equally random.
  
                      OrbitDriver vesOrb = target.orbitDriver;
                      Orbit orbit = vesOrb.orbit;
                      Orbit newOrbit = new Orbit(orbit.inclination, orbit.eccentricity, orbit.semiMajorAxis, orbit.LAN, orbit.argumentOfPeriapsis, orbit.meanAnomalyAtEpoch, orbit.epoch, orbit.referenceBody);
                      newOrbit.UpdateFromStateVectors(destination.orbit.pos + spread, transferVelOffset, destination.mainBody, Planetarium.GetUniversalTime());
                      target.Landed = false;
                      target.Splashed = false;
                      target.landedAt = string.Empty;
  
                      OrbitPhysicsManager.HoldVesselUnpack(60);
  
                      List<Vessel> allVessels = FlightGlobals.Vessels;
                      foreach (Vessel v in allVessels.AsEnumerable())
                      {
  
gt:                         if (v.packed == false)