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

namespace ServiceSim.EditorTools
{
    /// <summary>
    /// 人とロボットが「いま何をしているか」を、頭の上に1行で出す（Scene ビューだけ）。
    ///
    /// 見るだけのもの。読むのは、人とロボットのスクリプトがすでに持っている
    /// 「今の仕事」の値だけで、表示のための値は持たない。
    /// 当たり判定も処理の順番も変えない（Editor 側で描くだけ）。
    ///
    /// 動画に映す表示（Game ビュー）は、段階4で設計書 8.6 と一緒に作る。
    /// </summary>
    [InitializeOnLoad]
    public static class ServiceSimLabels
    {
        const string ShowKey = "ServiceSim.ShowLabels";
        const string SeatedKey = "ServiceSim.ShowSeatedLabels";
        const string WhoKey = "ServiceSim.LabelWho";
        const string MenuShow = "ServiceSim/Show Labels";
        const string MenuSeated = "ServiceSim/Show Labels (椅子を引いている客も)";
        const string MenuWhoRobot = "ServiceSim/Show Labels/ロボットだけ";
        const string MenuWhoStaff = "ServiceSim/Show Labels/ロボットとスタッフ";
        const string MenuWhoAll = "ServiceSim/Show Labels/全員";
        const string PlaceKey = "ServiceSim.ShowPlaceNames";
        const string MenuPlaces = "ServiceSim/Show Place Names（場所の名前）";

        /// <summary>出す相手（B5-3）</summary>
        enum Who { RobotOnly = 0, RobotAndStaff = 1, All = 2 }

        // ---- 見た目（B5）----
        /// <summary>字の大きさ（点）。Scene ビューの既定より大きくする</summary>
        const int FontSize = 13;

        /// <summary>帯どうしが重ならないように、上下にずらす幅（画面の点）</summary>
        const float RowHeightPx = 17f;

        /// <summary>この距離（画面の点）より近い帯は、重なっているとみなして上へずらす</summary>
        const float OverlapPx = 90f;

        // ---- 場所の名前（吹き出しと見分けがつくように、小さい字・薄い背景・枠つき）----
        /// <summary>場所の名前の字の大きさ（点）。吹き出しより小さくする</summary>
        const int PlaceFontSize = 10;

        static readonly Color PlaceFill = new Color(1.00f, 1.00f, 1.00f, 0.55f);
        static readonly Color PlaceLine = new Color(0.20f, 0.22f, 0.28f, 0.90f);
        static readonly Color PlaceText = new Color(0.10f, 0.11f, 0.14f);

        static GUIStyle _placeStyle;
        static GUIStyle _style;
        static StoreBuilder _store;
        static readonly List<StoreBuilder.TableInfo> _tables = new List<StoreBuilder.TableInfo>();

        /// <summary>この回に描いた帯の画面上の場所（重なりを避けるのに使う）</summary>
        static readonly List<Rect> _drawn = new List<Rect>();

        // 誰の字幕かを分ける、帯の左端の色（B5-1）
        static readonly Color TagRobot = new Color(0.95f, 0.95f, 0.97f);
        static readonly Color TagRobotStopped = new Color(1.00f, 0.85f, 0.25f);
        static readonly Color TagRobotStuck = new Color(1.00f, 0.30f, 0.25f);
        static readonly Color TagStaff = new Color(0.20f, 0.55f, 1.00f);
        static readonly Color TagGuest = new Color(1.00f, 0.82f, 0.15f);
        static readonly Color TagWaiting = new Color(0.65f, 0.40f, 0.95f);
        static readonly Color TagChair = new Color(1.00f, 0.55f, 0.25f);

        /// <summary>場所の名前を出すか（初期値は出す）</summary>
        static bool ShowPlaces
        {
            get { return EditorPrefs.GetBool(PlaceKey, true); }
            set { EditorPrefs.SetBool(PlaceKey, value); }
        }

        static bool Show
        {
            get { return EditorPrefs.GetBool(ShowKey, false); }
            set { EditorPrefs.SetBool(ShowKey, value); }
        }

        static bool ShowSeated
        {
            get { return EditorPrefs.GetBool(SeatedKey, true); }
            set { EditorPrefs.SetBool(SeatedKey, value); }
        }

        /// <summary>出す相手。初期値はロボットとスタッフ（B5-3）</summary>
        static Who ShowWho
        {
            get { return (Who)EditorPrefs.GetInt(WhoKey, (int)Who.RobotAndStaff); }
            set { EditorPrefs.SetInt(WhoKey, (int)value); }
        }

        static ServiceSimLabels()
        {
            SceneView.duringSceneGui -= OnSceneGui;
            SceneView.duringSceneGui += OnSceneGui;
        }

