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

namespace Streamingle.Gaze.Editor
{
    /// <summary>
    /// Guided calibration UI for authoring the five cardinal gaze poses and optionally
    /// refining the generated 3x3 grid. Preview writes are owned by a disposable session
    /// so opening the tool never permanently changes the face renderer.
    /// </summary>
    public sealed class GazeCalibrationWindow : EditorWindow
    {
        private const float PadMinimumSize = 230f;
        private const float PadMaximumSize = 380f;
        private const float MarkerSize = 32f;
        private const float MarkerHitRadius = 23f;

        private static readonly GazeCalibrationPoint[] BasicPoints =
        {
            GazeCalibrationPoint.Center,
            GazeCalibrationPoint.CenterLeft,
            GazeCalibrationPoint.CenterRight,
            GazeCalibrationPoint.UpCenter,
            GazeCalibrationPoint.DownCenter
        };

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

        private GazeCalibrationPreviewSession previewSession;

        [SerializeField] private BlendshapeGazeDriver driver;
        [SerializeField] private GazeCalibrationPoint selectedPoint = GazeCalibrationPoint.Center;
        private Vector2 scrollPosition;
        [SerializeField] private bool advancedGrid;
        [SerializeField] private bool liveInterpolation = true;
        private float[] draftWeights = Array.Empty<float>();
        private GazeCalibrationPoint draftPoint;
        private string draftLayout;
        private bool hasDraft;
        private BlendshapeGazeProfile calibrationProfile;
        private BlendshapeGazeProfile workingProfile;
        private bool isFinishingCalibration;
        private float[] copiedWeights;
        private string copiedLayout;
        private string feedback;
        private MessageType feedbackType = MessageType.Info;

        [MenuItem("Tools/Streamingle Utilities/Gaze/Calibration")]
        private static void OpenFromMenu()
        {
            var selectedDriver = Selection.activeGameObject != null
                ? Selection.activeGameObject.GetComponentInParent<BlendshapeGazeDriver>()
                : Selection.activeObject as BlendshapeGazeDriver;
            Open(selectedDriver);
        }

        public static void Open(BlendshapeGazeDriver driver)
        {
            var window = GetWindow<GazeCalibrationWindow>();
            window.titleContent = new GUIContent("Gaze Calibration");
            window.minSize = new Vector2(430f, 600f);
            window.SetDriver(driver);
            window.Show();
            window.Focus();
        }

        private GazeCalibrationPreviewSession Session
        {
            get
            {
                if (previewSession == null)
                {
                    previewSession = new GazeCalibrationPreviewSession();
                    previewSession.Stopped += OnPreviewSessionStopped;
                }

                return previewSession;
            }
        }

        private bool IsCalibrating => previewSession != null && previewSession.IsActive;

        private void OnEnable()
        {
            titleContent = new GUIContent("Gaze Calibration");
            minSize = new Vector2(430f, 600f);
            Undo.undoRedoPerformed += OnUndoRedo;
        }

        private void OnDisable()
        {
            Undo.undoRedoPerformed -= OnUndoRedo;
            FinishCalibration(false, false);
            if (previewSession != null)
            {
                previewSession.Stopped -= OnPreviewSessionStopped;
                previewSession.Dispose();
            }
            previewSession = null;
        }

        private void OnGUI()
        {
            // A session can stop itself on play-mode changes or invalid mappings.
            // Treat that as Cancel so a broken/closed preview never commits a draft.
            if (!IsCalibrating && workingProfile != null)
                FinishCalibration(false, false);

            scrollPosition = EditorGUILayout.BeginScrollView(scrollPosition);
            DrawHeader();
            EditorGUILayout.Space(6f);
            DrawDriverField();
            EditorGUILayout.Space(6f);
            DrawSetup();
            EditorGUILayout.Space(8f);
            DrawCalibrationControls();

            if (IsCalibrating)
            {
                EditorGUILayout.Space(10f);
                DrawDirectionLimits();
                EditorGUILayout.Space(10f);
                DrawPoseTools();
                EditorGUILayout.Space(8f);
                DrawCalibrationPad();
                EditorGUILayout.Space(10f);
                DrawSelectedPoseEditor();
            }

            if (!string.IsNullOrEmpty(feedback))
            {
                EditorGUILayout.Space(8f);
                EditorGUILayout.HelpBox(feedback, feedbackType);
            }

            EditorGUILayout.EndScrollView();
        }

        private void DrawHeader()
        {
            EditorGUILayout.LabelField("Blendshape Gaze Calibration", EditorStyles.boldLabel);
            EditorGUILayout.HelpBox(
                "Tune Center, Left, Right, Up and Down directly here. Generate the four corners, " +
                "then use the pad to inspect interpolation without editing the renderer inspector. " +
                "Direction labels use the character's perspective.",
                MessageType.Info);
        }

        private void DrawDriverField()
        {
            using (new EditorGUI.DisabledScope(IsCalibrating))
            {
                var nextDriver = (BlendshapeGazeDriver)EditorGUILayout.ObjectField(
                    "Gaze Driver",
                    driver,
                    typeof(BlendshapeGazeDriver),
                    true);
                if (nextDriver != driver)
                    SetDriver(nextDriver);
            }

            if (driver == null)
                EditorGUILayout.HelpBox("Assign a Blendshape Gaze Driver to begin.", MessageType.Warning);
        }

