using System;
using System.Collections.Generic;
using System.Globalization;
using System.Text;
using UnityEngine;

namespace ServiceSim
{
    /// <summary>
    /// サイトが書き出した「店の形の JSON」を読んで店を建てる StoreLayout。
    ///
    /// 2026-10-22 にエイイチと決めた「何について正か」（unity_design_v0.md の 0 章）:
    ///   店の形（卓・壁・部屋・止まる位置）は **サイトが正**。Unity は形を作らない。
    /// なので、このクラスは JSON を StorePlan に写すだけで、足りないものを自分で埋めない。
    /// 合わないものが出たら、埋めずに報告へ書いて **建てずに止める**（Compose が null を返す）。
    ///
    /// 読むもの:
    ///   layoutJson   https://pixelstomotors.com/sim/layouts/unity/{名前}.json を写したもの
    ///   summaryJson  https://pixelstomotors.com/sim/layouts/summary.json を写したもの
    /// 版の番号（meta.generation）が summary.json と違ったら建てない。
    ///
    /// 座標: JSON の x → Unity の X、JSON の y → Unity の Z、高さ → Y。単位は m。
    ///       原点は店の外壁の内側の左下の角。
    /// </summary>
    [CreateAssetMenu(fileName = "StoreLayout_Json", menuName = "ServiceSim/Json Store Layout", order = 12)]
    public class JsonStoreLayout : StoreLayout
    {
        [Header("読む JSON（サイトから写したもの）")]
        [Tooltip("店の形の JSON（unity/{名前}.json）")]
        public TextAsset layoutJson;

        [Tooltip("一覧の JSON（summary.json）。版の番号を突き合わせる")]
        public TextAsset summaryJson;

        [Tooltip("summary.json の file と突き合わせる名前（例 p1_efficient_corner）")]
        public string layoutFileName = "";

        [Header("写した記録（報告に出す）")]
        [Tooltip("サイトから写した日時")]
        public string fetchedAt = "";

        [Tooltip("写し元の URL")]
        public string sourceUrl = "";

        [Header("見た目だけ（当たりも NavMesh も持たない）")]
        [Tooltip("ベンチの座面の見た目の厚み（m）。0 で出さない")]
        public float benchSeatPadThickness = 0.03f;

        [Tooltip("座面の見た目を、ベンチのふちから内側へ詰める長さ（m）")]
        public float benchSeatInset = 0.03f;

        // ---- 建てたときの覚え書き（報告に出す。Compose のたびに作り直す）----
        readonly List<string> _problems = new List<string>();
        readonly List<string> _doors = new List<string>();
        string _generation = "";
        string _storeName = "";
        float _jsonNarrowWidth = 0f;
        float _normalWidth = 0f;
        int _tableCount, _seatCount, _narrowAisleCount, _chairCount, _spotCount, _seatPointCount;
        int _roomCount, _placeCount, _wallCount, _dividerCount;
        int _walkableRoomCount, _staffOnlyRoomCount;
        int _routeFromPass, _routeBetween;

        public override string StoreKindName
        {
            get { return "JsonStoreLayout(" + (string.IsNullOrEmpty(layoutFileName) ? "?" : layoutFileName) + ")"; }
        }

        /// <summary>この JSON の版の番号（meta.generation）。建てたあとに読める</summary>
        public string Generation { get { return _generation; } }

        /// <summary>
        /// 版の番号を、建てる前でも読む（試算の値と突き合わせるのに使う）。
        /// 建てたあとは Compose が読んだものを返す。
        /// </summary>
        public string ReadGeneration()
        {
            if (!string.IsNullOrEmpty(_generation)) return _generation;
            if (layoutJson == null) return "";
            string err;
            JsonValue root = JsonValue.Parse(layoutJson.text, out err);
            return root != null && root.IsObject ? root["meta"]["generation"].S("") : "";
        }

        /// <summary>建てた卓の数（JSON の tables の数）</summary>
        public int TableCount { get { return _tableCount; } }

        /// <summary>
        /// サイトの決まりの「ふだん走らせる道の幅」（meta.settings.normal_width, m）。
        /// これ未満の幅の通路が「狭い通路」。0 なら JSON に無い
        /// </summary>
        public float NormalWidth { get { return _normalWidth; } }