        /// <summary>
        /// 計測の実行中は字幕を出さない（B11）。
        /// 字幕は Scene ビューで毎コマ描くので、35卓ぶんあると倍速に効く。
        /// メニューの設定（EditorPrefs）は変えない。回し終われば自動で元に戻る。
        /// </summary>
        static bool MeasuringNow
        {
            get
            {
                if (!EditorApplication.isPlaying) return false;
                ExperimentRunner r = UnityEngine.Object.FindObjectOfType<ExperimentRunner>();
                if (r == null || !r.Running) return false;
                // 見る用の回（Run One > 見る用）は消さない。
                // ExperimentPlan で「回している間も出す」にしてあるときも消さない
                if (r.viewMode) return false;
                return r.plan == null || !r.plan.showLabelsWhileRunning;
            }
        }

        [MenuItem(MenuPlaces, false, 104)]
        static void TogglePlaces()
        {
            ShowPlaces = !ShowPlaces;
            SceneView.RepaintAll();
        }

        [MenuItem(MenuPlaces, true)]
        static bool TogglePlacesValidate()
        {
            Menu.SetChecked(MenuPlaces, ShowPlaces);
            return true;
        }

        [MenuItem(MenuShow, false, 100)]
        static void Toggle()
        {
            Show = !Show;
            SceneView.RepaintAll();
            Debug.Log("[ServiceSim] 頭の上の字幕: " + (Show ? "出す" : "消す") + "（Scene ビューだけ）");
        }

        [MenuItem(MenuShow, true)]
        static bool ToggleValidate() { Menu.SetChecked(MenuShow, Show); return true; }

        [MenuItem("ServiceSim/Show Labels (いまの出し方を見る)", false, 102)]
        static void Report()
        {
            Debug.Log("[ServiceSim] 頭の上の字幕: " + (Show ? "出す" : "消す") +
                      " / 椅子を引いている客も: " + (ShowSeated ? "出す" : "消す") +
                      "\n  計測の実行中は自動で切る（いまは " + (MeasuringNow ? "計測中なので出していません" : "計測していません") + "）");
        }

        [MenuItem(MenuSeated, false, 101)]
        static void ToggleSeated() { ShowSeated = !ShowSeated; SceneView.RepaintAll(); }

        [MenuItem(MenuSeated, true)]
        static bool ToggleSeatedValidate() { Menu.SetChecked(MenuSeated, ShowSeated); return Show; }

        [MenuItem(MenuWhoRobot, false, 110)]
        static void WhoRobot() { ShowWho = Who.RobotOnly; SceneView.RepaintAll(); }
        [MenuItem(MenuWhoRobot, true)]
        static bool WhoRobotValidate()
        {
            Menu.SetChecked(MenuWhoRobot, ShowWho == Who.RobotOnly);
            return Show;
        }

        [MenuItem(MenuWhoStaff, false, 111)]
        static void WhoStaff() { ShowWho = Who.RobotAndStaff; SceneView.RepaintAll(); }
        [MenuItem(MenuWhoStaff, true)]
        static bool WhoStaffValidate()
        {
            Menu.SetChecked(MenuWhoStaff, ShowWho == Who.RobotAndStaff);
            return Show;
        }

        [MenuItem(MenuWhoAll, false, 112)]
        static void WhoAll() { ShowWho = Who.All; SceneView.RepaintAll(); }
        [MenuItem(MenuWhoAll, true)]
        static bool WhoAllValidate()
        {
            Menu.SetChecked(MenuWhoAll, ShowWho == Who.All);
            return Show;
        }

        // ---------------- 描く ----------------

        static void OnSceneGui(SceneView view)
        {
            // キッチンのモニターの吹き出しは、**測る回でも出す**（B11 とは別。2026-10-23）。
            // 1つだけなので、倍速にほとんど効かない
            bool monitorOnly = MeasuringNow;
            if (!Show && !ShowPlaces && !monitorOnly) return;

            EnsureStyle();
            RefreshStore();
            _drawn.Clear();

            if (monitorOnly)
            {
                Handles.BeginGUI();
                DrawKitchenMonitor(view);
                Handles.EndGUI();
                return;
            }

            Handles.BeginGUI();
            if (ShowPlaces) DrawPlaceNames();   // 先に描いて、吹き出しがその上に重ならないようにする
            if (Show)
            {
                DrawRobots();
                if (ShowWho != Who.RobotOnly) DrawStaff();
                if (ShowWho == Who.All) DrawGuests();
                if (ShowSeated) DrawSeated();   // 椅子を引いている卓は、相手の設定に関わらず出す
            }
            // 画面に貼り付けるパネルは、いちばん最後（ほかの札の重なり避けには入れない）
            DrawKitchenMonitor(view);
            Handles.EndGUI();
        }