        private void DrawSetup()
        {
            EditorGUILayout.LabelField("1. Setup", EditorStyles.boldLabel);
            if (driver == null)
                return;

            using (new EditorGUI.DisabledScope(IsCalibrating))
            {
                var nextRenderer = (SkinnedMeshRenderer)EditorGUILayout.ObjectField(
                    "Target Renderer",
                    driver.TargetRenderer,
                    typeof(SkinnedMeshRenderer),
                    true);
                if (nextRenderer != driver.TargetRenderer)
                    AssignRenderer(nextRenderer);

                var nextProfile = (BlendshapeGazeProfile)EditorGUILayout.ObjectField(
                    "Calibration Profile",
                    driver.Profile,
                    typeof(BlendshapeGazeProfile),
                    false);
                if (nextProfile != driver.Profile)
                    AssignProfile(nextProfile);

                var nextOrigin = (Transform)EditorGUILayout.ObjectField(
                    "Gaze Origin",
                    driver.GazeOrigin,
                    typeof(Transform),
                    true);
                if (nextOrigin != driver.GazeOrigin)
                    AssignOrigin(nextOrigin);
            }

            DrawSetupChecklist();

            using (new EditorGUI.DisabledScope(IsCalibrating))
            {
                using (new EditorGUILayout.HorizontalScope())
                {
                    using (new EditorGUI.DisabledScope(driver.Profile != null))
                    {
                        if (GUILayout.Button("Create Profile"))
                            CreateProfile();
                    }

                    using (new EditorGUI.DisabledScope(
                               driver.TargetRenderer == null
                               || driver.TargetRenderer.sharedMesh == null
                               || driver.Profile == null))
                    {
                        if (GUILayout.Button("Sync ARKit EyeLook 8"))
                            SyncEyeLookChannels();
                    }
                }

                using (new EditorGUI.DisabledScope(driver.Profile == null))
                {
                    if (GUILayout.Button("Edit Profile Channels Manually"))
                    {
                        Selection.activeObject = driver.Profile;
                        EditorGUIUtility.PingObject(driver.Profile);
                        SetFeedback(
                            "Edit Channels in the selected profile Inspector, then return here to calibrate.",
                            MessageType.Info);
                    }
                }

                using (new EditorGUILayout.HorizontalScope())
                {
                    using (new EditorGUI.DisabledScope(driver.TargetRenderer == null))
                    {
                        if (GUILayout.Button(driver.GazeOrigin == null
                                ? "Auto Create Gaze Origin"
                                : "Recalculate Gaze Origin"))
                            AutoCreateOrRecalculateOrigin();
                    }

                    using (new EditorGUI.DisabledScope(driver.GazeOrigin == null))
                    {
                        if (GUILayout.Button("Align Origin Forward to Scene Camera"))
                            AlignOriginForwardToSceneCamera();
                    }
                }
            }

            if (driver.HeadReference != null)
            {
                EditorGUILayout.HelpBox(
                    "Gaze Origin rotation is the runtime direction basis. Head Reference is used only as a fallback " +
                    "when Gaze Origin is not assigned.",
                    MessageType.Info);
            }
        }

        private void DrawSetupChecklist()
        {
            var renderer = driver.TargetRenderer;
            var profile = driver.Profile;
            var hasRendererMesh = renderer != null && renderer.sharedMesh != null;
            var sourceMeshMatches = profile == null
                                    || profile.SourceMesh == null
                                    || (hasRendererMesh && profile.SourceMesh == renderer.sharedMesh);
            DrawCheck("Renderer and mesh", hasRendererMesh && sourceMeshMatches);
            DrawCheck("Calibration profile", profile != null);
            DrawCheck("Gaze origin", driver.GazeOrigin != null);

            if (hasRendererMesh && profile != null && !sourceMeshMatches)
            {
                EditorGUILayout.HelpBox(
                    $"Profile mesh '{profile.SourceMesh.name}' does not match renderer mesh " +
                    $"'{renderer.sharedMesh.name}'. Sync the intended Face mesh before calibration.",
                    MessageType.Error);
            }

            var configured = profile != null ? profile.Channels.Count : 0;
            var mapped = 0;
            var missing = new List<string>();
            if (renderer != null && renderer.sharedMesh != null && profile != null)
            {
                for (var index = 0; index < profile.Channels.Count; index++)
                {
                    var channel = profile.Channels[index];
                    if (channel != null && profile.FindBlendShapeIndex(renderer.sharedMesh, index) >= 0)
                        mapped++;
                    else
                        missing.Add(channel != null ? channel.BlendShapeName : $"Channel {index + 1}");
                }
            }

            DrawCheck($"Channels mapped ({mapped}/{configured})", configured > 0 && mapped == configured);
            if (missing.Count > 0)
            {
                EditorGUILayout.HelpBox(
                    "Missing on renderer: " + string.Join(", ", missing),
                    MessageType.Warning);
            }
        }

        private static void DrawCheck(string label, bool complete)
        {
            var previous = GUI.color;
            GUI.color = complete ? new Color(0.55f, 0.95f, 0.6f) : new Color(1f, 0.68f, 0.45f);
            EditorGUILayout.LabelField(complete ? "\u2713  " + label : "\u25cb  " + label);
            GUI.color = previous;
        }

        private void DrawCalibrationControls()
        {
            EditorGUILayout.LabelField("2. Calibration", EditorStyles.boldLabel);
            if (!IsCalibrating)
            {
                var canStart = CanStartCalibration(out var reason);
                using (new EditorGUI.DisabledScope(!canStart))
                {
                    if (GUILayout.Button("Start Calibration", GUILayout.Height(34f)))
                        StartCalibration();
                }

                if (driver != null && !canStart && !string.IsNullOrEmpty(reason))
                    EditorGUILayout.HelpBox(reason, MessageType.Warning);
            }
            else
            {
                var profile = Session.Profile;
                var cardinalCount = CountCaptured(profile, BasicPoints);
                var totalCount = CountCaptured(profile, AllPoints);
                var fullyCalibrated = profile != null && profile.IsFullyCalibrated;
                EditorGUILayout.HelpBox(
                    $"Cardinal poses: {cardinalCount}/5    Full grid: {totalCount}/9" +
                    (fullyCalibrated
                        ? "\nReady for runtime interpolation."
                        : "\nYou can save a draft now, but runtime gaze stays inactive until 9/9."),
                    fullyCalibrated ? MessageType.Info : MessageType.Warning);

                using (new EditorGUILayout.HorizontalScope())
                {
                    var previous = GUI.backgroundColor;
                    GUI.backgroundColor = new Color(0.55f, 0.85f, 0.65f);
                    var applyLabel = fullyCalibrated
                        ? "Apply & Finish"
                        : "Save Draft & Finish";
                    if (GUILayout.Button(applyLabel, GUILayout.Height(34f)))
                    {
                        GUI.backgroundColor = previous;
                        FinishCalibration(true, true);
                        return;
                    }
                    GUI.backgroundColor = previous;

                    if (GUILayout.Button("Cancel & Revert", GUILayout.Height(34f)))
                    {
                        FinishCalibration(false, true);
                        return;
                    }
                }

                EditorGUILayout.HelpBox(
                    "Apply saves profile edits. Cancel or closing this window restores both the profile snapshot " +
                    "and the face pose from when calibration started.",
                    MessageType.None);
            }
        }

