# -*- coding: utf-8 -*-
"""
Universal Format Converter Module
Author  : CraftPilot

Provides conversion functionality between common file formats (PNG/JPG/OBJ/USD, etc.) and internal data types.
"""

import os
import json
import numpy as np
from typing import List, Dict, Optional, Tuple
from PIL import Image

from .dson_geo import Texture, TextureNode, StaticMesh, StaticMeshNode, image2node, node2image
from .dson_field import FileField

# USD converter module (lazy import)
try:
    from . import usd_converter
    USD_SUPPORT_AVAILABLE = usd_converter.check_usd_support()
except ImportError:
    usd_converter = None
    USD_SUPPORT_AVAILABLE = False


# ============================================================================
# Image Format Conversion (Image/Texture)
# ============================================================================

def image_to_texture(image_path: str, name: str = None) -> Texture:
    """
    Convert common image files (PNG, JPG, TGA, BMP) to Texture object
    
    Args:
        image_path: Absolute path to the image file
        name: Texture node name, defaults to filename (without extension)
        
    Returns:
        Texture: Converted Texture object
        
    Raises:
        FileNotFoundError: Image file does not exist
        ValueError: Unsupported image format
    """
    if not os.path.exists(image_path):
        raise FileNotFoundError(f"Image file does not exist: {image_path}")
    
    # Supported image formats
    supported_formats = {'.png', '.jpg', '.jpeg', '.tga', '.bmp', '.tiff'}
    ext = os.path.splitext(image_path)[1].lower()
    if ext not in supported_formats:
        raise ValueError(f"Unsupported image format: {ext}, supported formats: {supported_formats}")
    
    # Use filename as default name
    if name is None:
        name = os.path.splitext(os.path.basename(image_path))[0]
    
    # Load image
    image = Image.open(image_path)
    
    # Ensure image has at least 3 channels (RGB)
    if image.mode == 'L':  # Grayscale
        image = image.convert('RGB')
    elif image.mode == 'LA':  # Grayscale + Alpha
        image = image.convert('RGBA')
    elif image.mode == 'P':  # Palette mode
        image = image.convert('RGBA')
    elif image.mode not in ('RGB', 'RGBA'):
        image = image.convert('RGB')
    
    # Use existing Texture.create_from_images method
    texture = Texture.create_from_images([image])
    
    # Rename texture node
    if texture.texture_nodes:
        old_name = list(texture.texture_nodes.keys())[0]
        texture.texture_nodes[name] = texture.texture_nodes.pop(old_name)
        texture.texture_nodes[name].name = name
    
    return texture


def texture_to_image(texture: Texture, output_path: str, node_name: str = None) -> None:
    """
    Export Texture object to common image format (PNG)
    
    Args:
        texture: Texture object to export
        output_path: Output file path (supports .png, .jpg, etc.)
        node_name: Texture node name to export, defaults to first node
        
    Raises:
        ValueError: Texture object is empty or specified node does not exist
    """
    if not texture.texture_nodes:
        raise ValueError("Texture object has no texture nodes")
    
    # Select node to export
    if node_name is None:
        node_name = list(texture.texture_nodes.keys())[0]
    
    if node_name not in texture.texture_nodes:
        raise ValueError(f"Texture node '{node_name}' does not exist")
    
    texture_node = texture.texture_nodes[node_name]
    image = node2image(texture_node)
    
    # Ensure output directory exists
    output_dir = os.path.dirname(output_path)
    if output_dir and not os.path.exists(output_dir):
        os.makedirs(output_dir)
    
    image.save(output_path)


def texture_to_images(texture: Texture, output_dir: str, format: str = 'png') -> Dict[str, str]:
    """
    Export all texture nodes of Texture object to image files
    
    Args:
        texture: Texture object to export
        output_dir: Output directory path
        format: Output format (png, jpg, etc.)
        
    Returns:
        Dict[str, str]: Mapping from node name to output file path
    """
    if not os.path.exists(output_dir):
        os.makedirs(output_dir)
    
    result = {}
    images_dict = texture.to_images()
    for name, image in images_dict.items():
        output_path = os.path.join(output_dir, f"{name}.{format}")
        image.save(output_path)
        result[name] = output_path
    
    return result


