using System.Collections.Generic;
using UnityEngine;

namespace ServiceSim
{
    /// <summary>
    /// 客を「1組ごとに、入ってから出るまで」で動かす（試算の group() と同じ流れ）。
    ///
    ///   入店 → 卓が空くまで待合（あふれたら店の外） → 案内 → 着席 → 料理 → 食事
    ///   （食事中にドリンクバー・トイレ・呼び出し） → 会計 → 退店 → 片付け
    ///
    /// 座っている人は人として置かない（通路に出ないため）。
    /// 立ち上がった人だけを、使い回しの体（Guest.prefab）で歩かせる。
    ///
    /// この script は人だけを扱う。卓の空きと料理の待ち行列は ServiceOps に任せる。
    /// 数字は ServiceSpec と PeopleSpec の .asset に置く。
    /// </summary>
    public class GroupManager : MonoBehaviour
    {
        /// <summary>1組の今の状態</summary>
        public enum GState { Waiting, Guided, Seated, Eating, Paying, Left, Clearing, Done }

        /// <summary>1組</summary>
        public class Group
        {
            public int id;
            public int size;
            public GState state = GState.Waiting;
            public TableSlot table;

            public float arriveS = -1f;
            public float guideStartS = -1f;
            public float seatedS = -1f;

            /// <summary>
            /// その組だけの乱数（依頼 v8b の B）。
            /// 種と組の番号から作るので、**いつ引いても同じ組なら同じ値**になる。
            /// 人数・料理ができるまで・食べる時間・割れ物の数・呼び出し・
            /// ドリンクバー・トイレは、ぜんぶここから決まった順に引く。
            /// 動きが1か所ずれても、ほかの組の中身は変わらない
            /// </summary>
            public System.Random rng;

            /// <summary>食べ始めた時刻（最初の料理が全部届いた時刻, 秒）。まだなら -1（依頼 v8 の 2）</summary>
            public float eatStartS = -1f;
            public float eatEndS = -1f;
            public float payS = -1f;
            public float leftS = -1f;
            public bool visibleWaiting;
            public bool warmup;              // 慣らしのために最初から座っている組

            public readonly List<DishOrder> dishes = new List<DishOrder>();
            public readonly List<float> cookAt = new List<float>();      // 受け渡し口に出す時刻
            public readonly List<bool> cookFragile = new List<bool>();
            public readonly List<float> errandAt = new List<float>();    // 立ち上がる時刻
            public readonly List<int> errandKind = new List<int>();      // 0=ドリンクバー 1=トイレ
            public readonly List<float> callAt = new List<float>();
            public int activeErrands;
            public int dishesOrdered;

            /// <summary>何人目が立ったか。いつも同じ人が同じ椅子を引くようにする（B6）</summary>
            public int chairTurn;

            /// <summary>会計・片付けで止まったままの秒数（B1。長びいたら卓を空ける）</summary>
            public float stuckS;

            /// <summary>
            /// 卓は空いたのに片付けが終わらず待った秒数（v5 3章・blocked_wait_mean）。
            /// サイトの試算（haizen_sim.py の _acc）と同じ数え方で、
            /// 「片付け待ちの卓の数だけ、待っている列の前から順に、そのコマの時間を足す」。
            /// </summary>
            public float blockedWaitS;

            public GuestAgent waitingBody;   // 待合に立たせている1人

            // ---- 会計（レジに立つのは組の1人だけ）----
            /// <summary>会計に立つ1人の体。ほかの人は席に座ったまま</summary>
            public GuestAgent payerBody;
            /// <summary>取ったレジの並ぶ点。-1 なら取れていない（席で待つ）</summary>
            public int payerSpot = -1;
            /// <summary>会計の人をレジへ送ったか</summary>
            public bool payerSent;
            /// <summary>会計を始めた時刻（食べ終わって Paying に入った時刻）</summary>
            public float payStartS = -1f;
            public StaffJob job;             // いま出している仕事
        }

        [Header("設定")]
        public ServiceSpec spec;
        public PeopleSpec people;
        public ServiceOps ops;
        public StoreBuilder store;

        [Tooltip("卓ごとの椅子の出し入れを読む先（B6）。無くても動く")]
        public PeopleSpawner spawner;