        private void DrawDirectionLimits()
        {
            var profile = Session.Profile;
            if (profile == null)
                return;

            EditorGUILayout.LabelField("3. Asymmetric View Limits", EditorStyles.boldLabel);
            EditorGUILayout.HelpBox(
                "Angles outside these limits clamp to the closest edge pose. Values are in character-local degrees; " +
                "Left/Right are named from the character's perspective.",
                MessageType.None);

            var left = profile.MinYaw;
            var right = profile.MaxYaw;
            var down = profile.MinPitch;
            var up = profile.MaxPitch;

            EditorGUI.BeginChangeCheck();
            using (new EditorGUILayout.HorizontalScope())
            {
                left = EditorGUILayout.Slider(new GUIContent("Left Yaw", "Negative yaw limit."), left, -90f, -0.1f);
                right = EditorGUILayout.Slider(new GUIContent("Right Yaw", "Positive yaw limit."), right, 0.1f, 90f);
            }
            using (new EditorGUILayout.HorizontalScope())
            {
                down = EditorGUILayout.Slider(new GUIContent("Down Pitch", "Negative pitch limit."), down, -90f, -0.1f);
                up = EditorGUILayout.Slider(new GUIContent("Up Pitch", "Positive pitch limit."), up, 0.1f, 90f);
            }

            if (EditorGUI.EndChangeCheck())
            {
                Undo.RecordObject(profile, "Change Gaze View Limits");
                profile.SetDirectionLimits(left, right, down, up);
                MarkProfileDirty();
            }
        }

        private void DrawPoseTools()
        {
            var profile = Session.Profile;
            if (profile == null)
                return;

            EditorGUILayout.LabelField("4. Pose Tools", EditorStyles.boldLabel);
            using (new EditorGUILayout.HorizontalScope())
            {
                if (GUILayout.Button("Auto Seed ARKit"))
                    SeedStandardEyeLook();

                using (new EditorGUI.DisabledScope(!profile.CanGenerateCorners))
                {
                    if (GUILayout.Button("Generate Corners"))
                        GenerateCorners();
                }
            }

            var opposite = GetHorizontalOpposite(selectedPoint);
            using (new EditorGUILayout.HorizontalScope())
            {
                using (new EditorGUI.DisabledScope(!profile.IsSampleCaptured(selectedPoint)))
                {
                    var mirrorLabel = opposite == selectedPoint
                        ? "Swap Left/Right Values"
                        : $"Mirror to {GetPointLabel(opposite)}";
                    if (GUILayout.Button(mirrorLabel))
                        MirrorSelected(opposite);
                }

                using (new EditorGUI.DisabledScope(
                           selectedPoint != GazeCalibrationPoint.Center
                           && !profile.IsSampleCaptured(GazeCalibrationPoint.Center)))
                {
                    var resetLabel = selectedPoint == GazeCalibrationPoint.Center
                        ? "Reset Center to Session Start"
                        : "Reset Pose to Center";
                    if (GUILayout.Button(resetLabel))
                        ResetSelectedToCenter();
                }
            }

            using (new EditorGUILayout.HorizontalScope())
            {
                using (new EditorGUI.DisabledScope(!profile.IsSampleCaptured(selectedPoint)))
                {
                    if (GUILayout.Button("Copy Pose"))
                        CopySelected();
                }

                var canPaste = copiedWeights != null
                               && copiedWeights.Length == profile.Channels.Count
                               && copiedLayout == GetChannelLayout(profile);
                using (new EditorGUI.DisabledScope(!canPaste))
                {
                    if (GUILayout.Button("Paste Pose"))
                        PasteSelected();
                }
            }
        }

        private void DrawCalibrationPad()
        {
            var profile = Session.Profile;
            if (profile == null)
                return;

            EditorGUILayout.LabelField("5. Gaze Pad", EditorStyles.boldLabel);
            using (new EditorGUILayout.HorizontalScope())
            {
                var nextAdvanced = EditorGUILayout.ToggleLeft(
                    "Advanced 3x3",
                    advancedGrid,
                    GUILayout.Width(125f));
                if (advancedGrid && !nextAdvanced && !ContainsPoint(BasicPoints, selectedPoint))
                    SelectPoint(GazeCalibrationPoint.Center);
                advancedGrid = nextAdvanced;
                liveInterpolation = EditorGUILayout.ToggleLeft(
                    "Live Interpolation",
                    liveInterpolation,
                    GUILayout.Width(145f));
            }

            if (liveInterpolation && !profile.IsFullyCalibrated)
            {
                EditorGUILayout.HelpBox(
                    "Live interpolation becomes available after all nine points exist. Tune five points, then Generate Corners.",
                    MessageType.None);
            }

            var size = Mathf.Clamp(position.width - 72f, PadMinimumSize, PadMaximumSize);
            using (new EditorGUILayout.HorizontalScope())
            {
                GUILayout.FlexibleSpace();
                var padRect = GUILayoutUtility.GetRect(size, size, GUILayout.Width(size), GUILayout.Height(size));
                GUILayout.FlexibleSpace();
                DrawPadBackground(padRect);
                HandlePadInput(padRect, advancedGrid ? AllPoints : BasicPoints);
                DrawPadMarkers(padRect, advancedGrid ? AllPoints : BasicPoints);
            }

            EditorGUILayout.LabelField(
                $"Selected: {GetPointLabel(selectedPoint)}" +
                (!profile.IsSampleCaptured(selectedPoint)
                    ? "   |   Draft (move a slider to save this pose)"
                    : string.Empty) +
                (Session.IsFreePreview
                    ? $"   |   Live: ({Session.PreviewCoordinates.x:0.00}, {Session.PreviewCoordinates.y:0.00})"
                    : string.Empty),
                EditorStyles.centeredGreyMiniLabel);
        }