        /// <summary>
        /// 試算の値（serving-robot-params.json）の stores の名前（fam_p1 / fam_p5）。
        /// layoutFileName（p1_efficient_corner / p5_crammed）の頭の "p1" "p5" から作る。
        /// 合っているかは SimParams.Store() が null かどうかで分かる。
        /// </summary>
        public string ParamsStoreKey
        {
            get
            {
                if (string.IsNullOrEmpty(layoutFileName)) return "";
                int cut = layoutFileName.IndexOf('_');
                string head = cut > 0 ? layoutFileName.Substring(0, cut) : layoutFileName;
                return "fam_" + head;
            }
        }

        /// <summary>
        /// Build Store が store_report と Console に出す欄。
        /// JSON から建てたときは、版の番号・写した日時・数えたもの・合わなかったものをここに出す。
        /// </summary>
        public override string FrontFitReport()
        {
            StringBuilder sb = new StringBuilder();
            sb.AppendLine("JSON から建てた店（Unity では形を作っていない）");
            sb.AppendLine("  ファイル: " + (layoutJson != null ? layoutJson.name : "（未設定）") +
                          " / 名前: " + _storeName);
            sb.AppendLine("  版の番号 (meta.generation): " + _generation);
            sb.AppendLine("  写した日時: " + (string.IsNullOrEmpty(fetchedAt) ? "（未記入）" : fetchedAt));
            sb.AppendLine("  写し元: " + (string.IsNullOrEmpty(sourceUrl) ? "（未記入）" : sourceUrl));
            sb.AppendLine("  卓 " + _tableCount + " / 席 " + _seatCount +
                          " / 細い通路 " + _narrowAisleCount + " 本（幅 " + F(_jsonNarrowWidth) + " m）");
            sb.AppendLine("  通れない部屋 " + _roomCount +
                          " / 通れる部屋 " + _walkableRoomCount +
                          "（うちスタッフだけ " + _staffOnlyRoomCount + "）");
            sb.AppendLine("  場所の点 " + _placeCount +
                          " / 壁 " + _wallCount + " / 仕切り " + _dividerCount +
                          " / 椅子 " + _chairCount +
                          " / 席に着く点 " + _spotCount + " / 座る位置 " + _seatPointCount);
            sb.AppendLine("  道順 (routes): 受け渡し口から " + _routeFromPass + " 本 / 卓から卓 " +
                          _routeBetween + " 本"
                          + "（狭い道を使うかは routes の use が正。Unity では印を持たない）");
            sb.AppendLine("  doors: " + (_doors.Count == 0
                ? "0（壁の切れ目として、もう空いている）"
                : _doors.Count + " → " + string.Join(" / ", _doors.ToArray())));
            sb.AppendLine("  refuges（よけ場所）: 0（JSON にまだ無い。空のまま）");
            sb.AppendLine("  止まる位置は JSON の stop をそのまま使う"
                          + "（deliveryOffsetFromAisleCenter と aisleEntranceSetback は使わない）");
            if (_problems.Count == 0) sb.AppendLine("  合わないもの: なし");
            else
            {
                sb.AppendLine("  合わないもの: " + _problems.Count + " 件（建てていない）");
                for (int i = 0; i < _problems.Count; i++) sb.AppendLine("    - " + _problems[i]);
            }
            return sb.ToString().TrimEnd();
        }