        [Header("部品")]
        [Tooltip("歩く客の体。待合の客にも使い回す")]
        public GameObject guestPrefab;

        [Tooltip("使っていない（段階4の場面の再現用に残してある）")]
        public GameObject waitingGuestPrefab;

        [Tooltip("歩く人を置く親")]
        public Transform bodyRoot;

        [Header("いまの様子（読むだけ）")]
        [SerializeField] int _groupsArrived;
        [SerializeField] int _groupsSeated;
        [SerializeField] int _groupsDone;
        [SerializeField] int _waitingGroups;
        [SerializeField] int _walkingBodies;

        readonly List<Group> _groups = new List<Group>();
        readonly List<Group> _waiting = new List<Group>();
        readonly List<GuestAgent> _pool = new List<GuestAgent>();
        readonly List<GuestAgent> _live = new List<GuestAgent>();

        PeopleTargets _targets;
        /// <summary>
        /// 客の中身でないもの（待つ場所を選ぶなど）に使う乱数。
        /// 来客の間隔は _arrivalRng、組ごとのことは Group.rng（依頼 v8b の B）
        /// </summary>
        System.Random _rng;

        /// <summary>
        /// 来客の間隔だけの乱数（依頼 v8b の B）。
        /// ほかの引きと混ぜないので、動きがずれても **来る組と来る時刻は変わらない**
        /// </summary>
        System.Random _arrivalRng;

        /// <summary>組ごとの乱数を作るもとの種</summary>
        int _seed;
        float _nextArrivalS;
        int _nextGroupId = 1;
        CrowdLevel _crowd;

        public IList<Group> Groups { get { return _groups; } }
        public int GroupsArrived { get { return _groupsArrived; } }
        public int GroupsSeated { get { return _groupsSeated; } }
        public int GroupsDone { get { return _groupsDone; } }
        public int WaitingGroups { get { return _waitingGroups; } }

        /// <summary>いま通路を歩いている人の数（スタッフを除く）</summary>
        public int WalkingBodies { get { return _walkingBodies; } }

        // ---------------- 用意 ----------------

        /// <summary>
        /// 来客を受け付ける長さ（店の中の秒）。これを過ぎたら新しい組は来ない（v5 3章）。
        /// サイトの決まりは「空の店から始め、peak_min だけ来客し、全員が帰るまで回す」。
        /// 0 以下なら、これまでどおり止めずに来客し続ける。
        /// </summary>
        public float arrivalsEndS;

        /// <summary>
        /// 慣らしのために、始めから食事中の組で卓を埋めるか（v5 3章）。
        /// サイトと同じ「空の店から」にするときは false。
        /// </summary>
        public bool seedWarmupGroups = true;

        /// <summary>まだ店の中にいる組があるか（全員が帰ったかの判定。v5 3章）</summary>
        public bool AnyGroupInStore
        {
            get
            {
                if (_waiting.Count > 0) return true;
                for (int i = 0; i < _groups.Count; i++)
                    if (_groups[i].state != GState.Done) return true;
                return false;
            }
        }

        public void Setup(CrowdLevel crowd, int seed, PeopleTargets targets)
        {
            _crowd = crowd;
            _seed = seed;
            _rng = new System.Random(seed * 7919 + 13);
            _arrivalRng = new System.Random(seed * 7919 + 101);   // 来客だけの列（依頼 v8b の B）
            _targets = targets;

            _groups.Clear();
            _waiting.Clear();
            ReturnAllBodies();

            _groupsArrived = 0;
            _groupsSeated = 0;
            _groupsDone = 0;
            _nextGroupId = 1;

            float rate = spec.ArrivalsPerHour(crowd) / 3600f;
            _nextArrivalS = ServiceSpec.ExponentialSec(_arrivalRng, rate);

            if (seedWarmupGroups) SeedWarmupGroups();
        }