        private static void DrawPadBackground(Rect rect)
        {
            EditorGUI.DrawRect(rect, new Color(0.105f, 0.115f, 0.13f));
            EditorGUI.DrawRect(new Rect(rect.x + rect.width * 0.5f, rect.y, 1f, rect.height), new Color(0.35f, 0.38f, 0.42f));
            EditorGUI.DrawRect(new Rect(rect.x, rect.y + rect.height * 0.5f, rect.width, 1f), new Color(0.35f, 0.38f, 0.42f));
            EditorGUI.DrawRect(new Rect(rect.x, rect.y, rect.width, 1f), new Color(0.45f, 0.48f, 0.52f));
            EditorGUI.DrawRect(new Rect(rect.x, rect.yMax - 1f, rect.width, 1f), new Color(0.45f, 0.48f, 0.52f));
            EditorGUI.DrawRect(new Rect(rect.x, rect.y, 1f, rect.height), new Color(0.45f, 0.48f, 0.52f));
            EditorGUI.DrawRect(new Rect(rect.xMax - 1f, rect.y, 1f, rect.height), new Color(0.45f, 0.48f, 0.52f));
        }

        private void DrawPadMarkers(Rect rect, IReadOnlyList<GazeCalibrationPoint> visiblePoints)
        {
            var profile = Session.Profile;
            var markerStyle = new GUIStyle(EditorStyles.miniButton)
            {
                alignment = TextAnchor.MiddleCenter,
                fontStyle = FontStyle.Bold
            };

            for (var index = 0; index < visiblePoints.Count; index++)
            {
                var point = visiblePoints[index];
                var center = PointToPad(rect, CoordinatesFor(point));
                var marker = new Rect(
                    center.x - MarkerSize * 0.5f,
                    center.y - MarkerSize * 0.5f,
                    MarkerSize,
                    MarkerSize);

                var previous = GUI.backgroundColor;
                if (point == selectedPoint)
                    GUI.backgroundColor = new Color(0.35f, 0.72f, 1f);
                else if (profile.IsSampleCaptured(point))
                    GUI.backgroundColor = new Color(0.45f, 0.86f, 0.5f);
                else
                    GUI.backgroundColor = new Color(1f, 0.65f, 0.3f);

                GUI.Label(marker, GetPointGlyph(point), markerStyle);
                GUI.backgroundColor = previous;
            }

            if (Session.IsFreePreview)
            {
                var live = PointToPad(rect, Session.PreviewCoordinates);
                EditorGUI.DrawRect(new Rect(live.x - 3f, live.y - 3f, 6f, 6f), Color.cyan);
            }
        }

        private void HandlePadInput(Rect rect, IReadOnlyList<GazeCalibrationPoint> visiblePoints)
        {
            var current = Event.current;
            var controlId = GUIUtility.GetControlID("StreamingleGazeCalibrationPad".GetHashCode(), FocusType.Passive, rect);

            if (current.type == EventType.MouseDown && current.button == 0 && rect.Contains(current.mousePosition))
            {
                var closest = FindClosestPoint(rect, current.mousePosition, visiblePoints, out var distance);
                if (distance <= MarkerHitRadius || !liveInterpolation || !Session.Profile.IsFullyCalibrated)
                {
                    SelectPoint(closest);
                }
                else
                {
                    GUIUtility.hotControl = controlId;
                    PreviewPadPosition(rect, current.mousePosition);
                }
                current.Use();
            }
            else if (current.type == EventType.MouseDrag && GUIUtility.hotControl == controlId)
            {
                PreviewPadPosition(rect, current.mousePosition);
                current.Use();
            }
            else if (current.type == EventType.MouseUp && GUIUtility.hotControl == controlId)
            {
                PreviewPadPosition(rect, current.mousePosition);
                GUIUtility.hotControl = 0;
                current.Use();
            }
        }

        private void PreviewPadPosition(Rect rect, Vector2 mousePosition)
        {
            if (!liveInterpolation || !Session.Profile.IsFullyCalibrated)
                return;

            var normalized = new Vector2(
                Mathf.Clamp((mousePosition.x - rect.x) / rect.width * 2f - 1f, -1f, 1f),
                Mathf.Clamp((rect.yMax - mousePosition.y) / rect.height * 2f - 1f, -1f, 1f));
            Session.PreviewNormalized(normalized);
            SceneView.RepaintAll();
            Repaint();
        }

        private void DrawSelectedPoseEditor()
        {
            var profile = Session.Profile;
            if (profile == null || profile.Channels.Count == 0)
                return;

            EnsureDraftLoaded();
            if (draftWeights.Length != profile.Channels.Count)
                return;

            EditorGUILayout.LabelField($"6. {GetPointLabel(selectedPoint)} Pose", EditorStyles.boldLabel);
            EditorGUILayout.HelpBox(
                profile.IsSampleCaptured(selectedPoint)
                    ? "The face preview updates immediately. Apply & Finish keeps this pose."
                    : "This is a temporary draft copied from Center. Moving any slider saves this point; " +
                      "selecting it alone does not mark it complete.",
                profile.IsSampleCaptured(selectedPoint) ? MessageType.None : MessageType.Info);

            EditorGUI.BeginChangeCheck();
            DrawChannelGroup("Left Eye", ChannelSide.Left, profile, draftWeights);
            DrawChannelGroup("Right Eye", ChannelSide.Right, profile, draftWeights);
            DrawChannelGroup("Other / Correctives", ChannelSide.Other, profile, draftWeights);
            if (EditorGUI.EndChangeCheck())
            {
                Undo.RecordObject(profile, "Edit Gaze Calibration Pose");
                profile.SetSample(selectedPoint, draftWeights);
                MarkProfileDirty();
                Session.PreviewWeights(draftWeights, selectedPoint);
                SceneView.RepaintAll();
            }
        }

