#!/usr/bin/env python3
# ServiceSim 詰まりの地図（散布図）を、決まった形で描く。形の決まりは JAM_MAP_FORMAT.md（2026-11-08 エイイチ了承）。
# 形は依頼119 の fig119.py（fig_where_by_type_sb119.png）に固定した。色と字は 2026-11-08 に style.py へ移した。手で別の形を作らない。
#
# 使い方：
#   python3 make_jam_map.py out.png --layout-dir DIR --store p6_large_center \
#     --set "EP.csv|RUN_GLOB|並べる回の名前" --set "EP2.csv|RUN_GLOB2|名前2" \
#     --note "⑧5台・すぐ押す（種0〜9 の 10 回）"
#   EP.csv   … agg*.py が作る ep_<label>_r5.csv（列：run,dur_s,x,y,type …）
#   RUN_GLOB … その回のフォルダ（stops.csv・summary.json がある所）を指す glob
#   --view x0,x1,y0,y1 … 見る範囲（m）。無ければ待機点の外側から 左2.4・右1.3・下1.95・上0.85 m
import argparse, base64, csv, glob, gzip, json, os, statistics as stt, sys
from collections import defaultdict
import numpy as np
import matplotlib
matplotlib.use('Agg')
sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
import style as st   # 色と字は style.py（決まりは STYLE.md）。ここで色や字を決めない（2026-11-08）
st.mpl_setup()        # 日本語は Noto Sans、英数字は Inter
import matplotlib.pyplot as plt
import matplotlib.patches as mp

# ---- 形（変えるときはエイイチに確かめる）----
PANEL_W, PANEL_H = 9.5, 7.0          # 1枚あたりのインチ（2枚で 19x7）
DPI = 120
MIN_S = 30                           # 30 秒以上の詰まりだけ
RED = st.BAD       # 詰まり＝悪い色
def size(sec): return max(8, sec / 3)  # 円の大きさ＝詰まりの秒（秒/3・最小 8）
LEGEND_SEC = (30, 300, 900)
MARGIN = (2.4, 1.3, 1.95, 0.85)      # 左・右・下・上（m）

def _rgb(h): return tuple(int(h[i:i + 2], 16) / 255 for i in (1, 3, 5))
WALL, NARROW = _rgb(st.MAP_WALL), _rgb(st.RULE)   # 地図の灰2段
FREE = _rgb(st.SHEET)                           # 通れる所＝白


def load_layout(ldir, store):
    lay = json.load(open(os.path.join(ldir, store + '.json'), encoding='utf-8'))
    m = json.load(open(os.path.join(ldir, store + '_map.json'), encoding='utf-8'))
    fc = np.frombuffer(gzip.decompress(base64.b64decode(m['free_class'])), dtype=np.uint8).reshape(m['ny'], m['nx'])
    return lay, (m['res'], m['nx'], m['ny'], fc)


def draw_base(ax, lay, mp_, view):
    x0, x1, y0, y1 = view
    res, nx, ny, fc = mp_
    img = np.empty(fc.shape + (3,)); img[:] = FREE
    img[fc == 0] = WALL                    # 通れない＝濃い灰
    img[fc == 1] = NARROW                  # 狭い道だけ＝薄い灰（黄はお客さんの色なので使わない）
    ax.imshow(img, origin='lower', extent=(0, nx * res, 0, ny * res), interpolation='nearest')
    for t in lay['tables']:
        f = t['footprint']
        if f[2] < x0 - 1 or f[0] > x1 + 1 or f[3] < y0 - 1 or f[1] > y1 + 1:
            continue
        ax.add_patch(mp.Rectangle((f[0], f[1]), f[2] - f[0], f[3] - f[1], fc='none', ec=st.FAINT, lw=0.8))
        ax.text((f[0] + f[2]) / 2, (f[1] + f[3]) / 2, t['id'], ha='center', va='center', fontsize=6, color=st.FAINT, clip_on=True)
    for i, r in enumerate(p['rect'] for p in lay['places'] if p['kind'] == 'RobotStandby'):
        ax.add_patch(mp.Rectangle((r[0], r[1]), r[2] - r[0], r[3] - r[1], fc=st.MAIN_SOFT, ec=st.MAIN, lw=0.8, ls='--'))   # 待機点＝この図の主役
        ax.text((r[0] + r[2]) / 2, r[3] + 0.05, '待機点%d' % (i + 1), ha='center', va='bottom', fontsize=6, color=st.MAIN)
    ps = next(p['point'] for p in lay['places'] if p['kind'] == 'FoodPass')
    ax.plot([ps[0]], [ps[1]], marker='*', ms=11, color=st.INK)
    ax.text(ps[0], ps[1] + 0.15, '受け渡し口の前', ha='center', va='bottom', fontsize=7, color=st.INK)
    for p in lay['places']:
        if p['kind'] == 'StaffPost' and x0 <= p['point'][0] <= x1 and y0 <= p['point'][1] <= y1:
            ax.plot([p['point'][0]], [p['point'][1]], marker='^', ms=4, color=st.FAINT)   # スタッフの持ち場（青は主役に使うので灰の三角）
    ax.set_xlim(x0, x1); ax.set_ylim(y0, y1); ax.set_aspect('equal')
    ax.set_xlabel('x（m）'); ax.set_ylabel('y（m）')


