using System;
using System.Collections.Generic;
using System.Reflection;
using UnityEditor;
using UnityEngine;

namespace Streamingle.Gaze.Editor
{
    [CustomEditor(typeof(BlendshapeGazeDriver))]
    public sealed class BlendshapeGazeDriverEditor : UnityEditor.Editor
    {
        private static readonly string[] StandardEyeLookChannels =
        {
            "eyeLookUpLeft",
            "eyeLookUpRight",
            "eyeLookDownLeft",
            "eyeLookDownRight",
            "eyeLookInLeft",
            "eyeLookInRight",
            "eyeLookOutLeft",
            "eyeLookOutRight"
        };

        private static readonly GazeCalibrationPoint[] GridSamples =
        {
            GazeCalibrationPoint.UpLeft,
            GazeCalibrationPoint.UpCenter,
            GazeCalibrationPoint.UpRight,
            GazeCalibrationPoint.CenterLeft,
            GazeCalibrationPoint.Center,
            GazeCalibrationPoint.CenterRight,
            GazeCalibrationPoint.DownLeft,
            GazeCalibrationPoint.DownCenter,
            GazeCalibrationPoint.DownRight
        };

        private static readonly string[] GridLabels =
        {
            "Up Left", "Up", "Up Right",
            "Left", "Center", "Right",
            "Down Left", "Down", "Down Right"
        };

        private static readonly string[] RendererPropertyNames =
        {
            "targetRenderer", "_targetRenderer", "m_TargetRenderer", "renderer", "faceRenderer"
        };

        private static readonly string[] ProfilePropertyNames =
        {
            "profile", "_profile", "m_Profile", "calibrationProfile"
        };

        private static readonly string[] OriginPropertyNames =
        {
            "gazeOrigin", "_gazeOrigin", "m_GazeOrigin", "origin", "gazeBasis"
        };

        private static readonly string[] HeadPropertyNames =
        {
            "headReference", "_headReference", "m_HeadReference", "head", "headTransform"
        };

        private readonly Dictionary<int, float> previewRestoreWeights = new Dictionary<int, float>();
        private readonly HashSet<int> capturedThisSession = new HashSet<int>();

        private SkinnedMeshRenderer previewRenderer;
        private SerializedProperty targetRendererProperty;
        private SerializedProperty profileProperty;
        private SerializedProperty gazeOriginProperty;
        private SerializedProperty headReferenceProperty;
        private int selectedSampleIndex = 4;
        private bool editOriginInScene;
        private string feedbackMessage;
        private MessageType feedbackType = MessageType.Info;
        private GazeOriginEstimate lastOriginEstimate;
        private bool hasOriginEstimate;

        private BlendshapeGazeDriver Driver => (BlendshapeGazeDriver)target;

        private string SessionKey => $"Streamingle.Gaze.SelectedSample.{target.GetInstanceID()}";

        private void OnEnable()
        {
            CacheDriverProperties();
            selectedSampleIndex = Mathf.Clamp(SessionState.GetInt(SessionKey, 4), 0, GridSamples.Length - 1);
        }

        private void OnDisable()
        {
            // Applying a sample is a non-destructive preview. Do not leave it stuck on the face when
            // the inspector is closed or the user selects another object.
            RestorePreview(false);
        }

        public override void OnInspectorGUI()
        {
            serializedObject.Update();
            EditorGUI.BeginChangeCheck();
            DrawPropertiesExcluding(serializedObject, "m_Script");
            bool defaultInspectorChanged = EditorGUI.EndChangeCheck();
            serializedObject.ApplyModifiedProperties();

            if (defaultInspectorChanged)
            {
                RestorePreview(false);
                CacheDriverProperties();
                RefreshDriverCache();
            }

            EditorGUILayout.Space(8f);
            DrawCalibrationTools();
        }

        private void DrawCalibrationTools()
        {
            EditorGUILayout.LabelField("Gaze Calibration", EditorStyles.boldLabel);

            SkinnedMeshRenderer renderer = Driver.TargetRenderer;
            BlendshapeGazeProfile profile = Driver.Profile;

            using (new EditorGUILayout.VerticalScope(EditorStyles.helpBox))
            {
                if (GUILayout.Button("Open Guided Calibration", GUILayout.MinHeight(36f)))
                {
                    GazeCalibrationWindow.Open(Driver);
                }

                EditorGUILayout.LabelField(
                    "Recommended: edit five poses directly, generate corners, and test interpolation in one window.",
                    EditorStyles.wordWrappedMiniLabel);
            }

            DrawValidation(renderer, profile);

            if (!string.IsNullOrWhiteSpace(feedbackMessage))
            {
                EditorGUILayout.HelpBox(feedbackMessage, feedbackType);
            }
        }

