import argparse
import os
import sys
import trimesh
import numpy as np
import PIL.Image
from io import BytesIO
import matplotlib

matplotlib.use("Agg")
import matplotlib.pyplot as plt
from mpl_toolkits.mplot3d import Axes3D
import base64
import random
from typing import List, Tuple, Optional, Union
import traceback

os.environ["PYGLET_HEADLESS"] = "1"
os.environ["PYOPENGL_PLATFORM"] = "egl"
PI = np.pi


class ModelLoader:
    """Class responsible for loading 3D models from files."""

    @staticmethod
    def load_from_glb(file_path: str) -> trimesh.Scene:
        """
        Load a 3D model from a GLB file.
        Args:
            file_path: Path to the .glb file
        Returns:
            trimesh.Scene object containing the model
        Raises:
            FileNotFoundError: If the file doesn't exist
            ValueError: If the file can't be loaded as a GLB
        """
        if not os.path.exists(file_path):
            raise FileNotFoundError(f"Model file not found: {file_path}")
        try:
            with open(file_path, "rb") as file_obj:
                mesh = trimesh.load(file_obj, file_type="glb")
            return trimesh.Scene(mesh)
        except Exception as e:
            raise ValueError(f"Failed to load GLB file: {str(e)}")


class BoundingBox:
    """Class for creating and manipulating bounding boxes around 3D models."""

    def __init__(self, scene: trimesh.Scene, scale_factor: float = 1.0):
        """
        Initialize BoundingBox with a scene.
        Args:
            scene: trimesh.Scene object
            scale_factor: Factor to scale the bounding box by
        """
        self.scene = scene
        self.centroid = scene.centroid
        self.bounds = scene.bounds
        self.scale_factor = scale_factor
        self.min_bound, self.max_bound = self._calculate_scaled_bounds()

    def _calculate_scaled_bounds(self) -> Tuple[np.ndarray, np.ndarray]:
        """
        Calculate the scaled bounds of the bounding box.
        Returns:
            Tuple of (min_bound, max_bound) arrays
        """
        min_bound, max_bound = self.bounds
        original_half_size = (max_bound - min_bound) / 2.0
        scaled_half_size = original_half_size * self.scale_factor
        scaled_min_bound = self.centroid - scaled_half_size
        scaled_max_bound = self.centroid + scaled_half_size
        return scaled_min_bound, scaled_max_bound

    def add_to_scene(self) -> trimesh.Scene:
        """
        Add bounding box visualization to the scene.
        Returns:
            Updated scene with bounding box
        """
        corners = np.array([
            [self.min_bound[0], self.min_bound[1], self.min_bound[2]],
            [self.max_bound[0], self.min_bound[1], self.min_bound[2]],
            [self.max_bound[0], self.max_bound[1], self.min_bound[2]],
            [self.min_bound[0], self.max_bound[1], self.min_bound[2]],
            [self.min_bound[0], self.min_bound[1], self.max_bound[2]],
            [self.max_bound[0], self.min_bound[1], self.max_bound[2]],
            [self.max_bound[0], self.max_bound[1], self.max_bound[2]],
            [self.min_bound[0], self.max_bound[1], self.max_bound[2]],
        ])
        edges = np.array([
            [0, 1],
            [1, 2],
            [2, 3],
            [3, 0],
            [4, 5],
            [5, 6],
            [6, 7],
            [7, 4],
            [0, 4],
            [1, 5],
            [2, 6],
            [3, 7],
        ])
        for edge in edges:
            line_points = np.array([corners[edge[0]], corners[edge[1]]])
            line = trimesh.path.Path3D(entities=[trimesh.path.entities.Line([0, 1])], vertices=line_points)
            self.scene.add_geometry(line, node_name=f"bound_edge_{edge[0]}_{edge[1]}")
        return self.scene

    def calculate_face_centers(self) -> List[Tuple[float, float, float]]:
        """
        Calculate the center points of each face of the bounding box.
        Returns:
            List of face center coordinates
        """
        return [
            (
                self.min_bound[0],
                (self.min_bound[1] + self.max_bound[1]) / 2,
                (self.min_bound[2] + self.max_bound[2]) / 2,
            ),
            (
                self.max_bound[0],
                (self.min_bound[1] + self.max_bound[1]) / 2,
                (self.min_bound[2] + self.max_bound[2]) / 2,
            ),
            (
                (self.min_bound[0] + self.max_bound[0]) / 2,
                self.min_bound[1],
                (self.min_bound[2] + self.max_bound[2]) / 2,
            ),
            (
                (self.min_bound[0] + self.max_bound[0]) / 2,
                self.max_bound[1],
                (self.min_bound[2] + self.max_bound[2]) / 2,
            ),
            (
                (self.min_bound[0] + self.max_bound[0]) / 2,
                (self.min_bound[1] + self.max_bound[1]) / 2,
                self.min_bound[2],
            ),
            (
                (self.min_bound[0] + self.max_bound[0]) / 2,
                (self.min_bound[1] + self.max_bound[1]) / 2,
                self.max_bound[2],
            ),
        ]


