# -*- coding: utf-8 -*-
"""Pass 5b: BOM 표(부품표)의 규격 컬럼에서 도번별 두께를 읽는다.

콜아웃(pass5)은 '도번과 두께가 같은 말풍선 안에 붙어 있는' 경우만 잡는다.
그런데 이 도면은 대부분 두께가 콜아웃이 아니라 BOM 표의 규격칸(PL12, T9 …)에 있다.

BOM 표의 구조적 특징(숫자 튜닝이 아니라 표의 정의):
  - 한 행(row) = 같은 Y 좌표. 도번칸과 규격칸이 같은 높이에 나란히 있다.
  - 규격칸은 도번칸의 오른쪽에 있다.
따라서 '도번 텍스트와 같은 Y 밴드에, 오른쪽으로 가까이 있는 규격 셀이 정확히 1개'
일 때만 그 행을 BOM 행으로 인정한다. 0개(표가 아님) 또는 2개 이상(행 구분 실패)은
파싱 실패로 남긴다 — 추측해서 고르지 않는다.

두께의 '위치'는 BOM 표 안이지 도면 위가 아니다. 그래서 부재를 찾을 좌표(at)로는
BOM 행 좌표를 쓰면 안 된다. 같은 도번이 도면 위에 붙어 있는 라벨(= BOM 행이 아닌
출현)의 좌표를 쓴다.

거르는 것:
  - 형강/각관/채널(SPSR, CHANNEL, PIPE, ANGLE …): 숫자는 '살두께'라서 평판으로
    압출하면 형상이 틀린다. 판재가 아니므로 제외.
  - 규격에 두께가 여러 개(PL25-PL30T 등): 어느 쪽인지 확정 불가 → 실패 목록.
  - 콜아웃 두께와 BOM 두께가 다르면 → 만들지 않고 [CEO확인] 목록.
"""
import json, re, collections, sys

import product          # 제품 고유값 모음 — 기본값 500pro
SCR = product.SCR

# --- BOM 행 판정 기준(표의 기하 구조에서 나온 값) ---
ROW_DY = 2.0     # 같은 행으로 볼 Y 오차. 표의 행 간격(12~20mm)의 1/6 이하로 잡아 옆 행 침범 차단
# 규격칸은 도번칸 오른쪽 이 거리 안. 표의 열 배치가 제품마다 달라 product.py 로 옮겼다.
ROW_DX = product.BOM_DX

PN = product.PN            # 도번 형식은 제품마다 다르다 (product.py 참조)
THK = re.compile(r'(?:PL\s*[-.]?\s*(\d{1,3}(?:\.\d)?))'
                 r'|(?:(\d{1,3}(?:\.\d)?)\s*[Tt](?![A-Za-z]))'
                 r'|(?:\bT\s*(\d{1,3}(?:\.\d)?)\b)')
# 판재가 아닌 단면(형강·각관·채널·파이프) — 숫자가 살두께라 평판 압출 대상이 아니다
SECTION = re.compile(r'SPSR|CHANNEL|PIPE|ANGLE|H-?BEAM|C-?CHAN|\bSQ\b', re.I)
# '100x50xPL4.5' 처럼 단면치수 2개가 앞에 붙은 규격도 각관/형강이다(평판 아님)
# D800 의 실제 표기 '100X50X.T4.5' 처럼 두께 앞에 마침표·하이픈이 끼는 변형이 있다.
# 그 한 글자 때문에 각파이프가 판재로 새어 들어와 4.5T 평판으로 압출됐다(RJ102-0090).
# 정규식을 느슨하게 만든 게 아니라, 같은 '가로x세로x살두께' 표기의 구두점 변형을 담은 것이다.
SECTION_DIM = re.compile(r'\d+\s*[x*×]\s*\d+\s*[x*×]\s*[.\-]?\s*(?:PL|T)?\s*\d', re.I)


def clean(s):
    s = re.sub(r'\{\\[fF][^;]*;', '', s)
    s = re.sub(r'\\[A-Za-z][^;\\]*;', '', s)
    s = s.replace('{', ' ').replace('}', ' ').replace('\\P', ' ')
    return re.sub(r'\s+', ' ', s).strip()


def thicknesses(s):
    out = []
    for m in THK.finditer(s):
        v = float(m.group(1) or m.group(2) or m.group(3))
        if 3 <= v <= 80:          # 판재 두께로 말이 되는 범위
            out.append(v)
    return sorted(set(out))


