"""취수탑 3D 파라메트릭 빌더.

IntakeTowerParams → PyVista 메쉬 리스트 (구성요소별)

구성요소:
1. 주 본체 (concrete shell, 직사각 or L자)
2. 연장부 (접근수로 방향, L자의 경우)
3. 수문 개구부 × N (사각 홀)
4. 수문 개폐장치 × N (원통 + 모터박스)
5. 상부 호이스트 크레인 (beam + trolley)
6. 호이스트 레일 (I-beam)
7. 점검구 커버 (계단식)
8. 각 EL 바닥 slab
9. 출입 계단 (외부)
10. 지붕
11. 난간 / 파라펫
"""

from __future__ import annotations


import numpy as np
import pyvista as pv

from intake_tower_parser import IntakeTowerParams


# 색상 팔레트
COLORS = {
    "body":        "#B8B5A8",  # 콘크리트
    "body_light":  "#C5C2B5",  # 상부/내부
    "gate_steel":  "#3D4A5C",  # 수문 강재
    "actuator":    "#5A4A3A",  # 개폐장치 모터박스
    "actuator_cyl":"#8D9BA6",  # 원통 (잭)
    "hoist_rail":  "#4A4A4A",  # H빔
    "hoist_crane": "#D97706",  # 호이스트 크레인 (주황)
    "stairs":      "#8B7D6B",  # 계단
    "parapet":     "#A8A59B",  # 난간
    "floor":       "#9A968C",  # 바닥
    "inspect":     "#D4A373",  # 점검구 커버
    "roof":        "#7A6A5A",  # 지붕
    "water":       "#3A7AA8",  # 수면
    "ground":      "#7F6F5F",  # 지반
}