        protected override StorePlan Compose()
        {
            _problems.Clear();
            _doors.Clear();
            _generation = ""; _storeName = ""; _jsonNarrowWidth = 0f; _normalWidth = 0f;
            _tableCount = _seatCount = _narrowAisleCount = _chairCount = 0;
            _spotCount = _seatPointCount = _roomCount = _placeCount = _wallCount = _dividerCount = 0;
            _walkableRoomCount = _staffOnlyRoomCount = 0;
            _routeFromPass = _routeBetween = 0;

            if (layoutJson == null)
            {
                Fail("店の形の JSON（layoutJson）が入っていません");
                return null;
            }

            string err;
            JsonValue root = JsonValue.Parse(layoutJson.text, out err);
            if (root == null || !root.IsObject)
            {
                Fail("店の形の JSON を読めません: " + err);
                return null;
            }

            JsonValue meta = root["meta"];
            _generation = meta["generation"].S("");
            _storeName = root["name"].S("");

            // サイトの決まりの「ふだん走らせる道の幅」（meta.settings.normal_width）。
            // これ未満の通路が「狭い通路」。往復ごとの測りを中と外で分けるのに使う（v5 3章）
            _normalWidth = meta["settings"]["normal_width"].F(0f);

            if (!CheckGeneration()) return null;

            // ---- 高さ（JSON の meta.heights。Unity 側では決めない）----
            JsonValue heights = meta["heights"];
            float hWall = Req(heights, "wall", "meta.heights.wall");
            float hTable = Req(heights, "table", "meta.heights.table");
            float hBench = Req(heights, "bench", "meta.heights.bench");
            float hBenchSeat = Req(heights, "bench_seat", "meta.heights.bench_seat");
            float hChair = Req(heights, "chair", "meta.heights.chair");
            float hCounter = Req(heights, "counter", "meta.heights.counter");
            float hPartition = heights["partition"].F(1.2f);   // これ以下の walls は仕切り


            StorePlan plan = new StorePlan();

            // ---- 床 ----
            JsonValue floor = root["floor"];
            for (int i = 0; i < floor.Count; i++)
            {
                PlanRect r;
                if (TryRect(floor[i], "floor[" + i + "]", out r)) plan.AddFloor(r);
            }
            if (plan.floorPieces.Count == 0) Fail("floor が空です");

            // ---- 外壁 ----
            JsonValue outerWalls = root["outer_walls"];
            for (int i = 0; i < outerWalls.Count; i++)
            {
                PlanRect r;
                if (!TryRect(outerWalls[i], "outer_walls[" + i + "]", out r)) continue;
                plan.AddBox("OuterWall" + i.ToString("00"), BoxStyle.Wall, r, hWall);
                _wallCount++;
            }

            // ---- 中の壁・仕切り（高さで分ける）----
            JsonValue walls = root["walls"];
            for (int i = 0; i < walls.Count; i++)
            {
                JsonValue w = walls[i];
                PlanRect r;
                if (!TryRect(w["rect"], "walls[" + i + "].rect", out r)) continue;
                float h = w["height"].F(hWall);
                bool divider = h <= hPartition + 0.0005f;
                string label = w["label"].S("Wall" + i.ToString("00"));
                plan.AddBox(Safe(label) + i.ToString("00"),
                            divider ? BoxStyle.Divider : BoxStyle.Wall, r, h);
                if (divider) _dividerCount++; else _wallCount++;
            }

            // ---- 場所の点（先に入れる。PointOf がこちらを先に拾うように）----
            Dictionary<PlaceKind, Vector3> firstPoint = new Dictionary<PlaceKind, Vector3>();
            JsonValue places = root["places"];
            for (int i = 0; i < places.Count; i++)
            {
                JsonValue p = places[i];
                string kindName = p["kind"].S("");
                PlaceKind kind;
                if (!TryParseKind(kindName, out kind))
                {
                    Fail("places[" + i + "]: PlaceKind に無い kind \"" + kindName + "\"");
                    continue;
                }
                Vector3 point;
                if (!TryPoint(p["point"], "places[" + i + "].point", out point)) continue;

                PlanRect rect;
                if (!p.Has("rect") || !TryRect(p["rect"], "places[" + i + "].rect", out rect))
                    rect = new PlanRect(0f, 0f, 0f, 0f);

                // 箱は置かない（通れない箱は rooms が持つ）。ここは点だけ。
                plan.AddPlace(kind, Safe(p["label"].S(kindName)) + i.ToString("00"), rect, 0f, point, false);
                if (!firstPoint.ContainsKey(kind)) firstPoint[kind] = point;
                _placeCount++;
            }

            // ---- 部屋（通れない箱）----
            JsonValue rooms = root["rooms"];
            for (int i = 0; i < rooms.Count; i++)
            {
                JsonValue rm = rooms[i];
                string label = rm["label"].S("");
                PlaceKind kind;
                if (!TryRoomKind(label, out kind))
                {
                    Fail("rooms[" + i + "]: PlaceKind に対応の無いラベル \"" + label + "\"");
                    continue;
                }
                PlanRect r;
                if (!TryRect(rm["rect"], "rooms[" + i + "].rect", out r)) continue;

                bool walkable = rm["walkable"].B(false);
                bool staffOnly = rm["staff_only"].B(false);

                // スタッフだけが入れる区画は、いまの作りでは「レジの内側」だけ
                // （NavMeshBaker.AddRegisterBackArea が PlaceKind.RegisterBack の rect を見て
                //   区画の番号を変え、客の NavMeshAgent がその区画を外して歩く）。
                // ほかの部屋に staff_only が付いたら、こちらで作らずに報告して止める。
                if (staffOnly && kind != PlaceKind.RegisterBack)
                {
                    Fail("rooms[" + i + "] \"" + label + "\": staff_only は今の作りでは" +
                         "レジの内側（RegisterBack）だけです");
                    continue;
                }

                if (walkable)
                {
                    // 通れない箱にしない（人が入る部屋）。場所の点だけ置く。
                    // 同じ種類の点が places にあれば、そちらが区画の広がりも持っているので足さない。
                    if (!firstPoint.ContainsKey(kind))
                    {
                        plan.AddPlace(kind, Safe(label) + "_Room" + i.ToString("00"), r, 0f, r.Center, false);
                        firstPoint[kind] = r.Center;
                    }
                    _walkableRoomCount++;
                    if (staffOnly) _staffOnlyRoomCount++;
                    continue;
                }

                // レジとドリンクバーはカウンターの高さ、それ以外は壁の高さ
                float h = (kind == PlaceKind.Register || kind == PlaceKind.DrinkBar) ? hCounter : hWall;

                // 点は、同じ種類の場所の点（JSON が決めた立ち位置）に合わせる。無ければ真ん中
                Vector3 point;
                if (!firstPoint.TryGetValue(kind, out point)) point = r.Center;

                plan.AddPlace(kind, Safe(label) + "_Room" + i.ToString("00"), r, h, point, true);
                _roomCount++;
            }

            // ---- 細い通路 ----
            Dictionary<string, int> aisleIndexById = new Dictionary<string, int>();
            JsonValue aisles = root["aisles"];
            for (int i = 0; i < aisles.Count; i++)
            {
                JsonValue a = aisles[i];
                PlanRect r;
                if (!TryRect(a["rect"], "aisles[" + i + "].rect", out r)) continue;
                Vector3 entrance;
                if (!TryPoint(a["entrance"], "aisles[" + i + "].entrance", out entrance)) continue;
                string id = a["id"].S("A" + (i + 1).ToString("00"));
                bool narrow = a["narrow"].B(true);
                aisleIndexById[id] = plan.aisles.Count;
                plan.AddAisle(id, r, narrow, entrance);
                float aw = a["width"].F(0f);
                if (narrow)
                {
                    _narrowAisleCount++;
                    if (_jsonNarrowWidth <= 0f) _jsonNarrowWidth = aw;
                    else if (Mathf.Abs(_jsonNarrowWidth - aw) > 0.0005f)
                        Fail("細い通路の幅がそろっていません: " + F(_jsonNarrowWidth) + " と " + F(aw));
                }
            }

            // ---- 卓 ----
            JsonValue tables = root["tables"];
            for (int i = 0; i < tables.Count; i++)
            {
                ComposeTable(plan, tables[i], i, aisleIndexById,
                             hTable, hBench, hBenchSeat, hChair);
            }
            _tableCount = plan.seats.Count;

            // ---- ロボットの道順（v5b。サイトが決めた曲がり角の点）----
            plan.routes = ReadRoutes(root["routes"]);

            // ---- doors（箱は置かない。報告に一覧だけ出す）----
            JsonValue doors = root["doors"];
            for (int i = 0; i < doors.Count; i++)
            {
                JsonValue d = doors[i];
                string label = d["label"].S(d["id"].S("door" + i));
                PlanRect r;
                string where = TryRect(d["rect"], "doors[" + i + "].rect", out r) ? r.ToString() : "（rect なし）";
                _doors.Add(label + " " + where);
            }

            // ---- 細い通路の幅が .asset の値と合っているか ----
            if (_jsonNarrowWidth > 0f && Mathf.Abs(AisleWidth - _jsonNarrowWidth) > 0.0005f)
            {
                Fail("細い通路の幅が .asset と JSON で違います: .asset " + F(AisleWidth) +
                     " m / JSON " + F(_jsonNarrowWidth) + " m" +
                     "（aisleWidthOptions を JSON の幅だけにしてください）");
            }

            if (_problems.Count > 0)
            {
                Debug.LogError("[ServiceSim] " + name + ": JSON と合わないものが " + _problems.Count +
                               " 件あるので建てませんでした。\n" + FrontFitReport());
                return null;
            }

            return plan;
        }

