using System;
using System.Collections.Generic;
using System.Linq;
using UnityEngine;
using UnityEngine.EventSystems;
using UnityEngine.Rendering;
using UnityEngine.Rendering.Universal;

namespace SicknessReduction.Visual.DoF
{
    [Serializable]
    public struct CocRadius
    {
        public float factor;
        public float min;
        public float max;
    }
    
    //TODO: look at https://catlikecoding.com/unity/tutorials/advanced-rendering/depth-of-field/ or pseudocode in paper
    public class DynamicDoF : MonoBehaviour
    {
        private const int NUMBER_OF_RAYS = 9; // Carneige, Rhee (2015)
        private const float RAY_OFFSET = 6f; //Exact value not mentioned in paper
        
        public CocRadius cocRadius; //Carneige, Rhee (2015)

        public float refocusTimePerMeter = 0.0000017f; //seconds; Carneige, Rhee (2015)
        public float maxFocusDistance = 30000f; //metres; Carneige, Rhee (2015

        [Tooltip("Offset in degrees of the ray from the center of the image (+ upwards/- downwards")]
        public float offsetFromCenter = 10f;

        public Camera playerCamera;
        public VolumeProfile postProcessProfile;

        private Transform cameraTransform;
        private DepthOfField doF;
        private bool doFAvailable;
        private float maxCocRadius;

        private List<float> rayDistances = new List<float>(NUMBER_OF_RAYS);
        private Vector3[] ends = new Vector3[NUMBER_OF_RAYS];
      
        
        //TODO: debug, remove
        public GameObject gizmoPrefab;
        private GameObject[] hits =  new GameObject[NUMBER_OF_RAYS];

        private void Start()
        {
            cameraTransform = playerCamera.transform;
            //Debug.Log($"Screen Width = {playerCamera.pixelWidth}, scaled = {playerCamera.scaledPixelWidth}");
            //maxCocRadius = maxFactorRadiusCOC * playerCamera.scaledPixelWidth; FIXME: waaaay to small
            for (int i = 0; i < NUMBER_OF_RAYS; i++)
            {
                hits[i] = Instantiate(gizmoPrefab);
                hits[i].SetActive(false);
            } //TODO: debug, remove
            doF = (DepthOfField) postProcessProfile.components.FirstOrDefault(c => c is DepthOfField);
            doFAvailable = doF != null;
            if (doFAvailable)
            {
                // ReSharper disable once PossibleNullReferenceException
                doF.mode.value = DepthOfFieldMode.Bokeh;
                doF.focalLength.min = 0f;
                doF.focalLength.max = float.MaxValue;
                doF.aperture.max = float.MaxValue;
                doF.aperture.min = 0f;
            }
            else
            {
                Debug.LogWarning("No DepthOfField found in PostProcessing Profile!");
            }
        }

        private void Update()
        {
            if (!doFAvailable) return;
            var focusDistance = CastRays();
            if (focusDistance < 0)
            {
                focusDistance = doF.focusDistance.value;
                //return;
            }

            /*For real-time performance, we
simply assume all users will take a static 500 ms
to refocus from an infinite distance to a close distance
(≈ 1 m), using the value calculated in earlier
work13 (assuming a typical adult’s eyes). This
translates to a linear interpolation between focal
distances that takes ≈ 1.7*/
            doF.active = true;
            var timeNeededToRefocus = Mathf.Abs(focusDistance - doF.focusDistance.value) * refocusTimePerMeter;
            focusDistance = Mathf.Lerp(doF.focusDistance.value, focusDistance, Time.deltaTime / timeNeededToRefocus);
            doF.focusDistance.value = focusDistance;
            doF.focalLength.value = 1;

            //var coc = maxFactorRadiusCOC * 1 / Mathf.Pow(focusDistance, pow);
            var coc = Mathf.Clamp(cocRadius.factor * (-Mathf.Log(focusDistance) + 1.9f * Mathf.Exp(1)), cocRadius.min, cocRadius.max);
            Debug.Log($"COC = {coc}, focusDistance = {focusDistance}");
            //var coc = maxFactorRadiusCOC * 1 - ()
            doF.aperture.value = ApertureForCocAndFocusDistance(coc, focusDistance);
        }

        private float ApertureForCocAndFocusDistance(float coc, float focusDistance) =>
            1 / (1000 * coc * (focusDistance - 0.001f));

        private float CastRays()
        {
            var position = cameraTransform.position;
            var forward = cameraTransform.forward;
            var up = cameraTransform.up;

            var adjustedForward = Vector3.RotateTowards(forward, up, offsetFromCenter * Mathf.Deg2Rad, 0f);
            
            var start = position + forward * playerCamera.nearClipPlane;

            ends[0] = position + adjustedForward * playerCamera.farClipPlane;
            ends[1] = ends[0] + cameraTransform.TransformDirection(new Vector3(RAY_OFFSET, 0, 0));
            ends[2] = ends[0] + cameraTransform.TransformDirection(new Vector3(-RAY_OFFSET, 0, 0));
            ends[3] = ends[0] + cameraTransform.TransformDirection(new Vector3(0, RAY_OFFSET, 0));
            ends[4] = ends[0] + cameraTransform.TransformDirection(new Vector3(0, -RAY_OFFSET, 0));
            ends[5] = ends[0] + cameraTransform.TransformDirection(new Vector3(RAY_OFFSET, RAY_OFFSET, 0));
            ends[6] = ends[0] + cameraTransform.TransformDirection(new Vector3(-RAY_OFFSET, RAY_OFFSET, 0));
            ends[7] = ends[0] + cameraTransform.TransformDirection(new Vector3(RAY_OFFSET, -RAY_OFFSET, 0));
            ends[8] = ends[0] + cameraTransform.TransformDirection(new Vector3(-RAY_OFFSET, -RAY_OFFSET, 0));

            rayDistances.Clear();
            for (var i = 0; i < ends.Length; i++)
            {
                var end = ends[i];
                if (Physics.Linecast(start, end, out var hit, Physics.DefaultRaycastLayers))
                {
                    Debug.DrawLine(start, hit.point, Color.green);
                    hits[i].transform.position = hit.point;
                    hits[i].transform.localScale = Vector3.one * (hit.distance * 0.01f); 
                    hits[i].SetActive(true);
                    //Debug.Log("DoF - Hit, Distance = " + hit.distance);
                    rayDistances.Add(hit.distance);
                }
                else
                {
                    hits[i].SetActive(false);
                    Debug.DrawRay(position, position + forward * playerCamera.farClipPlane, Color.red);
                }
            }

            if (rayDistances.Count < 1) return -1;
            return Helpers.RemoveOutliers(rayDistances).Average();
        }
    }
}