"""구조물 메쉬를 지형 위에 배치하는 유틸.

구조물 빌더들(intake_tower, valve_chamber, retaining_wall 등)은 구조물을
**원점 중심 + Z=구조물 설계 EL** 로컬 좌표계에서 생성한다.

이를 전체계획 평면도의 해당 구조물 위치에 배치하려면:
  1. XY: 평면도 centroid로 평행이동
  2. 회전: 평면 오리엔테이션(예: 옹벽 길이축)으로 Z축 기준 회전
  3. Z: 옵션1) 구조물 bottom_el 그대로 유지 (설계 EL 기준)
        옵션2) TIN 표면 고도로 이동 (지형에 내려앉힘)

사용법:
    from structure_placement import apply_placement

    placed = apply_placement(
        meshes=template.build_meshes(params),
        plan_centroid=(x, y),
        rotation_deg=0.0,
        z_mode="design",  # or "terrain"
        terrain_mesh=tin_mesh,  # z_mode="terrain" 시 필요
        z_offset=0.0,
    )
"""

from __future__ import annotations

import math

import numpy as np
import pyvista as pv


def rotate_points_z(points: np.ndarray, angle_rad: float,
                     cx: float = 0, cy: float = 0) -> np.ndarray:
    """Z축 기준 회전 (각 points[i] = [x, y, z])."""
    cos_a = math.cos(angle_rad)
    sin_a = math.sin(angle_rad)
    out = points.copy()
    dx = out[:, 0] - cx
    dy = out[:, 1] - cy
    out[:, 0] = cx + dx * cos_a - dy * sin_a
    out[:, 1] = cy + dx * sin_a + dy * cos_a
    return out


def interpolate_terrain_z(tin_mesh: pv.PolyData, xy: tuple[float, float],
                           origin: np.ndarray = None) -> float | None:
    """TIN 표면의 (x, y) 위치에서 Z값 보간.

    Args:
        tin_mesh: PyVista PolyData (TIN 메쉬, 로컬 좌표)
        xy: 월드 좌표 (x, y)
        origin: TIN의 원점 보정 offset [x, y, z] (scanvas_maker.self.origin)

    Returns:
        보간된 Z값 (월드 EL, m) 또는 None (범위 밖)
    """
    if tin_mesh is None:
        return None

    try:
        from scipy.interpolate import LinearNDInterpolator
        pts = np.array(tin_mesh.points)

        # origin 보정: xy를 TIN 로컬 좌표계로 변환
        if origin is not None:
            local_x = xy[0] - origin[0]
            local_y = xy[1] - origin[1]
        else:
            local_x, local_y = xy[0], xy[1]

        interp = LinearNDInterpolator(pts[:, :2], pts[:, 2])
        z_local = interp(local_x, local_y)

        if np.isnan(z_local):
            # 범위 밖 → 중앙값 사용
            z_local = float(np.median(pts[:, 2]))

        # origin Z 복원 (월드 EL로)
        if origin is not None:
            return float(z_local) + origin[2]
        return float(z_local)
    except Exception:
        return None