        static void EnsureStyle()
        {
            if (_placeStyle == null)
            {
                // 場所の名前は、吹き出しより小さい字・濃い文字色（背景は薄い色＋枠）
                _placeStyle = new GUIStyle(EditorStyles.label);
                _placeStyle.alignment = TextAnchor.MiddleCenter;
                _placeStyle.fontStyle = FontStyle.Normal;
                _placeStyle.fontSize = PlaceFontSize;
                _placeStyle.normal.background = null;
                _placeStyle.padding = new RectOffset(5, 5, 1, 1);
                _placeStyle.normal.textColor = PlaceText;
            }
            if (_style != null) return;
            // 日本語は、エディタの既定のフォントがそのまま出る（font は差し替えない）
            _style = new GUIStyle(EditorStyles.label);
            _style.alignment = TextAnchor.MiddleLeft;
            _style.fontStyle = FontStyle.Bold;
            _style.fontSize = FontSize;
            _style.normal.background = null;
            _style.padding = new RectOffset(6, 6, 2, 2);
            _style.normal.textColor = Color.white;
        }

        static void RefreshStore()
        {
            if (_store == null) _store = Object.FindObjectOfType<StoreBuilder>();
            _tables.Clear();
            if (_store == null) return;
            IList<StoreBuilder.TableInfo> t = _store.Tables;
            for (int i = 0; i < t.Count; i++) _tables.Add(t[i]);
        }

        /// <summary>
        /// 頭の上に1行の字幕を出す（B5）。
        /// 濃い半透明の帯を敷いて文字は白にし、誰の字幕かは帯の左端の色で分ける。
        /// 近くの帯どうしは上へずらして重ならないようにする。
        /// </summary>
        static void Label(Transform tr, float heightM, string text, Color tag)
        {
            if (tr == null || string.IsNullOrEmpty(text)) return;

            Camera cam = Camera.current;
            if (cam == null) return;

            Vector3 world = tr.position + Vector3.up * heightM;
            Vector3 vp = cam.WorldToViewportPoint(world);
            if (vp.z <= 0f || vp.x < -0.1f || vp.x > 1.1f || vp.y < -0.1f || vp.y > 1.1f) return;

            Vector2 size = _style.CalcSize(new GUIContent(text));
            float w = size.x + 8f;           // 左端の色の帯のぶん
            float h = Mathf.Max(size.y, RowHeightPx);

            // 画面の座標（GUI は左上が原点）
            Vector3 sp = HandleUtility.WorldToGUIPointWithDepth(world);
            if (sp.z < 0f) return;

            Rect rect = new Rect(sp.x - w * 0.5f, sp.y - h, w, h);
            rect = AvoidOverlap(rect);
            _drawn.Add(rect);

            // 帯（濃い半透明）
            EditorGUI.DrawRect(rect, new Color(0.05f, 0.06f, 0.09f, 0.82f));

            // 左端の色（誰の字幕か）
            EditorGUI.DrawRect(new Rect(rect.x, rect.y, 4f, rect.height), tag);

            // 文字（白）
            GUI.Label(new Rect(rect.x + 4f, rect.y, rect.width - 4f, rect.height), text, _style);
        }

        /// <summary>
        /// すでに描いた帯と重なるなら、重ならなくなるまで上へずらす。
        /// 近い帯どうしだけを見る（画面の離れたところは見ない）。
        /// </summary>
        static Rect AvoidOverlap(Rect rect)
        {
            for (int guard = 0; guard < 12; guard++)
            {
                bool moved = false;
                for (int i = 0; i < _drawn.Count; i++)
                {
                    Rect o = _drawn[i];
                    if (Mathf.Abs(o.center.x - rect.center.x) > OverlapPx) continue;
                    if (!rect.Overlaps(o)) continue;
                    rect.y = o.y - rect.height - 2f;
                    moved = true;
                }
                if (!moved) break;
            }
            return rect;
        }

        // ---------------- キッチンのモニター（画面に貼り付けるパネル）----------------

        const string MonitorXKey = "ServiceSim.MonitorX";
        const string MonitorYKey = "ServiceSim.MonitorY";
        const string MonitorOpenKey = "ServiceSim.MonitorOpen";

        /// <summary>パネルの見出し（掴んで動かす・押してたたむ）</summary>
        const string MonitorTitle = "キッチンのモニター";

        /// <summary>画面のふちからの余白（点）</summary>
        const float MonitorMarginPx = 8f;

        static GUIStyle _monitorStyle;
        static GUIStyle _monitorTitleStyle;
        static bool _monitorDragging;
        static bool _monitorMoved;
        static Vector2 _monitorGrab;

        /// <summary>パネルを開いているか（たたんでいないか）</summary>
        static bool MonitorOpen
        {
            get { return EditorPrefs.GetBool(MonitorOpenKey, true); }
            set { EditorPrefs.SetBool(MonitorOpenKey, value); }
        }