class IntakeTowerBuilder:
    """취수탑 파라메트릭 3D 빌더."""

    def __init__(self, params: IntakeTowerParams):
        self.p = params
        self.meshes: list[tuple[pv.PolyData, str, float]] = []

    def build_all(self) -> list[tuple[pv.PolyData, str, float]]:
        self.meshes = []

        self._build_main_body()
        self._build_extension()       # L자 연장부
        self._build_gate_openings()   # 수문 개구부 (구멍)
        self._build_gate_actuators()  # 개폐장치 × N
        self._build_floor_slabs()     # 각 EL 바닥
        self._build_roof()
        self._build_hoist_rail()
        self._build_hoist_crane()
        self._build_inspection_cover()
        self._build_entry_stairs()
        self._build_parapet()
        self._build_ground_and_water()

        return self.meshes

    # --- 주 본체 (L자 또는 직사각) ---

    def _build_main_body(self):
        """주 본체: 벽체 4면 + 바닥.

        좌표계:
        - 원점 = 본체 평면 중심 + 바닥 EL
        - X: 가로 (body_width)
        - Y: 세로 (body_depth)
        - Z: 표고 (body_bottom_el ~ body_top_el)
        """
        p = self.p
        hw = p.body_width / 2
        hd = p.body_depth / 2
        z0 = p.body_bottom_el
        z1 = p.body_top_el

        # 벽 두께
        wall_t = 0.5

        # 4개 벽: 외곽에서 안쪽으로 wall_t 두께
        # 전면 벽 (Y = -hd)
        self._add_wall(-hw, hw, -hd, -hd + wall_t, z0, z1, COLORS["body"])
        # 후면 벽
        self._add_wall(-hw, hw, hd - wall_t, hd, z0, z1, COLORS["body"])
        # 좌측 벽
        self._add_wall(-hw, -hw + wall_t, -hd + wall_t, hd - wall_t, z0, z1, COLORS["body"])
        # 우측 벽
        self._add_wall(hw - wall_t, hw, -hd + wall_t, hd - wall_t, z0, z1, COLORS["body"])

        # 바닥 slab
        self._add_wall(-hw, hw, -hd, hd, z0, z0 + 0.5, COLORS["body"])

    def _add_wall(self, x0, x1, y0, y1, z0, z1, color):
        """박스 형태의 벽 추가."""
        mesh = self._make_box(x0, x1, y0, y1, z0, z1)
        self.meshes.append((mesh, color, 1.0))

    def _make_box(self, x0, x1, y0, y1, z0, z1) -> pv.PolyData:
        pts = np.array([
            [x0, y0, z0], [x1, y0, z0], [x1, y1, z0], [x0, y1, z0],
            [x0, y0, z1], [x1, y0, z1], [x1, y1, z1], [x0, y1, z1],
        ])
        faces = np.hstack([
            [4, 0, 3, 2, 1], [4, 4, 5, 6, 7],
            [4, 0, 1, 5, 4], [4, 2, 3, 7, 6],
            [4, 1, 2, 6, 5], [4, 0, 4, 7, 3],
        ])
        return pv.PolyData(pts, faces)

    # --- 연장부 (L자의 경우 수로 방향) ---

    def _build_extension(self):
        """L자 연장부 — 본체 하류 방향으로 뻗은 접근수로 옹벽."""
        p = self.p
        if not p.has_l_extension or p.extension_length <= 0:
            return

        hw = p.extension_width / 2
        L = p.extension_length
        z0 = p.extension_bottom_el
        z1 = p.body_top_el - 5.0  # 연장부는 본체보다 낮음

        # 연장부: Y+ 방향 (본체 후면에서 계속)
        body_hd = p.body_depth / 2

        # 양쪽 옹벽
        wall_t = 0.5
        # 좌측 옹벽
        self._add_wall(-hw, -hw + wall_t, body_hd, body_hd + L, z0, z1, COLORS["body"])
        # 우측 옹벽
        self._add_wall(hw - wall_t, hw, body_hd, body_hd + L, z0, z1, COLORS["body"])
        # 바닥 slab
        self._add_wall(-hw, hw, body_hd, body_hd + L, z0, z0 + 0.5, COLORS["body"])

    # --- 수문 개구부 (벽에 구멍) ---

    def _build_gate_openings(self):
        """수문 개구부: 본체 전면 벽에 사각 홀로 표현.

        주의: 진짜 boolean cut 대신 어두운 사각형을 벽 앞면에 배치하여 시각적 표현.
        """
        p = self.p
        hd = p.body_depth / 2

        for g in p.gates:
            gx = g.center_x
            gw = g.gate_width
            gh = g.gate_height
            z_center = g.elevation
            z0 = z_center - gh / 2
            z1 = z_center + gh / 2

            # 전면 벽 앞쪽에 약간 돌출시킨 검은 사각형 (어두운 개구부 느낌)
            # 벽 앞면은 Y=-hd이므로 그 앞에 얇은 판 배치
            opening = self._make_box(
                gx - gw / 2, gx + gw / 2,
                -hd - 0.05, -hd + 0.01,   # 벽 전면 바로 앞 (Y-방향으로 밖)
                z0, z1,
            )
            self.meshes.append((opening, "#1A1A1A", 1.0))  # 어두운 홀

    # --- 수문 개폐장치 (원통 + 모터박스) ---

    def _build_gate_actuators(self):
        """각 수문 위에 수직 잭(원통) + 상부 모터박스."""
        p = self.p
        hd = p.body_depth / 2

        for g in p.gates:
            gx = g.center_x
            # 개폐장치는 본체 내부 (벽에서 1m 안쪽)
            ay = -hd + 1.0
            # 잭 원통: 수문 EL부터 상단까지
            jack_bottom = g.elevation + g.gate_height / 2
            jack_top = p.body_top_el - 1.5
            if jack_top <= jack_bottom:
                continue

            try:
                cyl = pv.Cylinder(
                    center=(gx, ay, (jack_bottom + jack_top) / 2),
                    direction=(0, 0, 1),
                    radius=g.actuator_radius * 0.3,  # 잭 지름은 장치 크기의 30%
                    height=(jack_top - jack_bottom),
                    resolution=16,
                ).extract_surface()
                self.meshes.append((cyl, COLORS["actuator_cyl"], 1.0))
            except Exception:
                pass

            # 상부 모터박스 (권양기)
            motor_w = g.actuator_radius * 1.4
            motor_h = 1.0
            motor_z0 = jack_top - motor_h / 2
            motor_z1 = motor_z0 + motor_h
            motor = self._make_box(
                gx - motor_w, gx + motor_w,
                ay - motor_w * 0.7, ay + motor_w * 0.7,
                motor_z0, motor_z1,
            )
            self.meshes.append((motor, COLORS["actuator"], 1.0))

    # --- 바닥 slabs (각 EL) ---

    def _build_floor_slabs(self):
        """각 중간 EL에 얇은 바닥 slab."""
        p = self.p
        hw = p.body_width / 2
        hd = p.body_depth / 2
        slab_t = 0.3

        for el in p.floor_elevations:
            if el <= p.body_bottom_el + 0.5:
                continue
            if el >= p.body_top_el - 0.5:
                continue
            slab = self._make_box(
                -hw + 0.5, hw - 0.5,
                -hd + 0.5, hd - 0.5,
                el - slab_t / 2, el + slab_t / 2,
            )
            self.meshes.append((slab, COLORS["floor"], 1.0))

    # --- 지붕 ---

    def _build_roof(self):
        p = self.p
        hw = p.body_width / 2 + 0.3  # 처마
        hd = p.body_depth / 2 + 0.3
        z0 = p.body_top_el
        z1 = z0 + p.roof_thickness

        roof = self._make_box(-hw, hw, -hd, hd, z0, z1)
        self.meshes.append((roof, COLORS["roof"], 1.0))

    # --- 호이스트 레일 ---

    def _build_hoist_rail(self):
        p = self.p
        if not p.has_hoist:
            return

        # 레일은 본체 상단 천장 부근에 Y축 방향으로 길게
        # 본체 폭 전체에 걸치거나 body_width의 80% 수준
        hw = p.body_width / 2
        rail_z = p.hoist_rail_el
        rail_w = 0.3  # H빔 폭
        rail_h = 0.4  # H빔 높이

        # 레일 2개 (앞쪽, 뒤쪽)
        hd = p.body_depth / 2
        for y_rail in [-hd + 1.5, hd - 1.5]:
            rail = self._make_box(
                -hw + 0.5, hw - 0.5,
                y_rail - rail_w / 2, y_rail + rail_w / 2,
                rail_z, rail_z + rail_h,
            )
            self.meshes.append((rail, COLORS["hoist_rail"], 1.0))

    # --- 호이스트 크레인 (trolley) ---

    def _build_hoist_crane(self):
        p = self.p
        if not p.has_hoist:
            return

        # 크레인 주황색 박스 (레일 중간에 매달린 형태)
        crane_x = 0.0  # 중앙
        crane_w = 1.5
        crane_d = 2.5  # Y 방향
        crane_h = 1.0
        crane_z = p.hoist_rail_el + 0.4  # 레일 위

        crane = self._make_box(
            crane_x - crane_w / 2, crane_x + crane_w / 2,
            -crane_d / 2, crane_d / 2,
            crane_z, crane_z + crane_h,
        )
        self.meshes.append((crane, COLORS["hoist_crane"], 1.0))

    # --- 점검구 커버 ---

    def _build_inspection_cover(self):
        p = self.p
        if not p.has_inspection_cover:
            return

        hw = p.body_width / 2
        hd = p.body_depth / 2

        # 점검구는 지붕 위의 작은 네모 (계단식 2단)
        cx = p.inspection_cover_x
        cy = p.inspection_cover_y
        size = p.inspection_cover_size

        # 본체 내부로 위치 맞춤
        cx = max(-hw + size, min(hw - size, cx))
        cy = max(-hd + size, min(hd - size, cy))

        z_roof = p.body_top_el
        # 2단 계단
        step1 = self._make_box(
            cx - size / 2, cx + size / 2,
            cy - size / 2, cy + size / 2,
            z_roof + p.roof_thickness, z_roof + p.roof_thickness + 0.3,
        )
        self.meshes.append((step1, COLORS["inspect"], 1.0))

        step2 = self._make_box(
            cx - size / 3, cx + size / 3,
            cy - size / 3, cy + size / 3,
            z_roof + p.roof_thickness + 0.3, z_roof + p.roof_thickness + 0.6,
        )
        self.meshes.append((step2, COLORS["inspect"], 1.0))

    # --- 외부 출입 계단 ---

    def _build_entry_stairs(self):
        p = self.p
        if not p.has_entry_stairs:
            return

        hw = p.body_width / 2

        # 지상(body_bottom_el 주변)에서 body_top_el까지 계단
        # 좌측에서 진입 기본
        stair_w = p.stairs_width
        z0 = p.body_bottom_el
        z1 = p.body_top_el
        total_rise = z1 - z0

        # 계단 개수 (step height 0.17m 표준)
        n_steps = max(int(total_rise / 0.17), 10)
        step_h = total_rise / n_steps
        step_d = 0.28  # 디딤판 깊이

        # 계단 위치: 본체 좌측 외부
        x_start = -hw - 1.0
        if p.stairs_side == "right":
            x_start = hw + 1.0

        # 계단을 한 덩어리 경사판으로 표현 (간략화)
        # 또는 각 단을 박스로
        for i in range(n_steps):
            z_step_bottom = z0
            z_step_top = z0 + (i + 1) * step_h
            if p.stairs_side == "left":
                sx0 = x_start - (i + 1) * step_d
                sx1 = x_start - i * step_d
            else:
                sx0 = x_start + i * step_d
                sx1 = x_start + (i + 1) * step_d

            step = self._make_box(
                sx0, sx1,
                -stair_w / 2, stair_w / 2,
                z_step_bottom, z_step_top,
            )
            self.meshes.append((step, COLORS["stairs"], 1.0))

    # --- 난간 / 파라펫 ---

    def _build_parapet(self):
        p = self.p
        if not p.has_parapet:
            return

        hw = p.body_width / 2
        hd = p.body_depth / 2
        z_base = p.body_top_el + p.roof_thickness
        z_top = z_base + p.parapet_height
        thick = 0.15

        # 4면 파라펫
        # 전
        self._add_wall(-hw, hw, -hd, -hd + thick, z_base, z_top, COLORS["parapet"])
        # 후
        self._add_wall(-hw, hw, hd - thick, hd, z_base, z_top, COLORS["parapet"])
        # 좌
        self._add_wall(-hw, -hw + thick, -hd + thick, hd - thick, z_base, z_top, COLORS["parapet"])
        # 우
        self._add_wall(hw - thick, hw, -hd + thick, hd - thick, z_base, z_top, COLORS["parapet"])

    # --- 지반 + 수면 ---

    def _build_ground_and_water(self):
        p = self.p
        # 지반
        ground_margin = max(p.body_width, p.body_depth) * 1.5
        hw = p.body_width / 2 + ground_margin
        hd = p.body_depth / 2 + ground_margin
        # 연장부 고려
        if p.has_l_extension:
            hd += p.extension_length / 2

        ground_pts = np.array([
            [-hw, -hd, p.body_bottom_el - 0.5],
            [hw, -hd, p.body_bottom_el - 0.5],
            [hw, hd, p.body_bottom_el - 0.5],
            [-hw, hd, p.body_bottom_el - 0.5],
        ])
        ground = pv.PolyData(ground_pts, np.array([4, 0, 1, 2, 3]))
        self.meshes.append((ground, COLORS["ground"], 1.0))

        # 상류측 수면 (전방 Y=-hd 방향으로 평판)
        if p.gates:
            # 수면 EL = 가장 높은 수문 EL + 1m (상시만수위 근사)
            max_gate_el = max(g.elevation for g in p.gates)
            water_el = max_gate_el + 2.0  # 약간 여유
            water_hw = p.body_width / 2 + 20
            front_edge = -p.body_depth / 2
            water_pts = np.array([
                [-water_hw, front_edge - 30, water_el],
                [water_hw, front_edge - 30, water_el],
                [water_hw, front_edge + 0.3, water_el],
                [-water_hw, front_edge + 0.3, water_el],
            ])
            water = pv.PolyData(water_pts, np.array([4, 0, 1, 2, 3]))
            self.meshes.append((water, COLORS["water"], 0.8))


# 편의 함수
def build_intake_tower_meshes(params: IntakeTowerParams):
    return IntakeTowerBuilder(params).build_all()


if __name__ == "__main__":
    from intake_tower_parser import parse_intake_tower

    paths = [
        "SAMPLE_CAD/12996710-M40-001 신설 취수탑 설비 설치도(1／2).dxf",
        "SAMPLE_CAD/12996710-M40-002 신설 취수탑 설비 설치도(2／2).dxf",
    ]

    params = parse_intake_tower(paths)
    print(params.summary())

    builder = IntakeTowerBuilder(params)
    meshes = builder.build_all()
    print(f"\n{len(meshes)}개 구성요소 생성:")
    for i, (m, c, o) in enumerate(meshes):
        print(f"  [{i:2}] {m.n_points:5}pts {m.n_cells:5}cells color={c}")