def apply_placement(
    meshes: list[tuple[pv.PolyData, str, float]],
    plan_centroid: tuple[float, float],
    rotation_deg: float = 0.0,
    z_mode: str = "design",
    terrain_mesh: pv.PolyData | None = None,
    terrain_origin: np.ndarray | None = None,
    z_offset: float = 0.0,
    structure_bottom_el: float | None = None,
    skip_ground: bool = False,
    scale: float = 1.0,
    skip_terrain: bool = False,
    pad_surface_z: float | None = None,
    embed_offset: float = 0.02,
) -> list[tuple[pv.PolyData, str, float]]:
    """구조물 메쉬 리스트를 지형 위 해당 위치에 배치.

    Args:
        meshes: 템플릿이 생성한 로컬 좌표 메쉬들
        plan_centroid: 평면도에서 구조물의 중심 월드 좌표 (x, y)
        rotation_deg: Z축 기준 회전 (도). 옹벽 등 선형 구조물 방향 맞춤용
        z_mode:
          - "design": 구조물 설계 EL 그대로 (Z 이동 없음)
          - "terrain": TIN 표면 고도에 bottom_el이 오도록 이동
          - "offset": z_offset만큼 이동
        terrain_mesh: z_mode="terrain"일 때 사용할 TIN
        terrain_origin: TIN의 origin 보정값
        z_offset: z_mode="offset"일 때 적용할 Z 이동량
        structure_bottom_el: 구조물 바닥 EL (z_mode="terrain"에서 지형에 맞출 때)
        skip_ground: True면 구조물 자체 "ground" 메쉬 제외 (지형 TIN과 중복 방지)
        scale: XY 방향 균등 스케일 (Geo-Referencing 결과). 1.0이면 무변화.
               Z는 의도적으로 유지 (구조물 설계 EL 보존).
        skip_terrain: True면 물/지면/에이프런/뒤채움 관련 메쉬 전부 제외
                      (geo_referencing.EXCLUDE_COLORS 집합 사용)

    Returns:
        변환된 (mesh, color, opacity) 리스트
    """
    if not meshes:
        return []

    # 모든 좌표를 TIN 로컬 좌표계로 통일
    # 템플릿 메쉬: XY=(0,0) 중심, Z=설계 EL (월드)
    # TIN 메쉬: origin이 빠진 로컬 좌표
    # → plan_centroid(월드) - origin = 로컬 배치 위치
    origin = terrain_origin if terrain_origin is not None else np.zeros(3)

    # XY 이동량: 월드 → 로컬
    dx = plan_centroid[0] - origin[0]
    dy = plan_centroid[1] - origin[1]

    # Z 이동량
    dz = 0.0
    if z_mode == "terrain" and structure_bottom_el is not None:
        if pad_surface_z is not None:
            z_local = float(pad_surface_z)
        elif terrain_mesh is not None:
            # TIN 로컬 좌표에서 직접 보간 (interpolate_terrain_z는 월드 EL 반환 → 사용 안 함)
            from scipy.interpolate import LinearNDInterpolator
            tin_pts = np.array(terrain_mesh.points)
            interp = LinearNDInterpolator(tin_pts[:, :2], tin_pts[:, 2])
            z_local = float(interp(dx, dy))
            if np.isnan(z_local):
                z_local = float(np.median(tin_pts[:, 2]))
        else:
            z_local = None
        if z_local is not None:
            # 구조물 바닥 EL(설계 월드값)을 TIN 로컬 표면에 맞춤
            # + embed_offset만큼 아래로 → TIN이 구조물 underside를 가림
            dz = z_local - structure_bottom_el - float(embed_offset)
    elif z_mode == "offset":
        dz = z_offset

    angle_rad = math.radians(rotation_deg)

    out = []
    ground_colors = {"#8B7D6B", "#7F6F5F"}  # ground 색상

    # 물/지면류 전체 제외용 색상 집합 (geo_referencing.EXCLUDE_COLORS와 동일)
    terrain_exclude_colors = {
        "#3A7AA8", "#7F6F5F", "#8B7D6B", "#9A968C", "#8B7355",
    }

    def _norm_color(c):
        if not isinstance(c, str):
            return ""
        s = c.strip().upper()
        if not s.startswith("#"):
            s = "#" + s
        return s

    # CW quad 보정 — apply_placement은 centroid/rotation 기반 폴백이므로
    # plan_centroid만 주어진 경우엔 Y-flip이 무의미함. 하지만 rotation_deg가
    # CW convention으로 주어졌다고 가정하면 y_sign 적용.
    # 단순화를 위해 apply_placement은 현재 CW Y-flip은 적용하지 않음.

    for mesh, color, opacity in meshes:
        norm = _norm_color(color)
        # ground 메쉬 건너뛰기 (지형이 있으면 중복)
        if skip_ground and norm in {c.upper() for c in ground_colors}:
            continue
        # 물/지면/apron/backfill 전부 건너뛰기 (미리보기·최종 배치용)
        if skip_terrain and norm in {c.upper() for c in terrain_exclude_colors}:
            continue

        try:
            new_pts = np.array(mesh.points).copy()

            # 1) XY 스케일 (Z 보존)
            if abs(scale - 1.0) > 1e-6:
                new_pts[:, 0] *= scale
                new_pts[:, 1] *= scale

            # 2) 회전 (원점 기준)
            if abs(rotation_deg) > 0.01:
                new_pts = rotate_points_z(new_pts, angle_rad, cx=0, cy=0)

            # 3) 평행이동 (XY + Z)
            new_pts[:, 0] += dx
            new_pts[:, 1] += dy
            new_pts[:, 2] += dz

            new_mesh = mesh.copy()
            new_mesh.points = new_pts
            out.append((new_mesh, color, opacity))
        except Exception:
            # 개별 메쉬 변환 실패시 건너뜀
            continue

    return out