        /// <summary>パネルの画面の上の位置（点。はじめは左上）</summary>
        static Vector2 MonitorPos
        {
            get
            {
                return new Vector2(EditorPrefs.GetFloat(MonitorXKey, MonitorMarginPx),
                                   EditorPrefs.GetFloat(MonitorYKey, MonitorMarginPx));
            }
            set
            {
                EditorPrefs.SetFloat(MonitorXKey, value.x);
                EditorPrefs.SetFloat(MonitorYKey, value.y);
            }
        }

        /// <summary>
        /// キッチンのモニター（2026-10-23）。Scene ビューの画面に貼り付けるパネル1つ。
        /// カメラを回してもズームしても、画面の上の位置は変わらない。
        /// 見出しを掴むと動かせ、押すとたためる。位置とたたみ方は EditorPrefs に覚える。
        /// 測る回でも出す（頭の上の字幕を切る B11 とは別）。
        /// 切り替えは ExperimentPlan.showKitchenMonitor（KitchenMonitor.show に入っている）。
        /// </summary>
        static void DrawKitchenMonitor(SceneView view)
        {
            KitchenMonitor monitor = Object.FindObjectOfType<KitchenMonitor>();
            if (monitor == null || !monitor.show) return;

            // 回していないあいだは見本（場所と大きさを確かめられるように）
            string text = monitor.Text;
            if (string.IsNullOrEmpty(text))
            {
                if (EditorApplication.isPlaying) return;
                text = KitchenMonitor.SampleText();
            }

            EnsureMonitorStyle();

            GUIContent title = new GUIContent((MonitorOpen ? "▼ " : "▶ ") + MonitorTitle);
            GUIContent body = new GUIContent(text);
            Vector2 titleSize = _monitorTitleStyle.CalcSize(title);
            Vector2 bodySize = MonitorOpen ? _monitorStyle.CalcSize(body) : Vector2.zero;

            float w = Mathf.Max(titleSize.x, bodySize.x) + 10f;      // 左端の色の帯のぶん
            float headerH = Mathf.Max(titleSize.y, 16f);
            float h = headerH + (MonitorOpen ? bodySize.y + 4f : 0f);

            Vector2 pos = MonitorPos;
            Rect panel = new Rect(pos.x, pos.y, w, h);
            Rect header = new Rect(pos.x, pos.y, w, headerH);
            MonitorMouse(view, panel, header, w, h);

            pos = MonitorPos;                                        // 動かしたぶんを読み直す
            panel = new Rect(pos.x, pos.y, w, h);
            header = new Rect(pos.x, pos.y, w, headerH);

            EditorGUI.DrawRect(panel, new Color(0.05f, 0.06f, 0.09f, 0.88f));
            EditorGUI.DrawRect(new Rect(panel.x + 4f, header.yMax - 1f, panel.width - 4f, 1f),
                               new Color(1f, 1f, 1f, 0.25f));

            GUI.Label(new Rect(header.x + 5f, header.y, header.width - 5f, header.height),
                      title, _monitorTitleStyle);
            if (MonitorOpen)
                GUI.Label(new Rect(panel.x + 5f, header.yMax + 2f, panel.width - 6f, bodySize.y),
                          body, _monitorStyle);
        }

        /// <summary>
        /// パネルの上のマウスを受け取る。
        ///   ・見出しを掴んで動かす。押すとたためる・開ける
        ///   ・**パネルの上のクリックとドラッグは、ここで受け取って先へ渡さない**
        ///     （後ろにある卓・人・ロボットが選ばれたり動いたりしないようにする）
        ///   ・画面の外に出たら中へ戻す（パネルがつかめなくならないように）
        /// </summary>
        static void MonitorMouse(SceneView view, Rect panel, Rect header, float w, float h)
        {
            Event e = Event.current;
            if (e == null) return;

            bool onPanel = panel.Contains(e.mousePosition);

            if (e.type == EventType.MouseDown && onPanel)
            {
                if (e.button == 0 && header.Contains(e.mousePosition))
                {
                    _monitorDragging = true;
                    _monitorMoved = false;
                    _monitorGrab = e.mousePosition - new Vector2(header.x, header.y);
                }
                e.Use();            // 後ろの物を選ばせない
                return;
            }

            if (e.type == EventType.MouseDrag)
            {
                if (_monitorDragging)
                {
                    _monitorMoved = true;
                    MonitorPos = ClampToView(view, e.mousePosition - _monitorGrab, w, h);
                    e.Use();
                    if (view != null) view.Repaint();
                    return;
                }
                if (onPanel) { e.Use(); return; }    // 後ろの物を動かさせない
            }

            if (e.type == EventType.MouseUp)
            {
                if (_monitorDragging)
                {
                    _monitorDragging = false;
                    if (!_monitorMoved) MonitorOpen = !MonitorOpen;   // 動かさずに押したら、たたむ・開ける
                    MonitorPos = ClampToView(view, MonitorPos, w, h);
                    e.Use();
                    if (view != null) view.Repaint();
                    return;
                }
                if (onPanel) { e.Use(); return; }
            }

            // 画面の大きさが変わったときのために、描くたびに中へ戻す
            MonitorPos = ClampToView(view, MonitorPos, w, h);
        }