        /// <summary>
        /// 慣らしのために、卓の一部を「食事の途中の組」で埋めて始める。
        /// 空の店から始めると、はじめの数十分が本番と違う形になるため。
        /// </summary>
        void SeedWarmupGroups()
        {
            if (people == null || ops == null) return;
            int tables = ops.Tables.Count;
            int want = Mathf.RoundToInt(tables * Mathf.Clamp01(people.warmupOccupancy));
            int want0 = want;
            int placed = 0;

            for (int i = 0; i < tables && want > 0; i++)
            {
                TableSlot slot = ops.TakeTableAt(i, _nextGroupId);
                if (slot == null) continue;

                Group g = new Group();
                g.id = _nextGroupId++;
                g.rng = NewGroupRng(g.id);      // その組だけの乱数（依頼 v8b の B）
                // 2人席に3人以上を座らせない。卓の席数で頭を切る
                g.size = Mathf.Min(spec.PickGroupSize(g.rng), Mathf.Max(1, slot.seatCount));
                g.table = slot;
                g.warmup = true;
                g.arriveS = 0f;
                g.seatedS = 0f;

                // 食事のどの時点かを決める（B3）。
                //   0.00〜0.25：座ったばかりで料理を待っている（1皿めも2皿めもまだ）
                //   0.25〜0.50：1皿めは届いたが2皿めを待っている
                //   0.50〜1.00：受け取って食べている〜食べ終わりかけ
                // でたらめにすると食べ終わりかけが固まり、そのぶんの会計がいちどに来て
                // レジが詰まるので、均等にばらす
                double phase = (placed + 0.5) / Mathf.Max(1, want0);
                if (phase < 0.25) SeedWaitingFirstDishes(g, 2);
                else if (phase < 0.5) SeedWaitingFirstDishes(g, 1);
                else SeedEating(g, (float)((phase - 0.5) / 0.5));
                placed++;

                _groups.Add(g);
                _groupsArrived++;
                _groupsSeated++;
                want--;
            }
        }

        /// <summary>
        /// 慣らし：座ったばかりで料理を待っている組（B3）。
        /// まだ届いていない皿は、作る時間の残りをでたらめに決めて受け渡し口の待ち行列に入る。
        /// </summary>
        void SeedWaitingFirstDishes(Group g, int pending)
        {
            g.state = GState.Seated;
            g.dishesOrdered = 2;

            // 1皿め・2皿めの作る時間。pending が 1 なら1皿めは届いた扱いにする
            float cook1 = ServiceSpec.TriangularSec(g.rng, spec.firstDish1CookMin);
            float cook2 = ServiceSpec.TriangularSec(g.rng, spec.firstDish2CookMin);

            if (pending >= 2)
            {
                g.cookAt.Add((float)g.rng.NextDouble() * cook1);
                g.cookFragile.Add(false);
            }
            else
            {
                // 1皿めは届いている
                g.dishes.Add(ops.SeedArrivedDish(g.id, g.table));
            }

            g.cookAt.Add((float)g.rng.NextDouble() * cook2);
            g.cookFragile.Add(false);
        }

        /// <summary>
        /// 慣らし：もう食べている組（B3）。
        /// done は食事の進み具合（0 なら食べ始め、1 なら食べ終わりかけ）。
        /// 均等にばらすので、食べ終わりがいちどに来ない。
        /// </summary>
        void SeedEating(Group g, float done)
        {
            g.state = GState.Eating;
            g.dishesOrdered = 2;

            // 最初の料理は届いている扱い
            for (int k = 0; k < 2; k++) g.dishes.Add(ops.SeedArrivedDish(g.id, g.table));

            float eat = ServiceSpec.TriangularSec(g.rng, spec.eatMin);
            g.eatEndS = eat * (1f - Mathf.Clamp01(done));
            ScheduleDuringMeal(g, g.eatEndS);
        }

        // ---------------- 毎コマ ----------------

        public void Tick(float dt)
        {
            if (spec == null || ops == null || _rng == null) return;
            float now = ops.NowS;

            Arrive(now);
            CountBlockedWait(dt);
            SeatWaiting(now);
            for (int i = 0; i < _groups.Count; i++) Advance(_groups[i], now, dt);

            _waitingGroups = _waiting.Count;
            _walkingBodies = _live.Count;
        }