def main():
    T = json.load(open(SCR + r'\index.json', encoding='utf-8'))['texts']
    rows = [(x, y, clean(s)) for h, lay, x, y, s in T]

    spec = [(x, y, c, thicknesses(c)) for x, y, c in rows if thicknesses(c)]
    pnocc = [(p, x, y, c) for x, y, c in rows for p in set(PN.findall(c))]
    allpn = sorted({p for p, _, _, _ in pnocc})

    # ---------- BOM 표의 도번 컬럼 찾기 ----------
    # 표의 도번칸은 X 가 완전히 같은 값으로 여러 행에 걸쳐 반복된다(수작업 라벨은 그럴 수 없다).
    # 이 컬럼 안의 출현은 '표 안의 칸'이지 도면 위 부재 위치가 아니므로 위치 후보에서 뺀다.
    # (표 좌표를 부재 위치로 쓰면 표 옆 도형을 부재로 오인식한다.)
    colcnt = collections.defaultdict(set)
    for p, px, py, pc in pnocc:
        colcnt[round(px, 3)].add(p)
    pn_cols = {x for x, s in colcnt.items() if len(s) >= 2}

    # ---------- 각 도번 출현이 BOM 행인지 판정 ----------
    bom_rows = collections.defaultdict(list)   # pn -> [(spectext, thk리스트, x, y)]
    label_at = collections.defaultdict(list)   # pn -> [(x, y)]  (BOM 행이 아닌 = 도면 위 라벨)
    fail = {}                                  # pn -> 실패 사유

    for p, px, py, pc in pnocc:
        hit = [s for s in spec if abs(s[1]-py) <= ROW_DY and 0 < s[0]-px <= ROW_DX]
        if len(hit) == 1:
            bom_rows[p].append((hit[0][2], hit[0][3], hit[0][0], hit[0][1]))
        elif len(hit) == 0:
            if round(px, 3) not in pn_cols:      # 표 컬럼 안이면 위치 라벨이 아니다
                label_at[p].append((px, py))
        else:
            fail[p] = '같은 행에 규격 셀이 %d개 — 행 구분 실패' % len(hit)

    ok, skipped, ceo = [], [], []
    for p in allpn:
        brs = bom_rows.get(p)
        if not brs:
            if p not in fail:
                fail[p] = 'BOM 표에서 이 도번의 행을 못 찾음(규격칸 없음)'
            continue

        texts = sorted({b[0] for b in brs})
        # 판재가 아닌 단면 제외
        if any(SECTION.search(t) or SECTION_DIM.search(t) for t in texts):
            skipped.append(dict(pn=p, why='판재 아님(형강/각관/채널) — 평판 압출 대상 아님',
                                bom=' | '.join(texts)))
            continue
        vals = sorted({v for b in brs for v in b[1]})
        if len(vals) != 1:
            skipped.append(dict(pn=p, why='규격에 두께가 %d개(%s) — 확정 불가'
                                % (len(vals), ', '.join(str(v) for v in vals)),
                                bom=' | '.join(texts)))
            continue
        thk = vals[0]
        at = label_at.get(p)
        if not at:
            skipped.append(dict(pn=p, why='도면상 위치 라벨 없음(모든 출현이 BOM 표 안) — 부재 위치 불명',
                                bom=' | '.join(texts)))
            continue
        # 라벨이 여럿이면 각각을 후보 좌표로 넘긴다(pass6 가 게이트로 걸러낸다)
        ok.append(dict(pn=[p], thk=[thk], at=list(at[0]), at_all=[list(a) for a in at],
                       lines=['BOM: ' + ' | '.join(texts)], src='BOM'))

    # ---------- 콜아웃과 대조 ----------
    callout = {}
    try:
        cb = json.load(open(SCR + r'\thk_blocks.json', encoding='utf-8'))['ok']
        callout = {b['pn'][0]: (b['thk'][0], ' / '.join(b.get('lines', []))) for b in cb}
    except Exception as ex:
        print('(콜아웃 결과 읽기 실패: %s)' % ex)

    final = []
    for r in ok:
        p = r['pn'][0]
        if p in callout:
            cthk, clines = callout[p]
            if abs(cthk - r['thk'][0]) > 1e-6:
                ceo.append(dict(pn=p, callout=cthk, bom=r['thk'][0],
                                callout_text=clines, bom_text=r['lines'][0]))
                continue        # 충돌 부재는 만들지 않는다
            # 일치 → 콜아웃이 이미 처리하므로 BOM 항목은 중복 제외
            continue
        final.append(r)

    print('=== BOM 파싱 결과 ===')
    print('전체 도번            : %d' % len(allpn))
    print('BOM 행 파싱 성공     : %d' % len(bom_rows))
    print('BOM 행 파싱 실패     : %d' % len(fail))
    print('  두께 확정(판재)    : %d' % len(ok))
    print('  제외(형강/모호/무위치): %d' % len(skipped))
    print('콜아웃과 충돌[CEO확인]: %d' % len(ceo))
    print('신규 압출 대상        : %d' % len(final))

    print('\n--- 신규 압출 대상 ---')
    for r in sorted(final, key=lambda r: r['pn'][0]):
        print('  %-18s %5.1fT  @(%.0f,%.0f)  %s'
              % (r['pn'][0], r['thk'][0], r['at'][0], r['at'][1], r['lines'][0]))
    print('\n--- 제외 ---')
    for s in skipped:
        print('  %-18s %s  [%s]' % (s['pn'], s['why'], s['bom']))
    print('\n--- [CEO확인] 콜아웃 vs BOM 충돌 ---')
    for c in ceo:
        print('  %-18s 콜아웃 %sT vs BOM %sT | 콜아웃원문: %s | BOM: %s'
              % (c['pn'], c['callout'], c['bom'], c['callout_text'][:60], c['bom_text']))
    print('\n--- BOM 행 파싱 실패(두께 출처 없음) %d ---' % len(fail))
    for p in sorted(fail):
        print('  %-18s %s' % (p, fail[p]))

    json.dump(dict(ok=final, skipped=skipped, ceo=ceo,
                   fail={k: v for k, v in fail.items()},
                   parsed=len(bom_rows)),
              open(SCR + r'\bom_blocks.json', 'w', encoding='utf-8'),
              ensure_ascii=False, indent=1)
    print('\n-> bom_blocks.json 저장')


if __name__ == '__main__':
    main()
