using UnityEditor;
using UnityEditor.EditorTools;
using UnityEditor.ShortcutManagement;
using UnityEngine;
using TD.Core;

namespace TD.Levels.Editor
{
    /// <summary>
    /// Custom scene-view tool for resizing <see cref="VolumeBase"/> volumes by dragging individual
    /// edges, instead of Unity's default symmetric BoxCollider sizing. Each drag is snapped to
    /// whole tiles (1.0 world units), so volumes always stay tile-aligned.
    /// </summary>
    /// <remarks>
    /// Activate from the scene-view toolbar when a VolumeBase is selected. Four handles appear at
    /// the midpoints of the volume's four horizontal edges (N/S/E/W). Dragging a handle moves only
    /// that edge — the opposite edge stays put.
    ///
    /// The tool refuses to operate on rotated volumes (P2-19 hard-error in bake). Rotation must
    /// be zeroed before the tool will edit. This surfaces the bake constraint at authoring time.
    ///
    /// Edits modify <see cref="BoxCollider.size"/> and <see cref="BoxCollider.center"/> together;
    /// the GameObject's Transform is left alone. This keeps the asymmetry contained to the
    /// collider component, where it belongs.
    /// </remarks>
    [EditorTool("Resize Volume Edge", typeof(VolumeBase))]
    public class VolumeEditTool : EditorTool
    {
        // Visual constants.
        private const float HandleY = 0.10f;          // world Y at which to draw handles
        private const float HandleSize = 0.30f;       // world-space radius of the handle disc
        private const float MinSize = 1.0f;           // smallest allowed size on either tile axis (1 tile)
        private const float SnapIncrement = 1.0f;     // tile size — the snap unit

        // Handle colors (X-axis red, Z-axis blue, matching Unity's standard convention).
        private static readonly Color XAxisColor = new Color(0.95f, 0.35f, 0.35f, 1f);
        private static readonly Color ZAxisColor = new Color(0.35f, 0.55f, 0.95f, 1f);

        public override void OnToolGUI(EditorWindow window)
        {
            // Multi-selection: don't draw anything when more than one volume is selected. Bulk
            // edge-edits are ambiguous (which volumes' edges align?) and out of scope for now.
            if (targets == null) return;
            int count = 0;
            foreach (var _ in targets) { count++; if (count > 1) break; }
            if (count != 1) return;

            var volume = target as VolumeBase;
            if (volume == null) return;

            var col = volume.GetComponent<BoxCollider>();
            if (col == null) return;

            // Refuse to operate on rotated volumes. Show an in-scene warning instead.
            Vector3 e = volume.transform.eulerAngles;
            if (!IsZeroRotation(e))
            {
                DrawRotationWarning(volume, e);
                return;
            }

            DrawEdgeHandles(volume, col);
        }

        private static bool IsZeroRotation(Vector3 eulerAngles)
        {
            const float tol = 0.01f;
            // Normalize each axis to (-180..180] before comparing.
            float dx = NormalizeAngle(eulerAngles.x);
            float dy = NormalizeAngle(eulerAngles.y);
            float dz = NormalizeAngle(eulerAngles.z);
            return Mathf.Abs(dx) <= tol && Mathf.Abs(dy) <= tol && Mathf.Abs(dz) <= tol;
        }

        private static float NormalizeAngle(float a)
        {
            a = a % 360f;
            if (a > 180f) a -= 360f;
            else if (a < -180f) a += 360f;
            return a;
        }

        private void DrawRotationWarning(VolumeBase volume, Vector3 euler)
        {
            var col = volume.GetComponent<BoxCollider>();
            Vector3 labelPos = col != null
                ? new Vector3(col.bounds.center.x, HandleY + 0.5f, col.bounds.center.z)
                : volume.transform.position + Vector3.up * 0.5f;

            // Use the editor's Handles label with a colored background-ish hint via a sphere.
            Handles.color = Color.red;
            Handles.SphereHandleCap(0, labelPos, Quaternion.identity, 0.2f, EventType.Repaint);
            Handles.Label(labelPos + new Vector3(0.3f, 0f, 0.3f),
                          $"VolumeEditTool: zero out rotation to edit edges (current: {euler}).");
        }

        private void DrawEdgeHandles(VolumeBase volume, BoxCollider col)
        {
            // Compute the four edge-midpoint world positions. We treat the volume's local axes as
            // world axes (zero rotation already enforced above). The transform's position can still
            // translate the volume, so we use TransformPoint to get world positions.
            //
            // BoxCollider.center and BoxCollider.size are LOCAL to the GameObject's transform.
            Vector3 localCenter = col.center;
            Vector3 localSize = col.size;
            float halfX = localSize.x * 0.5f;
            float halfZ = localSize.z * 0.5f;

            Vector3 eastLocal = localCenter + new Vector3(halfX, 0f, 0f);
            Vector3 westLocal = localCenter + new Vector3(-halfX, 0f, 0f);
            Vector3 northLocal = localCenter + new Vector3(0f, 0f, halfZ);
            Vector3 southLocal = localCenter + new Vector3(0f, 0f, -halfZ);

            Transform t = volume.transform;
            Vector3 eastWorld = WithY(t.TransformPoint(eastLocal), HandleY);
            Vector3 westWorld = WithY(t.TransformPoint(westLocal), HandleY);
            Vector3 northWorld = WithY(t.TransformPoint(northLocal), HandleY);
            Vector3 southWorld = WithY(t.TransformPoint(southLocal), HandleY);

            // Render and process each handle. Handles.Slider returns the new world position; we
            // compute the snapped tile-delta and apply it to the collider's size/center.
            DrawAxisHandle(col, eastWorld, Vector3.right, axisIsX: true, edgeIsPositive: true, XAxisColor);
            DrawAxisHandle(col, westWorld, Vector3.right, axisIsX: true, edgeIsPositive: false, XAxisColor);
            DrawAxisHandle(col, northWorld, Vector3.forward, axisIsX: false, edgeIsPositive: true, ZAxisColor);
            DrawAxisHandle(col, southWorld, Vector3.forward, axisIsX: false, edgeIsPositive: false, ZAxisColor);
        }