        /// <summary>
        /// 「卓は空いたのに片付けが終わらず待った」分を数える（v5 3章・blocked_wait_mean）。
        ///
        /// サイトの試算（haizen_sim.py の _acc）と同じ数え方にそろえた：
        ///   片付け待ちの卓の数だけ、待っている列の**前から順に**、そのコマの時間を足す。
        /// 片付けが終わっていれば、その卓の数だけ先頭の組が座れていたはず、という数え方。
        ///
        /// full_wait は「席待ち − blocked_wait」で出す（サイトと同じ）。
        /// 卓は空いているのに案内が追いつかない分（guide_wait）は、
        /// 案内の仕事が板に出てから取られるまでで測る（OnGuided で記録する）。
        ///
        /// サイトの試算には卓の大きさが無いので、ここも席数を見ずに列の前から数える
        /// （2人席・4人席は Unity だけのもの）。
        /// </summary>
        void CountBlockedWait(float dt)
        {
            int clearing = ops.ClearingTableCount;
            if (clearing <= 0) return;
            int n = Mathf.Min(clearing, _waiting.Count);
            for (int i = 0; i < n; i++) _waiting[i].blockedWaitS += dt;
        }

        /// <summary>来客（指数分布。試算の arrivals() と同じ）</summary>
        void Arrive(float now)
        {
            // サイトの決まり：peak_min だけ来客し、そのあとは新しい組は来ない（v5 3章）
            if (arrivalsEndS > 0f && now >= arrivalsEndS) return;
            if (now < _nextArrivalS) return;

            float rate = spec.ArrivalsPerHour(_crowd) / 3600f;
            _nextArrivalS = now + ServiceSpec.ExponentialSec(_arrivalRng, rate);

            Group g = new Group();
            g.id = _nextGroupId++;
            g.rng = NewGroupRng(g.id);      // その組だけの乱数（依頼 v8b の B）
            g.size = spec.PickGroupSize(g.rng);
            g.arriveS = now;
            g.state = GState.Waiting;
            _groups.Add(g);
            _waiting.Add(g);
            _groupsArrived++;

            // 待合に立たせるのは上限まで。あふれた組は店の外で待つ扱い（人として置かない）
            if (CountVisibleWaiting() < spec.waitingVisibleMax)
            {
                g.visibleWaiting = true;
                g.waitingBody = BorrowBody(_targets.RandomWaitingSpot(_rng), true);
                if (g.waitingBody != null)
                    g.waitingBody.Begin(people, GuestAgent.Errand.Waiting, g.waitingBody.transform.position, 0f,
                                        false, Vector3.zero, null);
            }
        }

        int CountVisibleWaiting()
        {
            int n = 0;
            for (int i = 0; i < _waiting.Count; i++) if (_waiting[i].visibleWaiting) n++;
            return n;
        }

        /// <summary>
        /// その組だけの乱数を作る（依頼 v8b の B）。
        ///
        /// 種と組の番号だけで決まるので、**いつ作っても同じ組なら同じ列**になる。
        /// 引く順番も組ごとに決まっている（人数 → 最初の2皿の作る時間 → 食べる時間 →
        /// 割れ物 → 呼び出し → ドリンクバー → トイレ）ので、
        /// 動きが1か所ずれても、ほかの組の中身は変わらない。
        ///
        /// 引く数の分布・決まり（ServiceSpec の値）は変えていない。
        /// 変わったのは「どの乱数の列から引くか」だけ。
        /// </summary>
        System.Random NewGroupRng(int groupId)
        {
            unchecked
            {
                int h = _seed * 73856093;
                h ^= groupId * 19349663;
                h = h * 83492791 + 0x5bd1e995;
                return new System.Random(h & 0x7fffffff);   // 負の種を避ける
            }
        }

        /// <summary>
        /// 空いた卓に、待っている組を先着順で入れる。
        /// 2人席と4人席があるので、先頭の組が入れる卓が無いときは、
        /// その組を飛ばして次の組を見る（4人の組が先頭にいるあいだ、
        /// 2人席が空いているのに誰も座れない、を避ける）。
        /// </summary>
        void SeatWaiting(float now)
        {
            while (_waiting.Count > 0 && ops.FreeTableCount > 0)
            {
                int pick = -1;
                for (int i = 0; i < _waiting.Count; i++)
                    if (ops.HasFreeTableFor(_waiting[i].size)) { pick = i; break; }
                if (pick < 0) break;

                Group g = _waiting[pick];
                TableSlot slot = ops.TakeFreeTable(g.id, g.size);
                if (slot == null) break;

                _waiting.RemoveAt(pick);
                g.table = slot;
                g.guideStartS = now;
                g.state = GState.Guided;
                ops.RecordSeatWait(g.arriveS);

                // スタッフが待合へ迎えに行き、卓まで案内する
                Group captured = g;
                StaffJob job = ops.PostJob(StaffJobKind.Guide, g.id, slot, null, 1);
                job.onDone = delegate { OnGuided(captured); };
                g.job = job;
            }
        }