        private static void DrawChannelGroup(
            string title,
            ChannelSide side,
            BlendshapeGazeProfile profile,
            float[] weights)
        {
            var hasAny = false;
            for (var index = 0; index < profile.Channels.Count; index++)
            {
                var channel = profile.Channels[index];
                if (GetChannelSide(channel != null ? channel.BlendShapeName : null) == side)
                {
                    hasAny = true;
                    break;
                }
            }

            if (!hasAny)
                return;

            EditorGUILayout.Space(3f);
            EditorGUILayout.LabelField(title, EditorStyles.miniBoldLabel);
            for (var index = 0; index < profile.Channels.Count; index++)
            {
                var channel = profile.Channels[index];
                if (channel == null || GetChannelSide(channel.BlendShapeName) != side)
                    continue;

                var tooltip = channel.Usage == GazeChannelUsage.Corrective
                    ? $"Corrective ({channel.CorrectiveBlendMode})"
                    : "EyeLook override";
                var minimum = channel.Usage == GazeChannelUsage.Corrective ? -100f : 0f;
                weights[index] = EditorGUILayout.Slider(
                    new GUIContent(channel.BlendShapeName, tooltip),
                    weights[index],
                    minimum,
                    100f);
            }
        }

        private void StartCalibration()
        {
            if (!CanStartCalibration(out var reason))
            {
                SetFeedback(reason, MessageType.Error);
                return;
            }

            var originalProfile = driver.Profile;
            var editableProfile = Instantiate(originalProfile);
            editableProfile.name = originalProfile.name;
            editableProfile.hideFlags = HideFlags.HideAndDontSave;

            if (!Session.Start(driver, editableProfile, out var error))
            {
                DestroyImmediate(editableProfile);
                SetFeedback(error, MessageType.Error);
                return;
            }

            calibrationProfile = originalProfile;
            workingProfile = editableProfile;
            selectedPoint = GazeCalibrationPoint.Center;
            hasDraft = false;
            LoadDraftForSelectedPoint();
            PreviewDraft();
            SetFeedback(
                "Calibration started. Tune Center first, then the four cardinal poses.",
                MessageType.Info);
            SceneView.RepaintAll();
        }

        private void FinishCalibration(bool applyChanges, bool showFeedback)
        {
            if (isFinishingCalibration)
                return;

            isFinishingCalibration = true;
            var originalProfile = calibrationProfile != null
                ? calibrationProfile
                : driver != null ? driver.Profile : null;
            var editableProfile = workingProfile != null
                ? workingProfile
                : previewSession != null ? previewSession.Profile : null;

            try
            {
                if (applyChanges && originalProfile != null && editableProfile != null
                    && originalProfile != editableProfile)
                {
                    Undo.RecordObject(originalProfile, "Apply Gaze Calibration");
                    EditorUtility.CopySerializedManagedFieldsOnly(editableProfile, originalProfile);
                    EditorUtility.SetDirty(originalProfile);
                }

                // Keep the driver override until the original renderer pose has
                // been restored and any working-copy data has been committed.
                previewSession?.Stop();

                if (applyChanges && originalProfile != null)
                {
                    AssetDatabase.SaveAssetIfDirty(originalProfile);
                    driver?.RebuildCache();
                }
            }
            finally
            {
                previewSession?.Stop();
                if (editableProfile != null && editableProfile != originalProfile)
                {
                    Undo.ClearUndo(editableProfile);
                    DestroyImmediate(editableProfile);
                }

                calibrationProfile = null;
                workingProfile = null;
                hasDraft = false;
                draftWeights = Array.Empty<float>();
                draftLayout = null;
                isFinishingCalibration = false;
                SceneView.RepaintAll();
            }

            if (showFeedback)
            {
                SetFeedback(
                    applyChanges
                        ? "Calibration changes were saved. The original face pose was restored."
                        : "Calibration changes were reverted. The original face pose was restored.",
                    MessageType.Info);
            }
        }

        private void OnPreviewSessionStopped()
        {
            if (isFinishingCalibration || workingProfile == null)
                return;

            FinishCalibration(false, false);
            Repaint();
        }

        private void EnsureDraftLoaded()
        {
            if (!IsCalibrating || Session.Profile == null)
                return;

            var layout = GetChannelLayout(Session.Profile);
            if (hasDraft
                && draftPoint == selectedPoint
                && draftWeights.Length == Session.Profile.Channels.Count
                && string.Equals(draftLayout, layout, StringComparison.Ordinal))
                return;

            LoadDraftForSelectedPoint();
        }

        private void LoadDraftForSelectedPoint()
        {
            if (!IsCalibrating || Session.Profile == null)
                return;

            var profile = Session.Profile;
            if (profile.IsSampleCaptured(selectedPoint))
            {
                draftWeights = profile.GetSampleWeightsCopy(selectedPoint);
            }
            else if (selectedPoint != GazeCalibrationPoint.Center
                && profile.IsSampleCaptured(GazeCalibrationPoint.Center))
            {
                draftWeights = profile.GetSampleWeightsCopy(GazeCalibrationPoint.Center);
            }
            else
            {
                draftWeights = Session.GetOriginalWeightsCopy();
            }

            if (draftWeights.Length != profile.Channels.Count)
                draftWeights = new float[profile.Channels.Count];

            draftPoint = selectedPoint;
            draftLayout = GetChannelLayout(profile);
            hasDraft = true;
        }

        private void PreviewDraft()
        {
            if (!IsCalibrating)
                return;
            EnsureDraftLoaded();
            if (hasDraft)
                Session.PreviewWeights(draftWeights, selectedPoint);
        }

        private void ReloadDraftAndPreview()
        {
            hasDraft = false;
            LoadDraftForSelectedPoint();
            PreviewDraft();
            SceneView.RepaintAll();
            Repaint();
        }

        private void SelectPoint(GazeCalibrationPoint point)
        {
            selectedPoint = point;
            hasDraft = false;
            LoadDraftForSelectedPoint();
            PreviewDraft();
            SceneView.RepaintAll();
            Repaint();
        }