class VisualElements:
    """Class for creating visual elements like arrows and markers for scene visualization."""

    def __init__(self, scene: trimesh.Scene, bounding_box: BoundingBox):
        """
        Initialize VisualElements with a scene and bounding box.
        Args:
            scene: trimesh.Scene object
            bounding_box: BoundingBox object
        """
        self.scene = scene
        self.bounding_box = bounding_box
        self.face_colors = [
            [255, 0, 0, 255],
            [0, 255, 0, 255],
            [0, 0, 255, 255],
            [255, 255, 0, 255],
            [255, 0, 255, 255],
            [0, 255, 255, 255],
        ]
        self.centroid_color = [255, 255, 255, 255]

    def create_arrow(
        self,
        start_point: Tuple[float, float, float],
        end_point: Tuple[float, float, float],
        color: List[int],
    ) -> Optional[trimesh.Trimesh]:
        """
        Create an arrow pointing from start_point to end_point.
        Args:
            start_point: Starting coordinates of the arrow
            end_point: Ending coordinates of the arrow
            color: RGBA color for the arrow
        Returns:
            Arrow mesh or None if creation fails
        """
        direction = np.array(end_point) - np.array(start_point)
        distance = np.linalg.norm(direction)
        if distance <= 0:
            return None
        direction = direction / distance
        box_size = np.linalg.norm(self.bounding_box.max_bound - self.bounding_box.min_bound)
        arrow_shaft_radius = box_size * 0.005
        arrow_head_radius = arrow_shaft_radius * 3
        arrow_head_length = box_size * 0.03
        arrow_length = min(distance * 0.7, box_size * 0.3)
        shaft_length = arrow_length - arrow_head_length
        if shaft_length <= 0:
            return None
        shaft = trimesh.creation.cylinder(radius=arrow_shaft_radius, height=shaft_length, sections=12)
        shaft.vertices[:, 2] -= shaft_length / 2
        head = trimesh.creation.cone(radius=arrow_head_radius, height=arrow_head_length, sections=12)
        head_transform = np.eye(4)
        head_transform[:3, 3] = [0, 0, shaft_length]
        head.apply_transform(head_transform)
        arrow = trimesh.util.concatenate([shaft, head])
        arrow.visual.face_colors = color
        current_direction = np.array([0, 0, 1])
        rotation_axis = np.cross(current_direction, direction)
        rotation_axis_norm = np.linalg.norm(rotation_axis)
        transform = np.eye(4)
        if rotation_axis_norm > 1e-6:
            rotation_axis = rotation_axis / rotation_axis_norm
            rotation_angle = np.arccos(np.clip(np.dot(current_direction, direction), -1.0, 1.0))
            rotation = trimesh.transformations.rotation_matrix(rotation_angle, rotation_axis)
            transform[:3, :3] = rotation[:3, :3]
        else:
            if np.dot(current_direction, direction) < 0:
                rotation = trimesh.transformations.rotation_matrix(np.pi, [1, 0, 0])
                transform[:3, :3] = rotation[:3, :3]
        transform[:3, 3] = start_point
        arrow.apply_transform(transform)
        return arrow

    def add_face_arrows(self) -> trimesh.Scene:
        """
        Add arrows pointing from each face center to the centroid.
        Returns:
            Updated scene with face arrows
        """
        face_centers = self.bounding_box.calculate_face_centers()
        centroid = self.bounding_box.centroid
        for i, center in enumerate(face_centers):
            arrow = self.create_arrow(center, centroid, self.face_colors[i % len(self.face_colors)])
            if arrow is not None:
                self.scene.add_geometry(arrow, node_name=f"face_arrow_{i}")
        return self.scene

    def add_centroid_marker(self) -> trimesh.Scene:
        """
        Add a marker for the centroid.
        Returns:
            Updated scene with centroid marker
        """
        box_size = np.linalg.norm(self.bounding_box.max_bound - self.bounding_box.min_bound)
        radius = 0.015 * box_size
        centroid_sphere = trimesh.primitives.Sphere(radius=radius, center=self.bounding_box.centroid)
        centroid_sphere.visual.face_colors = self.centroid_color
        self.scene.add_geometry(centroid_sphere, node_name="centroid")
        return self.scene