def fit_meshes_to_quad(
    meshes: list[tuple[pv.PolyData, str, float]],
    quad_world_pts: list,
    terrain_mesh: pv.PolyData | None = None,
    terrain_origin: np.ndarray | None = None,
    structure_bottom_el: float | None = None,
    z_mode: str = "terrain",
    z_offset: float = 0.0,
    skip_ground: bool = True,
    skip_terrain: bool = True,
    scale_mode: str = "none",
    pad_surface_z: float | None = None,
    embed_offset: float = 0.02,
    detail_quad_pts: list | None = None,
    plan_frame_angle_deg: float = 0.0,
    flip_y_for_cw_quad: bool = True,
) -> list[tuple[pv.PolyData, str, float]]:
    """3D 메쉬를 TIN 평면도의 4점 사각형에 **강제 맞춤** (anisotropic scale).

    동작(모든 모드 공통):
      1. 메쉬 XY bbox 중심을 원점으로 이동
      2. scale_mode에 따라 스케일 적용
      3. quad 첫 엣지 각도로 XY 회전
      4. quad 중심(TIN 로컬)으로 XY 이동
      5. z_mode에 따라 Z 이동 (기본: 메쉬 z_min을 TIN 표면에 맞춤)

    Args:
        meshes: 템플릿 빌드 결과 (mesh, color_hex, opacity)
        quad_world_pts: TIN 평면도 4점 (시계방향, 월드 좌표)
        terrain_mesh: TIN PolyData (z_mode="terrain"일 때 표면 Z 보간)
        terrain_origin: TIN 로컬 좌표 변환용 origin (self.origin)
        structure_bottom_el: 호환용 (실제로는 스케일 후 mesh z_min 사용)
        z_mode: "terrain"(표면 안착) | "design"(Z 유지) | "offset"(z_offset)
        skip_ground: True면 ground 색상 메쉬 제외
        skip_terrain: True면 물/지면/apron/backfill 색상 전체 제외
        scale_mode:
          - "none" (기본/권장): 스케일 안 함. 구조물 설계 크기 유지.
          - "xy_only": quad에 맞춰 X/Y만 anisotropic 스케일 (Z 보존).
          - "xyz_uniform": sqrt(sx·sy) 균등 스케일을 X/Y/Z 모두에 적용
            + [0.01, 100] 범위로 clamp (extreme 단위 mismatch 방지)
        pad_surface_z: 굴착 pad 표면 Z (TIN 로컬). 지정 시 quad 중심 보간값 대신
            이 값을 z_surface로 사용 (굴착 영역 내부는 평탄 pad이므로 quad 중심
            보간이 아주 약간 흔들릴 수 있는 경우에도 안정).
        embed_offset: 구조물 바닥 z_min을 표면보다 이만큼 **아래로** 내림 (m).
            TIN(pad 표면)이 구조물의 아래면보다 살짝 위에 있어야 아래에서 위로
            봤을 때 TIN이 underside를 가림 (z-fighting 및 관통 방지).
            기본 0.02m(2cm) — bedding/거푸집 두께 수준으로 물리적으로도 자연스러움.
        detail_quad_pts: 구조물 상세 평면도의 4점 (사용자가 시계방향으로 picked).
            지정 시 detail ↔ TIN의 **상대 회전**을 사용하여 detail이 DXF 원본에서
            비수평으로 그려진 경우도 올바르게 처리. 없으면 절대 angle(TIN only) 폴백.
        plan_frame_angle_deg: detail DXF 내에서 mesh의 +X축이 향하는 각도(도).
            파서가 plan outline의 PCA 주축을 추출해 전달. mesh는 이 각도만큼 먼저
            회전되어 detail의 span 방향에 정렬된 후 detail→TIN 전체 변환이 적용됨.
            0 = 이미 detail의 +X를 따르는 경우.
        flip_y_for_cw_quad: True면 회전 전 mesh의 Y를 반전. 사용자가 picks를
            **시계방향**으로 찍었을 때 기본 회전만으로는 mesh의 +Y가 quad의 edge 3→0
            방향(반시계)으로 가버려 앞뒤가 뒤집힌다. Y-flip으로 mesh +Y가 edge 1→2
            방향(사용자 의도의 "downward/downstream")을 따르도록 보정.

    Returns:
        변환된 (mesh, color, opacity) 리스트 (좌표 = TIN 로컬)
    """
    if not meshes or not quad_world_pts or len(quad_world_pts) < 4:
        return []

    # 색상 필터
    ground_colors = {"#8B7D6B", "#7F6F5F"}
    terrain_colors = {"#3A7AA8", "#7F6F5F", "#8B7D6B", "#9A968C", "#8B7355"}
    exclude = set()
    if skip_ground:
        exclude |= ground_colors
    if skip_terrain:
        exclude |= terrain_colors
    exclude_upper = {c.upper() for c in exclude}

    def _norm(c):
        if not isinstance(c, str):
            return ""
        s = c.strip().upper()
        if not s.startswith("#"):
            s = "#" + s
        return s

    filtered = [(m, c, o) for (m, c, o) in meshes
                if _norm(c) not in exclude_upper]
    if not filtered:
        return []

    # 전체 메쉬 XY/Z bounding box (모든 컴포넌트 통합)
    all_pts_list = []
    for m, _, _ in filtered:
        try:
            pts = np.asarray(m.points, dtype=np.float64)
            if pts.size:
                all_pts_list.append(pts)
        except Exception:
            continue
    if not all_pts_list:
        return []
    all_pts = np.concatenate(all_pts_list, axis=0)
    mesh_xmin, mesh_ymin = float(all_pts[:, 0].min()), float(all_pts[:, 1].min())
    mesh_xmax, mesh_ymax = float(all_pts[:, 0].max()), float(all_pts[:, 1].max())
    aggregate_z_min = float(all_pts[:, 2].min())  # 모든 컴포넌트 중 최저 Z (상대 Z 보존용)
    mesh_cx = (mesh_xmin + mesh_xmax) / 2.0
    mesh_cy = (mesh_ymin + mesh_ymax) / 2.0
    mesh_w = mesh_xmax - mesh_xmin
    mesh_h = mesh_ymax - mesh_ymin

    if mesh_w < 1e-6 or mesh_h < 1e-6:
        return filtered

    # TIN quad (월드)
    quad = np.asarray(quad_world_pts[:4], dtype=np.float64)
    q_center_world = quad.mean(axis=0)

    # 월드 → TIN 로컬 (terrain_origin 차감)
    origin = np.asarray(terrain_origin if terrain_origin is not None else np.zeros(3),
                         dtype=np.float64)
    q_center_local_x = float(q_center_world[0] - origin[0])
    q_center_local_y = float(q_center_world[1] - origin[1])

    edge_01 = quad[1] - quad[0]
    edge_12 = quad[2] - quad[1]
    q_w = float(np.linalg.norm(edge_01))
    q_h = float(np.linalg.norm(edge_12))
    if q_w < 1e-6 or q_h < 1e-6:
        return filtered

    # TIN quad 첫 엣지 각도 (반시계 양수)
    tin_angle = math.atan2(float(edge_01[1]), float(edge_01[0]))

    # Detail quad의 첫 엣지 각도 (있으면 상대회전 계산용)
    if detail_quad_pts and len(detail_quad_pts) >= 4:
        d_quad = np.asarray(detail_quad_pts[:4], dtype=np.float64)
        d_edge_01 = d_quad[1] - d_quad[0]
        if np.linalg.norm(d_edge_01) >= 1e-6:
            detail_angle = math.atan2(float(d_edge_01[1]), float(d_edge_01[0]))
        else:
            detail_angle = 0.0
    else:
        # 폴백: detail의 +X가 edge 0→1과 같다고 가정 (기존 동작)
        detail_angle = 0.0

    # mesh가 detail 프레임 내에서 frame_angle만큼 회전되어 있으면 그만큼 상쇄
    # 최종 회전 = (TIN 각도) - (Detail 각도) - (mesh의 detail 내 회전)
    q_angle = tin_angle - detail_angle - math.radians(plan_frame_angle_deg)
    cos_a = math.cos(q_angle)
    sin_a = math.sin(q_angle)
    # CW quad 보정: True면 mesh Y 반전 후 회전 (mesh +Y가 edge 1→2 따라가도록)
    y_sign = -1.0 if flip_y_for_cw_quad else 1.0

    # Scale 선택
    raw_sx = q_w / mesh_w
    raw_sy = q_h / mesh_h
    if scale_mode == "xy_only":
        scale_x, scale_y, scale_z_val = raw_sx, raw_sy, 1.0
    elif scale_mode == "xyz_uniform":
        s = math.sqrt(abs(raw_sx * raw_sy))
        s = max(0.01, min(100.0, s))  # extreme 방지
        scale_x = scale_y = scale_z_val = s
    else:  # "none" 기본: 구조물 설계 크기 유지
        scale_x = scale_y = scale_z_val = 1.0

    # TIN 표면 Z (quad 중심에서 보간) — terrain 모드 공통
    z_surface = None
    if z_mode == "terrain":
        if pad_surface_z is not None:
            # 굴착 pad가 있으면 TIN 보간 대신 pad Z 직접 사용 (더 정확/안정)
            z_surface = float(pad_surface_z)
        elif terrain_mesh is not None:
            try:
                from scipy.interpolate import LinearNDInterpolator
                tin_pts = np.asarray(terrain_mesh.points, dtype=np.float64)
                interp = LinearNDInterpolator(tin_pts[:, :2], tin_pts[:, 2])
                zs = float(interp(q_center_local_x, q_center_local_y))
                if np.isnan(zs):
                    zs = float(np.median(tin_pts[:, 2]))
                z_surface = zs
            except Exception:
                z_surface = None

    out = []
    for mesh, color, opacity in filtered:
        try:
            pts = np.asarray(mesh.points, dtype=np.float64).copy()

            # 1) XY 중심을 원점으로 이동 (rotation/scale을 원점 기준으로)
            pts[:, 0] -= mesh_cx
            pts[:, 1] -= mesh_cy

            # 2) Anisotropic XY scale + Z scale (기하평균)
            pts[:, 0] *= scale_x
            pts[:, 1] *= scale_y
            if abs(scale_z_val - 1.0) > 1e-9:
                pts[:, 2] *= scale_z_val

            # 2.5) CW quad 보정: mesh Y 반전
            # 사용자가 시계방향으로 picks를 찍으면 quad의 local +Y가 edge 1→2 방향
            # (수학적 CCW 기준 -Y)이 됨. mesh의 +Y를 이쪽으로 정렬하려면 먼저 Y를
            # 반전한 후 표준 CCW 회전을 적용해야 함.
            if y_sign < 0:
                pts[:, 1] = -pts[:, 1]

            # 3) XY 회전 (상대 각도: TIN - detail - plan_frame_angle)
            x = pts[:, 0].copy()
            y = pts[:, 1].copy()
            pts[:, 0] = x * cos_a - y * sin_a
            pts[:, 1] = x * sin_a + y * cos_a

            # 4) XY를 quad 중심(TIN 로컬)으로 이동
            pts[:, 0] += q_center_local_x
            pts[:, 1] += q_center_local_y

            # 5) Z 이동 — 전체 구조물 최저 z_min을 TIN 표면에 맞춤
            # (컴포넌트 개별 z_min이 아니라 aggregate z_min 사용 → 상대 Z 보존)
            if z_mode == "terrain" and z_surface is not None:
                # embed_offset만큼 구조물을 아래로 내림 → TIN이 구조물 bottom보다
                # 살짝 위에 있어 아래에서 보면 TIN이 underside를 가림
                dz = z_surface - aggregate_z_min * scale_z_val - float(embed_offset)
                pts[:, 2] += dz
            elif z_mode == "offset":
                pts[:, 2] += float(z_offset)
            # z_mode == "design" 이면 Z 유지

            new_mesh = mesh.copy()
            new_mesh.points = pts
            out.append((new_mesh, color, opacity))
        except Exception:
            continue

    return out


