using System;
using System.Drawing;
using UnityEngine;

namespace Cryville.EEW.Unity.Map {
	[RequireComponent(typeof(Camera))]
	sealed class CameraController : MonoBehaviour {
		Camera _camera;

		[SerializeField]
		Transform m_layerTile;
		[SerializeField]
		MapElementManager m_layerElement;
		[SerializeField]
		MapElementManager m_layerElementSub;
		[SerializeField]
		GameObject m_prefabTile;

		readonly MapTileCacheManager _tiles = new();
		float _elementLayerZ;

		void Start() {
			_camera = GetComponent<Camera>();
			_tiles.Parent = m_layerTile;
			_tiles.PrefabTile = m_prefabTile;
			_tiles.CacheDir = Application.temporaryCachePath;
			_camera.orthographicSize = 0.5f / MathF.Max(1, (float)_camera.pixelWidth / _camera.pixelHeight);
			_elementLayerZ = m_layerElement.transform.position.z;
			_mapElementUpdated = true;
		}
		void OnDestroy() {
			_tiles.Dispose();
		}

		float Scale {
			get => _camera.orthographicSize;
			set => _camera.orthographicSize = Mathf.Clamp(value, 1e-3f, 0.5f / MathF.Max(1, (float)_camera.pixelWidth / _camera.pixelHeight));
		}

		static readonly Rect _viewportRect = new(0, 0, 1, 1);
		Vector3? ppos;
		bool _mapElementUpdated;
		void Update() {
			bool flag = false;
			var cpos = Input.mousePosition;
			bool isMouseInViewport = _viewportRect.Contains(_camera.ScreenToViewportPoint(Input.mousePosition));
			if (Input.GetMouseButtonDown(0) && isMouseInViewport) {
				ppos = cpos;
			}
			if (Input.GetMouseButton(0) && ppos is Vector3 pos0) {
				var delta = _camera.ScreenToWorldPoint(pos0) - _camera.ScreenToWorldPoint(cpos);
				transform.position += delta;
				ppos = cpos;
				flag = true;
			}
			if (Input.GetMouseButtonUp(0)) {
				ppos = null;
			}
			if (Input.mouseScrollDelta.y != 0 && isMouseInViewport) {
				Scale *= Mathf.Pow(2, -Input.mouseScrollDelta.y / 8);
				flag = true;
			}
			if (_mapElementUpdated) {
				_mapElementUpdated = false;
				ZoomToMapElement();
				flag = true;
			}
			if (flag) {
				UpdateTransform(); // Ensure UpdateTransform is called at most once per frame for tiles to unload correctly
			}
		}
		void ZoomToMapElement() {
			var aabb = m_layerElement.AABB;
			if (aabb is not RectangleF b) return;
			if (b.Width * _camera.pixelHeight < _camera.pixelWidth * b.Height)
				Scale = b.Height;
			else
				Scale = b.Width * _camera.pixelHeight / _camera.pixelWidth;
			Scale *= 0.6f;
			if (Scale < 0.01f) Scale = 0.01f;
			transform.localPosition = new PointF(b.X + b.Width / 2, b.Y + b.Height / 2).ToVector2();
		}
		public void OnMapElementUpdated() {
			_mapElementUpdated = true;
		}

		void UpdateTransform() {
			float h = _camera.orthographicSize * 2;
			float w = h * _camera.pixelWidth / _camera.pixelHeight;
			float vz = 1 / h * _camera.pixelHeight;
			float nx = transform.position.x % 1;
			if (nx < 0) nx += 1;
			transform.localPosition = new(nx, Math.Clamp(transform.position.y, h / 2 - 1, -h / 2), -20);

			var bounds = new Bounds((Vector2)transform.position, new Vector2(w, h));
			int zoom = Math.Clamp((int)Math.Log(vz / 256, 2) + 1, 0, 10);
			int zoomScale = 1 << zoom;
			_tiles.MoveTo(
				new(Mathf.FloorToInt(bounds.min.x * zoomScale), Mathf.FloorToInt(-bounds.max.y * zoomScale), zoom),
				new(Mathf.CeilToInt(bounds.max.x * zoomScale), Mathf.CeilToInt(-bounds.min.y * zoomScale), zoom)
			);

			m_layerElement.Scale = h;
			m_layerElementSub.Scale = h;

			if (nx - w / 2 < 0) {
				m_layerElementSub.gameObject.SetActive(true);
				m_layerElementSub.transform.localPosition = new(-1, 0, _elementLayerZ);
			}
			else if (nx + w / 2 > 1) {
				m_layerElementSub.gameObject.SetActive(true);
				m_layerElementSub.transform.localPosition = new(1, 0, _elementLayerZ);
			}
			else {
				m_layerElementSub.gameObject.SetActive(false);
			}
		}
	}
}