        /// <summary>
        /// routes を読む（v5b）。from_pass["T05"] と between["T05|T12"]。
        /// 点は [x, y] で、JSON の y → Unity の Z。
        /// </summary>
        RouteBook ReadRoutes(JsonValue routes)
        {
            if (!routes.IsObject)
            {
                Fail("routes がありません（v5b の版の JSON を写してください）");
                return null;
            }
            RouteBook book = new RouteBook();

            JsonValue fromPass = routes["from_pass"];
            foreach (string id in fromPass.Keys)
            {
                RouteBook.Route r = ReadRoute(fromPass[id], "routes.from_pass[" + id + "]");
                if (r != null) book.AddFromPass(NumberFromId(id, -1), r);
            }

            JsonValue between = routes["between"];
            foreach (string key in between.Keys)
            {
                int bar = key.IndexOf('|');
                if (bar <= 0)
                {
                    Fail("routes.between のキーが \"T01|T02\" の形ではありません: " + key);
                    continue;
                }
                RouteBook.Route r = ReadRoute(between[key], "routes.between[" + key + "]");
                if (r != null)
                    book.AddBetween(NumberFromId(key.Substring(0, bar), -1),
                                    NumberFromId(key.Substring(bar + 1), -1), r);
            }

            _routeFromPass = book.FromPassCount;
            _routeBetween = book.BetweenCount;
            if (_routeFromPass == 0) Fail("routes.from_pass が空です");
            return book;
        }