        private void DrawProfileTools(SkinnedMeshRenderer renderer, BlendshapeGazeProfile profile)
        {
            using (new EditorGUILayout.HorizontalScope())
            {
                using (new EditorGUI.DisabledScope(profile != null))
                {
                    if (GUILayout.Button("Create Profile Asset"))
                    {
                        CreateProfileAsset();
                        GUIUtility.ExitGUI();
                    }
                }

                using (new EditorGUI.DisabledScope(profile == null || renderer == null || renderer.sharedMesh == null))
                {
                    if (GUILayout.Button("Sync ARKit EyeLook 8"))
                    {
                        SyncStandardChannels(renderer, profile);
                    }
                }
            }

            if (renderer == null)
            {
                EditorGUILayout.HelpBox("Assign Target Renderer before calibrating.", MessageType.Warning);
            }
            else if (renderer.sharedMesh == null)
            {
                EditorGUILayout.HelpBox("Target Renderer has no shared mesh.", MessageType.Warning);
            }
        }

        private void DrawOriginTools(SkinnedMeshRenderer renderer, BlendshapeGazeProfile profile)
        {
            EditorGUILayout.LabelField("Gaze Origin", EditorStyles.boldLabel);
            using (new EditorGUILayout.HorizontalScope())
            {
                using (new EditorGUI.DisabledScope(renderer == null))
                {
                    string originLabel = Driver.GazeOrigin == null
                        ? "Auto Create Gaze Origin"
                        : "Recalculate Gaze Origin";
                    if (GUILayout.Button(originLabel))
                    {
                        AutoCreateOrRecalculateOrigin(renderer, profile);
                    }
                }

                using (new EditorGUI.DisabledScope(Driver.GazeOrigin == null && renderer == null))
                {
                    if (GUILayout.Button("Set Forward From Scene Camera"))
                    {
                        SetNeutralForwardFromSceneCamera(renderer, profile);
                    }
                }
            }

            using (new EditorGUI.DisabledScope(Driver.GazeOrigin == null))
            {
                bool newEditState = EditorGUILayout.ToggleLeft("Edit origin position in Scene view", editOriginInScene);
                if (newEditState != editOriginInScene)
                {
                    editOriginInScene = newEditState;
                    SceneView.RepaintAll();
                }
            }
        }

        private void DrawSampleGrid(BlendshapeGazeProfile profile)
        {
            for (int row = 0; row < 3; row++)
            {
                using (new EditorGUILayout.HorizontalScope())
                {
                    for (int column = 0; column < 3; column++)
                    {
                        int index = row * 3 + column;
                        bool isSelected = selectedSampleIndex == index;
                        bool isCaptured = IsSampleCaptured(profile, index);
                        Color previousColor = GUI.backgroundColor;

                        if (isSelected)
                        {
                            GUI.backgroundColor = new Color(0.35f, 0.72f, 1f);
                        }
                        else if (isCaptured)
                        {
                            GUI.backgroundColor = new Color(0.45f, 0.8f, 0.48f);
                        }

                        string state = isCaptured ? "  [Captured]" : string.Empty;
                        if (GUILayout.Button(GridLabels[index] + state, GUILayout.MinHeight(30f)))
                        {
                            selectedSampleIndex = index;
                            SessionState.SetInt(SessionKey, selectedSampleIndex);
                            SceneView.RepaintAll();
                        }

                        GUI.backgroundColor = previousColor;
                    }
                }
            }

            EditorGUILayout.LabelField(
                $"Selected: {GridLabels[selectedSampleIndex]}",
                EditorStyles.miniLabel);
        }

        private void DrawSampleActions(SkinnedMeshRenderer renderer, BlendshapeGazeProfile profile)
        {
            bool canUseSamples = renderer != null && renderer.sharedMesh != null && profile != null;
            using (new EditorGUI.DisabledScope(!canUseSamples))
            using (new EditorGUILayout.HorizontalScope())
            {
                if (GUILayout.Button("Capture Current Weights"))
                {
                    CaptureSelectedSample(renderer, profile);
                }

                if (GUILayout.Button("Apply Selected Sample"))
                {
                    ApplySelectedSample(renderer, profile);
                }

                using (new EditorGUI.DisabledScope(previewRestoreWeights.Count == 0))
                {
                    if (GUILayout.Button("Restore"))
                    {
                        RestorePreview(true);
                    }
                }
            }
        }