class SceneRenderer:
    """Class for rendering 3D scenes to images."""

    def __init__(self, scene: trimesh.Scene):
        """
        Initialize SceneRenderer with a scene.
        Args:
            scene: trimesh.Scene object to render
        """
        self.scene = scene

    def render_image(
            self,
            resolution: Tuple[int, int] = (1024, 1024),
            output_path: str = "object.png",
    ) -> str:
        """
        Render the scene and save the image.
        Args:
            resolution: Tuple of (width, height) for the output image
            output_path: Path to save the rendered image
        Returns:
            Path to the saved image
        """
        try:
            png = self.scene.save_image(resolution=resolution, visible=True)
            with open(output_path, "wb") as f:
                f.write(png)
            return output_path
        except Exception as e:
            print(f"Error rendering scene: {str(e)}")
            raise

    def render_from_direction(
            self,
            camera_position: Tuple[float, float, float],
            resolution: Tuple[int, int] = (1024, 1024),
            output_path: str = "object.png",
    ) -> str:
        """
        Render the scene from a specific camera position.
        Args:
            camera_position: Position of the camera
            resolution: Tuple of (width, height) for the output image
            output_path: Path to save the rendered image
        Returns:
            Path to the saved image
        """
        view_scene = self.scene.copy()
        centroid = view_scene.centroid
        camera_target = centroid
        forward = np.array(camera_position) - np.array(camera_target)
        distance = np.linalg.norm(forward)
        if distance > 0:
            forward = forward / distance
        else:
            forward = np.array([0, 0, 1])
        world_up = np.array([0, 0, 1])
        right = np.cross(world_up, forward)
        if np.linalg.norm(right) > 0:
            right = right / np.linalg.norm(right)
        else:
            right = np.array([1, 0, 0])
        camera_up = np.cross(forward, right)
        rotation = np.eye(4)
        rotation[:3, 0] = right
        rotation[:3, 1] = camera_up
        rotation[:3, 2] = forward
        translation = np.eye(4)
        translation[:3, 3] = camera_position
        camera_transform = np.dot(translation, rotation)
        view_scene.camera.fov = [60, 60]
        view_scene.camera.resolution = resolution
        view_scene.camera_transform = camera_transform
        try:
            png = view_scene.save_image(resolution=resolution, visible=True)
            with open(output_path, "wb") as f:
                f.write(png)
            return output_path
        except Exception as e:
            print(f"Error rendering scene from direction: {str(e)}")
            raise

    def render_from_position_and_direction(
        self,
        camera_position: Tuple[float, float, float],
        camera_direction: Tuple[float, float, float],
        resolution: Tuple[int, int] = (1024, 1024),
        output_path: str = "object.png",
        return_png: bool = False,
    ) -> Union[str, bytes]:
        """
        Render the scene from a specific camera position pointing in a specific direction.
        Args:
            camera_position: Position of the camera
            camera_direction: Direction vector the camera is pointing (not normalized)
            resolution: Tuple of (width, height) for the output image
            output_path: Path to save the rendered image
            return_png: If True, return the PNG data instead of saving to file
        Returns:
            Path to the saved image or PNG data as bytes if return_png=True
        """
        view_scene = self.scene.copy()
        forward = np.array(camera_direction)
        distance = np.linalg.norm(forward)
        if distance > 0:
            forward = forward / distance
        else:
            forward = np.array([0, 0, 1])
        world_up = np.array([0, 0, 1])
        right = np.cross(world_up, forward)
        if np.linalg.norm(right) > 0:
            right = right / np.linalg.norm(right)
        else:
            right = np.array([1, 0, 0])
        camera_up = np.cross(forward, right)
        rotation = np.eye(4)
        rotation[:3, 0] = right
        rotation[:3, 1] = camera_up
        rotation[:3, 2] = forward
        translation = np.eye(4)
        translation[:3, 3] = camera_position
        camera_transform = np.dot(translation, rotation)
        view_scene.camera.fov = [60, 60]
        view_scene.camera.resolution = resolution
        view_scene.camera_transform = camera_transform
        try:
            png = view_scene.save_image(resolution=resolution, visible=True)
            if return_png:
                return png
            else:
                with open(output_path, "wb") as f:
                    f.write(png)
                return output_path
        except Exception as e:
            print(f"Error rendering scene from position and direction: {str(e)}{traceback.format_exc()} ")
            raise


