# -*- coding: utf-8 -*-
"""Pass 7: 두께 미상 도번의 '단품도' 영역을 글자 읽히는 해상도로 타일 PNG 렌더.
   렌더된 그림을 사람(VLM)이 직접 보고 도번-두께를 짝지어 읽는다."""
import json, re, os, sys, collections
import numpy as np
import matplotlib; matplotlib.use('Agg')
import matplotlib.pyplot as plt
from matplotlib import font_manager, rcParams
for f in ('Malgun Gothic', 'Gulim', 'Batang'):
    if any(f == x.name for x in font_manager.fontManager.ttflist):
        rcParams['font.family'] = f; break
rcParams['axes.unicode_minus'] = False

import product          # 제품 고유값 모음 — 기본값 500pro
# 작업파일(index/index2/vlm_targets/tiles)은 제품별 작업폴더에 있다.
# 예전에는 스크립트 폴더로 고정돼 있어 400Q 로는 아예 돌릴 수 없었다.
# 500PRO 작업파일은 이미 product.SCR(_SCR_BASE) 안에 있으므로 동작이 바뀌지 않는다.
SCR = product.SCR
PN = product.PN


def clean(s):
    s = re.sub(r'\{\\f[^;]*;', '', s)
    s = re.sub(r'\\[A-Za-z][^;]*;', '', s)
    s = s.replace('{', '').replace('}', '').replace('\\P', ' ')
    return s.strip()


def load():
    d1 = json.load(open(SCR + r'\index.json', encoding='utf-8'))
    d2 = json.load(open(SCR + r'\index2.json', encoding='utf-8'))
    T = [[t[2], t[3], clean(t[4])] for t in d1['texts']]
    return d1, d2, T


def locate(T, wanted):
    """도번 -> 도면상 위치들 (부품표 열은 같은 x 에 여러 도번이 쌓이므로 제외)"""
    loc = collections.defaultdict(list)
    for x, y, s in T:
        for m in PN.finditer(s.replace(' ', '')):
            loc[m.group()].append((x, y))
    xcount = collections.Counter(round(x) for p in loc for x, y in loc[p])
    out = {}
    for p in wanted:
        pts = [(x, y) for x, y in loc.get(p, []) if xcount[round(x)] < 5]
        out[p] = pts or loc.get(p, [])
    return out


def drop_ref_cells(T, pts):
    """표제란 'REF. NO.' 칸에 적힌 도번 자리를 뺀다.

    왜 필요한가: REF. NO. 칸의 도번은 '이 시트의 부재'가 아니라 다른 도면을 가리키는
    참조다. 그 자리를 타일로 렌더하면 남의 표제란만 보이고 정작 그 도번의 단품도는
    한 번도 렌더되지 않는다(실측: D800 에서 RJ102-0142 는 라벨 15자리, RJ102-0020 은
    60자리인데 첫 자리만 타일이 됐고 그 자리가 REF.NO. 칸이었다).
    판정은 도면이 그렇게 그려져 있다는 사실에 근거한다 — 'REF. NO.' 글자 바로 오른쪽
    아래 칸 안에 있으면 참조 칸이다. 임계값 맞추기가 아니다.
    """
    refs = [(x, y) for x, y, s in T if s.strip().upper().replace(' ', '') == 'REF.NO.']
    if not refs:
        return pts
    out = {}
    for p, ps in pts.items():
        keep = [(x, y) for x, y in ps
                if not any(rx - 60 <= x <= rx + 900 and ry - 220 <= y <= ry + 60
                           for rx, ry in refs)]
        out[p] = keep or ps      # 전부 참조 칸이면 원래대로 둔다(부재를 잃지 않는다)
    return out