        RouteBook.Route ReadRoute(JsonValue v, string where)
        {
            JsonValue pts = v["points"];
            if (pts.Count < 2)
            {
                Fail(where + ": points が 2 点未満です");
                return null;
            }
            RouteBook.Route r = new RouteBook.Route();
            for (int i = 0; i < pts.Count; i++)
            {
                Vector3 p;
                if (!TryPoint(pts[i], where + ".points[" + i + "]", out p)) return null;
                r.points.Add(p);
            }
            r.use = v["use"].S("normal");
            r.width = v["width"].F(0f);
            r.length = v["length"].F(r.PolylineLength);
            if (r.use != "normal" && r.use != "narrow")
            {
                Fail(where + ": use が normal / narrow ではありません（" + r.use + "）");
                return null;
            }
            return r;
        }

        // ---------------- 卓 ----------------

        void ComposeTable(StorePlan plan, JsonValue t, int index,
                          Dictionary<string, int> aisleIndexById,
                          float hTable, float hBench, float hBenchSeat, float hChair)
        {
            string id = t["id"].S("T" + (index + 1).ToString("00"));
            string where = "tables[" + index + "] " + id;

            SeatKind kind;
            string type = t["type"].S("");
            if (!TryTableKind(type, out kind))
            {
                Fail(where + ": SeatKind に対応の無い type \"" + type + "\"");
                return;
            }

            JsonValue parts = t["parts"];
            PlanRect top;
            if (!TryRect(parts["top"], where + ".parts.top", out top)) return;

            Vector3 stop;
            if (!TryPoint(t["stop"], where + ".stop", out stop)) return;

            PlanSeat seat = new PlanSeat();
            seat.tableId = NumberFromId(id, index + 1);
            seat.kind = kind;
            seat.tableRect = top;
            seat.tableHeight = hTable;
            seat.seatCount = t["seats"].I(0);
            seat.deliveryPoint = stop;             // JSON の stop をそのまま（ずらさない）

            // 通る細い通路
            string aisleId = t["aisle"].S(null);
            if (string.IsNullOrEmpty(aisleId)) seat.aisleIndex = -1;
            else
            {
                int ai;
                if (aisleIndexById.TryGetValue(aisleId, out ai)) seat.aisleIndex = ai;
                else { Fail(where + ": aisles に無い通路 \"" + aisleId + "\""); return; }
            }

            // 天板
            plan.AddBox(id + "_Top", BoxStyle.Table, top, hTable);

            // ベンチ（座面の見た目は今までの作りどおり、座面の上に当たりなしの板を敷く）
            JsonValue benches = parts["benches"];
            for (int b = 0; b < benches.Count; b++)
            {
                PlanRect r;
                if (!TryRect(benches[b], where + ".parts.benches[" + b + "]", out r)) continue;
                plan.AddBox(id + "_Bench" + (b + 1), BoxStyle.Bench, r, hBench);
                AddBenchSeatLook(plan, id + "_Bench" + (b + 1), r, hBenchSeat);
            }

            // 椅子
            JsonValue chairs = parts["chairs"];
            for (int c = 0; c < chairs.Count; c++)
            {
                JsonValue ch = chairs[c];
                PlanRect r;
                if (!TryRect(ch["rect"], where + ".parts.chairs[" + c + "].rect", out r)) continue;
                PlanChair chair = new PlanChair();
                chair.rect = r;
                chair.height = hChair;
                chair.pullable = ch["pullable"].B(false);
                JsonValue dir = ch["pull_dir"];
                chair.pullDirection = new Vector3(dir[0].F(0f), 0f, dir[1].F(0f));
                if (chair.pullDirection.sqrMagnitude > 0.000001f)
                    chair.pullDirection = chair.pullDirection.normalized;
                seat.chairs.Add(chair);
                _chairCount++;
            }

            // 座る位置
            JsonValue sp = parts["seat_points"];
            for (int s = 0; s < sp.Count; s++)
            {
                Vector3 p;
                if (!TryPoint(sp[s], where + ".parts.seat_points[" + s + "]", out p)) continue;
                seat.seatPoints.Add(p);
                _seatPointCount++;
            }

            // 席に着くために立つ点
            JsonValue spots = t["spots"];
            for (int s = 0; s < spots.Count; s++)
            {
                Vector3 p;
                if (!TryPoint(spots[s]["point"], where + ".spots[" + s + "].point", out p)) continue;
                seat.spots.Add(new PlanSeatSpot(spots[s]["side"].S("spot" + (s + 1)), p));
                _spotCount++;
            }

            plan.seats.Add(seat);
            _seatCount += seat.seatCount;
        }