        /// <summary>
        /// その組の卓の椅子を1脚引く（B6）。人が動く瞬間だけ呼ぶ。
        /// 何人目かを順に進めて、同じ人がいつも同じ椅子を引くようにする。
        /// </summary>
        void PullChair(Group g, string reason)
        {
            if (spawner == null || g == null || g.table == null) return;
            SeatedGuest s = spawner.SeatedAt(g.table.index);
            if (s == null) return;
            s.PullFor(reason, g.chairTurn++);
        }

        void OnGuided(Group g)
        {
            // 入店待ちの内訳を1組ぶん残す（v5 3章。分け方はサイトの試算と同じ）。
            //   entry   … 店に来てから卓に着くまで（案内の歩きと guide_s を含む）
            //   seat    … 店に来てから案内が始まるまで
            //   blocked … そのうち、卓は空いたのに片付けが終わらず待った分
            //   guide   … 案内の仕事が板に出てから、スタッフが取るまで
            // full = seat − blocked、staff = blocked + guide は ServiceOps で出す
            if (g.arriveS >= 0f && !g.warmup)
            {
                float entry = ops.NowS - g.arriveS;
                float seat = g.guideStartS >= 0f ? g.guideStartS - g.arriveS : 0f;
                float guideWait = g.job != null && g.job.takenS >= 0f
                    ? Mathf.Max(0f, g.job.takenS - g.job.postedS) : 0f;
                ops.RecordEntryWait(entry, seat, g.blockedWaitS, guideWait);
            }

            g.job = null;
            g.state = GState.Seated;
            g.seatedS = ops.NowS;

            // 席に着くので椅子を引く（B6）
            PullChair(g, SeatedGuest.ReasonSit);

            // 案内された組は席に着く（待合の体を返す）
            if (g.waitingBody != null) { ReturnBody(g.waitingBody); g.waitingBody = null; }
            g.visibleWaiting = false;

            // 最初の注文（試算の first_dishes と同じ数・同じ作る時間）
            g.cookAt.Add(g.seatedS + ServiceSpec.TriangularSec(g.rng, spec.firstDish1CookMin));
            g.cookFragile.Add(false);
            g.cookAt.Add(g.seatedS + ServiceSpec.TriangularSec(g.rng, spec.firstDish2CookMin));
            g.cookFragile.Add(false);
            g.dishesOrdered = 2;
        }

        /// <summary>食事中に起きること（デザート・呼び出し・ドリンクバー・トイレ）を先に決める</summary>
        void ScheduleDuringMeal(Group g, float eatEndS)
        {
            float now = ops.NowS;
            float eat = Mathf.Max(1f, eatEndS - now);

            int nFragile = ServiceSpec.PickCount(g.rng, spec.fragilePerGroup);
            for (int i = 0; i < nFragile; i++)
            {
                float at = now + (0.3f + 0.55f * (float)g.rng.NextDouble()) * eat;
                g.cookAt.Add(at + ServiceSpec.TriangularSec(g.rng, spec.fragileCookMin));
                g.cookFragile.Add(true);
                g.dishesOrdered++;
            }

            // 呼び出し（試算は2回ぶん call_rate/2 で判定）
            for (int i = 0; i < 2; i++)
                if (g.rng.NextDouble() < spec.callRate / 2f)
                    g.callAt.Add(now + (float)g.rng.NextDouble() * eat);

            // ドリンクバー（1人あたり）とトイレ（1組あたり）
            int drinks = ServiceSpec.PickCount(g.rng, spec.drinkTripsPerPerson * g.size);
            for (int i = 0; i < drinks; i++)
            {
                g.errandAt.Add(now + (float)g.rng.NextDouble() * eat);
                g.errandKind.Add(0);
            }
            int toilets = ServiceSpec.PickCount(g.rng, spec.toiletTripsPerGroup);
            for (int i = 0; i < toilets; i++)
            {
                g.errandAt.Add(now + (float)g.rng.NextDouble() * eat);
                g.errandKind.Add(1);
            }
        }