        /// <summary>パネルを画面の中に収める</summary>
        static Vector2 ClampToView(SceneView view, Vector2 pos, float w, float h)
        {
            if (view == null) return pos;
            float vw = view.position.width;
            float vh = view.position.height;
            float maxX = Mathf.Max(MonitorMarginPx, vw - w - MonitorMarginPx);
            float maxY = Mathf.Max(MonitorMarginPx, vh - h - MonitorMarginPx);
            return new Vector2(Mathf.Clamp(pos.x, MonitorMarginPx, maxX),
                               Mathf.Clamp(pos.y, MonitorMarginPx, maxY));
        }

        static void EnsureMonitorStyle()
        {
            if (_monitorStyle == null)
            {
                // 字の大きさは、場所の名前の札と同じ。文は左寄せの複数行
                _monitorStyle = new GUIStyle(EditorStyles.label);
                _monitorStyle.alignment = TextAnchor.UpperLeft;
                _monitorStyle.fontStyle = FontStyle.Normal;
                _monitorStyle.fontSize = PlaceFontSize;
                _monitorStyle.normal.background = null;
                _monitorStyle.padding = new RectOffset(0, 0, 0, 0);
                _monitorStyle.normal.textColor = Color.white;
                _monitorStyle.wordWrap = false;
            }
            if (_monitorTitleStyle != null) return;
            _monitorTitleStyle = new GUIStyle(_monitorStyle);
            _monitorTitleStyle.fontStyle = FontStyle.Bold;
            _monitorTitleStyle.alignment = TextAnchor.MiddleLeft;
        }

        // ---------------- 場所の名前 ----------------

        /// <summary>
        /// 場所の名前を、その場所の床の真ん中に出す。
        ///
        /// 吹き出し（スタッフの「待機」など）と同じ作り方
        /// （Handles.BeginGUI の中の GUI.Label）なので、
        ///   ・カメラの向きに関係なく画面に正対する
        ///   ・ズームしても字の大きさが変わらない
        /// 見分けがつくように、字は小さく・背景は薄く・枠をつける。
        /// </summary>
        static void DrawPlaceNames()
        {
            if (_store == null || _store.Plan == null) return;

            List<string> names = new List<string>();
            List<Vector3> at = new List<Vector3>();
            CollectPlaceNames(_store, names, at);

            for (int i = 0; i < names.Count; i++) PlaceLabel(at[i], names[i]);
        }

        /// <summary>
        /// 出す場所と短い名前（13個）。1つの場所に1つだけ。
        /// 同じ種類が2つ以上ある待機の点は、まとめて1つにする。
        /// </summary>
        public static void CollectPlaceNames(StoreBuilder store, List<string> names, List<Vector3> at)
        {
            names.Clear();
            at.Clear();
            StorePlan plan = store != null ? store.Plan : null;
            if (plan == null) return;

            PlaceKind[] kinds =
            {
                PlaceKind.Entrance, PlaceKind.Vestibule, PlaceKind.Waiting,
                PlaceKind.Register, PlaceKind.RegisterBack, PlaceKind.Kitchen,
                PlaceKind.FoodPass, PlaceKind.DishReturn, PlaceKind.Pantry,
                PlaceKind.DrinkBar, PlaceKind.Toilet
            };
            string[] text =
            {
                "入口", "風除室", "待合",
                "レジ", "レジの内側", "厨房",
                "受け渡し口", "返却口", "パントリー",
                "ドリンクバー", "トイレ"
            };

            for (int k = 0; k < kinds.Length; k++)
            {
                bool found = false, hasRect = false;
                PlanRect area = new PlanRect(0f, 0f, 0f, 0f);
                Vector3 point = Vector3.zero;

                for (int i = 0; i < plan.places.Count; i++)
                {
                    PlanPlace p = plan.places[i];
                    if (p.kind != kinds[k]) continue;
                    if (!found) { point = p.point; found = true; }
                    if (p.rect.IsEmpty) continue;
                    area = hasRect
                        ? new PlanRect(Mathf.Min(area.xMin, p.rect.xMin), Mathf.Min(area.zMin, p.rect.zMin),
                                       Mathf.Max(area.xMax, p.rect.xMax), Mathf.Max(area.zMax, p.rect.zMax))
                        : p.rect;
                    hasRect = true;
                }
                if (!found) continue;

                names.Add(text[k]);
                at.Add(hasRect ? area.Center : point);      // 場所の床の真ん中
            }

            AddStandbyName(store.RobotStandbyPoints, "ロボット待機", names, at);
            AddStandbyName(store.StaffStandbyPoints, "スタッフ待機", names, at);
        }