        /// <summary>
        /// 座面の見た目（当たりなし）。ベンチの箱の上に、ふちから少し詰めた板を1枚置く。
        /// NavMesh・前のセンサー・左右の空きの測りには効かない。
        /// </summary>
        void AddBenchSeatLook(StorePlan plan, string name, PlanRect bench, float seatHeight)
        {
            if (benchSeatPadThickness <= 0.0005f) return;
            float m = Mathf.Max(0f, benchSeatInset);
            PlanRect r = new PlanRect(bench.xMin + m, bench.zMin + m, bench.xMax - m, bench.zMax - m);
            if (r.IsEmpty) return;
            PlanBox b = plan.AddBox(name + "_SeatLook", BoxStyle.BenchSeat, r, benchSeatPadThickness);
            if (b != null) b.baseY = seatHeight;
        }

        // ---------------- 小さな道具 ----------------

        bool CheckGeneration()
        {
            if (string.IsNullOrEmpty(_generation))
            {
                Fail("meta.generation がありません");
                return false;
            }
            if (summaryJson == null)
            {
                Fail("summary.json（summaryJson）が入っていません。版の番号を突き合わせられません");
                return false;
            }
            string err;
            JsonValue sum = JsonValue.Parse(summaryJson.text, out err);
            if (sum == null || !sum.IsArray)
            {
                Fail("summary.json を読めません: " + err);
                return false;
            }
            string key = string.IsNullOrEmpty(layoutFileName)
                ? (layoutJson != null ? layoutJson.name : "")
                : layoutFileName;
            for (int i = 0; i < sum.Count; i++)
            {
                if (sum[i]["file"].S("") != key) continue;
                string g = sum[i]["generation"].S("");
                if (g != _generation)
                {
                    Fail("版の番号が summary.json と違います: この JSON " + _generation +
                         " / summary.json " + g + "（写し直してください）");
                    return false;
                }
                return true;
            }
            Fail("summary.json に \"" + key + "\" がありません（layoutFileName を確かめてください）");
            return false;
        }