        void Advance(Group g, float now, float dt)
        {
            // できた料理を受け渡し口に出す
            for (int i = g.cookAt.Count - 1; i >= 0; i--)
            {
                if (now < g.cookAt[i]) continue;
                if (g.table != null) g.dishes.Add(ops.PutDish(g.id, g.table, g.cookFragile[i]));
                g.cookAt.RemoveAt(i);
                g.cookFragile.RemoveAt(i);
            }

            // 呼び出し
            for (int i = g.callAt.Count - 1; i >= 0; i--)
            {
                if (now < g.callAt[i]) continue;
                if (g.table != null) ops.PostJob(StaffJobKind.Call, g.id, g.table, null, 1);
                g.callAt.RemoveAt(i);
            }

            // 立ち上がってドリンクバー・トイレへ
            for (int i = g.errandAt.Count - 1; i >= 0; i--)
            {
                if (now < g.errandAt[i]) continue;
                StartErrand(g, g.errandKind[i]);
                g.errandAt.RemoveAt(i);
                g.errandKind.RemoveAt(i);
            }

            switch (g.state)
            {
                case GState.Seated:
                    // 最初の料理が全部届いたら食事が始まる（試算の AllOf(firsts) と同じ）
                    if (AllFirstDishesArrived(g))
                    {
                        g.state = GState.Eating;
                        g.eatStartS = now;          // 食べ始めた時刻（依頼 v8 の 2）
                        g.eatEndS = now + ServiceSpec.TriangularSec(g.rng, spec.eatMin);
                        ScheduleDuringMeal(g, g.eatEndS);
                    }
                    break;

                case GState.Eating:
                    if (now < g.eatEndS) break;
                    if (g.cookAt.Count > 0 || !AllDishesArrived(g) || g.activeErrands > 0) break;
                    g.state = GState.Paying;
                    g.stuckS = 0f;
                    g.payStartS = now;
                    g.payerSent = false;
                    g.payerSpot = -1;
                    break;

                case GState.Paying:
                    // 会計に立つのは組の1人だけ。レジの並ぶ点が空くまでは席で待つ
                    SendPayerIfPossible(g, now);
                    break;

                case GState.Left:
                    // 退店したら、スタッフが片付けに行く（優先はいちばん後）
                    g.state = GState.Clearing;
                    g.stuckS = 0f;
                    if (g.table != null) g.table.clearing = true;
                    {
                        Group captured = g;
                        StaffJob job = ops.PostJob(StaffJobKind.Clear, g.id, g.table, null, 2);
                        job.onDone = delegate { OnCleared(captured); };
                        g.job = job;
                    }
                    break;
            }

            // 座っている客の形を、いま席にいる人数に合わせる（見た目だけ）
            UpdateSeatedBodies(g);

            // 会計・片付けで止まって卓が空かないままなら、時間を見て卓を空ける（B1）
            if (g.state == GState.Paying || g.state == GState.Clearing)
            {
                g.stuckS += dt;
                if (spec.tableStuckFreeSec > 0f && g.stuckS >= spec.tableStuckFreeSec)
                    ForceFreeTable(g);
            }
        }

        /// <summary>
        /// 会計・片付けが進まないまま時間が過ぎた組の卓を、強引に空ける（B1）。
        /// これが多いときは、人が通れない店になっているしるし。
        /// </summary>
        /// <summary>
        /// 座っている客の形を、いま席にいる人数に合わせる（見た目だけ）。
        /// 席にいるのは「組の人数 − 立って歩いている人 − 会計に立っている人」。
        /// 席を立つ・卓を空けると 0 になる。
        /// </summary>
        void UpdateSeatedBodies(Group g)
        {
            if (spawner == null || g.table == null) return;
            SeatedGuest seated = spawner.SeatedAt(g.table.index);
            if (seated == null) return;

            bool sitting = g.state == GState.Seated || g.state == GState.Eating ||
                           g.state == GState.Paying;
            int n = 0;
            if (sitting)
            {
                n = g.size - g.activeErrands - (g.payerBody != null ? 1 : 0);
                if (n < 0) n = 0;
            }
            seated.ShowSeated(n);
        }