        private void DrawValidation(SkinnedMeshRenderer renderer, BlendshapeGazeProfile profile)
        {
            if (renderer == null || profile == null)
            {
                return;
            }

            Mesh mesh = renderer.sharedMesh;
            if (mesh == null)
            {
                EditorGUILayout.HelpBox("Target Renderer has no shared mesh.", MessageType.Warning);
                return;
            }

            if (profile.Channels.Count == 0)
            {
                EditorGUILayout.HelpBox("Sync the eight eyeLook channels before capturing samples.", MessageType.Warning);
                return;
            }

            var missingChannels = new List<string>();
            if (!profile.IsCompatibleWith(mesh))
            {
                EditorGUILayout.HelpBox(
                    $"This profile was calibrated for '{profile.SourceMesh.name}', not '{mesh.name}'.",
                    MessageType.Warning);
                return;
            }

            for (int index = 0; index < profile.Channels.Count; index++)
            {
                GazeBlendShapeChannel channel = profile.Channels[index];
                if (channel == null || BlendshapeGazeProfile.FindBlendShapeIndex(mesh, channel.BlendShapeName) < 0)
                {
                    missingChannels.Add(channel != null ? channel.BlendShapeName : "<null>");
                }
            }

            if (missingChannels.Count > 0)
            {
                EditorGUILayout.HelpBox(
                    "Missing blendshapes on Target Renderer: " + string.Join(", ", missingChannels),
                    MessageType.Warning);
                return;
            }

            int capturedCount = 0;
            for (int index = 0; index < profile.Samples.Count; index++)
            {
                if (profile.Samples[index] != null && profile.Samples[index].Captured)
                {
                    capturedCount++;
                }
            }

            EditorGUILayout.HelpBox(
                profile.IsFullyCalibrated
                    ? "All 9 gaze samples are calibrated."
                    : $"Calibration progress: {capturedCount} / 9 samples.",
                profile.IsFullyCalibrated ? MessageType.Info : MessageType.None);
        }

        private void CreateProfileAsset()
        {
            string driverName = string.IsNullOrWhiteSpace(Driver.gameObject.name)
                ? "Character"
                : Driver.gameObject.name;
            string path = EditorUtility.SaveFilePanelInProject(
                "Create Gaze Calibration Profile",
                driverName + "_GazeCalibration",
                "asset",
                "Choose where to save the character-specific gaze calibration profile.");

            if (string.IsNullOrWhiteSpace(path))
            {
                return;
            }

            var profile = ScriptableObject.CreateInstance<BlendshapeGazeProfile>();
            profile.name = System.IO.Path.GetFileNameWithoutExtension(path);
            AssetDatabase.CreateAsset(profile, path);
            AssetDatabase.SaveAssets();

            Undo.RecordObject(Driver, "Assign Gaze Calibration Profile");
            if (!SetDriverObjectReference(profileProperty, ProfilePropertyNames, profile))
            {
                AssetDatabase.DeleteAsset(path);
                SetFeedback("Could not find the driver's serialized Profile field.", MessageType.Error);
                return;
            }

            EditorUtility.SetDirty(Driver);
            EditorUtility.SetDirty(profile);
            PrefabUtility.RecordPrefabInstancePropertyModifications(Driver);
            RefreshDriverCache();
            SetFeedback("Created and assigned a gaze calibration profile.", MessageType.Info);
            EditorGUIUtility.PingObject(profile);
        }