class GLBRenderer:
    """Class that combines all functionality to render images from GLB files."""

    @staticmethod
    def render_single_view(
        file_path: str,
        resolution: Tuple[int, int] = (1024, 1024),
        show_bounds: bool = False,
        show_arrows: bool = False,
        output_path: str = "object.png",
    ) -> str:
        """
        Render a single view of a GLB model with visualization elements.
        Args:
            file_path: Path to the .glb file
            resolution: Tuple of (width, height) for the output image
            show_bounds: Whether to show bounding box
            show_arrows: Whether to show arrows and centroid marker
            output_path: Path to save the rendered image
        Returns:
            Path to the saved image
        """
        try:
            scene = ModelLoader.load_from_glb(file_path)
            if show_bounds or show_arrows:
                scale_factor = 1.0 if show_bounds else 8.0
                bbox = BoundingBox(scene, scale_factor)
                if show_bounds:
                    scene = bbox.add_to_scene()
                    print(f"Raw bounding box bounds: [{bbox.min_bound}, {bbox.max_bound}]")
                if show_arrows:
                    visuals = VisualElements(scene, bbox)
                    scene = visuals.add_face_arrows()
                    scene = visuals.add_centroid_marker()
            renderer = SceneRenderer(scene)
            image_path = renderer.render_image(resolution, output_path)
            print(f"Image saved to {image_path}")
            return image_path
        except Exception as e:
            print(f"Error rendering GLB file: {str(e)}")
            raise

    @staticmethod
    def render_six_views(
        file_path: str,
        resolution: Tuple[int, int] = (1024, 1024),
        output_prefix: str = "object",
        show_bounds: bool = False,
        show_arrows: bool = False,
    ) -> List[str]:
        """
        Render six orthogonal views of a GLB model.
        Args:
            file_path: Path to the .glb file
            resolution: Tuple of (width, height) for the output images
            output_prefix: Prefix for output image filenames
            show_bounds: Whether to show bounding box
            show_arrows: Whether to show arrows and centroid marker
        Returns:
            List of paths to the saved images
        """
        try:
            scene = ModelLoader.load_from_glb(file_path)
            scale_factor = 1.0 if show_bounds else 8.0
            bbox = BoundingBox(scene, scale_factor)
            if show_bounds:
                scene = bbox.add_to_scene()
                print(f"Raw bounding box bounds: [{bbox.min_bound}, {bbox.max_bound}]")
            if show_arrows:
                visuals = VisualElements(scene, bbox)
                scene = visuals.add_face_arrows()
                scene = visuals.add_centroid_marker()
            face_centers = bbox.calculate_face_centers()
            direction_names = ["front", "back", "left", "right", "bottom", "top"]
            image_paths = []
            renderer = SceneRenderer(scene)
            for i, center in enumerate(face_centers):
                image_path = f"{output_prefix}_{direction_names[i]}.png"
                renderer.render_from_direction(center, resolution, image_path)
                image_paths.append(image_path)
                print(f"Image saved to {image_path}")
            return image_paths
        except Exception as e:
            print(f"Error rendering six views: {str(e)}")
            raise

    @staticmethod
    def render_from_arrows(
            file_path: str,
            arrow_positions_and_directions: List[Tuple[Tuple[float, float, float], Tuple[float, float, float]]],
            resolution: Tuple[int, int] = (1024, 1024),
            output_prefix: str = "arrow_view",
    ) -> List[str]:
        """
        Render views from arbitrary camera positions and directions.
        Args:
            file_path: Path to the .glb file
            arrow_positions_and_directions: List of (position, direction) tuples
            resolution: Tuple of (width, height) for the output images
            output_prefix: Prefix for output image filenames
        Returns:
            List of paths to the saved images
        """
        try:
            scene = ModelLoader.load_from_glb(file_path)
            image_paths = []
            renderer = SceneRenderer(scene)
            for i, (position, direction) in enumerate(arrow_positions_and_directions):
                image_path = f"{output_prefix}_{i}.png"
                renderer.render_from_position_and_direction(position, direction, resolution, image_path)
                image_paths.append(image_path)
                print(f"Image saved to {image_path}")
            return image_paths
        except Exception as e:
            print(f"Error rendering from arrows: {str(e)}")
            raise

    @staticmethod
    def render_six_arrow_views(
        file_path: str,
        resolution: Tuple[int, int] = (1024, 1024),
        output_prefix: str = "arrow_view",
        show_bounds: bool = False,
        show_arrows: bool = False,
    ) -> List[str]:
        """
        Render six views using calculated arrow positions and directions.
        Args:
            file_path: Path to the .glb file
            resolution: Tuple of (width, height) for the output images
            output_prefix: Prefix for output image filenames
            show_bounds: Whether to show bounding box
            show_arrows: Whether to show arrows and centroid marker
        Returns:
            List of paths to the saved images
        """
        try:
            scene = ModelLoader.load_from_glb(file_path)
            scale_factor = 1.0 if show_bounds else 8.0
            bbox = BoundingBox(scene, scale_factor)
            if show_bounds:
                scene = bbox.add_to_scene()
                print(f"Raw bounding box bounds: [{bbox.min_bound}, {bbox.max_bound}]")
            if show_arrows:
                visuals = VisualElements(scene, bbox)
                scene = visuals.add_face_arrows()
                scene = visuals.add_centroid_marker()
            arrows = GLBRenderer.calculate_six_arrows(scene)
            direction_names = ["front", "back", "left", "right", "bottom", "top"]
            image_paths = []
            renderer = SceneRenderer(scene)
            for i, (position, direction) in enumerate(arrows):
                image_path = f"{output_prefix}_{direction_names[i]}.png"
                renderer.render_from_position_and_direction(position, direction, resolution, image_path)
                image_paths.append(image_path)
                print(f"Image saved to {image_path}")
            return image_paths
        except Exception as e:
            print(f"Error rendering six arrow views: {str(e)}")
            raise

    @staticmethod
    def calculate_six_arrows(
        scene: trimesh.Scene, ) -> List[Tuple[Tuple[float, float, float], Tuple[float, float, float]]]:
        """
        Calculate six camera positions and directions based on the scene's bounding box.
        Args:
            scene: The 3D scene
        Returns:
            List of (position, direction) tuples for camera placement
        """
        bbox = BoundingBox(scene)
        centroid = bbox.centroid
        face_centers = bbox.calculate_face_centers()
        arrows = []
        for center in face_centers:
            position = center
            direction = np.array(center) - np.array(centroid)
            arrows.append((position, tuple(direction)))
        return arrows

    @staticmethod
    def render_from_polaris_position(
        file_path: str,
        position: Tuple[float, float, float],
        resolution: Tuple[int, int] = (1024, 1024),
        output_path: str = "polaris_view.png",
        distance_factor: float = 1.0,
        show_bounds: bool = False,
        return_png: bool = False,
    ) -> Union[str, bytes]:
        """
        Render a view from a specified position in the Polaris system,
        with camera direction calculated as position-to-centroid vector.
        Args:
            file_path: Path to the .glb file
            position: Camera position in the Polaris system
            resolution: Tuple of (width, height) for the output image
            output_path: Path to save the rendered image
            distance_factor: Factor to multiply the bounding box diagonal length by to determine camera distance
            show_bounds: Whether to show bounding box
            return_png: If True, return the PNG data instead of saving to file
        Returns:
            Path to the saved image or PNG data as bytes if return_png=True
        """
        try:
            scene = ModelLoader.load_from_glb(file_path)
            bbox = BoundingBox(scene)
            if show_bounds:
                scene = bbox.add_to_scene()
            centroid = scene.centroid
            diagonal_length = np.linalg.norm(bbox.max_bound - bbox.min_bound)
            direction_vector = np.array(position) - np.array(centroid)
            direction_norm = np.linalg.norm(direction_vector)
            if direction_norm > 0:
                normalized_direction = direction_vector / direction_norm
                adjusted_distance = diagonal_length * distance_factor
                adjusted_position = (np.array(centroid) + normalized_direction * adjusted_distance)
                camera_position = tuple(adjusted_position)
                direction = tuple(normalized_direction)
            else:
                camera_position = position
                direction = tuple(direction_vector)
            renderer = SceneRenderer(scene)
            result = renderer.render_from_position_and_direction(
                camera_position,
                direction,
                resolution,
                output_path,
                return_png=return_png,
            )
            if not return_png:
                print(
                    f"Image saved to {output_path} with distance factor {distance_factor} (diagonal: {diagonal_length:.2f})"
                )
            return result
        except Exception as e:
            print(f"Error rendering from Polaris position: {str(e)}")
            raise

    @staticmethod
    def render_six_views_polaris(
        file_path: str,
        resolution: Tuple[int, int] = (1024, 1024),
        output_prefix: str = "polaris_view",
        distance_factor: float = 1.0,
        show_bounds: bool = False,
        return_paths: bool = True,
    ) -> Union[List[str], List[bytes]]:
        """
        Render six orthogonal views using the polaris position approach.
        Args:
            file_path: Path to the .glb file
            resolution: Tuple of (width, height) for the output images
            output_prefix: Prefix for output image filenames
            distance_factor: Factor to multiply the bounding box diagonal length to determine camera distance
            show_bounds: Whether to show bounding box
            return_paths: If True, return file paths, otherwise return in-memory PNG data
        Returns:
            List of paths to the saved images or list of PNG data as bytes if return_paths=False
        """
        try:
            scene = ModelLoader.load_from_glb(file_path)
            bbox = BoundingBox(scene)
            face_centers = bbox.calculate_face_centers()
            direction_names = ["front", "back", "left", "right", "bottom", "top"]
            results = []
            for i, position in enumerate(face_centers):
                image_path = f"{output_prefix}_{direction_names[i]}.png"
                result = GLBRenderer.render_from_polaris_position(
                    file_path,
                    position,
                    resolution,
                    image_path,
                    distance_factor,
                    show_bounds,
                    return_png=not return_paths,
                )
                results.append(result)
            return results
        except Exception as e:
            print(f"Error rendering six views with polaris: {str(e)}")
            raise