        // Draw a single edge handle and apply the resulting size/center change to the collider.
        // - `direction`: the world axis the handle slides along (Vector3.right for E/W, Vector3.forward for N/S).
        // - `axisIsX`: true if this handle modifies size.x/center.x; false if size.z/center.z.
        // - `edgeIsPositive`: true if this is the +axis edge (East or North); false for -axis (West or South).
        private void DrawAxisHandle(BoxCollider col, Vector3 worldPos, Vector3 direction,
                                    bool axisIsX, bool edgeIsPositive, Color color)
        {
            Handles.color = color;

            EditorGUI.BeginChangeCheck();
            // FreeMoveHandle would allow drag in any direction; we constrain to the axis using Slider.
            Vector3 newWorldPos = Handles.Slider(worldPos, direction, HandleSize, Handles.SphereHandleCap, 0f);
            if (!EditorGUI.EndChangeCheck()) return;

            // How far did the handle move along its axis, in world units?
            float worldDelta = axisIsX
                ? (newWorldPos.x - worldPos.x)
                : (newWorldPos.z - worldPos.z);

            // Snap to whole tiles. Discard sub-tile motion until the user drags a full tile.
            int tileDelta = Mathf.RoundToInt(worldDelta / SnapIncrement);
            if (tileDelta == 0) return;

            float worldDeltaSnapped = tileDelta * SnapIncrement;

            // For the negative edge (W or S), dragging "outward" means dragging toward more
            // negative axis values; the size should still grow. Normalize so a positive `outwardDelta`
            // always means "grow."
            float outwardDelta = edgeIsPositive ? worldDeltaSnapped : -worldDeltaSnapped;

            Vector3 size = col.size;

            float currentSize = axisIsX ? size.x : size.z;
            float newSize = currentSize + outwardDelta;

            // Clamp at minimum tile size. If the user tries to shrink past min, discard the input
            // (don't move the edge at all). This is more predictable than partially honoring it.
            if (newSize < MinSize) return;

            // Apply the change. The edge that's NOT being dragged should stay put. We achieve this
            // by adjusting transform.position rather than BoxCollider.center, so the collider's
            // center stays locked at (0, 0, 0) — the "center" of the volume's local frame is always
            // the geometric center of the box. The position shifts by half the size change in the
            // direction of the edge being dragged.
            //
            // Example (east edge dragged outward by 2 tiles): size.x += 2; transform.position.x += 1.
            // Example (west edge dragged outward by 1 tile):   size.x += 1; transform.position.x -= 0.5.
            //
            // Note: positions may land at half-integer values when the size is odd. That's correct
            // under the edge-aligned tile convention — bounds align with tile edges when
            //   (position - size/2)  and  (position + size/2)  are both integers,
            // which requires position to have the same fractional part as size/2.
            float positionShift = (edgeIsPositive ? 1f : -1f) * (outwardDelta * 0.5f);

            if (axisIsX)
            {
                size.x = newSize;
            }
            else
            {
                size.z = newSize;
            }

            Vector3 newPosition = col.transform.position;
            if (axisIsX) newPosition.x += positionShift;
            else         newPosition.z += positionShift;

            Undo.RecordObject(col.transform, "Resize Volume Edge");
            Undo.RecordObject(col, "Resize Volume Edge");
            col.transform.position = newPosition;
            col.size = size;
            // Force-lock collider center to zero in case it had drifted from prior edits made
            // before this behavior change. Safe to do unconditionally — by design, this tool now
            // never wants a non-zero center.
            col.center = Vector3.zero;
            EditorUtility.SetDirty(col);
            EditorUtility.SetDirty(col.transform);
        }

        private static Vector3 WithY(Vector3 v, float y)
        {
            v.y = y;
            return v;
        }

        // -------------------------------------------------------------------
        // Hotkey activation. The [Shortcut] attribute registers this with Unity's Shortcut Manager
        // (Edit → Shortcuts), where it can be rebound. The default binding is B, mnemonic for "Box".
        //
        // The shortcut activates the tool whether or not a VolumeBase is currently selected — when
        // nothing relevant is selected, the tool is "active" but draws nothing. The handles appear
        // as soon as a VolumeBase is selected.
        // -------------------------------------------------------------------

        [Shortcut("Tools/Resize Volume Edge", KeyCode.B)]
        private static void ActivateShortcut()
        {
            ToolManager.SetActiveTool<VolumeEditTool>();
        }
    }
}