        private void SyncStandardChannels(SkinnedMeshRenderer renderer, BlendshapeGazeProfile profile)
        {
            if (!TryFindStandardChannels(renderer.sharedMesh, out List<BlendShapeMatch> matches, out string matchIssue))
            {
                SetFeedback(matchIssue, MessageType.Error);
                return;
            }

            var preservedSamples = new Dictionary<GazeCalibrationPoint, Dictionary<string, float>>();
            IReadOnlyList<GazeBlendShapeChannel> existingChannels = profile.Channels;
            IReadOnlyList<GazeCalibrationSample> existingSamples = profile.Samples;
            for (int pointIndex = 0; pointIndex < 9 && pointIndex < existingSamples.Count; pointIndex++)
            {
                GazeCalibrationSample sample = existingSamples[pointIndex];
                if (sample == null || !sample.Captured)
                {
                    continue;
                }

                var weightsByChannel = new Dictionary<string, float>(StringComparer.Ordinal);
                for (int channelIndex = 0; channelIndex < existingChannels.Count; channelIndex++)
                {
                    GazeBlendShapeChannel channel = existingChannels[channelIndex];
                    if (channel == null || channelIndex >= sample.Weights.Count)
                    {
                        continue;
                    }

                    weightsByChannel[GetStableChannelKey(channel.BlendShapeName)] = sample.Weights[channelIndex];
                }

                preservedSamples[(GazeCalibrationPoint)pointIndex] = weightsByChannel;
            }

            var replacements = new List<GazeBlendShapeChannel>(matches.Count + existingChannels.Count);
            for (int index = 0; index < matches.Count; index++)
            {
                replacements.Add(new GazeBlendShapeChannel(matches[index].MeshName, GazeChannelUsage.EyeLook));
            }

            // Channel sync owns the primary eight, but must not destroy optional eyelid or other
            // corrective channels that were explicitly authored later.
            for (int index = 0; index < existingChannels.Count; index++)
            {
                GazeBlendShapeChannel channel = existingChannels[index];
                if (channel == null || channel.Usage != GazeChannelUsage.Corrective)
                {
                    continue;
                }

                bool duplicate = false;
                for (int replacementIndex = 0; replacementIndex < replacements.Count; replacementIndex++)
                {
                    if (string.Equals(
                            replacements[replacementIndex].BlendShapeName,
                            channel.BlendShapeName,
                            StringComparison.OrdinalIgnoreCase))
                    {
                        duplicate = true;
                        break;
                    }
                }

                if (!duplicate)
                {
                    replacements.Add(new GazeBlendShapeChannel(
                        channel.BlendShapeName,
                        GazeChannelUsage.Corrective,
                        channel.CorrectiveBlendMode));
                }
            }

            Undo.RecordObject(profile, "Sync ARKit EyeLook Channels");
            profile.ReplaceChannels(replacements);
            profile.SetSourceMesh(renderer.sharedMesh);

            foreach (KeyValuePair<GazeCalibrationPoint, Dictionary<string, float>> pair in preservedSamples)
            {
                var restoredWeights = new float[replacements.Count];
                for (int channelIndex = 0; channelIndex < replacements.Count; channelIndex++)
                {
                    string key = GetStableChannelKey(replacements[channelIndex].BlendShapeName);
                    if (pair.Value.TryGetValue(key, out float value))
                    {
                        restoredWeights[channelIndex] = value;
                    }
                }

                profile.SetSample(pair.Key, restoredWeights);
            }

            EditorUtility.SetDirty(profile);
            AssetDatabase.SaveAssetIfDirty(profile);
            RefreshDriverCache();
            SetFeedback(
                $"Synced all {StandardEyeLookChannels.Length} ARKit eyeLook channels from {renderer.sharedMesh.name}.",
                MessageType.Info);
        }

        private void CaptureSelectedSample(SkinnedMeshRenderer renderer, BlendshapeGazeProfile profile)
        {
            Undo.RecordObject(profile, "Capture Gaze Calibration Sample");
            if (!profile.CaptureSample(GridSamples[selectedSampleIndex], renderer))
            {
                SetFeedback(
                    "Capture failed. Sync channels and make sure every configured blendshape exists on Target Renderer.",
                    MessageType.Error);
                return;
            }

            capturedThisSession.Add(selectedSampleIndex);
            EditorUtility.SetDirty(profile);
            AssetDatabase.SaveAssetIfDirty(profile);
            RefreshDriverCache();
            SetFeedback($"Captured {GridLabels[selectedSampleIndex]} from the current blendshape weights.", MessageType.Info);
            Repaint();
            SceneView.RepaintAll();
        }

        private void ApplySelectedSample(SkinnedMeshRenderer renderer, BlendshapeGazeProfile profile)
        {
            GazeCalibrationPoint point = GridSamples[selectedSampleIndex];
            int sampleIndex = (int)point;
            if (sampleIndex < 0 || sampleIndex >= profile.Samples.Count
                || profile.Samples[sampleIndex] == null
                || !profile.Samples[sampleIndex].Captured)
            {
                SetFeedback("The selected sample has not been captured yet.", MessageType.Warning);
                return;
            }

            CachePreviewWeights(renderer);
            Undo.RecordObject(renderer, "Preview Gaze Calibration Sample");
            GazeCalibrationSample sample = profile.Samples[sampleIndex];
            Mesh mesh = renderer.sharedMesh;
            for (int channelIndex = 0; channelIndex < profile.Channels.Count; channelIndex++)
            {
                int blendShapeIndex = profile.FindBlendShapeIndex(mesh, channelIndex);
                if (blendShapeIndex < 0 || channelIndex >= sample.Weights.Count)
                {
                    continue;
                }

                renderer.SetBlendShapeWeight(blendShapeIndex, sample.Weights[channelIndex]);
            }

            EditorUtility.SetDirty(renderer);
            PrefabUtility.RecordPrefabInstancePropertyModifications(renderer);
            SetFeedback($"Previewing {GridLabels[selectedSampleIndex]}. Restore returns to the previous face pose.", MessageType.Info);
            SceneView.RepaintAll();
        }

