﻿using UdonSharp;
using UnityEngine;
using VRC.SDKBase;
using VRC.Udon;
using TMPro;

[UdonBehaviourSyncMode(BehaviourSyncMode.None)]
public class OSSCelestial : UdonSharpBehaviour {
	public OSSCelestial parent;
	public double mass, radius;
	public double SMA;
	public double eccentricity,AOP;
	public double inclination,LAN;
	public double rotationPeriod = 1000.0;
	public bool isTidallyLocked;
	public bool isRootBody;
	public bool rotates = true;
	public double initialRotation = 0;
	public double orbitalPeriod = 0;
	public double anomalyAtEpoch = 0;
	public Vector3 pos;
	public Material skyboxMat;
	public int idxInSkybox = 0;
	public TextMeshProUGUI infoDisp;
	public int dispType = 0;
	[HideInInspector] public bool isCamera = false;
	
	private const double BIG_G = 6.67408e-11;
	private const double eps = 1e-4;
	private const double TAU = Mathf.PI + Mathf.PI;
	
	private OSSCelestial[] children = new OSSCelestial[12];
	private int childCount = 0;
	
	public double axisRotation { get; private set; }
	private double posX,posY,posZ;
	private bool stateDirty = true;
	private bool orbitalPeriodReady;
	private double absPosX,absPosY,absPosZ;
	private double parPosX,parPosY,parPosZ;
	
	/*
	 * Some stuff cached in vars
	 */
	private double pX, pY, pZ, qX, qY, qZ, e2, sqrtGravParamOverSMA, totalMass;
	
	void Start() {
		if(parent != null) parent._AddChild(this);
		if(isRootBody) posX = posY = posZ = 0;
		orbitalPeriodReady = orbitalPeriod != 0;
		axisRotation = 0;
	}
	
	public void _UpdateRotation(double time) {
		if(rotates) {
			double rotationState;
			if(isTidallyLocked) rotationState = time / orbitalPeriod;
			else rotationState = time / rotationPeriod;
			rotationState -= System.Math.Floor(rotationState);
			rotationState *= 360.0;
			axisRotation = rotationState;
			transform.localRotation = Quaternion.Euler(0, 0, (float)(rotationState + initialRotation));
		}
	}
	
	public void _Initialize() {
		orbitalPeriodReady = true;
		orbitalPeriod = 2.0 * Mathf.PI * System.Math.Pow((SMA * SMA * SMA) / (BIG_G * parent.mass), 0.5);
		if(isTidallyLocked) rotationPeriod = orbitalPeriod;
		
		
		//Orbital params are expected to be constant after start, so these are only calculated once
		double inRads = AOP * Mathf.Deg2Rad;
		double Cw = System.Math.Cos(inRads);
		double Sw = System.Math.Sin(inRads);
		inRads = LAN * Mathf.Deg2Rad;
		double co = System.Math.Cos(inRads);
		double so = System.Math.Sin(inRads);
		inRads = inclination * Mathf.Deg2Rad;
		double ci = System.Math.Cos(inRads);
		double si = System.Math.Sin(inRads);
		double swci = Sw * ci;
		double cwci = Cw * ci;
		pX = Cw * co - so * swci;
		pY = Cw * so + co * swci;
		pZ = Sw * si;
		qX = -Sw * co - so * cwci;
		qY = -Sw * so + co * cwci;
		qZ = Cw * si;
		
		e2 = SMA * System.Math.Pow(System.Math.Abs(1.0 - eccentricity * eccentricity), 0.5);
		
		totalMass = parent.mass + mass;
		sqrtGravParamOverSMA = System.Math.Pow((BIG_G * totalMass) / SMA, 0.5);
	}
	