        private void SeedStandardEyeLook()
        {
            var profile = Session.Profile;
            if (HasAnyCapturedSamples(profile)
                && !EditorUtility.DisplayDialog(
                    "Replace Calibration Poses?",
                    "Auto Seed ARKit replaces all nine calibration poses with standard 0/100 values.",
                    "Replace",
                    "Cancel"))
                return;

            Undo.RecordObject(profile, "Auto Seed ARKit Gaze Poses");
            if (!profile.SeedStandardEyeLook())
            {
                SetFeedback("Auto Seed requires all eight standard ARKit eyeLook channels.", MessageType.Error);
                return;
            }

            MarkProfileDirty();
            ReloadDraftAndPreview();
            SetFeedback("Seeded five cardinal poses and generated four corners.", MessageType.Info);
        }

        private void GenerateCorners()
        {
            var profile = Session.Profile;
            var hasExistingCorner = profile.IsSampleCaptured(GazeCalibrationPoint.DownLeft)
                                    || profile.IsSampleCaptured(GazeCalibrationPoint.DownRight)
                                    || profile.IsSampleCaptured(GazeCalibrationPoint.UpLeft)
                                    || profile.IsSampleCaptured(GazeCalibrationPoint.UpRight);
            if (hasExistingCorner
                && !EditorUtility.DisplayDialog(
                    "Regenerate Corners?",
                    "This replaces all four corner poses using Center + horizontal delta + vertical delta.",
                    "Regenerate",
                    "Cancel"))
                return;

            Undo.RecordObject(profile, "Generate Gaze Corner Poses");
            if (!profile.GenerateCornersFromCardinals())
            {
                SetFeedback("Center, Left, Right, Up and Down must be completed first.", MessageType.Warning);
                return;
            }

            MarkProfileDirty();
            ReloadDraftAndPreview();
            SetFeedback("Generated four corner poses. Enable Advanced 3x3 to refine them.", MessageType.Info);
        }

        private void MirrorSelected(GazeCalibrationPoint destination)
        {
            var profile = Session.Profile;
            if (destination != selectedPoint && profile.IsSampleCaptured(destination)
                && !EditorUtility.DisplayDialog(
                    "Replace Mirrored Pose?",
                    $"{GetPointLabel(destination)} already contains a pose.",
                    "Replace",
                    "Cancel"))
                return;

            Undo.RecordObject(profile, "Mirror Gaze Calibration Pose");
            if (!profile.CopySample(selectedPoint, destination, true))
                return;

            MarkProfileDirty();
            SelectPoint(destination);
        }

        private void ResetSelectedToCenter()
        {
            var profile = Session.Profile;
            float[] values;
            if (selectedPoint == GazeCalibrationPoint.Center)
                values = Session.GetOriginalWeightsCopy();
            else
                values = profile.GetSampleWeightsCopy(GazeCalibrationPoint.Center);

            if (values.Length != profile.Channels.Count)
                return;

            Undo.RecordObject(profile, "Reset Gaze Pose to Center");
            profile.SetSample(selectedPoint, values);
            MarkProfileDirty();
            ReloadDraftAndPreview();
        }

        private void CopySelected()
        {
            copiedWeights = Session.Profile.GetSampleWeightsCopy(selectedPoint);
            copiedLayout = GetChannelLayout(Session.Profile);
            SetFeedback($"Copied {GetPointLabel(selectedPoint)} pose.", MessageType.Info);
        }

        private void PasteSelected()
        {
            var profile = Session.Profile;
            if (copiedWeights == null || copiedWeights.Length != profile.Channels.Count
                                      || copiedLayout != GetChannelLayout(profile))
                return;

            Undo.RecordObject(profile, "Paste Gaze Calibration Pose");
            profile.SetSample(selectedPoint, (float[])copiedWeights.Clone());
            MarkProfileDirty();
            ReloadDraftAndPreview();
        }

        private void CreateProfile()
        {
            if (driver == null)
                return;

            var defaultName = driver.gameObject.name + "_GazeProfile";
            var path = EditorUtility.SaveFilePanelInProject(
                "Create Gaze Calibration Profile",
                defaultName,
                "asset",
                "Choose where to create the gaze calibration profile.");
            if (string.IsNullOrWhiteSpace(path))
                return;

            var profile = CreateInstance<BlendshapeGazeProfile>();
            profile.name = System.IO.Path.GetFileNameWithoutExtension(path);
            AssetDatabase.CreateAsset(profile, path);
            Undo.RegisterCreatedObjectUndo(profile, "Create Gaze Calibration Profile");
            AssignProfile(profile);
            EditorGUIUtility.PingObject(profile);
            SetFeedback("Created and assigned a calibration profile.", MessageType.Info);
        }

        private void SyncEyeLookChannels()
        {
            var profile = driver.Profile;
            var renderer = driver.TargetRenderer;
            if (profile == null || renderer == null)
                return;

            if (HasAnyCapturedSamples(profile)
                && !EditorUtility.DisplayDialog(
                    "Resync EyeLook Channels?",
                    "Changing the channel layout invalidates existing calibration poses.",
                    "Sync and Invalidate",
                    "Cancel"))
                return;

            Undo.RecordObject(profile, "Sync ARKit EyeLook Channels");
            if (!profile.SyncEyeLookChannels(renderer))
            {
                SetFeedback(
                    "The renderer must contain all eight ARKit eyeLook blendshapes (prefixes are allowed).",
                    MessageType.Error);
                return;
            }

            EditorUtility.SetDirty(profile);
            driver.RebuildCache();
            SetFeedback("Mapped all eight ARKit eyeLook channels.", MessageType.Info);
        }

