using System.Collections.Generic;
using System.Threading;
using UnityEngine;
using UnityEngine.Playables;
#if UNITY_EDITOR
using UnityEditor;
#endif

namespace Streamingle.Gaze
{
    /// <summary>
    /// Blends Timeline clip requests into one command for the bound driver.
    /// </summary>
    public sealed class BlendshapeGazeMixerBehaviour : PlayableBehaviour
    {
        private const float MinimumWeight = 0.0001f;

        private static int nextSourceId;
#if UNITY_EDITOR
        private static readonly HashSet<BlendshapeGazeDriver> PendingEditorEvaluations =
            new HashSet<BlendshapeGazeDriver>();
        private static bool editorEvaluationScheduled;
#endif

        private readonly int sourceId = Interlocked.Increment(ref nextSourceId);

        private BlendshapeGazeDriver boundDriver;
        private bool hasSubmittedState;
        private bool hasProcessedFrame;

        public override void PrepareFrame(Playable playable, FrameData info)
        {
            if (hasSubmittedState && boundDriver != null)
                boundDriver.PrepareForTimelineEvaluation();
        }

        public override void ProcessFrame(Playable playable, FrameData info, object playerData)
        {
            var driver = playerData as BlendshapeGazeDriver;
            if (driver == null)
            {
                ClearBoundDriver(ShouldEvaluateImmediately());
                return;
            }

            if (boundDriver != driver)
            {
                ClearBoundDriver(ShouldEvaluateImmediately());
                boundDriver = driver;
            }

            var outputWeight = Mathf.Max(0f, info.effectiveWeight);
            var totalWeight = 0f;
            var gazeOriginPosition = driver.GetGazeOriginPosition();
            var weightedTargetDirection = Vector3.zero;
            var hasCommonTarget = false;
            var hasDifferentTargets = false;
            var hasUnresolvedTarget = false;
            Transform commonTarget = null;
            var dominantWeight = float.NegativeInfinity;
            BlendshapeGazeBehaviour dominantBehaviour = null;
            Camera resolvedCamera = null;

            var inputCount = playable.GetInputCount();
            for (var i = 0; i < inputCount; i++)
            {
                var inputPlayable = (ScriptPlayable<BlendshapeGazeBehaviour>)playable.GetInput(i);
                if (!inputPlayable.IsValid())
                    continue;

                var behaviour = inputPlayable.GetBehaviour();
                var inputWeight = Mathf.Max(0f, playable.GetInputWeight(i));
                var contribution = inputWeight * outputWeight * Mathf.Clamp01(behaviour.Strength);
                if (contribution <= MinimumWeight)
                    continue;

                totalWeight += contribution;

                var requestedTarget = behaviour.Target;
                if (!hasCommonTarget)
                {
                    commonTarget = requestedTarget;
                    hasCommonTarget = true;
                }
                else if (commonTarget != requestedTarget)
                {
                    hasDifferentTargets = true;
                }

                var resolvedTarget = requestedTarget;
                if (resolvedTarget == null)
                {
                    if (resolvedCamera == null)
                        resolvedCamera = driver.TargetCamera != null
                            ? driver.TargetCamera
                            : Camera.main;

                    if (resolvedCamera != null)
                        resolvedTarget = resolvedCamera.transform;
                }

                if (resolvedTarget != null)
                {
                    var targetOffset = resolvedTarget.position - gazeOriginPosition;
                    if (targetOffset.sqrMagnitude > MinimumWeight * MinimumWeight)
                    {
                        weightedTargetDirection += targetOffset.normalized * contribution;
                    }
                    else
                    {
                        hasUnresolvedTarget = true;
                    }
                }
                else
                {
                    hasUnresolvedTarget = true;
                }

                if (contribution > dominantWeight)
                {
                    dominantWeight = contribution;
                    dominantBehaviour = behaviour;
                }
            }

            var canDeferTargetResolution = hasCommonTarget && !hasDifferentTargets;

            // A shared Transform (including a null target meaning Main Camera) is
            // deliberately deferred to the driver. This lets LateUpdate observe the
            // final Cinemachine camera pose. Distinct overlapping targets must be
            // collapsed to a fixed blended direction during Timeline evaluation.
            // Directions, rather than positions, prevent a distant target from
            // outweighing a nearby target with the same Timeline contribution.
            var cannotBlendTargets = !canDeferTargetResolution
                                     && (weightedTargetDirection.sqrMagnitude <= MinimumWeight * MinimumWeight
                                         || hasUnresolvedTarget);

            if (totalWeight <= MinimumWeight || dominantBehaviour == null || cannotBlendTargets)
            {
                ClearSubmittedState(ShouldEvaluateImmediately());
                hasProcessedFrame = true;
                return;
            }

            var resetSmoothing = !hasProcessedFrame || info.seekOccurred || info.timeLooped;
            var deterministic = !Application.isPlaying
                                || info.evaluationType == FrameData.EvaluationType.Evaluate
                                || info.seekOccurred
                                || info.timeLooped;

            var state = new GazeTimelineState
            {
                TargetDirection = canDeferTargetResolution
                    ? Vector3.zero
                    : weightedTargetDirection.normalized,
                HasTargetDirection = !canDeferTargetResolution,
                Target = canDeferTargetResolution ? commonTarget : null,
                UseMainCamera = canDeferTargetResolution && commonTarget == null,
                Influence = Mathf.Clamp01(totalWeight),
                EnableCorrectives = dominantBehaviour.EnableCorrectives,
                OverrideCutResponse = dominantBehaviour.OverrideCutResponse,
                CutResponse = dominantBehaviour.CutResponse,
                CutTransitionDuration = dominantBehaviour.CutTransitionDuration,
                ResetSmoothing = resetSmoothing,
                Deterministic = deterministic
            };

            driver.SetTimelineState(sourceId, state);
            hasSubmittedState = true;
            hasProcessedFrame = true;

#if UNITY_EDITOR
            if (!Application.isPlaying)
            {
                // Evaluate after the whole Timeline graph has applied its animation
                // outputs. This pass sees the animated GazeOrigin, final camera pose
                // and facial blendshape baseline from this frame.
                ScheduleEditorFinalEvaluation(driver);
            }
#endif
        }