# ============================================================================
# OBJ Model Format Conversion (StaticMesh)
# ============================================================================

def obj_to_staticmesh(obj_path: str, name: str = None) -> StaticMesh:
    """
    Convert OBJ file to StaticMesh object
    
    Args:
        obj_path: Absolute path to the OBJ file
        name: Mesh node name, defaults to filename (without extension)
        
    Returns:
        StaticMesh: Converted StaticMesh object
        
    Raises:
        FileNotFoundError: OBJ file does not exist
        ValueError: OBJ file format error
    """
    if not os.path.exists(obj_path):
        raise FileNotFoundError(f"OBJ file does not exist: {obj_path}")
    
    # Use filename as default name
    if name is None:
        name = os.path.splitext(os.path.basename(obj_path))[0]
    
    positions = []
    tex_coords = []
    normals = []
    faces = []
    
    with open(obj_path, 'r', encoding='utf-8') as f:
        for line in f:
            line = line.strip()
            if not line or line.startswith('#'):
                continue
            
            parts = line.split()
            if not parts:
                continue
            
            if parts[0] == 'v':
                # Vertex position: v x y z [w]
                if len(parts) >= 4:
                    positions.append([float(parts[1]), float(parts[2]), float(parts[3])])
            
            elif parts[0] == 'vt':
                # Texture coordinate: vt u v [w]
                if len(parts) >= 3:
                    tex_coords.append([float(parts[1]), float(parts[2])])
            
            elif parts[0] == 'vn':
                # Normal: vn x y z
                if len(parts) >= 4:
                    normals.append([float(parts[1]), float(parts[2]), float(parts[3])])
            
            elif parts[0] == 'f':
                # Face: f v1[/vt1][/vn1] v2[/vt2][/vn2] v3[/vt3][/vn3] ...
                face_vertices = []
                for vertex in parts[1:]:
                    # Parse vertex index (OBJ index starts from 1)
                    indices = vertex.split('/')
                    v_idx = int(indices[0]) - 1  # Convert to 0-based index
                    face_vertices.append(v_idx)
                
                # Triangulate (if polygon)
                if len(face_vertices) >= 3:
                    for i in range(1, len(face_vertices) - 1):
                        faces.append([face_vertices[0], face_vertices[i], face_vertices[i + 1]])
    
    if not positions:
        raise ValueError(f"OBJ file has no valid vertex data: {obj_path}")
    
    # Create StaticMesh object
    static_mesh = StaticMesh()
    mesh_node = StaticMeshNode()
    mesh_node.name = name
    mesh_node.positions = positions
    mesh_node.faces = faces
    
    static_mesh.staticmesh_nodes[name] = mesh_node
    
    return static_mesh


def staticmesh_to_obj(staticmesh: StaticMesh, output_path: str, node_name: str = None) -> None:
    """
    Export StaticMesh object to OBJ format
    
    Args:
        staticmesh: StaticMesh object to export
        output_path: Output file path
        node_name: Mesh node name to export, defaults to first node
        
    Raises:
        ValueError: StaticMesh object is empty or specified node does not exist
    """
    if not staticmesh.staticmesh_nodes:
        raise ValueError("StaticMesh object has no mesh nodes")
    
    # Select node to export
    if node_name is None:
        node_name = list(staticmesh.staticmesh_nodes.keys())[0]
    
    if node_name not in staticmesh.staticmesh_nodes:
        raise ValueError(f"Mesh node '{node_name}' does not exist")
    
    mesh_node = staticmesh.staticmesh_nodes[node_name]
    
    # Ensure output directory exists
    output_dir = os.path.dirname(output_path)
    if output_dir and not os.path.exists(output_dir):
        os.makedirs(output_dir)
    
    with open(output_path, 'w', encoding='utf-8') as f:
        f.write(f"# OBJ file exported by CraftPilot\n")
        f.write(f"# Object: {node_name}\n")
        f.write(f"o {node_name}\n")
        
        # Write vertex positions
        for pos in mesh_node.positions:
            f.write(f"v {pos[0]} {pos[1]} {pos[2]}\n")
        
        # Write faces (OBJ index starts from 1)
        for face in mesh_node.faces:
            if isinstance(face, (list, tuple)) and len(face) >= 3:
                # Convert to 1-based index
                indices = [str(idx + 1) for idx in face]
                f.write(f"f {' '.join(indices)}\n")
            elif isinstance(face, int):
                # If faces is a flat index list, special handling needed
                pass  # This case needs data reorganization