        private void AutoCreateOrRecalculateOrigin()
        {
            var renderer = driver.TargetRenderer;
            if (renderer == null)
                return;

            if (EditorUtility.IsPersistent(driver) || EditorUtility.IsPersistent(renderer))
            {
                SetFeedback(
                    "Open the character in Prefab Mode or use a scene instance before creating Gaze Origin.",
                    MessageType.Error);
                return;
            }

            var estimate = GazeOriginEstimator.Estimate(renderer, GetConfiguredChannelNames(driver.Profile));
            var origin = driver.GazeOrigin;
            var parent = ResolveOriginParent(renderer);
            if (parent == null && origin != null)
                parent = origin.parent;
            if (parent == null)
            {
                SetFeedback(
                    "Could not find the character Head. Assign Head Reference, then create Gaze Origin again.",
                    MessageType.Error);
                return;
            }

            var created = false;
            if (origin == null)
            {
                origin = parent.Find("Face_GazeOrigin");
                if (origin == null)
                {
                    var originObject = new GameObject("Face_GazeOrigin");
                    Undo.RegisterCreatedObjectUndo(originObject, "Create Gaze Origin");
                    origin = originObject.transform;
                    created = true;
                }

                Undo.RecordObject(driver, "Assign Gaze Origin");
                driver.GazeOrigin = origin;
                EditorUtility.SetDirty(driver);
                PrefabUtility.RecordPrefabInstancePropertyModifications(driver);
            }

            if (!created)
                Undo.RecordObject(origin, "Recalculate Gaze Origin");

            if (origin.parent != parent)
                Undo.SetTransformParent(origin, parent, "Parent Gaze Origin Under Head");
            if (created)
                origin.rotation = parent.rotation;

            origin.position = estimate.WorldPosition;
            EditorUtility.SetDirty(origin);
            PrefabUtility.RecordPrefabInstancePropertyModifications(origin);
            driver.RebuildCache();
            SetFeedback(
                $"{estimate.Message} Parent: {parent.name}.",
                estimate.UsedEyeBlendShapes ? MessageType.Info : MessageType.Warning);
            SceneView.RepaintAll();
        }

        private Transform ResolveOriginParent(SkinnedMeshRenderer renderer)
        {
            if (driver.HeadReference != null)
                return driver.HeadReference;

            var animator = renderer != null
                ? renderer.GetComponentInParent<Animator>(true)
                : null;
            if (animator == null)
                animator = driver.GetComponentInParent<Animator>(true);
            if (animator != null && animator.avatar != null && animator.isHuman)
            {
                var humanoidHead = animator.GetBoneTransform(HumanBodyBones.Head);
                if (humanoidHead != null)
                    return humanoidHead;
            }

            Transform suffixMatch = null;
            Transform containsMatch = null;
            var bones = renderer != null ? renderer.bones : null;
            if (bones != null)
            {
                for (var index = 0; index < bones.Length; index++)
                {
                    var bone = bones[index];
                    if (bone == null)
                        continue;
                    var normalizedName = Normalize(bone.name);
                    if (normalizedName == "head")
                        return bone;
                    if (suffixMatch == null && normalizedName.EndsWith("head", StringComparison.Ordinal))
                        suffixMatch = bone;
                    else if (containsMatch == null && normalizedName.Contains("head"))
                        containsMatch = bone;
                }
            }

            return suffixMatch != null ? suffixMatch : containsMatch;
        }

        private void AlignOriginForwardToSceneCamera()
        {
            var origin = driver.GazeOrigin;
            var sceneCamera = SceneView.lastActiveSceneView != null
                ? SceneView.lastActiveSceneView.camera
                : null;
            if (origin == null || sceneCamera == null)
            {
                SetFeedback("Open a Scene view and assign Gaze Origin first.", MessageType.Error);
                return;
            }

            var direction = sceneCamera.transform.position - origin.position;
            if (direction.sqrMagnitude < 0.000001f)
            {
                SetFeedback("The Scene camera is too close to Gaze Origin.", MessageType.Error);
                return;
            }

            var up = origin.parent != null
                ? origin.parent.up
                : driver.HeadReference != null ? driver.HeadReference.up : Vector3.up;
            if (Mathf.Abs(Vector3.Dot(direction.normalized, up.normalized)) > 0.999f)
                up = Vector3.up;

            Undo.RecordObject(origin, "Align Gaze Origin Forward");
            origin.rotation = Quaternion.LookRotation(direction.normalized, up);
            EditorUtility.SetDirty(origin);
            PrefabUtility.RecordPrefabInstancePropertyModifications(origin);
            driver.RebuildCache();
            SetFeedback(
                "Gaze Origin forward now points toward the Scene camera and remains authoritative at runtime.",
                MessageType.Info);
            SceneView.RepaintAll();
        }

        private void AssignRenderer(SkinnedMeshRenderer renderer)
        {
            Undo.RecordObject(driver, "Assign Gaze Target Renderer");
            driver.TargetRenderer = renderer;
            MarkDriverDirty();
        }

        private void AssignProfile(BlendshapeGazeProfile profile)
        {
            Undo.RecordObject(driver, "Assign Gaze Calibration Profile");
            driver.Profile = profile;
            MarkDriverDirty();
        }

        private void AssignOrigin(Transform origin)
        {
            Undo.RecordObject(driver, "Assign Gaze Origin");
            driver.GazeOrigin = origin;
            MarkDriverDirty();
        }

        private void MarkDriverDirty()
        {
            EditorUtility.SetDirty(driver);
            PrefabUtility.RecordPrefabInstancePropertyModifications(driver);
            driver.RebuildCache();
        }

        private void MarkProfileDirty()
        {
            if (Session.Profile != null)
                EditorUtility.SetDirty(Session.Profile);
            Repaint();
        }

        private void SetDriver(BlendshapeGazeDriver value)
        {
            if (driver == value)
                return;

            FinishCalibration(false, false);
            driver = value;
            feedback = null;
            Repaint();
        }

        private void OnUndoRedo()
        {
            if (IsCalibrating)
            {
                ReloadDraftAndPreview();
            }
            Repaint();
        }

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

        private bool CanStartCalibration(out string reason)
        {
            reason = null;
            if (driver == null)
            {
                reason = "Assign a Blendshape Gaze Driver first.";
                return false;
            }

            var renderer = driver.TargetRenderer;
            if (renderer == null || renderer.sharedMesh == null)
            {
                reason = "Assign a Target Renderer with a Face mesh.";
                return false;
            }

            var profile = driver.Profile;
            if (profile == null)
            {
                reason = "Create or assign a Calibration Profile.";
                return false;
            }

            if (!profile.IsCompatibleWith(renderer.sharedMesh))
            {
                reason = "The profile Source Mesh does not match the assigned Face mesh.";
                return false;
            }

            if (driver.GazeOrigin == null)
            {
                reason = "Create or assign Gaze Origin before calibration.";
                return false;
            }

            if (profile.Channels.Count == 0)
            {
                reason = "Sync ARKit EyeLook 8 or configure profile Channels manually.";
                return false;
            }

            for (var index = 0; index < profile.Channels.Count; index++)
            {
                var channel = profile.Channels[index];
                if (channel == null
                    || string.IsNullOrWhiteSpace(channel.BlendShapeName)
                    || profile.FindBlendShapeIndex(renderer.sharedMesh, index) < 0)
                {
                    reason = channel == null
                        ? $"Profile channel {index + 1} is invalid."
                        : $"Blendshape '{channel.BlendShapeName}' is missing on the assigned Face mesh.";
                    return false;
                }
            }

            return true;
        }