	public void _UpdatePosition(double time) {
		if(isRootBody) {
			stateDirty = true;
			_UpdateRotation(time);
			return;
		}
		if(!orbitalPeriodReady) {
			_Initialize();
		}
		
		double orbitState = time / orbitalPeriod + anomalyAtEpoch / 360.0;
		orbitState -= System.Math.Floor(orbitState);
		stateDirty = true;
		
		double meanAnomaly = orbitState * TAU;
		CalcPosAtAnomaly(meanAnomaly);
		posX = calcPosX;
		posY = calcPosY;
		posZ = calcPosZ;
		_UpdateRotation(time);
		pos.x = (float)(posX / 1000.0);
		pos.y = (float)(posX / 1000.0);
		pos.z = (float)(posX / 1000.0);

		if(infoDisp != null && infoDisp.gameObject.activeSelf) {
			double edot = sqrtGravParamOverSMA / (SMA * (1.0 - eccentricity * ceca));
			double xdw = -SMA * edot * seca;
			double ydw = e2 * edot * ceca;
			double velX = xdw * pX + ydw * qX;
			double velY = xdw * pY + ydw * qY;
			double velZ = xdw * pZ + ydw * qZ;
			
			double a = System.Math.Pow(posX * posX + posY * posY + posZ * posZ, 0.5) - parent.radius;
			double b = System.Math.Pow(velX * velX + velY * velY + velZ * velZ, 0.5);
			
			long years, days, hours, minutes, seconds;
			
			double timeUntilNY = (TAU - meanAnomaly) * orbitalPeriod / TAU;
			if(dispType != 1) {
				years = (long)(timeUntilNY / (365*24*60*60));
				timeUntilNY -= years * 365L*24L*60L*60L;
				days = (long)(timeUntilNY / (24*60*60));
				timeUntilNY -= days * 24L*60L*60L;
			}else years = days = 0;
			hours = (long)(timeUntilNY / (60*60));
			timeUntilNY -= hours * 60*60;
			minutes = (long)(timeUntilNY / 60);
			timeUntilNY -= minutes * 60;
			seconds = (long)timeUntilNY;
			
			if(dispType == 1) {
				infoDisp.text = System.String.Format("Position:\n(X: {0:n}Km Y: {1:n}Km Z: {2:n}Km)\nAltitude: {3:n}Km\nVelocity:\n(X: {4}m/s Y: {5}m/s Z: {6}m/s)\nVel. Magnitude: {7}m/s\nMean Anomaly: {8}°\nTime until next day: {9}h {10}m {11}s",
					System.Math.Truncate(posX / 10) / 100,
					System.Math.Truncate(posY / 10) / 100,
					System.Math.Truncate(posZ / 10) / 100,
					System.Math.Truncate(a / 10) / 100,
					System.Math.Truncate(velX * 100) / 100,
					System.Math.Truncate(velY * 100) / 100,
					System.Math.Truncate(velZ * 100) / 100,
					System.Math.Truncate(b * 100) / 100,
					System.Math.Truncate(meanAnomaly * Mathf.Rad2Deg * 1000) / 1000,
					hours, minutes, seconds
				);
			}else if(dispType == 2) {
				infoDisp.text = System.String.Format("Position:\n(X: {0:n}Gm Y: {1:n}Gm Z: {2:n}Gm)\nAltitude: {3:n}Km - {4}AU\nVelocity:\n(X: {5}Km/s Y: {6}Km/s Z: {7}Km/s)\nVel. Magnitude: {8}Km/s\nMean Anomaly: {9}°\nTime until new year: {10}Y {11}d {12}h {13}m {14}s",
					System.Math.Truncate(posX / 10000000) / 100,
					System.Math.Truncate(posY / 10000000) / 100,
					System.Math.Truncate(posZ / 10000000) / 100,
					System.Math.Truncate(a / 100) / 10,
					System.Math.Truncate(a / 1.495979e+9) / 100,
					System.Math.Truncate(velX * 10) / 10000,
					System.Math.Truncate(velY * 10) / 10000,
					System.Math.Truncate(velZ * 10) / 10000,
					System.Math.Truncate(b * 10) / 10000,
					System.Math.Truncate(meanAnomaly * Mathf.Rad2Deg * 1000) / 1000,
					years, days, hours, minutes, seconds
				);
			}else {
				infoDisp.text = System.String.Format("Position:\n(X: {0:n}Mm Y: {1:n}Mm Z: {2:n}Mm)\nAltitude: {3:n}Km - {4}AU\nVelocity:\n(X: {5}m/s Y: {6}m/s Z: {7}m/s)\nVel. Magnitude: {8}m/s\nMean Anomaly: {9}°\nTime until new year: {10}Y {11}d {12}h {13}m {14}s",
					System.Math.Truncate(posX / 10000) / 100,
					System.Math.Truncate(posY / 10000) / 100,
					System.Math.Truncate(posZ / 10000) / 100,
					System.Math.Truncate(a / 100) / 10,
					System.Math.Truncate(a / 1.495979e+9) / 100,
					System.Math.Truncate(velX * 100) / 100,
					System.Math.Truncate(velY * 100) / 100,
					System.Math.Truncate(velZ * 100) / 100,
					System.Math.Truncate(b * 100) / 100,
					System.Math.Truncate(meanAnomaly * Mathf.Rad2Deg * 1000) / 1000,
					years, days, hours, minutes, seconds
				);
			}
		}
	}
	