def staticmesh_to_objs(staticmesh: StaticMesh, output_dir: str) -> Dict[str, str]:
    """
    Export all mesh nodes of StaticMesh object to OBJ files
    
    Args:
        staticmesh: StaticMesh object to export
        output_dir: Output directory path
        
    Returns:
        Dict[str, str]: Mapping from node name to output file path
    """
    if not os.path.exists(output_dir):
        os.makedirs(output_dir)
    
    result = {}
    for name in staticmesh.staticmesh_nodes.keys():
        output_path = os.path.join(output_dir, f"{name}.obj")
        staticmesh_to_obj(staticmesh, output_path, name)
        result[name] = output_path
    
    return result


# ============================================================================
# Simple OBJ Model Generation
# ============================================================================

def create_cube_obj(size: float = 1.0) -> Tuple[List[List[float]], List[List[int]]]:
    """
    Create vertex and face data for a simple cube
    
    Args:
        size: Half of the cube edge length
        
    Returns:
        Tuple[positions, faces]: Vertex position list and face index list
    """
    s = size
    positions = [
        [-s, -s, -s], [s, -s, -s], [s, s, -s], [-s, s, -s],  # Front
        [-s, -s, s], [s, -s, s], [s, s, s], [-s, s, s],       # Back
    ]
    
    # Triangle faces (each quad split into 2 triangles)
    faces = [
        # Front
        [0, 1, 2], [0, 2, 3],
        # Back
        [5, 4, 7], [5, 7, 6],
        # Left
        [4, 0, 3], [4, 3, 7],
        # Right
        [1, 5, 6], [1, 6, 2],
        # Top
        [3, 2, 6], [3, 6, 7],
        # Bottom
        [4, 5, 1], [4, 1, 0],
    ]
    
    return positions, faces


def create_cube_staticmesh(name: str = "cube", size: float = 1.0) -> StaticMesh:
    """
    Create a StaticMesh object for a simple cube
    
    Args:
        name: Mesh name
        size: Half of the cube edge length
        
    Returns:
        StaticMesh: StaticMesh object of the cube
    """
    positions, faces = create_cube_obj(size)
    
    static_mesh = StaticMesh()
    mesh_node = StaticMeshNode()
    mesh_node.name = name
    mesh_node.positions = positions
    mesh_node.faces = faces
    
    static_mesh.staticmesh_nodes[name] = mesh_node
    
    return static_mesh


def save_cube_obj(output_path: str, size: float = 1.0) -> None:
    """
    Save a simple cube OBJ file
    
    Args:
        output_path: Output file path
        size: Half of the cube edge length
    """
    positions, faces = create_cube_obj(size)
    
    # Ensure output directory exists
    output_dir = os.path.dirname(output_path)
    if output_dir and not os.path.exists(output_dir):
        os.makedirs(output_dir)
    
    with open(output_path, 'w', encoding='utf-8') as f:
        f.write("# OBJ file - Simple Cube\n")
        f.write("o cube\n")
        
        for pos in positions:
            f.write(f"v {pos[0]} {pos[1]} {pos[2]}\n")
        
        for face in faces:
            # OBJ index starts from 1
            f.write(f"f {face[0]+1} {face[1]+1} {face[2]+1}\n")