        private void CachePreviewWeights(SkinnedMeshRenderer renderer)
        {
            if (previewRestoreWeights.Count > 0 && previewRenderer == renderer)
            {
                return;
            }

            previewRestoreWeights.Clear();
            previewRenderer = renderer;
            Mesh mesh = renderer != null ? renderer.sharedMesh : null;
            if (mesh == null)
            {
                return;
            }

            for (int index = 0; index < mesh.blendShapeCount; index++)
            {
                previewRestoreWeights[index] = renderer.GetBlendShapeWeight(index);
            }
        }

        private void RestorePreview(bool recordUndo)
        {
            if (previewRenderer == null || previewRestoreWeights.Count == 0)
            {
                previewRestoreWeights.Clear();
                previewRenderer = null;
                return;
            }

            if (recordUndo)
            {
                Undo.RecordObject(previewRenderer, "Restore Face Before Gaze Preview");
            }

            Mesh mesh = previewRenderer.sharedMesh;
            if (mesh != null)
            {
                foreach (KeyValuePair<int, float> pair in previewRestoreWeights)
                {
                    if (pair.Key >= 0 && pair.Key < mesh.blendShapeCount)
                    {
                        previewRenderer.SetBlendShapeWeight(pair.Key, pair.Value);
                    }
                }

                EditorUtility.SetDirty(previewRenderer);
                PrefabUtility.RecordPrefabInstancePropertyModifications(previewRenderer);
            }

            previewRestoreWeights.Clear();
            previewRenderer = null;
            SceneView.RepaintAll();
        }

        private void AutoCreateOrRecalculateOrigin(
            SkinnedMeshRenderer renderer,
            BlendshapeGazeProfile profile)
        {
            IReadOnlyList<string> channelNames = GetConfiguredChannelNames(profile);
            GazeOriginEstimate estimate = GazeOriginEstimator.Estimate(renderer, channelNames);
            lastOriginEstimate = estimate;
            hasOriginEstimate = true;

            Transform origin = Driver.GazeOrigin;
            if (origin == null)
            {
                Transform parent = ResolveOriginParent();
                var marker = new GameObject(Driver.gameObject.name + "_GazeOrigin");
                Undo.RegisterCreatedObjectUndo(marker, "Create Gaze Origin");
                origin = marker.transform;
                Undo.SetTransformParent(origin, parent, "Parent Gaze Origin");
                origin.rotation = parent != null ? parent.rotation : Driver.transform.rotation;

                Undo.RecordObject(Driver, "Assign Gaze Origin");
                if (!SetDriverObjectReference(gazeOriginProperty, OriginPropertyNames, origin))
                {
                    Undo.DestroyObjectImmediate(marker);
                    SetFeedback("Could not find the driver's serialized Gaze Origin field.", MessageType.Error);
                    return;
                }
            }

            Undo.RecordObject(origin, "Recalculate Gaze Origin");
            origin.position = estimate.WorldPosition;
            EditorUtility.SetDirty(origin);
            EditorUtility.SetDirty(Driver);
            PrefabUtility.RecordPrefabInstancePropertyModifications(origin);
            PrefabUtility.RecordPrefabInstancePropertyModifications(Driver);
            RefreshDriverCache();

            SetFeedback(
                estimate.Message,
                estimate.UsedEyeBlendShapes ? MessageType.Info : MessageType.Warning);
            EditorGUIUtility.PingObject(origin.gameObject);
            SceneView.RepaintAll();
        }

        private void SetNeutralForwardFromSceneCamera(
            SkinnedMeshRenderer renderer,
            BlendshapeGazeProfile profile)
        {
            if (Driver.GazeOrigin == null)
            {
                if (renderer == null)
                {
                    SetFeedback("Assign Target Renderer before creating Gaze Origin.", MessageType.Error);
                    return;
                }

                AutoCreateOrRecalculateOrigin(renderer, profile);
            }

            Transform origin = Driver.GazeOrigin;
            Camera sceneCamera = SceneView.lastActiveSceneView != null
                ? SceneView.lastActiveSceneView.camera
                : null;
            if (origin == null || sceneCamera == null)
            {
                SetFeedback("No active Scene view camera was found.", MessageType.Error);
                return;
            }

            Vector3 direction = sceneCamera.transform.position - origin.position;
            if (direction.sqrMagnitude < 1e-8f)
            {
                direction = -sceneCamera.transform.forward;
            }

            Vector3 up = origin.parent != null ? origin.parent.up : Vector3.up;
            if (Mathf.Abs(Vector3.Dot(direction.normalized, up.normalized)) > 0.999f)
            {
                up = sceneCamera.transform.up;
            }

            Undo.RecordObject(origin, "Set Neutral Gaze Forward");
            origin.rotation = Quaternion.LookRotation(direction.normalized, up);
            EditorUtility.SetDirty(origin);
            PrefabUtility.RecordPrefabInstancePropertyModifications(origin);
            RefreshDriverCache();
            SetFeedback("Neutral forward now points from Gaze Origin to the Scene view camera.", MessageType.Info);
            SceneView.RepaintAll();
        }

