#nullable enable
using Nox.Platform;
using UnityEngine;
using UnityEngine.AddressableAssets;
using UnityEngine.InputSystem;

namespace Nox.Game {
    public class CameraController : ICameraController {
        private readonly PlatformSettings? platformSettings;
        private readonly InputSystem_Actions? inputActions;
        private readonly MapReference? mapReference;

        private CameraSettings? cameraSettings;

        private InputAction? zoomAction;
        private InputAction? panAction;
        private InputAction? holdToDragAction;
        private Bounds planeBounds;
        private Vector3 dragWorldOrigin;
        private bool isDragging;
        private readonly Plane groundPlane = new Plane(Vector3.up, Vector3.zero);
        private readonly Texture2D cursorPoint;
        private readonly Texture2D cursorDrag;

        public CameraReference? CameraReference { get; private set; }

        public CameraController(PlatformSettings? platformSettings, MapReference? mapReference) {
            this.platformSettings = platformSettings;
            this.mapReference = mapReference;
            inputActions = platformSettings?.inputSettings.inputActions;

            cursorPoint = Addressables.LoadAssetAsync<Texture2D>("Assets/Art/UI/Cursor_Pointer").WaitForCompletion();
            cursorDrag = Addressables.LoadAssetAsync<Texture2D>("Assets/Art/UI/Cursor_Drag").WaitForCompletion();
            Cursor.SetCursor(cursorPoint, new Vector2(12,4), CursorMode.Auto);
        }

        public void Initialize() {
            if(inputActions == null || !mapReference) {
                return;
            }
            zoomAction = inputActions.Player.Zoom;
            panAction = inputActions.Player.PanMap;
            holdToDragAction = inputActions.Player.HoldToDrag;
            planeBounds = mapReference.mapPlane.GetComponent<BoxCollider>().bounds;
            SetupCamera();
        }

        private void SetupCamera() {
            CameraReference = Object.FindAnyObjectByType<CameraReference>();

            if(!CameraReference) {
                if(platformSettings?.cameraPrefab != null) {
                    GameObject? go = Object.Instantiate(platformSettings.cameraPrefab);
                    CameraReference = go.GetComponentInChildren<CameraReference>();
                    if(!CameraReference) {
                        Debug.LogError("Camera prefab does not contain a CameraReference component");
                        return;
                    }
                }
                else {
                    Debug.LogError("No camera prefab found and no camera reference found");
                }
            }

            cameraSettings = CameraReference!.cameraSettings;
            if(!cameraSettings) {
                return;
            }

            // Set initial zoom to maximum allowed by map boundaries
            var camera = CameraReference.mainCamera;
            if(camera.orthographic) {
                float maxHalfWidth = planeBounds.size.x * 0.5f;
                float maxHalfHeight = planeBounds.size.z * 0.5f;
                float maxOrthoSize = Mathf.Min(maxHalfHeight, maxHalfWidth / camera.aspect);
                camera.orthographicSize = Mathf.Min(cameraSettings.maxZoom, maxOrthoSize);
            }
            else {
                // Perspective camera: set height so the map fits in view
                float mapWidth = planeBounds.size.x;
                float mapHeight = planeBounds.size.z;
                float aspect = camera.aspect;
                float fovRad = camera.fieldOfView * Mathf.Deg2Rad;
                float tanFov = Mathf.Tan(fovRad / 2f);
                float requiredHeightByWidth = mapWidth / (2f * tanFov * aspect);
                float requiredHeightByHeight = mapHeight / (2f * tanFov);
                float requiredHeight = Mathf.Max(requiredHeightByWidth, requiredHeightByHeight);
                // Center camera on map and set height
                Vector3 camPos = planeBounds.center;
                camPos.y = requiredHeight;
                camera.transform.position = camPos;
                // Look straight down
                camera.transform.rotation = Quaternion.Euler(90f, 0f, 0f);
            }
        }

        public void Tick() {
            PanCamera();
            ZoomCamera();
        }

        private void ZoomCamera() {
            if(zoomAction == null) {
                return;
            }
            float zoomDelta = zoomAction.ReadValue<Vector2>().y;
            if(zoomDelta == 0) {
                return;
            }
            var camera = CameraReference!.mainCamera;
            if(camera.orthographic) {
                camera.orthographicSize = Mathf.Clamp(camera.orthographicSize - (zoomDelta * cameraSettings!.zoomSpeed * Time.deltaTime), cameraSettings.minZoom, cameraSettings.maxZoom);
            }
            else {
                camera.transform.Translate(Vector3.forward * zoomDelta * cameraSettings!.zoomSpeed * Time.deltaTime, Space.Self);
            }
        }

        private void PanCamera() {
            if(holdToDragAction == null || CameraReference == null) {
                return;
            }

            if(holdToDragAction.IsInProgress()) {
                Camera camera = CameraReference.mainCamera;
                Vector2 screenPos = inputActions!.Player.Point.ReadValue<Vector2>();

                if(!isDragging) {
                    isDragging = true;
                    Cursor.lockState = CursorLockMode.Confined;
                    Cursor.SetCursor(cursorDrag, Vector2.zero, CursorMode.Auto);
                    dragWorldOrigin = ScreenToGroundPoint(camera, screenPos);
                    return;
                }

                Vector3 currentWorldPoint = ScreenToGroundPoint(camera, screenPos);
                Vector3 offset = dragWorldOrigin - currentWorldPoint;

                Vector3 camPos = camera.transform.position;
                camPos.x += offset.x;
                camPos.z += offset.z;

                camPos.x = Mathf.Clamp(camPos.x, planeBounds.min.x, planeBounds.max.x);
                camPos.z = Mathf.Clamp(camPos.z, planeBounds.min.z, planeBounds.max.z);
                camera.transform.position = camPos;

                dragWorldOrigin = ScreenToGroundPoint(camera, screenPos);
            }
            else {
                if(!isDragging) {
                    return;
                }
                isDragging = false;
                Cursor.lockState = CursorLockMode.None;
                Cursor.SetCursor(cursorPoint, new Vector2(12,4), CursorMode.Auto);
            }
        }

        private Vector3 ScreenToGroundPoint(Camera camera, Vector2 screenPos) {
            Ray ray = camera.ScreenPointToRay(new Vector3(screenPos.x, screenPos.y, 0f));
            if(groundPlane.Raycast(ray, out float distance)) {
                return ray.GetPoint(distance);
            }
            return camera.transform.position;
        }
        public void Dispose() {
            inputActions?.Disable();
        }
    }

}