        void ForceFreeTable(Group g)
        {
            // 卓を空ける前に、座っている客の形を消す
            if (spawner != null && g.table != null)
            {
                SeatedGuest seated = spawner.SeatedAt(g.table.index);
                if (seated != null) seated.ShowSeated(0);
            }

            Vector3 at = g.table != null ? g.table.deliveryPoint : Vector3.zero;
            ops.AddGiveUp("組", g.state == GState.Paying ? "会計" : "片付け",
                          g.table != null ? g.table.tableId : 0,
                          at, at, g.stuckS, "卓を空けた");

            // 出していた仕事を板から外す
            if (g.job != null) { ops.FinishJobSilently(g.job); g.job = null; }
            ReleasePayer(g);

            if (g.leftS < 0f) g.leftS = ops.NowS;
            ops.ReleaseTable(g.table, g.leftS);
            g.table = null;
            g.state = GState.Done;
            g.stuckS = 0f;
            _groupsDone++;
        }

        /// <summary>
        /// 会計に立つ1人をレジへ送る。
        ///   ・レジの並ぶ点が空いていなければ、席に座ったまま待つ（レジに団子にならないように）
        ///   ・送れたときだけ、スタッフの会計の仕事を板に出す（誰も居ないレジで会計しないように）
        /// 残りの人は会計が終わってから、全員で立って店を出る（OnLeft）。
        /// </summary>
        void SendPayerIfPossible(Group g, float now)
        {
            if (g.payerSent) return;

            // 前のとおり（誰もレジに立たず、スタッフだけで会計）に戻せるようにしておく。
            // レジの前の人数を直す前・直したあとで比べるため
            if (!spec.payAtRegisterByOne)
            {
                g.payerSent = true;
                PullChair(g, SeatedGuest.ReasonPay);
                Group old = g;
                StaffJob j = ops.PostJob(StaffJobKind.Pay, g.id, g.table, null, 1);
                j.onDone = delegate { OnPaid(old); };
                g.job = j;
                return;
            }

            Vector3 spot;
            int index = ops.TakeRegisterQueueSpot(out spot);
            if (index < 0) return;      // 並ぶ点が埋まっている。席で待つ

            g.payerSpot = index;
            g.payerSent = true;

            // 会計で立ち上がるので椅子を引く（B6）
            PullChair(g, SeatedGuest.ReasonPay);

            Vector3 from = g.table != null ? g.table.deliveryPoint : _targets.entrance;
            g.payerBody = BorrowBody(from, false);
            if (g.payerBody != null)
            {
                g.payerBody.SetTable(g.table != null ? g.table.tableId : 0);
                g.payerBody.Begin(people, GuestAgent.Errand.Pay, spot, float.MaxValue,
                                  false, from, null);
            }

            Group captured = g;
            StaffJob job = ops.PostJob(StaffJobKind.Pay, g.id, g.table, null, 1);
            job.onDone = delegate { OnPaid(captured); };
            g.job = job;
        }

        /// <summary>会計の人を席へ戻し、レジの並ぶ点を空ける</summary>
        void ReleasePayer(Group g)
        {
            if (g.payerBody != null) { ReturnBody(g.payerBody); g.payerBody = null; }
            if (g.payerSpot >= 0) { ops.ReleaseRegisterQueueSpot(g.payerSpot); g.payerSpot = -1; }
            g.payerSent = false;
        }

        void OnPaid(Group g)
        {
            ReleasePayer(g);
            g.job = null;
            g.payS = ops.NowS;
            g.leftS = ops.NowS;
            g.state = GState.Left;
            g.stuckS = 0f;

            // 退店するので椅子を引く（B6）
            PullChair(g, SeatedGuest.ReasonLeave);

            // 組の全員が立って入口から出る
            int tableId = g.table != null ? g.table.tableId : 0;
            for (int i = 0; i < g.size; i++)
            {
                Vector3 from = g.table != null ? g.table.deliveryPoint : _targets.entrance;
                GuestAgent a = BorrowBody(from, false);
                if (a == null) break;
                GuestAgent captured = a;
                a.SetTable(tableId);
                a.Begin(people, GuestAgent.Errand.Leave, _targets.entrance, 0f, false, Vector3.zero,
                        delegate { ReturnBody(captured); });
            }
        }

