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

[UdonBehaviourSyncMode(BehaviourSyncMode.None)]
public class OSS : UdonSharpBehaviour {
	[Header("Render config")]
	public Material skyboxMat;
	
	//i.e. Light-Emitting Celestial Bodies
	public GameObject[] sunLights; //the game objects corresponding to the below celestials. Their rotations will be set to point away from the celestial body in the sky. May contain directional lights or VFX
	public OSSCelestial[] sunCelestials; //the celestials for each sun
	
	public Light planetShine;
	public bool controlFogColor = true;
	public bool atmosphere = true;
	
	[Header("Reflection probes")]
	public ReflectionProbeUpdater probeUpdater;
	public int probeUpdateRate = 5;
	
	[Header("Orbital System Config")]
	public OSSCelestial root; //hierarchy root
	public OSSCelestial cameraCelestial; //which surface its rendering from
	//You have the option of allowing ONE celestial body be able to block out the sun light
	//Only ONE, though
	//Would love to be able to have all of them do that, but Udon is too slow for that
	//Tell VRC to hurry up with Udon 2 to get more!
	public OSSCelestial eclipseBody;
	public float updateInterval = 1; //in  seconds
	public Timekeeper timekeeper;
	
	[Header("Clock config")]
	public ClockController clock;
	public int weekLength = 7;
	
	[Header("Compatibility stuff")]
	public Material underwaterOverlayCompat;
	public Material fancyWaterCompat;
	public Material fancyWaterCompatRefl;
	public AudioSource[] nighttimeAmbientSounds;
	public Material cloudMaterial;
	
	private int updateCount = 0;
	private bool updateProbes = true;
	private double updateTimer = 0;
	private bool prevSunlightBlocked = false;
	private Color fogColor;
	private int b = 6;
	private float origPSIntensity = 0;
	private Color origPSColor;
	[HideInInspector] public int forcedUpdates = 0;
	private Light mainSunLight;
	private float skyboxRotAngleY;
	private float skyboxRotAngleX;
	
	public Quaternion cameraAxisRotation, skyboxRotationOffset;
	//private Vector3 DEBUG_Vec1 = Vector3.up;
	//private Vector3 DEBUG_Vec2 = Vector3.forward;
	
	void Start() {
		cameraCelestial.isCamera = true;
		if(planetShine != null) {
			origPSIntensity = planetShine.intensity; //Use whatever the light source intensity was at start as the maximum
			origPSColor = planetShine.color;
		}
		forcedUpdates = 5;
		mainSunLight = sunLights[0].GetComponent<Light>();
	}
	