        private Transform ResolveOriginParent()
        {
            Transform explicitHead = GetObjectReference<Transform>(headReferenceProperty, Driver, HeadPropertyNames);
            if (explicitHead != null)
            {
                return explicitHead;
            }

            Animator animator = Driver.GetComponentInParent<Animator>();
            if (animator != null && animator.isHuman)
            {
                Transform humanoidHead = animator.GetBoneTransform(HumanBodyBones.Head);
                if (humanoidHead != null)
                {
                    return humanoidHead;
                }
            }

            return Driver.transform;
        }

        private void OnSceneGUI()
        {
            Transform origin = Driver.GazeOrigin;
            if (origin == null)
            {
                return;
            }

            float handleSize = HandleUtility.GetHandleSize(origin.position);
            Handles.color = new Color(0.1f, 0.9f, 1f, 0.95f);
            Handles.ArrowHandleCap(
                0,
                origin.position,
                origin.rotation,
                handleSize * 0.65f,
                EventType.Repaint);
            Handles.Label(
                origin.position + origin.up * handleSize * 0.16f,
                $"Gaze Origin\n{GridLabels[selectedSampleIndex]}");

            if (hasOriginEstimate && lastOriginEstimate.UsedEyeBlendShapes)
            {
                Handles.color = new Color(0.3f, 1f, 0.4f, 0.8f);
                float eyeHandleSize = handleSize * 0.04f;
                Handles.SphereHandleCap(0, lastOriginEstimate.LeftEyeWorldPosition, Quaternion.identity, eyeHandleSize, EventType.Repaint);
                Handles.SphereHandleCap(0, lastOriginEstimate.RightEyeWorldPosition, Quaternion.identity, eyeHandleSize, EventType.Repaint);
                Handles.DrawDottedLine(
                    lastOriginEstimate.LeftEyeWorldPosition,
                    lastOriginEstimate.RightEyeWorldPosition,
                    3f);
            }

            bool hasDebugTarget = Driver.HasActiveRequest;
            Vector3 debugTargetPosition = Driver.LastTargetPosition;
            if (!hasDebugTarget)
            {
                Camera mainCamera = Camera.main;
                if (mainCamera != null)
                {
                    hasDebugTarget = true;
                    debugTargetPosition = mainCamera.transform.position;
                }
            }

            if (hasDebugTarget)
            {
                Vector3 targetDirection = debugTargetPosition - origin.position;
                if (targetDirection.sqrMagnitude > 1e-8f)
                {
                    Handles.color = new Color(1f, 0.78f, 0.1f, 0.8f);
                    Handles.DrawDottedLine(origin.position, debugTargetPosition, 4f);
                    Vector3 localDirection = origin.InverseTransformDirection(targetDirection.normalized);
                    float yaw = Mathf.Atan2(localDirection.x, localDirection.z) * Mathf.Rad2Deg;
                    float pitch = Mathf.Atan2(
                        localDirection.y,
                        Mathf.Sqrt(localDirection.x * localDirection.x + localDirection.z * localDirection.z)) * Mathf.Rad2Deg;
                    Handles.Label(
                        origin.position + targetDirection.normalized * handleSize * 0.8f,
                        $"Camera  yaw {yaw:F1} deg  pitch {pitch:F1} deg");
                }
            }

            if (!editOriginInScene)
            {
                return;
            }

            EditorGUI.BeginChangeCheck();
            Vector3 newPosition = Handles.PositionHandle(origin.position, origin.rotation);
            if (EditorGUI.EndChangeCheck())
            {
                Undo.RecordObject(origin, "Move Gaze Origin");
                origin.position = newPosition;
                EditorUtility.SetDirty(origin);
                PrefabUtility.RecordPrefabInstancePropertyModifications(origin);
                RefreshDriverCache();
            }
        }

        private void CacheDriverProperties()
        {
            serializedObject.UpdateIfRequiredOrScript();
            targetRendererProperty = FindProperty(serializedObject, RendererPropertyNames);
            profileProperty = FindProperty(serializedObject, ProfilePropertyNames);
            gazeOriginProperty = FindProperty(serializedObject, OriginPropertyNames);
            headReferenceProperty = FindProperty(serializedObject, HeadPropertyNames);
        }

