using System;
using System.Collections;
using UnityEngine;
using UnityEngine.Events;
using UnityEngine.InputSystem;

[Serializable]
public class IntEvent : UnityEvent<int> {}

public class WorldSwitcher : MonoBehaviour {
	[Range(0, 5)]
	[SerializeField] float transitionDuration;
	/*[Range(0, 5)]
	[SerializeField] float fadeDuration;*/

	[SerializeField] AnimationCurve transitionCurve;

	//[SerializeField] AnimationCurve fadeCurve;
	[SerializeField] WorldInfo[] worldInfos;

	[Range(0, 1)]
	[SerializeField] float tabWidth;
	
	[SerializeField] float healthBarPosFromTabEdge;
	[SerializeField] float buttonPosFromTabEdge;
	[SerializeField] float alertPosFromTabEdge;

	//TODO Hardcode
	[SerializeField]
	float quadOffset;

	int lastWorldIndex;
	int currentWorldIndex;
	Coroutine transition;
	bool supportHDR;

	static readonly int UVOffset = Shader.PropertyToID("_UVOffset");
	//static readonly int Opacity = Shader.PropertyToID("_Opacity");

	public IntEvent OnChangeWorld;

	public int CurrentWorldIndex => currentWorldIndex;

	void Awake() {
		if (worldInfos.Length != 3)
			Debug.LogWarning("For now, WorldSwitcher should have 3 worlds.");

		currentWorldIndex = 1;
		lastWorldIndex = 0;

		supportHDR = SystemInfo.SupportsRenderTextureFormat(RenderTextureFormat.DefaultHDR);

		for (int i = 0; i < worldInfos.Length; i++) {
			worldInfos[i].material = worldInfos[i].renderQuad.GetComponent<Renderer>().material;

			worldInfos[i].GenerateRenderTexture(quadOffset, supportHDR);
		}

		ResetQuadPositions();
	}

	public void SwitchWorld(int index) {
		if (transition != null)
			return;

		if (index == currentWorldIndex || index < 0 || index > worldInfos.Length - 1)
			return;

		//TODO Block window resize during transition?
		foreach (WorldInfo worldInfo in worldInfos) {
			if (Screen.width != worldInfo.texture.width || Screen.height != worldInfo.texture.height)
				worldInfo.GenerateRenderTexture(quadOffset, supportHDR);
		}

		lastWorldIndex = currentWorldIndex;
		currentWorldIndex = index;

		OnChangeWorld.Invoke(currentWorldIndex);

		transition = StartCoroutine(TransitionCamera(currentWorldIndex - lastWorldIndex < 0));
	}

	public void OnDimension1(InputAction.CallbackContext ctx) {
		if (ctx.performed)
			SwitchWorld(0);
	}

	public void OnDimension2(InputAction.CallbackContext ctx) {
		if (ctx.performed)
			SwitchWorld(1);
	}

	public void OnDimension3(InputAction.CallbackContext ctx) {
		if (ctx.performed)
			SwitchWorld(2);
	}

	/*void OnGUI() {
		for (int i = 0; i < worldInfos.Length; ++i) {
			if (GUILayout.Button($"World {i}"))
				SwitchWorld(i);
		}
	}*/

	IEnumerator TransitionCamera(bool fromRight) {
		WorldInfo lastWorld = worldInfos[lastWorldIndex];

		float startTime = Time.time;
		Camera lastCam = lastWorld.camera;
		
		if (fromRight) {
			for (int i = lastWorldIndex - 1; i >= currentWorldIndex; --i) {
				worldInfos[i].healthBar.gameObject.SetActive(false);
				worldInfos[i].button.gameObject.SetActive(false);
				worldInfos[i].alert.gameObject.SetActive(false);
			}
		} else {
			for (int i = lastWorldIndex + 1; i <= currentWorldIndex; ++i) {
				worldInfos[i].healthBar.gameObject.SetActive(false);
				worldInfos[i].button.gameObject.SetActive(false);
				worldInfos[i].alert.gameObject.SetActive(false);
			}
		}

		while (Time.time < startTime + transitionDuration) {
			float t = transitionCurve.Evaluate((Time.time - startTime) / transitionDuration);

			if (fromRight) {
				for (int i = lastWorldIndex - 1; i >= currentWorldIndex; --i) {
					worldInfos[i].renderQuad.position = GetQuadOffset(lastCam, worldInfos[i], i, true, true, t);
					worldInfos[i].material.SetFloat(UVOffset, GetUVOffset(t, true, i));
				}
			} else {
				for (int i = lastWorldIndex + 1; i <= currentWorldIndex; ++i) {
					worldInfos[i].renderQuad.position = GetQuadOffset(lastCam, worldInfos[i], i, false, true, t);
					worldInfos[i].material.SetFloat(UVOffset, GetUVOffset(t, false, i));
				}
			}

			yield return null;
		}

		/*startTime = Time.time;

		while (Time.time < startTime + fadeDuration) {
			worldInfos[currentWorldIndex].material.SetFloat(
				Opacity,
				1f - fadeCurve.Evaluate((Time.time - startTime) / fadeDuration)
			);

			yield return null;
		}*/

		ResetQuadPositions();
		transition = null;
	}