        static void AddStandbyName(IList<Vector3> points, string name,
                                   List<string> names, List<Vector3> at)
        {
            if (points == null || points.Count == 0) return;
            Vector3 sum = Vector3.zero;
            for (int i = 0; i < points.Count; i++) sum += points[i];
            names.Add(points.Count > 1 ? name + "×" + points.Count : name);
            at.Add(sum / points.Count);
        }

        /// <summary>場所の名前1つ（小さい字・薄い背景・枠つき）</summary>
        static void PlaceLabel(Vector3 world, string text)
        {
            Camera cam = Camera.current;
            if (cam == null) return;

            Vector3 vp = cam.WorldToViewportPoint(world);
            if (vp.z <= 0f || vp.x < -0.1f || vp.x > 1.1f || vp.y < -0.1f || vp.y > 1.1f) return;

            Vector3 sp = HandleUtility.WorldToGUIPointWithDepth(world);
            if (sp.z < 0f) return;

            Vector2 size = _placeStyle.CalcSize(new GUIContent(text));
            float w = size.x;
            float h = Mathf.Max(size.y, PlaceFontSize + 6f);

            Rect rect = new Rect(sp.x - w * 0.5f, sp.y - h * 0.5f, w, h);
            rect = AvoidOverlap(rect);
            _drawn.Add(rect);

            // 薄い背景
            EditorGUI.DrawRect(rect, PlaceFill);
            // 枠（1 点の線を4本）
            EditorGUI.DrawRect(new Rect(rect.x, rect.y, rect.width, 1f), PlaceLine);
            EditorGUI.DrawRect(new Rect(rect.x, rect.yMax - 1f, rect.width, 1f), PlaceLine);
            EditorGUI.DrawRect(new Rect(rect.x, rect.y, 1f, rect.height), PlaceLine);
            EditorGUI.DrawRect(new Rect(rect.xMax - 1f, rect.y, 1f, rect.height), PlaceLine);

            GUI.Label(rect, text, _placeStyle);
        }

        // ---------------- ロボット ----------------

        static void DrawRobots()
        {
            IReadOnlyList<ServingRobotController> robots = ServingRobotController.All;
            for (int i = 0; i < robots.Count; i++)
            {
                ServingRobotController r = robots[i];
                if (r == null || r.spec == null) continue;
                Color tag = TagRobot;
                string text = RobotText(r, ref tag);
                Label(r.transform, r.spec.height + 0.35f, "[" + r.robotId + "] " + text, tag);
            }
        }

        /// <summary>
        /// ロボットの字幕（B5-5 で短くした言い方）。
        /// 止まっている台は帯を黄、動けない台は帯を赤にする（B5-1）。
        /// </summary>
        static string RobotText(ServingRobotController r, ref Color tag)
        {
            switch (r.CurrentPhase)
            {
                case ServingRobotController.Phase.Idle: return "待機";
                case ServingRobotController.Phase.Done: return "走りおわり";

                case ServingRobotController.Phase.Dwell:
                    return "卓" + r.TableId.ToString("00");

                case ServingRobotController.Phase.StuckWait:
                    tag = TagRobotStuck;
                    // v5 3章：スタッフが歩いて来るのを待っている（受け渡し口へは移さない）
                    return "動けない・人を待つ " + Mathf.FloorToInt(r.StuckWaitRemainS) + "秒";

                case ServingRobotController.Phase.ToTable:
                case ServingRobotController.Phase.ToPass:
                    if (r.YieldHolding)
                    {
                        tag = TagRobotStopped;
                        return "入口待ち";
                    }
                    if (r.SpeedMps <= r.spec.stoppedSpeedEpsilon)
                    {
                        tag = TagRobotStopped;
                        return "停止：" + StopCauseText(r);
                    }
                    // 走っているときは行き先の卓だけ（場所は記録に出るので書かない）
                    return r.CurrentPhase == ServingRobotController.Phase.ToTable
                        ? "卓" + r.TableId.ToString("00") + TableListText(r)
                        : "戻り";
            }
            return "";
        }

        /// <summary>1走りで2卓以上回るときは、卓の数だけ添える（短く）</summary>
        static string TableListText(ServingRobotController r)
        {
            if (r.TableIds.Count <= 1) return "";
            return "（" + r.TableIds.Count + "卓）";
        }

        /// <summary>
        /// 止まった原因だけを短く。センサーがすでに持っている値から作る
        /// （「客」「スタッフ」「椅子」「ロボット」「道がない」）。
        /// </summary>
        static string StopCauseText(ServingRobotController r)
        {
            if (r.sensor == null) return "不明";
            FrontSensor.Reading s = r.sensor.Last;

            if (!s.blocked) return r.PathComplete ? "不明" : "道がない";
            if (s.nearestIsRobot) return "ロボット";
            if (s.nearestIsChair) return "椅子";
            if (s.nearestIsPerson)
                return s.nearestPersonKind == PersonKind.Staff ? "スタッフ" : "客";
            return "不明";
        }