        private void RefreshDriverCache()
        {
            const BindingFlags flags = BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic;
            MethodInfo refreshMethod = Driver.GetType().GetMethod("RebuildCache", flags)
                                       ?? Driver.GetType().GetMethod("InvalidateCache", flags);
            refreshMethod?.Invoke(Driver, null);
            EditorUtility.SetDirty(Driver);
        }

        private bool SetDriverObjectReference(
            SerializedProperty serializedProperty,
            IReadOnlyList<string> memberNames,
            UnityEngine.Object value)
        {
            if (serializedProperty != null && serializedProperty.propertyType == SerializedPropertyType.ObjectReference)
            {
                serializedObject.Update();
                serializedProperty.objectReferenceValue = value;
                serializedObject.ApplyModifiedProperties();
                CacheDriverProperties();
                return true;
            }

            return TrySetMemberValue(Driver, memberNames, value);
        }

        private static T GetObjectReference<T>(
            SerializedProperty serializedProperty,
            object owner,
            IReadOnlyList<string> memberNames)
            where T : UnityEngine.Object
        {
            if (serializedProperty != null && serializedProperty.propertyType == SerializedPropertyType.ObjectReference)
            {
                return serializedProperty.objectReferenceValue as T;
            }

            return GetMemberValue(owner, memberNames) as T;
        }

        private static bool TrySetMemberValue(
            object owner,
            IReadOnlyList<string> memberNames,
            object value)
        {
            if (owner == null)
            {
                return false;
            }

            const BindingFlags flags = BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic;
            Type type = owner.GetType();
            for (int index = 0; index < memberNames.Count; index++)
            {
                string memberName = memberNames[index];
                FieldInfo field = type.GetField(memberName, flags);
                if (field != null && (value == null || field.FieldType.IsInstanceOfType(value)))
                {
                    field.SetValue(owner, value);
                    return true;
                }

                PropertyInfo property = type.GetProperty(memberName, flags);
                if (property != null && property.CanWrite && (value == null || property.PropertyType.IsInstanceOfType(value)))
                {
                    property.SetValue(owner, value);
                    return true;
                }
            }

            return false;
        }

        private static object GetMemberValue(object owner, IReadOnlyList<string> memberNames)
        {
            if (owner == null)
            {
                return null;
            }

            const BindingFlags flags = BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic;
            Type type = owner.GetType();
            for (int index = 0; index < memberNames.Count; index++)
            {
                string memberName = memberNames[index];
                FieldInfo field = type.GetField(memberName, flags);
                if (field != null)
                {
                    return field.GetValue(owner);
                }

                PropertyInfo property = type.GetProperty(memberName, flags);
                if (property != null && property.CanRead)
                {
                    return property.GetValue(owner);
                }
            }

            return null;
        }

        private static SerializedProperty FindProperty(SerializedObject owner, IReadOnlyList<string> names)
        {
            if (owner == null)
            {
                return null;
            }

            for (int index = 0; index < names.Count; index++)
            {
                SerializedProperty exact = owner.FindProperty(names[index]);
                if (exact != null)
                {
                    return exact;
                }
            }

            var normalizedNames = new HashSet<string>(StringComparer.Ordinal);
            for (int index = 0; index < names.Count; index++)
            {
                normalizedNames.Add(Normalize(names[index]));
            }

            SerializedProperty iterator = owner.GetIterator();
            bool enterChildren = true;
            while (iterator.NextVisible(enterChildren))
            {
                enterChildren = false;
                if (iterator.depth == 0 && normalizedNames.Contains(Normalize(iterator.name)))
                {
                    return iterator.Copy();
                }
            }

            return null;
        }

        private static bool TryFindStandardChannels(
            Mesh mesh,
            out List<BlendShapeMatch> matches,
            out string issue)
        {
            matches = new List<BlendShapeMatch>(StandardEyeLookChannels.Length);
            var missing = new List<string>();

            for (int standardIndex = 0; standardIndex < StandardEyeLookChannels.Length; standardIndex++)
            {
                string standardName = StandardEyeLookChannels[standardIndex];
                int meshIndex = FindBestBlendShapeMatch(mesh, standardName);
                if (meshIndex < 0)
                {
                    missing.Add(standardName);
                    continue;
                }

                matches.Add(new BlendShapeMatch(
                    standardName,
                    mesh.GetBlendShapeName(meshIndex),
                    meshIndex));
            }

            if (missing.Count > 0)
            {
                issue = "The mesh is missing ARKit eyeLook channels: " + string.Join(", ", missing);
                return false;
            }

            issue = null;
            return true;
        }