        float Req(JsonValue obj, string key, string where)
        {
            JsonValue v = obj[key];
            if (v.kind != JsonValue.Kind.Number)
            {
                Fail(where + " がありません");
                return 0f;
            }
            return v.F(0f);
        }

        bool TryRect(JsonValue v, string where, out PlanRect rect)
        {
            rect = new PlanRect(0f, 0f, 0f, 0f);
            if (v.Count < 4)
            {
                Fail(where + " が [x0, y0, x1, y1] ではありません");
                return false;
            }
            rect = new PlanRect(v[0].F(0f), v[1].F(0f), v[2].F(0f), v[3].F(0f));
            return true;
        }

        bool TryPoint(JsonValue v, string where, out Vector3 p)
        {
            p = Vector3.zero;
            if (v.Count < 2)
            {
                Fail(where + " が [x, y] ではありません");
                return false;
            }
            p = new Vector3(v[0].F(0f), 0f, v[1].F(0f));
            return true;
        }

        static bool TryParseKind(string s, out PlaceKind kind)
        {
            kind = PlaceKind.Kitchen;
            if (string.IsNullOrEmpty(s)) return false;
            try
            {
                kind = (PlaceKind)Enum.Parse(typeof(PlaceKind), s, false);
                return true;
            }
            catch (Exception) { return false; }
        }

        /// <summary>部屋のラベルを PlaceKind に直す（依頼の対応表どおり）</summary>
        static bool TryRoomKind(string label, out PlaceKind kind)
        {
            kind = PlaceKind.Kitchen;
            if (string.IsNullOrEmpty(label)) return false;
            switch (label)
            {
                case "パントリー": kind = PlaceKind.Pantry; return true;
                case "キッチン": kind = PlaceKind.Kitchen; return true;
                case "冷蔵・冷凍・保存": kind = PlaceKind.Kitchen; return true;
                case "洗い場": kind = PlaceKind.Kitchen; return true;
                case "更衣室・社員トイレ": kind = PlaceKind.Office; return true;
                case "トイレ": kind = PlaceKind.Toilet; return true;
                case "待合": kind = PlaceKind.Waiting; return true;
                case "レジ": kind = PlaceKind.Register; return true;
                case "レジ内": kind = PlaceKind.RegisterBack; return true;
                case "ドリンクバー": kind = PlaceKind.DrinkBar; return true;
            }
            return false;
        }

        static bool TryTableKind(string type, out SeatKind kind)
        {
            kind = SeatKind.WallBooth;
            switch (type)
            {
                case "booth": kind = SeatKind.WallBooth; return true;
                case "t2": kind = SeatKind.WallTable; return true;
                case "booth6L": kind = SeatKind.LargeTable; return true;
            }
            return false;
        }

        static int NumberFromId(string id, int fallback)
        {
            if (string.IsNullOrEmpty(id)) return fallback;
            int n = 0; bool any = false;
            for (int i = 0; i < id.Length; i++)
            {
                char c = id[i];
                if (c < '0' || c > '9') continue;
                n = n * 10 + (c - '0');
                any = true;
            }
            return any ? n : fallback;
        }

        /// <summary>GameObject の名前に使える形にする（/ は階層になってしまう）</summary>
        static string Safe(string s)
        {
            if (string.IsNullOrEmpty(s)) return "x";
            return s.Replace('/', '_').Replace('\\', '_');
        }

        void Fail(string message)
        {
            _problems.Add(message);
        }

        static string F(float v)
        {
            return v.ToString("0.##", CultureInfo.InvariantCulture);
        }
    }
}