def rotate_camera_positions(positions: List[Tuple[float, float, float]],
                            centroid: Tuple[float, float, float]) -> List[Tuple[float, float, float]]:
    """
    Rotate a set of camera positions around the centroid by a random angle between 10-30 degrees.
    Args:
        positions: List of camera positions
        centroid: Center point to rotate around
    Returns:
        List of rotated camera positions
    """
    angle_x = np.radians(random.uniform(10, 30))
    angle_y = angle_x
    angle_z = angle_x
    rotation_x = np.array([
        [1, 0, 0],
        [0, np.cos(angle_x), -np.sin(angle_x)],
        [0, np.sin(angle_x), np.cos(angle_x)],
    ])
    rotation_y = np.array([
        [np.cos(angle_y), 0, np.sin(angle_y)],
        [0, 1, 0],
        [-np.sin(angle_y), 0, np.cos(angle_y)],
    ])
    rotation_z = np.array([
        [np.cos(angle_z), -np.sin(angle_z), 0],
        [np.sin(angle_z), np.cos(angle_z), 0],
        [0, 0, 1],
    ])
    rotation_matrix = np.dot(rotation_z, np.dot(rotation_y, rotation_x))
    rotated_positions = []
    for pos in positions:
        pos_array = np.array(pos)
        centroid_array = np.array(centroid)
        rel_pos = pos_array - centroid_array
        rotated_rel_pos = np.dot(rotation_matrix, rel_pos)
        rotated_pos = rotated_rel_pos + centroid_array
        rotated_positions.append(tuple(rotated_pos))
    return rotated_positions