        // ---------------- スタッフ ----------------

        static void DrawStaff()
        {
            StaffAgent[] all = Object.FindObjectsOfType<StaffAgent>();

            // 同じ文で近くに立っている人はまとめる（例「スタッフ×2 待機」）
            List<Transform> anchors = new List<Transform>();
            List<string> texts = new List<string>();
            List<int> counts = new List<int>();

            for (int i = 0; i < all.Length; i++)
            {
                StaffAgent a = all[i];
                if (a == null || a.spec == null) continue;

                string text = YieldText(a.gameObject) + StaffText(a);
                int at = -1;
                for (int k = 0; k < anchors.Count; k++)
                {
                    if (texts[k] != text) continue;
                    if (Vector3.Distance(anchors[k].position, a.transform.position) > MergeM) continue;
                    at = k; break;
                }
                if (at >= 0) { counts[at]++; continue; }
                anchors.Add(a.transform); texts.Add(text); counts.Add(1);
            }

            for (int k = 0; k < anchors.Count; k++)
            {
                string head = counts[k] > 1 ? "スタッフ×" + counts[k] + " " : "スタッフ ";
                Label(anchors[k], 1.7f + 0.3f, head + texts[k], TagStaff);
            }
        }

        /// <summary>この距離の中にいる同じ文の吹き出しは、1つにまとめる（m）</summary>
        const float MergeM = 1.0f;

        /// <summary>
        /// スタッフの字幕は仕事の名前だけにする（場所は記録に出るので書かない）。
        ///   会計・案内・料理・片付け・呼び出し・待機
        /// </summary>
        static string StaffText(StaffAgent a)
        {
            if (a.CurrentJob == null) return "待機";
            return StaffAgent.JobKindName(a.CurrentJob.kind);
        }

        // ---------------- 客 ----------------

        static void DrawGuests()
        {
            GuestAgent[] all = Object.FindObjectsOfType<GuestAgent>();

            // 待合の客は1人ずつ出さず、1つにまとめる（B5-2）
            int waitingBodies = 0;
            Transform waitingAnchor = null;

            for (int i = 0; i < all.Length; i++)
            {
                GuestAgent g = all[i];
                if (g == null || g.spec == null || !g.gameObject.activeInHierarchy) continue;

                if (g.CurrentErrand == GuestAgent.Errand.Waiting)
                {
                    waitingBodies++;
                    if (waitingAnchor == null) waitingAnchor = g.transform;
                    continue;
                }

                // ふだんは出さない。出すのは
                //   ・ロボットが止まった相手になったとき
                //   ・ロボットをよけて待っている・下がっているとき
                //   ・あきらめたとき
                string text = GuestShortText(g);
                if (text.Length == 0) continue;
                Label(g.transform, g.spec.personHeight + 0.3f, text, TagGuest);
            }

            if (waitingAnchor != null)
            {
                GroupManager gm = Object.FindObjectOfType<GroupManager>();
                int outside = gm != null ? Mathf.Max(0, gm.WaitingGroups - waitingBodies) : 0;
                Label(waitingAnchor, 2.2f,
                      "待合 " + waitingBodies + "人・外で待つ " + outside + "組", TagWaiting);
            }
        }

        /// <summary>
        /// 人がロボットをよけている最中かを、字幕の前に付ける。
        /// 「ロボット待ち」「よける」「回り道」。不注意の人は印を付ける。
        /// </summary>
        static string YieldText(GameObject go)
        {
            PersonRobotYield y = go != null ? go.GetComponent<PersonRobotYield>() : null;
            if (y == null) return "";

            string head = "";
            switch (y.CurrentMode)
            {
                case PersonRobotYield.Mode.WaitAtEntrance:
                    head = "ロボット待ち" + y.ModeSeconds.ToString("0") + "s"; break;
                case PersonRobotYield.Mode.BackOff: head = "よける"; break;
                case PersonRobotYield.Mode.Detour: head = "回り道"; break;
            }
            if (y.Careless) head += "〔不注意〕";
            return head.Length > 0 ? "[" + head + "] " : "";
        }

        /// <summary>
        /// 客の字幕（最低限）。出すのは3つのときだけ。
        ///   「待つ」「よける」「回り道」… ロボットをよけている
        ///   「ロボットの相手」… ロボットが止まった相手になった
        ///   「あきらめ」… 行き先に着けずあきらめた
        /// </summary>
        static string GuestShortText(GuestAgent g)
        {
            PersonRobotYield y = g.GetComponent<PersonRobotYield>();
            if (y != null)
            {
                switch (y.CurrentMode)
                {
                    case PersonRobotYield.Mode.WaitAtEntrance:
                        return "待つ" + y.ModeSeconds.ToString("0") + "s" + (y.Careless ? "〔不注意〕" : "");
                    case PersonRobotYield.Mode.BackOff:
                        return "よける" + (y.Careless ? "〔不注意〕" : "");
                    case PersonRobotYield.Mode.Detour:
                        return "回り道" + (y.Careless ? "〔不注意〕" : "");
                }
            }
            if (IsRobotStopCause(g.transform)) return "ロボットの相手";
            if (g.GaveUp) return "あきらめ";
            return "";
        }