def compute_orientation_from_points(points: list) -> float:
    """점들의 PCA로 주축 각도(도) 반환.

    Returns:
        주축과 X축 사이 각도 (-90 ~ +90 도)
    """
    if len(points) < 3:
        return 0.0
    arr = np.array(points, dtype=np.float64)
    arr = np.unique(arr, axis=0)
    if len(arr) < 3:
        return 0.0

    centered = arr[:, :2] - arr[:, :2].mean(axis=0)
    try:
        cov = np.cov(centered.T)
        eigenvals, eigenvecs = np.linalg.eigh(cov)
        # 최대 고유값 벡터가 주축
        idx = np.argmax(eigenvals)
        main_axis = eigenvecs[:, idx]
        angle = math.degrees(math.atan2(main_axis[1], main_axis[0]))
        # -90 ~ +90 범위로 정규화
        while angle > 90:
            angle -= 180
        while angle < -90:
            angle += 180
        return angle
    except Exception:
        return 0.0


def combine_meshes(mesh_groups: list[list[tuple]]) -> list[tuple]:
    """여러 구조물의 메쉬 리스트들을 하나로 합침."""
    combined = []
    for group in mesh_groups:
        combined.extend(group)
    return combined


if __name__ == "__main__":
    # 간단 테스트
    import pyvista as pv
    # 작은 박스
    box = pv.Box(bounds=(-1, 1, -1, 1, 0, 2))
    meshes = [(box, "#888888", 1.0)]
    placed = apply_placement(
        meshes=meshes,
        plan_centroid=(100.0, 50.0),
        rotation_deg=45.0,
        z_mode="offset",
        z_offset=10.0,
    )
    print(f"Original bounds: {meshes[0][0].bounds}")
    print(f"Placed bounds: {placed[0][0].bounds}")
    print("(X=99~101 → 99~101 회전, Y=49~51, Z=10~12)")