# ============================================================================
# File Handling (FileField)
# ============================================================================

def create_file_field(file_path: str, work_path: str = None) -> FileField:
    """
    Create FileField object
    
    Args:
        file_path: File path (relative to work_path or absolute path)
        work_path: Working directory path, if None uses file_path's directory
        
    Returns:
        FileField: Created FileField object
        
    Raises:
        FileNotFoundError: File does not exist
    """
    if work_path is None:
        # If absolute path, use file's directory as work_path
        if os.path.isabs(file_path):
            work_path = os.path.dirname(file_path)
            file_name = os.path.basename(file_path)
        else:
            work_path = os.getcwd()
            file_name = file_path
    else:
        # If work_path provided, file_path should be relative path or filename
        if os.path.isabs(file_path):
            file_name = os.path.relpath(file_path, work_path)
        else:
            file_name = file_path
    
    # Verify file exists
    full_path = os.path.join(work_path, file_name)
    if not os.path.exists(full_path):
        raise FileNotFoundError(f"File does not exist: {full_path}")
    
    return FileField(file_name, work_path)


def create_file_field_list(file_paths: List[str], work_path: str) -> List[FileField]:
    """
    Create list of FileField objects
    
    Args:
        file_paths: List of file paths (relative to work_path)
        work_path: Working directory path
        
    Returns:
        List[FileField]: List of FileField objects
    """
    return [create_file_field(fp, work_path) for fp in file_paths]


# ============================================================================
# Universal Loader (automatically selects conversion method based on file extension)
# ============================================================================

def load_texture_from_file(file_path: str, name: str = None) -> Texture:
    """
    Load Texture from file (automatically recognizes format)
    
    Supported formats: PNG, JPG, JPEG, TGA, BMP, TIFF
    """
    return image_to_texture(file_path, name)


def load_staticmesh_from_file(file_path: str, name: str = None) -> StaticMesh:
    """
    Load StaticMesh from file (automatically recognizes format)
    
    Supported formats: OBJ, USD/USDA/USDC
    """
    ext = os.path.splitext(file_path)[1].lower()
    if ext == '.obj':
        return obj_to_staticmesh(file_path, name)
    elif ext in {'.usd', '.usda', '.usdc'}:
        # USD format requires special handling, convert to JSON first
        raise ValueError(f"For USD format, please use usd_to_json() function to convert to JSON format first")
    else:
        raise ValueError(f"Unsupported 3D model format: {ext}, currently supported: .obj, .usd/.usda/.usdc")


# ============================================================================
# Test Data Creation Helper Functions
# ============================================================================

def create_test_text_file(output_path: str, content: str = "This is a test file.") -> None:
    """
    Create test text file
    
    Args:
        output_path: Output file path
        content: File content
    """
    output_dir = os.path.dirname(output_path)
    if output_dir and not os.path.exists(output_dir):
        os.makedirs(output_dir)
    
    with open(output_path, 'w', encoding='utf-8') as f:
        f.write(content)