def get_image_from_glb(glb_path: str) -> str:
    """
    Generate six views from the GLB file, with the orthogonal camera framework rotated by a random angle,
    and return a combined image as a single base64-encoded string.
    Args:
        glb_path: Path to the .glb file
        standard_view_num: Ignored - always generates six views
        rand_view_num: Ignored - no random views are generated
    Returns:
        Single base64-encoded PNG image as string containing all six views combined in a grid
    """
    temp_dir = os.path.dirname(glb_path)
    if not temp_dir:
        temp_dir = "."
    output_prefix = os.path.join(temp_dir, "temp_view")
    try:
        scene = ModelLoader.load_from_glb(glb_path)
        bbox = BoundingBox(scene)
        centroid = tuple(scene.centroid)
        face_centers = bbox.calculate_face_centers()
        rotated_positions = rotate_camera_positions(face_centers, centroid)
        direction_names = ["front", "back", "left", "right", "bottom", "top"]
        png_data_list = []
        for i, position in enumerate(rotated_positions):
            png_data = GLBRenderer.render_from_polaris_position(
                glb_path,
                position=position,
                resolution=(1024, 1024),
                output_path=os.path.join(temp_dir, f"temp_view_{direction_names[i]}.png"),
                distance_factor=1.0,
                show_bounds=True,
                return_png=True,
            )
            png_data_list.append(png_data)
        pil_images = []
        all_labels = direction_names
        for png_data in png_data_list:
            pil_images.append(PIL.Image.open(BytesIO(png_data)))
        layout = (3, 2)
        rows, cols = layout
        img_width, img_height = pil_images[0].size
        combined_width = cols * img_width
        combined_height = rows * img_height
        combined_img = PIL.Image.new("RGB", (combined_width, combined_height), color="white")
        from PIL import ImageDraw, ImageFont

        draw = ImageDraw.Draw(combined_img)
        try:
            font = ImageFont.truetype("arial.ttf", size=int(img_height * 0.15))
        except IOError:
            try:
                font = ImageFont.truetype(
                    "/usr/share/fonts/truetype/dejavu/DejaVuSans.ttf",
                    size=int(img_height * 0.075),
                )
            except IOError:
                font = ImageFont.load_default()
        for i, (img, label) in enumerate(zip(pil_images, all_labels)):
            row = i // cols
            col = i % cols
            x = col * img_width
            y = row * img_height
            combined_img.paste(img, (x, y))
            draw.text((x + 10, y + 10), label, fill=(0, 0, 0), font=font)
        buffer = BytesIO()
        combined_img.save(buffer, format="PNG")
        buffer.seek(0)
        combined_base64 = base64.b64encode(buffer.getvalue()).decode("utf-8")
        return combined_base64
    except Exception as e:
        print(f"Error in get_image_from_glb: {str(e)}")
        return ""