        public override void OnGraphStop(Playable playable)
        {
            ClearBoundDriver(ShouldEvaluateImmediately());
            hasProcessedFrame = false;
        }

        public override void OnPlayableDestroy(Playable playable)
        {
            ClearBoundDriver(ShouldEvaluateImmediately());
            hasProcessedFrame = false;
        }

        private void ClearSubmittedState(bool evaluateImmediately)
        {
            if (!hasSubmittedState)
                return;

            if (boundDriver != null)
            {
                boundDriver.ClearTimelineState(sourceId);

                if (evaluateImmediately)
                {
#if UNITY_EDITOR
                    ScheduleEditorFinalEvaluation(boundDriver);
#else
                    boundDriver.EvaluateNow(true);
#endif
                }
            }

            hasSubmittedState = false;
        }

        private void ClearBoundDriver(bool evaluateImmediately)
        {
            ClearSubmittedState(evaluateImmediately);
            boundDriver = null;
        }

        private static bool ShouldEvaluateImmediately()
        {
            return !Application.isPlaying;
        }

#if UNITY_EDITOR
        internal static void ScheduleEditorFinalEvaluation(BlendshapeGazeDriver driver)
        {
            if (driver == null)
                return;

            PendingEditorEvaluations.Add(driver);
            if (editorEvaluationScheduled)
                return;

            editorEvaluationScheduled = true;
            EditorApplication.delayCall += FlushPendingEditorEvaluations;
            // Manual/paused Timeline evaluations do not guarantee an ExecuteAlways
            // LateUpdate. Request one so the driver gets a post-animation final pass.
            EditorApplication.QueuePlayerLoopUpdate();
        }

        internal static void FlushPendingEditorEvaluations()
        {
            EditorApplication.delayCall -= FlushPendingEditorEvaluations;
            editorEvaluationScheduled = false;

            if (PendingEditorEvaluations.Count == 0)
                return;

            var drivers = new BlendshapeGazeDriver[PendingEditorEvaluations.Count];
            PendingEditorEvaluations.CopyTo(drivers);
            PendingEditorEvaluations.Clear();

            if (EditorApplication.isPlayingOrWillChangePlaymode)
                return;

            var evaluatedAny = false;
            for (var index = 0; index < drivers.Length; index++)
            {
                if (drivers[index] != null && drivers[index].isActiveAndEnabled)
                {
                    drivers[index].EvaluateNow(true);
                    evaluatedAny = true;
                }
            }

            if (evaluatedAny)
                SceneView.RepaintAll();
        }

        internal static void CancelEditorFinalEvaluation(BlendshapeGazeDriver driver)
        {
            if (object.ReferenceEquals(driver, null))
                return;

            PendingEditorEvaluations.Remove(driver);
            if (PendingEditorEvaluations.Count > 0 || !editorEvaluationScheduled)
                return;

            EditorApplication.delayCall -= FlushPendingEditorEvaluations;
            editorEvaluationScheduled = false;
        }
#endif
    }
}