def create_test_binary_file(output_path: str, size: int = 1024) -> None:
    """
    Create test binary file
    
    Args:
        output_path: Output file path
        size: File size (in bytes)
    """
    output_dir = os.path.dirname(output_path)
    if output_dir and not os.path.exists(output_dir):
        os.makedirs(output_dir)
    
    # Generate random binary data
    data = bytes(range(256)) * (size // 256 + 1)
    data = data[:size]
    
    with open(output_path, 'wb') as f:
        f.write(data)


def create_test_image(output_path: str, width: int = 64, height: int = 64, 
                      color: Tuple[int, int, int] = (255, 128, 0)) -> None:
    """
    Create test image file
    
    Args:
        output_path: Output file path
        width: Image width
        height: Image height
        color: RGB color value
    """
    output_dir = os.path.dirname(output_path)
    if output_dir and not os.path.exists(output_dir):
        os.makedirs(output_dir)
    
    # Create simple colored image
    image = Image.new('RGB', (width, height), color)
    
    # Add some variation to make image less monotonous
    pixels = image.load()
    for i in range(width):
        for j in range(height):
            r = (color[0] + i * 2) % 256
            g = (color[1] + j * 2) % 256
            b = (color[2] + (i + j)) % 256
            pixels[i, j] = (r, g, b)
    
    image.save(output_path)


# ============================================================================
# USD Format Conversion (USD -> JSON)
# ============================================================================

def is_usd_file(file_path: str) -> bool:
    """
    Check if file is USD format
    
    Args:
        file_path: File path
        
    Returns:
        bool: Whether it is a USD file (.usd, .usda, .usdc)
    """
    if usd_converter:
        return usd_converter.is_usd_file(file_path)
    else:
        ext = os.path.splitext(file_path)[1].lower()
        return ext in {'.usd', '.usda', '.usdc'}


def check_usd_support() -> bool:
    """
    Check if USD support is available
    
    Returns:
        bool: Whether USD library is installed and available
    """
    return USD_SUPPORT_AVAILABLE


def usd_to_json(usd_file_path: str, 
                output_json_path: Optional[str] = None,
                include_cameras: bool = False,
                include_lights: bool = False) -> Dict:
    """
    Convert USD file to PSON JSON format
    
    This is the main USD conversion interface provided by the format_converter module.
    
    Args:
        usd_file_path: USD file path (.usd/.usda/.usdc)
        output_json_path: Output JSON file path (optional)
        include_cameras: Whether to include camera data
        include_lights: Whether to include light data
        
    Returns:
        Dict: JSON data in PSON format
        
    Raises:
        ImportError: USD library not installed
        FileNotFoundError: USD file does not exist
        ValueError: USD file format error
        
    Example:
        >>> json_data = usd_to_json("model.usd", "output.json")
        >>> print(json_data.keys())
        dict_keys(['Output_Geo', 'metadata'])
    """
    if not USD_SUPPORT_AVAILABLE:
        raise ImportError(
            "USD library not installed. Please run: pip install usd-core\n"
            "If already installed and still getting errors, please check if USD library is properly configured."
        )
    
    return usd_converter.usd_to_pson_json(
        usd_file_path, 
        output_json_path,
        include_cameras=include_cameras,
        include_lights=include_lights
    )


def usd_file_to_json_file(usd_file_path: str, 
                          output_json_path: Optional[str] = None) -> str:
    """
    Convert USD file to JSON file (simplified interface)
    
    Args:
        usd_file_path: USD file path
        output_json_path: Output JSON path (if None, auto-generated)
        
    Returns:
        str: Output JSON file path
        
    Raises:
        ImportError: USD library not installed
    """
    if not USD_SUPPORT_AVAILABLE:
        raise ImportError("USD library not installed. Please run: pip install usd-core")
    
    return usd_converter.usd_file_to_json_file(usd_file_path, output_json_path)


def batch_convert_usd_to_json(usd_dir: str, 
                               output_dir: Optional[str] = None,
                               recursive: bool = False) -> List[Tuple[str, str]]:
    """
    Batch convert USD files in directory to JSON
    
    Args:
        usd_dir: Directory containing USD files
        output_dir: Output directory (if None, output to original directory)
        recursive: Whether to recursively search subdirectories
        
    Returns:
        List[Tuple[str, str]]: List of successfully converted files [(usd_path, json_path), ...]
        
    Raises:
        ImportError: USD library not installed
    """
    if not USD_SUPPORT_AVAILABLE:
        raise ImportError("USD library not installed. Please run: pip install usd-core")
    
    return usd_converter.batch_convert_usd_to_json(usd_dir, output_dir, recursive)