def main():
    """Main function to parse arguments and call appropriate renderer."""

    parser = argparse.ArgumentParser(description="Generate images from GLB files")
    parser.add_argument("file_path", help="Path to the .glb file")
    parser.add_argument("-s", "--six-views", action="store_true", help="Generate six orthogonal views")
    parser.add_argument(
        "-sr",
        "--six-view-with-two-random",
        action="store_true",
        help="Generate six orthogonal views plus two random views",
    )
    parser.add_argument(
        "-sv",
        "--standard-view-num",
        type=int,
        default=6,
        help="Number of standard views to use (max 6)",
    )
    parser.add_argument(
        "-rv",
        "--rand-view-num",
        type=int,
        default=2,
        help="Number of random views to generate",
    )
    parser.add_argument(
        "-p",
        "--polaris-position",
        type=float,
        nargs=3,
        help="Render from a specific position (x y z) with direction towards centroid",
    )
    parser.add_argument(
        "-d",
        "--distance-factor",
        type=float,
        default=1.0,
        help="Distance factor to multiply bounding box diagonal length",
    )
    parser.add_argument(
        "-b",
        "--show-bounds",
        action="store_true",
        help="Show bounding box in the rendered image",
    )
    parser.add_argument(
        "--resolution",
        type=int,
        nargs=2,
        default=[1024, 1024],
        help="Image resolution (width height)",
    )
    parser.add_argument("--output", default=None, help="Output image path/prefix")
    parser.add_argument(
        "--in-memory",
        action="store_true",
        help="Generate in-memory images instead of saving to files",
    )
    args = parser.parse_args()
    try:
        if args.polaris_position:
            output_path = args.output or "polaris_view.png"
            position = tuple(args.polaris_position)
            result = GLBRenderer.render_from_polaris_position(
                args.file_path,
                position,
                tuple(args.resolution),
                output_path,
                args.distance_factor,
                args.show_bounds,
                return_png=args.in_memory,
            )
            if args.in_memory:
                print(f"Generated in-memory image ({len(result)} bytes)")
        elif (args.six_views or args.six_view_with_two_random or args.standard_view_num > 0 or args.rand_view_num > 0):
            output_prefix = args.output or "polaris_view"
            if args.six_view_with_two_random:
                base64_image = get_image_from_glb(args.file_path)
            elif args.six_views:
                base64_image = get_image_from_glb(args.file_path)
            else:
                base64_image = get_image_from_glb(
                    args.file_path,
                    standard_view_num=args.standard_view_num,
                    rand_view_num=args.rand_view_num,
                )
            if output_prefix:
                combined_path = f"{output_prefix}_combined.png"
                img_data = base64.b64decode(base64_image)
                with open(combined_path, "wb") as f:
                    f.write(img_data)
                print(f"Combined image saved to {combined_path}")
        else:
            print(
                "Error: Please specify either --six-views (-s), --six-view-with-two-random (-sr), --standard-view-num (-sv), --rand-view-num (-rv), or --polaris-position (-p)"
            )
            sys.exit(1)
    except Exception as e:
        print(f"Error: {str(e)}")
        sys.exit(1)


if __name__ == "__main__":
    main()