        private static int CountCaptured(
            BlendshapeGazeProfile profile,
            IReadOnlyList<GazeCalibrationPoint> points)
        {
            if (profile == null || points == null)
                return 0;
            var count = 0;
            for (var index = 0; index < points.Count; index++)
            {
                if (profile.IsSampleCaptured(points[index]))
                    count++;
            }
            return count;
        }

        private static bool ContainsPoint(
            IReadOnlyList<GazeCalibrationPoint> points,
            GazeCalibrationPoint point)
        {
            for (var index = 0; index < points.Count; index++)
            {
                if (points[index] == point)
                    return true;
            }
            return false;
        }

        private static bool HasAnyCapturedSamples(BlendshapeGazeProfile profile)
        {
            if (profile == null)
                return false;
            for (var index = 0; index < 9; index++)
            {
                if (profile.IsSampleCaptured((GazeCalibrationPoint)index))
                    return true;
            }
            return false;
        }

        private static IReadOnlyList<string> GetConfiguredChannelNames(BlendshapeGazeProfile profile)
        {
            var names = new List<string>();
            if (profile == null)
                return names;
            for (var index = 0; index < profile.Channels.Count; index++)
            {
                var channel = profile.Channels[index];
                if (channel != null && !string.IsNullOrWhiteSpace(channel.BlendShapeName))
                    names.Add(channel.BlendShapeName);
            }
            return names;
        }

        private static string GetChannelLayout(BlendshapeGazeProfile profile)
        {
            if (profile == null)
                return string.Empty;
            var names = new string[profile.Channels.Count];
            for (var index = 0; index < names.Length; index++)
            {
                var channel = profile.Channels[index];
                names[index] = channel != null ? channel.BlendShapeName : "<null>";
            }
            return string.Join("|", names);
        }

        private static GazeCalibrationPoint GetHorizontalOpposite(GazeCalibrationPoint point)
        {
            var index = (int)point;
            var row = index / 3;
            var column = index % 3;
            return (GazeCalibrationPoint)(row * 3 + (2 - column));
        }

        private static Vector2 CoordinatesFor(GazeCalibrationPoint point)
        {
            var index = (int)point;
            return new Vector2(index % 3 - 1, index / 3 - 1);
        }

        private static Vector2 PointToPad(Rect rect, Vector2 normalized)
        {
            const float inset = MarkerSize * 0.7f;
            return new Vector2(
                Mathf.Lerp(rect.x + inset, rect.xMax - inset, (normalized.x + 1f) * 0.5f),
                Mathf.Lerp(rect.yMax - inset, rect.y + inset, (normalized.y + 1f) * 0.5f));
        }

        private static GazeCalibrationPoint FindClosestPoint(
            Rect rect,
            Vector2 mousePosition,
            IReadOnlyList<GazeCalibrationPoint> points,
            out float distance)
        {
            var closest = points[0];
            distance = float.PositiveInfinity;
            for (var index = 0; index < points.Count; index++)
            {
                var candidateDistance = Vector2.Distance(
                    mousePosition,
                    PointToPad(rect, CoordinatesFor(points[index])));
                if (candidateDistance < distance)
                {
                    closest = points[index];
                    distance = candidateDistance;
                }
            }
            return closest;
        }

        private static string GetPointGlyph(GazeCalibrationPoint point)
        {
            switch (point)
            {
                case GazeCalibrationPoint.DownLeft: return "\u2199";
                case GazeCalibrationPoint.DownCenter: return "\u2193";
                case GazeCalibrationPoint.DownRight: return "\u2198";
                case GazeCalibrationPoint.CenterLeft: return "\u2190";
                case GazeCalibrationPoint.Center: return "\u25cf";
                case GazeCalibrationPoint.CenterRight: return "\u2192";
                case GazeCalibrationPoint.UpLeft: return "\u2196";
                case GazeCalibrationPoint.UpCenter: return "\u2191";
                case GazeCalibrationPoint.UpRight: return "\u2197";
                default: return "?";
            }
        }

        private static string GetPointLabel(GazeCalibrationPoint point)
        {
            switch (point)
            {
                case GazeCalibrationPoint.DownLeft: return "Down Left";
                case GazeCalibrationPoint.DownCenter: return "Down";
                case GazeCalibrationPoint.DownRight: return "Down Right";
                case GazeCalibrationPoint.CenterLeft: return "Left";
                case GazeCalibrationPoint.Center: return "Center";
                case GazeCalibrationPoint.CenterRight: return "Right";
                case GazeCalibrationPoint.UpLeft: return "Up Left";
                case GazeCalibrationPoint.UpCenter: return "Up";
                case GazeCalibrationPoint.UpRight: return "Up Right";
                default: return point.ToString();
            }
        }

        private static ChannelSide GetChannelSide(string channelName)
        {
            if (string.IsNullOrWhiteSpace(channelName))
                return ChannelSide.Other;
            var normalized = Normalize(channelName);
            if (normalized.EndsWith("left", StringComparison.Ordinal))
                return ChannelSide.Left;
            if (normalized.EndsWith("right", StringComparison.Ordinal))
                return ChannelSide.Right;
            return ChannelSide.Other;
        }

        private static string Normalize(string value)
        {
            var characters = new char[value.Length];
            var count = 0;
            for (var index = 0; index < value.Length; index++)
            {
                var character = value[index];
                if (char.IsLetterOrDigit(character))
                    characters[count++] = char.ToLowerInvariant(character);
            }
            return new string(characters, 0, count);
        }

        private enum ChannelSide
        {
            Left,
            Right,
            Other
        }
    }
}
