context: /*
  This file is part of Extraplanetary Launchpads.
  
  Extraplanetary Launchpads is free software: you can redistribute it and/or
  modify it under the terms of the GNU General Public License as published by
  the Free Software Foundation, either version 3 of the License, or
  (at your option) any later version.
  
  Extraplanetary Launchpads is distributed in the hope that it will be useful,
  but WITHOUT ANY WARRANTY; without even the implied warranty of
  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
  GNU General Public License for more details.
  
  You should have received a copy of the GNU General Public License
  along with Extraplanetary Launchpads.  If not, see
  <http://www.gnu.org/licenses/>.
  */
  using System;
  using System.Collections;
  using System.Collections.Generic;
  using System.Linq;
  using System.Reflection;
  using System.IO;
  using UnityEngine;
  
  using KSP.Localization;
  
  namespace ExtraplanetaryLaunchpads {
  
  	public class ELBuildControl : ELWorkSink
  	{
  		public interface IBuilder
  		{
  			void Highlight (bool on);
  			void UpdateMenus (bool visible);
  			void SetCraftMass (double craft_mass);
  			void SetShipTransform (Transform shipTransform, Part rootPart);
  			Transform PlaceShip (Transform shipTransform, Box vessel_bounds);
  			void RepositionShip (Vessel ship);
  			void PostBuild (Vessel craftVessel);
  			void PadSelection_start ();
  			void PadSelection ();
  			void PadSelection_end ();
  
  			bool canBuild { get; }
  			bool capture { get; }
  			Vessel vessel { get; }
  			Part part { get; }
  			ELBuildControl control { get; }
  			string Name { get; set; }
  			string LandedAt { get; }
  			string LaunchedFrom { get; }
  
  			uint ID { get; }
  		}
  
  		public IBuilder builder { get; private set; }
  
  		public enum State { Idle, Planning, Building, Canceling, Dewarping, Complete, Transfer };
  
  		public ELCraftType craftType = ELCraftType.VAB;
  
  		public string filename { get; private set; }
  		public string flagname { get; set; }
  		public bool lockedParts { get; private set; }
  		public ConfigNode craftConfig { get; private set; }
  		public CraftHull craftHull { get; private set; }
  		public GameObject craftHullObject { get; private set; }
  		RMResourceManager padResourceManager;
  		public RMResourceSet padResources
  		{
  			get {
  				if (padResourceManager != null) {
  					return padResourceManager.masterSet;
  				}
  				return null;
  			}
  		}
  		RMResourceManager craftResourceManager;
  		public RMResourceSet craftResources
  		{
  			get {
  				if (craftResourceManager != null) {
  					return craftResourceManager.masterSet;
  				}
  				return null;
  			}
  		}
  		public List<string> craftBoM { get; private set; }
  		public bool craftBoMdirty { get; private set; }
  		public CostReport buildCost { get; private set; }
  		public CostReport builtStuff { get; private set; }
  		public State state { get; private set; }
  		public bool paused { get; private set; }
  		public string KACalarmID = "";
  
  		public ELVesselWorkNet workNet { get; set; }
  
  		public double productivity
  		{
  			get {
  				if (workNet != null) {
  					return workNet.Productivity;
  				}
  				return 0;
  			}
  		}
  
  		public DockedVesselInfo vesselInfo { get; private set; }
  		public Part craftRoot { get; private set; }
  		public string craftName
  		{
  			get {
  				if (vesselInfo != null) {
  					return vesselInfo.name;
  				} else if (craftConfig != null) {
  					return craftConfig.GetValue ("ship");
  				} else {
  					return "";
  				}
  			}
  			set {
  				if (vesselInfo != null) {
  					vesselInfo.name = value;
  				} else if (craftConfig != null) {
  					craftConfig.SetValue ("ship", value);
  				}
  			}
  		}
  
  		string _savesPath;
  		string savesPath
  		{
  			get {
  				if (_savesPath == null) {
  					_savesPath = KSPUtil.ApplicationRootPath + "saves/";
  					_savesPath += HighLogic.SaveFolder;
  				}
  				return _savesPath;
  			}
  		}
  
  		public void CancelBuild ()
  		{
  			if (state == State.Building || state == State.Complete) {
  				state = State.Canceling;
  			}
  		}
  
  		public void UnCancelBuild ()
  		{
  			if (state == State.Canceling) {
  				state = State.Building;
  			}
  		}
  
  		public void PauseBuild ()
  		{
  			if (state == State.Building || state == State.Canceling) {
  				paused = true;
  			}
  		}
  
  		public void ResumeBuild ()
  		{
  			paused = false;
  		}
  
  		public bool isActive
  		{
  			get {
  				return ((state == State.Building || state == State.Canceling)
  						&& !paused);
  			}
  		}
  
  		public double CalculateWork ()
  		{
  			if (paused) {
  				return 0;
  			}
  			double hours = 0;
  			var built = builtStuff.required;
  			var cost = buildCost.required;
  			if (state == State.Building) {
  				for (int i = built.Count; i-- > 0; ) {
  					var res = built[i];
  					hours += res.kerbalHours * res.amount;
  				}
  			} else if (state == State.Canceling) {
  				for (int i = built.Count; i-- > 0; ) {
  					var res = built[i];
  					var cres = ELBuildWindow.FindResource (cost, res.name);
  					hours += res.kerbalHours * (cres.amount - res.amount);
  				}
  			}
  			return hours;
  		}
  
  		public void DestroyPad ()
  		{
  			state = State.Idle;
  			builder.part.explosionPotential = 0.1f;
  			builder.part.explode ();
  		}
  
  		private IEnumerator DewarpAndBuildCraft ()
  		{
  			state = State.Dewarping;
  			Debug.Log (String.Format ("[EL Launchpad] dewarp"));
  			TimeWarp.SetRate (0, false);
  			while (builder.vessel.packed) {
  				yield return null;
  			}
  			if (!builder.vessel.LandedOrSplashed) {
  				while (builder.vessel.geeForce > 0.1) {
  					yield return null;
  				}
  			}
  			state = State.Complete;
  		}
  
  		void SetPadMass ()
  		{
  			double mass = 0;
  			if (builtStuff != null && buildCost != null) {
  				var built = builtStuff.required;
  				var cost = buildCost.required;
  
  				foreach (var bres in built) {
  					var cres = ELBuildWindow.FindResource (cost, bres.name);
  					mass += (cres.amount - bres.amount) * bres.density;
  				}
  			}
  			builder.SetCraftMass (mass);
  		}
  
  		private void DoWork_Build (double kerbalHours)
  		{
  			var required = builtStuff.required;
  
  			bool did_work;
  			int count;
  
  			for (int i = required.Count; i-- > 0; ) {
  				required[i].deltaAmount = 0;
  			}
  
  			do {
  				//Debug.LogFormat ("[EL Launchpad] KerbalHours: {0}", kerbalHours);
  				count = 0;
  				for (int i = required.Count; i-- > 0; ) {
  					var res = required[i];
  					if (res.amount > 0
  						&& padResources.ResourceAmount (res.name) > 0) {
  						count++;
  					}
  				}
  				if (kerbalHours == 0) {
  					break;
  				}
  				if (count == 0)
  					break;
  				did_work = false;
  				for (int i = required.Count; i-- > 0; ) {
  					var res = required[i];
  					if (res.amount == 0) {
  						continue;
  					}
  					double avail = padResources.ResourceAmount (res.name);
  					if (avail == 0) {
  						continue;
  					}
  					double work = kerbalHours / count--;
  					double amount = work / res.kerbalHours;
  					double base_amount = Math.Abs (amount);
  
  					if (amount > res.amount)
  						amount = res.amount;
  					if (amount > avail)
  						amount = avail;
  					//Debug.LogFormat ("[EL Launchpad] work:{0}:{1}:{2}:{3}:{4}",
  					//				 res.name, res.kerbalHours, res.amount, avail, amount);
  					if (amount <= 0)
  						continue;
  					kerbalHours -= work;
  					did_work = true;
  					// do only the work required to process the actual amount
  					// of consumed resource
  					double dkH = work * amount / base_amount;
  					if (dkH > work) {
  						dkH = work;
  					}
  					work -= dkH;
  					// return any unused kerbal-hours to the pool
  					kerbalHours += work;
  					res.amount -= amount;
  					//Debug.Log($"add delta: {amount}");
  					res.deltaAmount += amount;
  					padResources.TransferResource (res.name, -amount);
  				}
  				//Debug.LogFormat ("[EL Launchpad] work:{0}:{1}", did_work, kerbalHours);
  			} while (did_work);
  
  			SetPadMass ();
  
  			// recount resources that still need to be processed into the build
  			count = required.Where (r => r.amount > 0).Count ();
  			if (count == 0) {
  				(builder as PartModule).StartCoroutine (DewarpAndBuildCraft ());
  				KACalarmID = "";
  			}
  		}
  
  		int CountResources (List<BuildResource> built, List<BuildResource> cost)
  		{
  			int count = 0;
  			foreach (var bres in built) {
  				var cres = ELBuildWindow.FindResource (cost, bres.name);
  				if (cres.amount - bres.amount > 0) {
  					count++;
  				}
  			}
  			return count;
  		}
  
  		private void DoWork_Cancel (double kerbalHours)
  		{
  			var built = builtStuff.required;
  			var cost = buildCost.required;
  
  			for (int i = built.Count; i-- > 0; ) {
  				built[i].deltaAmount = 0;
  			}
  
  			bool did_work;
  			int count;
  			do {
  				count = CountResources (built, cost);
  				if (kerbalHours == 0) {
  					break;
  				}
  				if (count == 0) {
  					break;
  				}
  
  				did_work = false;
  				foreach (var bres in built) {
  					var cres = ELBuildWindow.FindResource (cost, bres.name);
  					double remaining = cres.amount - bres.amount;
  					if (remaining <= 0) {
  						continue;
  					}
  					double work = kerbalHours / count--;
  					double amount = work / bres.kerbalHours;
  					double base_amount = Math.Abs (amount);
  
  					if (amount > remaining) {
  						amount = remaining;
  					}
  					kerbalHours -= work;
  					did_work = true;
  					// do only the work required to process the actual amount
  					// of returned or disposed resource
  					double dkH = work * amount / base_amount;
  					if (dkH > work) {
  						dkH = work;
  					}
  					work -= dkH;
  					// return any unused kerbal-hours to the pool
  					kerbalHours += work;
  
  					bres.amount += amount;
  					//Debug.Log("remove delta: "+amount);
  					bres.deltaAmount += amount;
  
  					double capacity = padResources.ResourceCapacity (bres.name)
  									- padResources.ResourceAmount (bres.name);
  					if (amount > capacity) {
  						amount = capacity;
  					}
  					if (amount <= 0)
  						continue;
  					padResources.TransferResource (bres.name, amount);
  				}
  			} while (did_work);
  
  			// recount resources that still need to be taken from the build
  			count = CountResources (built, cost);
  			if (count == 0) {
  				state = State.Planning;
  				KACalarmID = "";
  				builtStuff = null;	// ensure pad mass gets reset
  			}
  
  			SetPadMass ();
  		}
  
  		public void DoWork (double kerbalHours)
  		{
  			if (state == State.Building) {
  				DoWork_Build (kerbalHours);
  			} else if (state == State.Canceling) {
  				DoWork_Cancel (kerbalHours);
  			}
  		}
  
  		internal void SetupCraftResources (Vessel vsl)
  		{
  			Part rootPart = vsl.parts[0].localRoot;
  			craftResourceManager = new RMResourceManager (vsl.parts, rootPart,
  														  false, true);
  			foreach (var br in buildCost.optional) {
  				var amount = craftResources.ResourceAmount (br.name);
  				craftResources.TransferResource (br.name, -amount);
  			}
  		}
  
  		private void TransferResources ()
  		{
  			foreach (var br in buildCost.optional) {
  				var a = padResources.TransferResource (br.name, -br.amount);
  				a += br.amount;
  				var b = craftResources.TransferResource (br.name, a);
  				padResources.TransferResource (br.name, b);
  			}
  		}
  
  		static ConfigNode orbit (Orbit orb)
  		{
  			var snap = new OrbitSnapshot (orb);
  			var node = new ConfigNode ();
  			snap.Save (node);
  			return node;
  		}
  
  		public Vector3 GetVesselWorldCoM (Vessel v)
  		{
  			var com = v.localCoM;
  			return v.rootPart.partTransform.TransformPoint (com);
  		}
  
  		private void SetCraftOrbit (Vessel craftVessel, OrbitDriver.UpdateMode mode)
  		{
  			craftVessel.orbitDriver.SetOrbitMode (mode);
  
  			var craftCoM = GetVesselWorldCoM (craftVessel);
  			var vesselCoM = GetVesselWorldCoM (builder.vessel);
  			var offset = (Vector3d.zero + craftCoM - vesselCoM).xzy;
  
  			var corb = craftVessel.orbit;
  			var orb = builder.vessel.orbit;
  			var UT = Planetarium.GetUniversalTime ();
  			var body = orb.referenceBody;
  			corb.UpdateFromStateVectors (orb.pos + offset, orb.vel, body, UT);
  
  			Debug.Log (String.Format ("[EL] {0} {1}", orbit(orb), orb.pos));
  			Debug.Log (String.Format ("[EL] {0} {1}", orbit(corb), corb.pos));
  		}
  
  		private void CoupleWithCraft (Vessel craftVessel)
  		{
  			craftRoot = craftVessel.rootPart;
  			vesselInfo = new DockedVesselInfo ();
  			vesselInfo.name = craftVessel.vesselName;
  			vesselInfo.vesselType = craftVessel.vesselType;
  			vesselInfo.rootPartUId = craftRoot.flightID;
  			craftRoot.Couple (builder.part);
  
  			if (builder.vessel != FlightGlobals.ActiveVessel) {
  				FlightGlobals.ForceSetActiveVessel (builder.vessel);
  			}
  		}
  
  		public delegate void PostCaptureDelegate ();
  		public PostCaptureDelegate PostCapture = () => { };
  
  		private IEnumerator CaptureCraft (Vessel craftVessel)
  		{
  			FlightGlobals.overrideOrbit = true;
  			while (true) {
  				Debug.Log($"[EL] Waiting to capture {craftVessel.vesselName}");
  				bool partsInitialized = true;
  				foreach (Part p in craftVessel.parts) {
  					if (!p.started) {
  						partsInitialized = false;
  						break;
  					}
  				}
  				builder.RepositionShip (craftVessel);
  				SetCraftOrbit (craftVessel, OrbitDriver.UpdateMode.UPDATE);
  
gt: 				if (partsInitialized) {