def med(xs):
    xs = [x for x in xs if x is not None]
    return stt.median(xs) if xs else None


def fmt(v, f):
    return '—' if v is None else f(v)


def main():
    a = argparse.ArgumentParser()
    a.add_argument('out')
    a.add_argument('--layout-dir', required=True)
    a.add_argument('--store', default='p6_large_center')
    a.add_argument('--set', action='append', required=True, help='EP.csv|RUN_GLOB|名前')
    a.add_argument('--note', required=True, help='下の説明の頭（店・台数・回数）')
    a.add_argument('--view')
    o = a.parse_args()

    lay, mp_ = load_layout(o.layout_dir, o.store)
    if o.view:
        view = tuple(float(v) for v in o.view.split(','))
    else:
        R = [p['rect'] for p in lay['places'] if p['kind'] == 'RobotStandby']
        view = (min(r[0] for r in R) - MARGIN[0], max(r[2] for r in R) + MARGIN[1],
                min(r[1] for r in R) - MARGIN[2], max(r[3] for r in R) + MARGIN[3])
    sets = [s.split('|', 2) for s in o.set]
    for s in sets:
        if len(s) != 3:
            sys.exit('--set は "EP.csv|RUN_GLOB|名前" の形で: %s' % s)

    n = len(sets)
    nrun = None
    fig, axs = plt.subplots(1, n, figsize=(PANEL_W * n, PANEL_H), squeeze=False)
    axs = axs[0]
    for ax, (ep, rg, name) in zip(axs, sets):
        R = [r for r in csv.DictReader(open(ep, encoding='utf-8')) if float(r['dur_s']) >= MIN_S]
        nd = [r for r in R if not r['type'].startswith('D')]
        dd = [r for r in R if r['type'].startswith('D')]
        dirs = sorted(glob.glob(rg))
        nrun = nrun or len(dirs)
        if not dirs:
            sys.exit('回のフォルダが見つからない: %s' % rg)
        per = defaultdict(float)
        for r in nd:
            per[r['run']] += float(r['dur_s'])
        jam = med([per[os.path.basename(d)] for d in dirs])
        stop = med([sum(float(x['duration_s']) for x in csv.DictReader(open(d + '/stops.csv', encoding='utf-8'))) for d in dirs])
        S = [json.load(open(d + '/summary.json', encoding='utf-8')) for d in dirs]
        ent = med([s.get('site_metrics', {}).get('entry_wait_mean') for s in S])
        call = med([s.get('stuck_rescue', {}).get('calls') for s in S])
        draw_base(ax, lay, mp_, view)
        ax.scatter([float(r['x']) for r in nd], [float(r['y']) for r in nd], s=[size(float(r['dur_s'])) for r in nd],
                   c=RED, alpha=0.55, edgecolors=st.INK, linewidths=0.3, zorder=5)
        if dd:
            ax.scatter([float(r['x']) for r in dd], [float(r['y']) for r in dd], s=60, marker='x', c=st.INK, zorder=6,
                       label='抜けられなかった行ったり来たり（型D）%d 件・%.0f 秒（合計に入れない）' % (len(dd), sum(float(r['dur_s']) for r in dd)))
            ax.legend(fontsize=7, loc='lower left', framealpha=0.9)
        tot = sum(float(r['dur_s']) for r in nd)
        ax.set_title('%s\n待機場所のそばの30秒以上の詰まり：%d 件・%s 秒（%d 回の和）\n1回あたり %s 秒\nロボットの止まりの合計：1回あたり %s 秒\n30秒の呼び出し：1回あたり %s 回　入店待ち：%s 分'
                     % (name, len(nd), format(round(tot), ','), len(dirs), fmt(jam, lambda v: format(round(v), ',')),
                        fmt(stop, lambda v: format(round(v), ',')), fmt(call, lambda v: '%.1f' % v), fmt(ent, lambda v: '%.2f' % v)),
                     fontsize=13, weight='bold')
    for s_ in LEGEND_SEC:
        axs[-1].scatter([], [], s=size(s_), c=RED, alpha=0.55, edgecolors=st.INK, linewidths=0.3, label='円の大きさ：%d 秒' % s_)
    axs[-1].legend(fontsize=7, loc='lower left', framealpha=0.9)
    fig.suptitle('%s：待機場所のそばの、ロボット同士の 30 秒以上の詰まりの始めの位置（止まった台の位置・赤い円の大きさ＝詰まりの秒）。1回あたりの数は %d 回の中央値' % (o.note, nrun),
                 fontsize=10, y=0.02)
    fig.savefig(o.out, dpi=DPI, bbox_inches='tight'); plt.close(fig)
    print(o.out)


if __name__ == '__main__':
    main()
