context: // Mental note:
  // It is not entirely clear which .NET version should KSP plugins be compiled for,
  // but the consensus is that 3.5 is the most appropriate because types introduced
  // in 4.0 can be verified not to work. It is a fact that you can use C#4 itself
  // with it with no ill effects, though -- at least all the features which rely
  // on the compiler, rather than on the libraries.
  // SCANsat is compiled for .NET 4.0 for some reason, which means that
  // this assembly also needs to be compiled for 4.0 to link to it.
  // The immediate drawback so far discovered is that I can't use LINQ,
  // while when I compile for 3.5 I can.
  // I wish there were some clarity on the subject.
  using SCANsat;
  using UnityEngine;
  using System;
  using System.Collections.Generic;
  using JSI;
  using System.Globalization;
  
  namespace SCANsatRPM
  {
  	public class JSISCANsatRPM: InternalModule
  	{
  		[KSPField]
  		public int buttonUp;
  		[KSPField]
  		public int buttonDown = 1;
  		[KSPField]
  		public int buttonEnter = 2;
  		[KSPField]
  		public int buttonEsc = 3;
  		[KSPField]
  		public int buttonHome = 4;
  		[KSPField]
  		public int maxZoom = 20;
  		[KSPField]
  		public float iconPixelSize = 8f;
  		[KSPField]
  		public Vector2 iconShadowShift = new Vector2(1, 1);
  		[KSPField]
  		public float redrawEdge = 0.8f;
  		[KSPField]
  		public string iconColorSelf = string.Empty;
  		private Color iconColorSelfValue = new Color(1f, 1f, 1f, 0.6f);
  		[KSPField]
  		public string iconColorTarget = string.Empty;
  		private Color iconColorTargetValue = new Color32(255, 235, 4, 153);
  		[KSPField]
  		public string iconColorUnvisitedAnomaly = string.Empty;
  		private Color iconColorUnvisitedAnomalyValue = new Color(1f, 0f, 0f, 0.5f);
  		[KSPField]
  		public string iconColorVisitedAnomaly = string.Empty;
  		private Color iconColorVisitedAnomalyValue = new Color(0f, 1f, 0f, 0.5f);
  		[KSPField]
  		public string iconColorShadow = string.Empty;
  		private Color iconColorShadowValue = new Color(0f, 0f, 0f, 0.5f);
  		[KSPField]
  		public string iconColorAP = string.Empty;
  		private Color iconColorAPValue = new Color(0f, 1f, 0f, 0.5f);
  		[KSPField]
  		public string iconColorPE = string.Empty;
  		private Color iconColorPEValue = new Color(1f, 0f, 0f, 0.5f);
  		[KSPField]
  		public string iconColorNode = string.Empty;
  		private Color iconColorNodeValue = XKCDColors.Aqua;
  		[KSPField]
  		public string iconColorANDN = string.Empty;
  		private Color iconColorANDNValue = XKCDColors.Mauve;
  		[KSPField]
  		public string trailColor = string.Empty;
  		private Color trailColorValue = new Color(0f, 0f, 1f, 0.6f);
  		[KSPField]
  		public float zoomModifier = 1.5f;
  		[KSPField]
  		public string scaleBar;
  		[KSPField]
  		public string scaleLabels;
  		[KSPField]
  		public string scaleLevels = "500000,200000,100000,50000,20000,10000,5000,1000";
  		[KSPField]
  		public Vector2 scaleBarPosition = new Vector2(16, 16);
  		[KSPField]
  		public float scaleBarSizeLimit = 512 / 2 - 16;
  		[KSPField]
  		public int trailLimit = 100;
  		[KSPField]
  		public float trailPointEvery = 30;
  		[KSPField]
  		public int orbitPoints = 120;
  		// That ends our glut of configurable values.
  		private bool showLines;
  		private int mapMode;
  		private int zoomLevel;
  		private int screenWidth;
  		private int screenHeight;
  		private Vector2d mapSizeScale;
  		private double mapCenterLong, mapCenterLat;
  		private SCANmap map;
  		private CelestialBody orbitingBody;
  		private Vessel targetVessel;
  		private double redrawDeviation;
  		private SCANdata.SCANanomaly[] localAnomalies;
  		private Material iconMaterial;
  		private PersistenceAccessor persistence;
  		private string persistentVarName;
  		private double pixelsPerKm;
  		private Texture2D scaleBarTexture, scaleLabelTexture;
  		private float[] scaleLevelValues;
  		private float scaleLabelSpan;
  		private readonly List<Vector2d> trail = new List<Vector2d>();
  		private static Material trailMaterial;
  		private double trailCounter;
  		private Rect screenSpace;
  		private bool pageActiveState;
  		private double start;
  		private readonly List<MapMarkupLine> mapMarkup = new List<MapMarkupLine>();
  		private readonly Color scaleTint = new Color(0.5f, 0.5f, 0.5f, 0.5f);
  		// Neutral tint.
  		private bool satFound;
  		private bool startupComplete;
  
  		private bool TestForActiveSCANsat()
  		{
  			if (satFound)
  				return true;
  			foreach (ScenarioModule thatScenario in ScenarioRunner.GetLoadedModules()) {
  				if (thatScenario.ClassName == "SCANcontroller") {
  					satFound = true;
  					return true;
  				}
  			}
  			return false;
  		}
  		// Analysis disable once UnusedParameter
  		public bool MapRenderer(RenderTexture screen, float cameraAspect)
  		{
  			// Just in case.
  			if (!HighLogic.LoadedSceneIsFlight)
  				return false;
  
  			if (!TestForActiveSCANsat())
  				return false;
  
  			if (!startupComplete)
  				JUtil.AnnoyUser(this);
  
  			if (screenWidth == 0 || screenHeight == 0) {
  				int? loadedMode = persistence.GetVar(persistentVarName + "mode");
  				mapMode = loadedMode ?? 0;
  				int? loadedZoom = persistence.GetVar(persistentVarName + "zoom");
  				zoomLevel = loadedZoom ?? 0;
  				int? loadedColors = persistence.GetVar(persistentVarName + "color");
  				SCANcontroller.controller.colours = loadedColors ?? 0;
  				screenWidth = screen.width;
  				screenHeight = screen.height;
  				iconMaterial = new Material(Shader.Find("KSP/Alpha/Unlit Transparent"));
  
  				screenSpace = new Rect(0, 0, screenWidth, screenHeight);
  
  				RedrawMap();
  
  				return false;
  			}
  
  			// In case SCANsat is present but not working, we can't run any of our code. :(
  			if (map == null) {
  				return false;
  			}
  
  			start = Planetarium.GetUniversalTime();
  
  			Graphics.Blit(map.map, screen);
  			GL.PushMatrix();
  			GL.LoadPixelMatrix(0, screenWidth, screenHeight, 0);
  
  			// Markup lines are the lowest layer.
  			if (showLines)
  				foreach (MapMarkupLine vectorLine in mapMarkup) {
  					if (vectorLine.body == orbitingBody && vectorLine.points.Count > 0) {
  						DrawTrail(vectorLine.points, vectorLine.color, Vector2d.zero);
  					}
  				}
  
  			// Trails go above markup lines
  			if (showLines && trailLimit > 0 && trail.Count > 0)
  				DrawTrail(trail, trailColorValue, new Vector2d(vessel.longitude, vessel.latitude), true);
  		
  			// Anomalies go above trails
  			foreach (SCANdata.SCANanomaly anomaly in localAnomalies) {
  				if (anomaly.known)
  					DrawIcon(anomaly.longitude, anomaly.latitude,
  						anomaly.detail ? (VesselType)int.MaxValue : VesselType.Unknown,
  						anomaly.detail ? iconColorVisitedAnomalyValue : iconColorUnvisitedAnomalyValue);
  			}
  			// Target orbit and targets go above anomalies
  			if (targetVessel != null && targetVessel.mainBody == orbitingBody) {
  				if (showLines && JUtil.OrbitMakesSense(targetVessel)) {
  					DrawOrbit(targetVessel, targetVessel.orbit, start, iconColorTargetValue);
  					// Connect our orbit and the target orbit with a line at the point of closest approach...
  					if (JUtil.OrbitMakesSense(vessel)) {
  						double closestApproachMoment;
  						JUtil.GetClosestApproach(vessel.orbit,targetVessel.orbit, out closestApproachMoment);
  						Vector2d targetCoord, vesselCoord;
  						bool targetCollision, vesselCollision;
  						// Analysis disable once CompareOfFloatsByEqualityOperator
  						if (closestApproachMoment != start &&
  						    GetPositionAtT(targetVessel, targetVessel.orbit, start, closestApproachMoment, out targetCoord, out targetCollision) && !targetCollision &&
  						    GetPositionAtT(vessel, targetVessel.orbit, start, closestApproachMoment, out vesselCoord, out vesselCollision) && !vesselCollision) {
  							var endpoints = new List<Vector2d>();
  							endpoints.Add(targetCoord);
  							endpoints.Add(vesselCoord);
  							DrawTrail(endpoints, iconColorTargetValue, Vector2d.zero);
  						}
  					}
  				}
  				DrawIcon(targetVessel.longitude, targetVessel.latitude, targetVessel.vesselType, iconColorTargetValue);
  				if (showLines) {
  					DrawOrbitIcon(targetVessel, MapIcons.OtherIcon.AP, iconColorTargetValue);
  					DrawOrbitIcon(targetVessel, MapIcons.OtherIcon.PE, iconColorTargetValue);
  				}
  
  
  			}
  			// Own orbit goes above that.
  			if (showLines && JUtil.OrbitMakesSense(vessel)) {
  				DrawOrbit(vessel, vessel.orbit, start, iconColorSelfValue);
  				DrawOrbitIcon(vessel, MapIcons.OtherIcon.AP, iconColorAPValue);
  				DrawOrbitIcon(vessel, MapIcons.OtherIcon.PE, iconColorPEValue);
  				if (targetVessel != null && JUtil.OrbitMakesSense(targetVessel)) {
  					if (vessel.orbit.AscendingNodeExists(targetVessel.orbit))
  						DrawOrbitIcon(vessel, MapIcons.OtherIcon.AN, iconColorANDNValue, vessel.orbit.TimeOfAscendingNode(targetVessel.orbit, start));
  					if (vessel.orbit.DescendingNodeExists(targetVessel.orbit))
  						DrawOrbitIcon(vessel, MapIcons.OtherIcon.DN, iconColorANDNValue, vessel.orbit.TimeOfDescendingNode(targetVessel.orbit, start));
  				}
  				// And the maneuver node and post-maneuver orbit: 
  				ManeuverNode node = vessel.patchedConicSolver.maneuverNodes.Count > 0 ? vessel.patchedConicSolver.maneuverNodes[0] : null;
  				if (node != null) {
  					DrawOrbit(vessel, node.nextPatch, node.UT, iconColorNodeValue);
  					DrawOrbitIcon(vessel, MapIcons.OtherIcon.NODE, iconColorNodeValue, node.UT);
  				}
  			}
  			// Own icon goes above that
  			DrawIcon(vessel.longitude, vessel.latitude, vessel.vesselType, iconColorSelfValue);
  			// And scale goes above everything.
  			DrawScale();
  
  			GL.PopMatrix();
  
  			return true;
  		}
  
  		private void DrawOrbitIcon(Vessel thatVessel, MapIcons.OtherIcon iconType, Color iconColor, double givenPoint = 0)
  		{
  			double timePoint = start;
  			switch (iconType) {
  				case MapIcons.OtherIcon.AP:
  					timePoint += thatVessel.orbit.timeToAp;
  					break;
  				case MapIcons.OtherIcon.PE:
  					timePoint += thatVessel.orbit.timeToPe;
  					break;
  				case MapIcons.OtherIcon.AN:
  				case MapIcons.OtherIcon.DN:
  				case MapIcons.OtherIcon.NODE:
  					timePoint = givenPoint;
  					break;
  			}
  
  			if (JUtil.OrbitMakesSense(thatVessel)) {
  				bool collision;
  				Vector2d coord;
  				if (GetPositionAtT(thatVessel, thatVessel.orbit, start, timePoint, out coord, out collision) && !collision) {
  					DrawIcon(coord.x, coord.y, thatVessel.vesselType, iconColor, iconType);
  				}
  			}
  		}
  
  		private static bool GetPositionAtT(Vessel thatVessel, Orbit thatOrbit, double initial, double timePoint, out Vector2d coordinates, out bool collision)
  		{
  			coordinates = Vector2d.zero;
  			collision = false;
  			if (double.IsNaN(thatOrbit.getObtAtUT(initial + timePoint)))
  				return false;
  			double rotOffset = 0;
  			if (thatVessel.mainBody.rotates) {
  				rotOffset = (360 * ((timePoint - initial) / thatVessel.mainBody.rotationPeriod)) % 360;
  			}
  			Vector3d pos = thatOrbit.getPositionAtUT(timePoint);
  			if (thatOrbit.Radius(timePoint) < thatVessel.mainBody.Radius + thatVessel.mainBody.TerrainAltitude(pos)) {
  				collision = true;
  				return false;
  			}
  			coordinates = new Vector2d(thatVessel.mainBody.GetLongitude(pos) - rotOffset, thatVessel.mainBody.GetLatitude(pos));
  			return true;
  		}
  
  		private void DrawOrbit(Vessel thatVessel, Orbit thatOrbit, double startMoment, Color32 thatColor)
  		{
  			if (orbitPoints == 0)
  				return;
  			double dTstep = Math.Floor(thatOrbit.period / orbitPoints);
  			var points = new List<Vector2d>();
  			for (double timePoint = startMoment; timePoint < (startMoment + thatOrbit.period); timePoint += dTstep) {
  				bool collision;
  				Vector2d coord;
  				if (GetPositionAtT(thatVessel, thatOrbit, start, timePoint, out coord, out collision))
  					points.Add(coord);
  				if (collision)
  					break;
  			}
  			DrawTrail(points, thatColor, Vector2d.zero);
  		}
  
  		private void DrawTrail(IList<Vector2d> points, Color32 lineColor, Vector2d endPoint, bool hasEndpoint = false)
  		{
  			if (points.Count < 2)
  				return;
  			GL.Begin(GL.LINES);
  			trailMaterial.SetPass(0);
  			GL.Color(lineColor);
  			float xStart, yStart;
  			// actualMapWidth is the width of the virtual map (accounting for
  			// zoom level).  We use this value to determine if a particular
  			// line segment wraps around from the right edge to the left edge.
  			// We compute the value once here, instead of doing it every single
  			// segment.
  			float actualMapWidth = (float)mapSizeScale.x * screenWidth;
  
  			xStart = (float)longitudeToPixels(points[0].x, points[0].y);
  			yStart = (float)latitudeToPixels(points[0].x, points[0].y);
  			for (int i = 1; i < points.Count; i++) {
  				float xEnd = (float)longitudeToPixels(points[i].x, points[i].y);
  				float yEnd = (float)latitudeToPixels(points[i].x, points[i].y);
  				DrawLine(xStart, yStart, xEnd, yEnd, screenSpace, actualMapWidth);
  
  				xStart = xEnd;
  				yStart = yEnd;
  			}
  			if (hasEndpoint)
  				DrawLine(xStart, yStart, (float)longitudeToPixels(endPoint.x, endPoint.y), (float)latitudeToPixels(endPoint.x, endPoint.y), screenSpace, actualMapWidth);
  			GL.End();
  		}
  
  		private static void DrawLine(float xStart, float yStart, float xEnd, float yEnd, Rect screenSpace, float actualMapWidth)
  		{
  			var start = new Vector2(xStart, yStart);
  			var end = new Vector2(xEnd, yEnd);
  
  			if (!screenSpace.Contains(start) && !screenSpace.Contains(end))
  				return;
  
  			// We order these so we don't have to mess with absolute values.
  			float leftX = Math.Min(start.x, end.x);
  			float rightX = Math.Max(start.x, end.x);
  
  			// MOARdV:
  			// We treat the map as a cylinder here, since it is one as far as
  			// the rectangular projection of it is concerned.
  			// Compute the x component of the Manhattan distance between the
  			// two points, and compare that to the distance if we move the
  			// left end point one scaled map width to the right (moving it to
  			// the right of the right end point).  If the repositioned point
  			// is closer than the original point, we infer that the line
  			// segment wraps around the edge of the map.  This will always be
  			// true as long as we don't generate a line segment longer than
  			// 1/2 of the map's width.  If the two points are diametrically
  			// opposed to each other on the map, we don't have enough
  			// information to guess which way is the right way, so do nothing.
  			// The Manhattan distance of the original arrangement of points
  			// is rightX - leftX.  The MD of the repositioned point is
  			// (leftX + actualMapWidth) - rightX.  Move like terms and
  			// divide by two, and here's what you get:
  			if (leftX + actualMapWidth * 0.5f < rightX) {
  				if (start.x < end.x) {
  					end.x -= actualMapWidth;
  				} else {
  
gt: 					start.x -= actualMapWidth;