        private static int FindBestBlendShapeMatch(Mesh mesh, string standardName)
        {
            if (mesh == null)
            {
                return -1;
            }

            string normalizedStandardName = Normalize(standardName);
            int bestIndex = -1;
            int bestScore = -1;
            int bestNameLength = int.MaxValue;
            for (int index = 0; index < mesh.blendShapeCount; index++)
            {
                string candidate = mesh.GetBlendShapeName(index);
                string normalizedCandidate = Normalize(candidate);
                int score = -1;

                if (string.Equals(candidate, standardName, StringComparison.OrdinalIgnoreCase))
                {
                    score = 100;
                }
                else if (candidate.EndsWith(standardName, StringComparison.OrdinalIgnoreCase))
                {
                    score = 80;
                }
                else if (normalizedCandidate == normalizedStandardName)
                {
                    score = 60;
                }
                else if (normalizedCandidate.EndsWith(normalizedStandardName, StringComparison.Ordinal))
                {
                    score = 40;
                }

                if (score > bestScore || score == bestScore && candidate.Length < bestNameLength)
                {
                    bestScore = score;
                    bestIndex = index;
                    bestNameLength = candidate.Length;
                }
            }

            return bestIndex;
        }

        private static bool IsNameOrSuffixMatch(string candidate, string standardName)
        {
            if (string.IsNullOrWhiteSpace(candidate) || string.IsNullOrWhiteSpace(standardName))
            {
                return false;
            }

            if (string.Equals(candidate, standardName, StringComparison.OrdinalIgnoreCase)
                || candidate.EndsWith(standardName, StringComparison.OrdinalIgnoreCase))
            {
                return true;
            }

            string normalizedCandidate = Normalize(candidate);
            string normalizedStandard = Normalize(standardName);
            return normalizedCandidate == normalizedStandard
                   || normalizedCandidate.EndsWith(normalizedStandard, StringComparison.Ordinal);
        }

        private static string GetStableChannelKey(string channelName)
        {
            for (int index = 0; index < StandardEyeLookChannels.Length; index++)
            {
                if (IsNameOrSuffixMatch(channelName, StandardEyeLookChannels[index]))
                {
                    return "arkit:" + Normalize(StandardEyeLookChannels[index]);
                }
            }

            return "custom:" + Normalize(channelName);
        }

        private static IReadOnlyList<string> GetConfiguredChannelNames(BlendshapeGazeProfile profile)
        {
            var names = new List<string>();
            if (profile == null)
            {
                names.AddRange(StandardEyeLookChannels);
                return names;
            }

            for (int index = 0; index < profile.Channels.Count; index++)
            {
                GazeBlendShapeChannel channel = profile.Channels[index];
                if (channel != null
                    && channel.Usage == GazeChannelUsage.EyeLook
                    && !string.IsNullOrWhiteSpace(channel.BlendShapeName))
                {
                    names.Add(channel.BlendShapeName);
                }
            }

            if (names.Count > 0)
            {
                return names;
            }

            names.AddRange(StandardEyeLookChannels);
            return names;
        }

        private bool IsSampleCaptured(BlendshapeGazeProfile profile, int sampleIndex)
        {
            if (profile == null)
            {
                return false;
            }

            if (capturedThisSession.Contains(sampleIndex))
            {
                return true;
            }

            int profileSampleIndex = (int)GridSamples[sampleIndex];
            if (profileSampleIndex < 0 || profileSampleIndex >= profile.Samples.Count)
            {
                return false;
            }

            GazeCalibrationSample sample = profile.Samples[profileSampleIndex];
            return sample != null && sample.Captured;
        }

        private void SetFeedback(string message, MessageType type)
        {
            feedbackMessage = message;
            feedbackType = type;
            Repaint();
        }

        private static string Normalize(string value)
        {
            if (string.IsNullOrEmpty(value))
            {
                return string.Empty;
            }

            var characters = new char[value.Length];
            int count = 0;
            for (int index = 0; index < value.Length; index++)
            {
                char character = value[index];
                if (!char.IsLetterOrDigit(character))
                {
                    continue;
                }

                characters[count++] = char.ToLowerInvariant(character);
            }

            return new string(characters, 0, count);
        }

        private readonly struct BlendShapeMatch
        {
            internal BlendShapeMatch(string canonicalName, string meshName, int meshIndex)
            {
                CanonicalName = canonicalName;
                MeshName = meshName;
                MeshIndex = meshIndex;
            }

            internal string CanonicalName { get; }
            internal string MeshName { get; }
            internal int MeshIndex { get; }
        }
    }
}