def cluster(pts, box=900.0, all_labels=False):
    """가까운 도번끼리 한 타일로 묶는다.

    all_labels=True 면 도번의 라벨 자리를 '전부' 타일 대상으로 삼는다(기본은 첫 자리만).
    기본값을 그대로 두므로 500PRO/400Q 동작은 100% 동일하다.
    """
    items = sorted(pts.items(), key=lambda kv: (kv[1][0][0] if kv[1] else 0))
    flat = []
    for p, ps in items:
        if not ps:
            continue
        flat.extend((p, x, y) for x, y in (ps if all_labels else ps[:1]))
    tiles = []
    for p, x, y in flat:
        for t in tiles:
            if abs(x - t['cx']) < box and abs(y - t['cy']) < box * 0.6:
                t['pns'].append((p, x, y))
                t['cx'] = np.mean([q[1] for q in t['pns']])
                t['cy'] = np.mean([q[2] for q in t['pns']])
                break
        else:
            tiles.append({'cx': x, 'cy': y, 'pns': [(p, x, y)]})
    return tiles


def render(tile, d1, d2, T, path, pad=260.0):
    xs = [q[1] for q in tile['pns']]; ys = [q[2] for q in tile['pns']]
    X0, X1 = min(xs) - pad, max(xs) + pad
    Y0, Y1 = min(ys) - pad, max(ys) + pad
    w, h = X1 - X0, Y1 - Y0
    # 글자 높이 15px 이상 확보: 도면 글자 ~4mm, 가로 1500px 기준
    fig, ax = plt.subplots(figsize=(15, max(4.0, 15.0 * h / w)))

    def inw(x, y):
        return X0 <= x <= X1 and Y0 <= y <= Y1
    for l in d2['lines']:
        if inw(l[0], l[1]) or inw(l[2], l[3]):
            ax.plot([l[0], l[2]], [l[1], l[3]], '-', color='0.45', lw=0.6)
    for a in d2['arcs']:
        if inw(a[0], a[1]):
            s, e = a[3], a[4]
            if e < s: e += 360
            t = np.radians(np.linspace(s, e, 40))
            ax.plot(a[0] + a[2] * np.cos(t), a[1] + a[2] * np.sin(t), '-', color='0.45', lw=0.6)
    for c in d1['circles']:
        if inw(c[2], c[3]):
            ax.add_patch(plt.Circle((c[2], c[3]), c[4], fill=False, color='0.6', lw=0.6))
    fs = max(5.0, min(11.0, 1400.0 / w * 7))
    for x, y, s in T:
        if inw(x, y) and s:
            tgt = bool(PN.search(s.replace(' ', '')))
            ax.text(x, y, s[:60], fontsize=fs + (1 if tgt else 0),
                    color=('#b00020' if tgt else '#0a5f2a'),
                    weight=('bold' if tgt else 'normal'), va='bottom')
    for p, x, y in tile['pns']:
        ax.plot([x], [y], marker='v', color='#0033cc', ms=9)
    ax.set_aspect('equal'); ax.set_xlim(X0, X1); ax.set_ylim(Y0, Y1)
    ax.set_xticks([]); ax.set_yticks([])
    ax.set_title('TARGET: ' + ', '.join(q[0][-8:] for q in tile['pns']), fontsize=11)
    plt.tight_layout()
    plt.savefig(path, dpi=115)
    plt.close(fig)


if __name__ == '__main__':
    d1, d2, T = load()
    wanted = json.load(open(SCR + r'\vlm_targets.json', encoding='utf-8'))
    pts = locate(T, wanted)
    # 제품이 켠 경우에만: 참조(REF. NO.) 칸 자리를 빼고, 남은 라벨 자리를 전부 렌더한다.
    # 기본은 꺼짐이라 500PRO/400Q 는 이 두 줄을 타지 않는다(동작 100% 동일).
    if getattr(product, 'TILE_ALL_LABELS', False):
        pts = drop_ref_cells(T, pts)
    tiles = cluster(pts, all_labels=getattr(product, 'TILE_ALL_LABELS', False))
    os.makedirs(SCR + r'\tiles', exist_ok=True)
    idx = []
    for i, t in enumerate(tiles):
        p = SCR + r'\tiles\tile_%02d.png' % i
        render(t, d1, d2, T, p)
        idx.append({'png': p, 'pns': [q[0] for q in t['pns']]})
        print('tile %02d  %d개  %s' % (i, len(t['pns']), ','.join(q[0][-8:] for q in t['pns'])))
    json.dump(idx, open(SCR + r'\tiles\index.json', 'w', encoding='utf-8'), ensure_ascii=False, indent=1)
    print('타일', len(tiles), '장')