	void Update() {
		if(clock) {
			clock.dayProgress += (Time.deltaTime * timekeeper.speedMultiplier) / cameraCelestial.GetSynodicPeriod();
			if(clock.dayProgress >= 1) updateTimer = updateInterval * 2;
		}
		
		updateTimer += Time.deltaTime;
		if(updateTimer < updateInterval && forcedUpdates == 0) return;
		updateTimer = 0;
		updateCount++;
		if(updateCount % probeUpdateRate == 0 || forcedUpdates == 1) {
			updateProbes = true;
		}
		if(forcedUpdates > 0) forcedUpdates--;
		
		//For some reason, it takes a bit for the fog color to become available. Accessing it in Start() gets you black.
		if(b > 0) {
			b--;
			if(b == 0) fogColor = RenderSettings.fogColor;
		}
		
		//This triggers the whole OSS hierarchy to be computed. Very performance intensive.
		root._OSSUpdate(timekeeper.time, cameraCelestial);
		
		skyboxMat.SetVector("_CamPos", new Vector4(
			(float)(cameraCelestial.GetAbsX() / 1000),
			(float)(cameraCelestial.GetAbsZ() / 1000),
			(float)(cameraCelestial.GetAbsY() / 1000), 0));
		skyboxMat.SetFloat("_CamRot", (float)cameraCelestial.axisRotation);
		
		if(clock) {
			double localTime = cameraCelestial.GetLocalTime(timekeeper.time);
			clock.dayProgress = localTime % 1.0f;
			clock.dayCount = weekLength;
			clock.currDay = (int)localTime % weekLength;
		}
		
		//User-defined rotation, added onto the one calculated by OSS
		skyboxRotAngleY = skyboxMat.GetFloat("_Rotation");
		skyboxRotAngleX = skyboxMat.GetFloat("_RotationX");
		
		Vector3 toSunVec;
		Vector3 toSunVecNoRot = Vector3.zero;
		Vector3 toSunVecNoSkybox;
		Vector3 sunDir = Vector3.right;
		bool sunlightBlocked = true;
		Quaternion newSunRot = Quaternion.Euler(0, 1, 0);
		
		cameraAxisRotation = Quaternion.Euler(0, 0, (float)(-cameraCelestial.axisRotation));
		skyboxRotationOffset = Quaternion.Euler(skyboxRotAngleX, 180 + (180 - skyboxRotAngleY), (float)(-cameraCelestial.axisRotation)); //Adjusts for the user-defined skybox rotation. I don’t know why it breaks if I don’t put the axisRotation here again or omitt the first rotation. Quaternions are hard. Send help, please.
		//Compute rotations for all sun objects
		//For now, OSS supports only one of them, element 0, to have an actual light source attached to it. This is still used for VFX on Avalon, though.
		for(int i = sunCelestials.Length - 1; i >= 0; i--) {
			{
				double x1 = cameraCelestial.GetAbsX() - sunCelestials[i].GetAbsX();
				double y1 = cameraCelestial.GetAbsY() - sunCelestials[i].GetAbsY();
				double z1 = cameraCelestial.GetAbsZ() - sunCelestials[i].GetAbsZ();
				double len = System.Math.Pow(x1*x1+y1*y1+z1*z1, 0.5);
				//Do not convert to float until the numbers are small enough to do so
				toSunVecNoRot = toSunVec = new Vector3(
					(float)(x1 / len),
					(float)(y1 / len),
					(float)(z1 / len)).normalized;
				toSunVecNoSkybox = cameraAxisRotation * toSunVec;
				toSunVec = skyboxRotationOffset * toSunVec;
				toSunVec = toSunVec.normalized;
				newSunRot = Quaternion.FromToRotation(Vector3.forward, toSunVec);
			}
			sunLights[i].transform.rotation = newSunRot;

			//This is why there is no arbitrary amount of eclipse bodies. That would become O(m*n) which Udon just isn’t ready for.
			sunDir = -toSunVec;
			sunlightBlocked = false;
			if(eclipseBody != null) {
				float dist;
				bool intersects = _IsEclipsing(eclipseBody, -sunDir, out dist);
				if(intersects) {
					sunLights[i].SetActive(false);
					sunlightBlocked = true;
				}else sunLights[i].SetActive(true);
				if(sunlightBlocked != prevSunlightBlocked) {
					prevSunlightBlocked = sunlightBlocked;
					updateProbes = true;
				}
			}
		}
		
		//Second reason why can only have one star with light: I don’t know if the shader could handle multiple of these
		Vector3 sunDirUnrotated = Quaternion.Euler(0, 0, (float)(cameraCelestial.axisRotation)) * sunDir;
		skyboxMat.SetVector("_LightDirectionA", sunlightBlocked ? new Vector4(0, 0, -1, 0) :
				new Vector4(sunDir.x, sunDir.z, sunDir.y, 0));
		skyboxMat.SetVector("_LightDirectionB", new Vector4(toSunVecNoRot.x, toSunVecNoRot.z, toSunVecNoRot.y, 0));
		
		//Scuffed code that determines if its day, night sunrise or sunset based on comparing the lightAngle to angles I chose somewhat arbitrarily
		//Time of day is derived from the sunlight's angle relative to "straight up". Probably doesn’t make perfect physical sense, but good enough
		float lightAngle = -Vector3.Angle(Vector3.up, sunDir);
		bool nighttime = false;
		float skyExposure = 1;
		float lightStrength = 1;
		if(sunlightBlocked) {
			skyExposure = 0;
			lightStrength = 0;
		}else {
			float inDeg = lightAngle;
			inDeg %= 360.0f;
			if(inDeg < 0) inDeg = 360.0f + inDeg;
			
			if(inDeg > 80 && inDeg < 100) {
				skyExposure = 1 - ((inDeg - 80) / 20);
			}
			if(inDeg >= 100 && inDeg < 280) {
				nighttime = true;
				skyExposure = 0;
			}
			if(inDeg > 260 && inDeg < 280) {
				skyExposure = (inDeg - 260) / 20;
			}
			
			if(inDeg > 75 && inDeg < 90) {
				lightStrength = 1 - ((inDeg - 75) / 15);
			}
			if(inDeg >= 90 && inDeg < 275) lightStrength = 0;
			if(inDeg > 275 && inDeg < 290) {
				lightStrength = (inDeg - 275) / 15;
			}
		}
		
		//Pass the lightStrength value to all kinds of shaders and, of course, the directional light
		if(cloudMaterial != null) cloudMaterial.SetFloat("_Brightness", lightStrength);
		if(mainSunLight != null) mainSunLight.intensity = lightStrength;
		if(underwaterOverlayCompat != null) underwaterOverlayCompat.SetColor("_Color", Color.white * (0.2f + lightStrength * 0.8f));
		if(fancyWaterCompat != null) fancyWaterCompat.SetFloat("_WaterDepthColorMul", lightStrength);
		if(fancyWaterCompatRefl != null) fancyWaterCompatRefl.SetFloat("_WaterDepthColorMul", lightStrength);
		skyboxMat.SetFloat("_BackgroundExposure", atmosphere ? 1 - skyExposure : 1);
		skyboxMat.SetFloat("_StarsExposure", atmosphere ? 1 - skyExposure * 1.04f : 1.04f);
		if(controlFogColor && b == 0) RenderSettings.fogColor = skyExposure * fogColor; //Also adjust fog color, of course
		
		//Adjust directional light representing light reflected from a celestial body, Valaya in this case
		//Also, I want some ambient sounds to be louder at night
		if(planetShine != null || nighttimeAmbientSounds.Length > 0) {
			Vector3 vec1 = new Vector3(
				(float)(eclipseBody.GetAbsX() - cameraCelestial.GetAbsX()),
				(float)(eclipseBody.GetAbsZ() - cameraCelestial.GetAbsZ()),
				(float)(eclipseBody.GetAbsY() - cameraCelestial.GetAbsY()));
			Vector3 vec2 = Vector3.right;
			vec2 = newSunRot * vec2;
			vec1 = Quaternion.Euler(0, 90 - skyboxRotAngleY, 0) * vec1;
			float angle = Vector3.SignedAngle(vec1, vec2, Vector3.up);
			
			//These angles are also a bit arbitrary, but it works on Avalon
			float lightStr = 0;
			if(angle > 45 && angle < 160) {
				lightStr = (angle - 45) / (160 - 45);
			}else if(angle > -160 && angle < -45) {
				lightStr = 1 - ((angle + 160) / (160 - 45));
			}else if(angle >= 160 || angle <= -160) lightStr = 1;
			else lightStr = 0;
			lightStr *= 1 - lightStrength;
			
			//My water shader needs the depth buffer and is disappears when there is no directional light active
			//This can actually happen during an eclipse (sun off, 0 intensity of reflected light from Valaya)
			//So just during this time, the planetShine light is configured into a funny black light to keep the depth buffer alive
			//Why is VRC like this?
			if(planetShine != null) {
				planetShine.intensity = sunlightBlocked ? 1 : Mathf.Max(lightStr * origPSIntensity, 1e-4f); //the intensity of this light must never reach 0 because of the depth buffer
				planetShine.color = sunlightBlocked ? Color.black : origPSColor;
				planetShine.gameObject.SetActive(lightStr > 1e-4f || lightStrength == 0);
			}
			
			for(int i = 0; i < nighttimeAmbientSounds.Length; i++) nighttimeAmbientSounds[i].volume = 0.15f + 0.3f * lightStr;
		}
		
		if(updateProbes && probeUpdater != null) {
			probeUpdater._UpdateProbes();
			updateProbes = false;
		}
	}
	