        /// <summary>その人が、いま止まっているロボットの相手になっているか</summary>
        static bool IsRobotStopCause(Transform who)
        {
            IReadOnlyList<ServingRobotController> robots = ServingRobotController.All;
            for (int i = 0; i < robots.Count; i++)
            {
                ServingRobotController r = robots[i];
                if (r == null || r.sensor == null || r.spec == null) continue;
                if (r.SpeedMps > r.spec.stoppedSpeedEpsilon) continue;
                FrontSensor.Reading s = r.sensor.Last;
                if (!s.blocked || !s.nearestIsPerson) continue;
                if (Vector3.Distance(r.transform.position, who.position) <= r.spec.sensorRange + 0.5f)
                    return true;
            }
            return false;
        }

        static string GuestText(GuestAgent g)
        {
            string tail = g.GaveUp ? "（あきらめた）" : "";

            switch (g.CurrentErrand)
            {
                case GuestAgent.Errand.Waiting: return "待合";
                case GuestAgent.Errand.Leave: return "退店" + tail;
                case GuestAgent.Errand.Pay: return "会計" + tail;

                case GuestAgent.Errand.DrinkBar:
                    if (g.CurrentLeg == GuestAgent.Leg.Going) return "ドリンクバーへ";
                    if (g.CurrentLeg == GuestAgent.Leg.Dwelling) return "ドリンクバー";
                    return "卓へ戻る" + tail;

                case GuestAgent.Errand.Toilet:
                    if (g.CurrentLeg == GuestAgent.Leg.Going) return "トイレへ";
                    if (g.CurrentLeg == GuestAgent.Leg.Dwelling) return "トイレ";
                    return "卓へ戻る" + tail;
            }
            return "";
        }

        static void DrawSeated()
        {
            SeatedGuest[] all = Object.FindObjectsOfType<SeatedGuest>();
            for (int i = 0; i < all.Length; i++)
            {
                SeatedGuest s = all[i];
                if (s == null || !s.IsPulling) continue;   // 引いているときだけ出す
                if (!BlamedByRobot(s.transform.position)) continue;  // ロボットが止まった相手だけ
                Label(s.transform, 1.3f, "椅子を引いている", TagChair);
            }
        }

        /// <summary>
        /// その卓の椅子が、いまロボットの止まった相手になっているか（B5-3）。
        /// 「全員」のときも、座っている客と椅子は、これに当てはまるときだけ出す。
        /// </summary>
        static bool BlamedByRobot(Vector3 at)
        {
            IReadOnlyList<ServingRobotController> robots = ServingRobotController.All;
            for (int i = 0; i < robots.Count; i++)
            {
                ServingRobotController r = robots[i];
                if (r == null || r.spec == null || r.sensor == null) continue;
                if (r.SpeedMps > r.spec.stoppedSpeedEpsilon) continue;
                if (!r.sensor.Last.nearestIsChair) continue;

                Vector3 d = r.transform.position - at;
                d.y = 0f;
                if (d.magnitude <= 2.5f) return true;
            }
            return false;
        }

        // ---------------- 点 → 場所の名前 ----------------

        static string PlaceNameAt(Vector3 p)
        {
            if (_store == null) return "";

            float best = 1.2f;
            string name = "";

            for (int i = 0; i < _tables.Count; i++)
            {
                float d = Flat(_tables[i].deliveryPoint - p);
                if (d < best) { best = d; name = "卓" + _tables[i].tableId.ToString("00"); }
            }

            TryPlace(p, PlaceKind.FoodPass, "受け渡し口", ref best, ref name);
            TryPlace(p, PlaceKind.DishReturn, "返却口", ref best, ref name);
            TryPlace(p, PlaceKind.Pantry, "パントリー", ref best, ref name);
            TryPlace(p, PlaceKind.Register, "レジ", ref best, ref name);
            TryPlace(p, PlaceKind.Toilet, "トイレ", ref best, ref name);
            TryPlace(p, PlaceKind.DrinkBar, "ドリンクバー", ref best, ref name);
            TryPlace(p, PlaceKind.Entrance, "入口", ref best, ref name);
            TryPlace(p, PlaceKind.Waiting, "待合", ref best, ref name);
            return name;
        }

        static void TryPlace(Vector3 p, PlaceKind kind, string label, ref float best, ref string name)
        {
            if (!_store.HasPoint(kind)) return;
            float d = Flat(_store.PointOf(kind) - p);
            if (d < best) { best = d; name = label; }
        }

        static float Flat(Vector3 d) { d.y = 0f; return d.magnitude; }
    }
}