	private double ceca,seca,e1;
	[HideInInspector] public double calcPosX,calcPosY,calcPosZ;
	public void CalcPosAtAnomaly(double meanAnomaly) {
		double eca = meanAnomaly + eccentricity / 2.0;
		double diff = 10000;
		e1 = 0;
		
		for(int i = 0; i < 13; i++) {
			e1 = eca - (eca - meanAnomaly - eccentricity * System.Math.Sin(eca)) / (1.0 - eccentricity * System.Math.Cos(eca));
			diff = e1 - eca;
			if(diff < 0) diff = -diff;
			eca = e1;
			if(diff < eps) break;
			if(i == 12) Debug.LogWarning("Max iterations exceeded calculating eca");
		}

		ceca = System.Math.Cos(eca);
		seca = System.Math.Sin(eca);
		double xw = SMA * (ceca - eccentricity);
		double yw = e2 * seca;

		calcPosX = xw * pX + yw * qX;
		calcPosY = xw * pY + yw * qY;
		calcPosZ = xw * pZ + yw * qZ;
	}
    
	public void _OSSUpdate(double time, OSSCelestial camCelestial) {
		_UpdatePosition(time);
		for(int i = 0; i < childCount; i++) {
			children[i]._OSSUpdate(time, camCelestial);
		}
		//Shader values update
		if(skyboxMat != null) {
			if(!isCamera && idxInSkybox > 0) {
				skyboxMat.SetFloat("_Radius" + idxInSkybox, (float)(radius / 500));
				skyboxMat.SetVector("_Pos" + idxInSkybox, new Vector4(
					(float)((GetAbsX() - camCelestial.GetAbsX()) / 1000),
					(float)((GetAbsZ() - camCelestial.GetAbsZ()) / 1000),
					(float)((GetAbsY() - camCelestial.GetAbsY()) / 1000), 0));
				skyboxMat.SetFloat("_Rot" + idxInSkybox, 3f - (float)(axisRotation / 300));
			}
		}
	}
	
	public void _PositionUpdate(double time) {
		for(int i = 0; i < childCount; i++) {
			children[i]._PositionUpdate(time);
		}
	}
	
	private void RecalcAbsPos() {
		if(isRootBody) {
			absPosX = parPosX = posX;
			absPosY = parPosY = posY;
			absPosZ = parPosZ = posZ;
			return;
		}
		absPosX = parent.GetAbsX() + posX;
		absPosY = parent.GetAbsY() + posY;
		absPosZ = parent.GetAbsZ() + posZ;
		parPosX = parent.GetX() + posX;
		parPosY = parent.GetX() + posY;
		parPosZ = parent.GetX() + posZ;
		stateDirty = false;
	}
	
	public double GetX() { return posX; }
	public double GetY() { return posY; }
	public double GetZ() { return posZ; }
	
	public double GetAbsX() {
		if(stateDirty) RecalcAbsPos();
		return absPosX;
	}
	
	public double GetAbsY() {
		if(stateDirty) RecalcAbsPos();
		return absPosY;
	}
	
	public double GetAbsZ() {
		if(stateDirty) RecalcAbsPos();
		return absPosZ;
	}
	
	public double GetParX() {
		if(stateDirty) RecalcAbsPos();
		return parPosX;
	}
	
	public double GetParY() {
		if(stateDirty) RecalcAbsPos();
		return parPosY;
	}
	
	public double GetParZ() {
		if(stateDirty) RecalcAbsPos();
		return parPosZ;
	}
	
	public void _AddChild(OSSCelestial child) {
		if(childCount >= children.Length) return;
		children[childCount] = child;
		childCount++;
	}
	
	public double GetOrbitalPeriod() {
		if(!orbitalPeriodReady) return 60*60*60;
		return orbitalPeriod;
	}
	
	public double GetSiderialPeriod() {
		if(!orbitalPeriodReady) return 60*60*60;
		if(rotates && !isTidallyLocked) return rotationPeriod;
		return orbitalPeriod;
	}
	
	//Breaks for sub-moons, but oh well
	public double GetSynodicPeriod() {
		if(!orbitalPeriodReady) return 60*60*60;
		double ownPeriod = GetSiderialPeriod();
		double parentPeriod = orbitalPeriod;
		if(!parent.isRootBody) parentPeriod = parent.GetOrbitalPeriod();
		return ownPeriod / parentPeriod * ownPeriod + ownPeriod;
	}
	
	public double GetLocalTime(double time) {
		if(!orbitalPeriodReady) return 0;
		return time / GetSynodicPeriod();
	}
}