	Vector3 GetQuadOffset(Camera cam, WorldInfo worldInfo, int index, bool fromRight, bool moving, float t = 0f) {
		float x = fromRight ? Mathf.Lerp((1 + index) * tabWidth, 1f - (worldInfos.Length - 1 - index) * tabWidth, t) : Mathf.Lerp(1f - tabWidth * (worldInfos.Length - index), index * tabWidth, t);
		Vector3 quadHalfWidthOffset = Vector3.right * (fromRight ? -worldInfo.renderQuad.localScale.x / 2f : worldInfo.renderQuad.localScale.x / 2f);

		float depthOffset = moving ? Mathf.Abs(currentWorldIndex - index) * .0001f : -Mathf.Abs(currentWorldIndex - index) * .0001f;

		//TODO Offset epsilon
		return cam.ViewportToWorldPoint(
			new Vector3(
				x,
				.5f,
				quadOffset + depthOffset
			)
		) + quadHalfWidthOffset;
	}

	float GetUVOffset(float t, bool fromRight, int index) {
		return fromRight ?
			Mathf.Lerp(-1f + tabWidth * (index + 1), -(worldInfos.Length - 1 - index) * tabWidth, t) :
			Mathf.Lerp(1f - tabWidth * (worldInfos.Length - index), index * tabWidth, t);
	}

	float GetTabUIPos(float t, bool fromRight, int index, float offset) {
		return fromRight ?
			Mathf.Lerp(tabWidth * (index + 1), -(worldInfos.Length - 1 - index) * tabWidth, t) - offset :
			Mathf.Lerp(1f - tabWidth * (worldInfos.Length - index), index * tabWidth, t) + offset;
	}

	void ResetQuadPositions() {
		Camera currCam = worldInfos[currentWorldIndex].camera;
		LayerMask currLayer = worldInfos[currentWorldIndex].layer;

		for (int i = 0; i < worldInfos.Length; ++i) {
			bool usingRenderTexture = i != currentWorldIndex;
			worldInfos[i].SetUsingRenderTexture(usingRenderTexture, currLayer);
			//worldInfos[i].material.SetFloat(Opacity, 1f);

			if (usingRenderTexture) {
				bool fromRight = i - currentWorldIndex < 0;
				worldInfos[i].renderQuad.position = GetQuadOffset(currCam, worldInfos[i], i, fromRight, false);
				worldInfos[i].material.SetFloat(UVOffset, GetUVOffset(0f, fromRight, i));
				worldInfos[i].healthBar.gameObject.SetActive(true);
				worldInfos[i].healthBar.position = new Vector3(
					Screen.width * GetTabUIPos(0f, fromRight, i, healthBarPosFromTabEdge),
					worldInfos[i].healthBar.position.y,
					worldInfos[i].healthBar.position.z
				);
				worldInfos[i].button.gameObject.SetActive(true);
				worldInfos[i].button.position = new Vector3(
					Screen.width * GetTabUIPos(0f, fromRight, i, buttonPosFromTabEdge),
					worldInfos[i].button.position.y,
					worldInfos[i].button.position.z
				);
				worldInfos[i].alert.gameObject.SetActive(true);
				worldInfos[i].alert.position = new Vector3(
					Screen.width * GetTabUIPos(0f, fromRight, i, alertPosFromTabEdge),
					worldInfos[i].alert.position.y,
					worldInfos[i].alert.position.z
				);
			}else {
				worldInfos[i].healthBar.gameObject.SetActive(false);
				worldInfos[i].button.gameObject.SetActive(false);
				worldInfos[i].alert.gameObject.SetActive(false);
			}
		}
	}
}

[Serializable]
struct WorldInfo {
	public Camera camera;
	[HideInInspector] public RenderTexture texture;
	public Transform renderQuad;
	[HideInInspector] public Material material;
	public RectTransform healthBar;
	public RectTransform button;
	public RectTransform alert;
	public int layer;

	static readonly int CameraTexture = Shader.PropertyToID("_CameraTexture");

	public void SetUsingRenderTexture(bool usingTexture, int layer) {
		if (usingTexture) {
			camera.targetTexture = texture;
			renderQuad.gameObject.SetActive(true);
			renderQuad.gameObject.layer = layer;
		} else {
			camera.targetTexture = null;
			renderQuad.gameObject.SetActive(false);
		}
	}

	public void GenerateRenderTexture(float quadOffset, bool supportHDR) {
		bool usingTexture = !ReferenceEquals(camera.targetTexture, null);
		if (usingTexture)
			camera.targetTexture = null;

		float height = 2f * Mathf.Tan(Mathf.Deg2Rad * camera.fieldOfView / 2f) * quadOffset;
		renderQuad.localScale = new Vector3(camera.aspect * height, height, 1f);
		texture = new RenderTexture(Screen.width, Screen.height, 32);
		if (supportHDR)
			texture.format = RenderTextureFormat.DefaultHDR;
		material.SetTexture(CameraTexture, texture);


		if (usingTexture)
			camera.targetTexture = texture;
	}
}