        void OnCleared(Group g)
        {
            g.job = null;
            ops.ReleaseTable(g.table, g.leftS);
            g.state = GState.Done;
            _groupsDone++;
        }

        void StartErrand(Group g, int kind)
        {
            if (g.table == null) return;
            Vector3 home = g.table.deliveryPoint;
            Vector3 target = kind == 1 ? _targets.toilet : _targets.drinkBar;
            float dwell = kind == 1 ? spec.toiletSec : spec.drinkBarSec;

            // ドリンクバーは、空いている機械の前へ行く。埋まっていたら端に並ぶ。
            // それも埋まっていたら、立ち上がらずにあきらめる（B4）
            int token = -1;
            if (kind == 0 && ops.HasDrinkBarSpots)
            {
                Vector3 spot;
                bool queued;
                token = ops.TakeDrinkBarSpot(out spot, out queued);
                if (token < 0)
                {
                    ops.AddGiveUp("客", "ドリンクバー", g.table.tableId, target, home, 0f, "あきらめた");
                    return;
                }
                target = spot;
            }

            GuestAgent a = BorrowBody(home, false);
            if (a == null) { ops.ReleaseDrinkBarSpot(token, 0f); return; }

            // 立ち上がるので椅子を引く（B6）
            PullChair(g, kind == 1 ? SeatedGuest.ReasonToilet : SeatedGuest.ReasonDrinkBar);

            g.activeErrands++;
            Group cg = g;
            GuestAgent ca = a;
            int ct = token;
            float startS = ops.NowS;
            a.SetTable(g.table.tableId);
            a.Begin(people, kind == 1 ? GuestAgent.Errand.Toilet : GuestAgent.Errand.DrinkBar,
                    target, dwell, true, home,
                    delegate
                    {
                        cg.activeErrands--;
                        ops.ReleaseDrinkBarSpot(ct, ops.NowS - startS - dwell);
                        PullChair(cg, SeatedGuest.ReasonSitBack);   // 戻って座り直す（B6）
                        ReturnBody(ca);
                    });
        }

        bool AllFirstDishesArrived(Group g)
        {
            if (g.cookAt.Count > 0) return false;
            if (g.dishes.Count < 2) return false;
            for (int i = 0; i < 2 && i < g.dishes.Count; i++)
                if (g.dishes[i].arrivedS < 0f) return false;
            return true;
        }

        bool AllDishesArrived(Group g)
        {
            for (int i = 0; i < g.dishes.Count; i++)
                if (g.dishes[i].arrivedS < 0f) return false;
            return true;
        }

        // ---------------- 歩く体の使い回し ----------------

        GuestAgent BorrowBody(Vector3 at, bool waitingKind)
        {
            // 待合の客も同じ体を使う（GuestAgent が「待っている」用事を持っている）
            GameObject prefab = guestPrefab;
            if (prefab == null || bodyRoot == null) return null;

            GuestAgent a = null;
            for (int i = _pool.Count - 1; i >= 0; i--)
            {
                GuestAgent c = _pool[i];
                _pool.RemoveAt(i);
                if (c == null) continue;
                a = c;
                break;
            }
            if (a == null)
            {
                GameObject go = Instantiate(prefab, bodyRoot);
                a = go.GetComponent<GuestAgent>();
                if (a == null) { Destroy(go); return null; }
            }

            a.gameObject.SetActive(true);
            a.ops = ops;             // あきらめたことを残す先（B1）

            // 人がロボットをよける仕掛け（prefab に無くても付ける）
            if (spawner != null) spawner.AttachRobotYield(a.gameObject, _nextGroupId * 31 + _live.Count);

            a.PlaceAt(at);
            _live.Add(a);
            return a;
        }

        void ReturnBody(GuestAgent a)
        {
            if (a == null) return;
            _live.Remove(a);
            a.Stop();
            a.gameObject.SetActive(false);
            _pool.Add(a);
        }

        void ReturnAllBodies()
        {
            for (int i = _live.Count - 1; i >= 0; i--) ReturnBody(_live[i]);
            _live.Clear();
        }

        public void ClearAll()
        {
            ReturnAllBodies();
            for (int i = 0; i < _pool.Count; i++)
                if (_pool[i] != null) Destroy(_pool[i].gameObject);
            _pool.Clear();
            _groups.Clear();
            _waiting.Clear();
        }
    }
}