	public bool _IsEclipsing(OSSCelestial eb, Vector3 sunDir, out float dist) {
		Vector3 target = new Vector3(
			(float)(cameraCelestial.GetAbsX() - eb.GetAbsX()),
			(float)(cameraCelestial.GetAbsY() - eb.GetAbsY()),
			(float)(cameraCelestial.GetAbsZ() - eb.GetAbsZ()));
		float len = target.magnitude;
		target /= len;
		target = Quaternion.Euler(skyboxRotAngleX, -skyboxRotAngleY, -(float)cameraCelestial.axisRotation) * target;
		target *= len;
		float radius = (float)eb.radius;
		//This is why the intersection function is simplified. The light is either blocked or it isn’t.
		//In Udon 2, I may make this way more complex to make the light change color due to atmospheric scattering before occluding entirely (I’ve seen this happen in KSP with Scatterer around Jool and its cool as fuck)
		float isect;
		if(!RaySphereIntersection(target, Vector3.zero, sunDir, radius*2, out isect)) {
			dist = 0;
			return false;
		}
		
		dist = isect;
		return true;
	}
	
	//Just test if the intersection happened or not
	public static bool RaySphereIntersection(Vector3 pos, Vector3 orig, Vector3 dir, float radius, out float dist) {
		dist = 0;
		float radiusSquared = radius * radius;
		Vector3 L = pos - orig;
		float tca = Vector3.Dot(dir, L);
		
		float L_2 = Vector3.Dot(L, L);
		if(tca <= 0 && L_2 > radiusSquared) return false;
		
		float d = L_2 - tca * tca;
		if(d >= radiusSquared) return false;
		
		dist = tca - Mathf.Pow(radiusSquared - d, 0.5f);
		return true;
	}
/*
	void OnDrawGizmos() {
		Gizmos.color = Color.green;
		Gizmos.DrawLine(transform.position, transform.position + new Vector3(1, 0, 0));
		Gizmos.color = Color.red;
		Gizmos.DrawLine(transform.position, transform.position + new Vector3(0.93f, -0.31f, 0.18f));
	}*/
}
