import bpy
import bmesh
import numpy as np
from pathlib import Path
from mathutils import Vector
from multiprocessing import Pool, cpu_count


def simple_blur(data, iterations=1):
    """
    Simple 3x3 box blur for 2D vector fields.
    """
    result = data.copy()
    h, w, c = data.shape

    for _ in range(iterations):
        blurred = np.zeros_like(result)
        for y in range(h):
            for x in range(w):
                count = 0
                total = np.zeros(c)

                for dy in [-1, 0, 1]:
                    for dx in [-1, 0, 1]:
                        ny, nx = y + dy, x + dx
                        if 0 <= ny < h and 0 <= nx < w:
                            total += result[ny, nx]
                            count += 1

                blurred[y, x] = total / count if count > 0 else result[y, x]

        result = blurred

    return result


def sample_curve_points(curve_obj, samples_per_segment=50):
    """
    Sample points along a Blender curve object (Bezier, NURBS, etc).

    Args:
        curve_obj: Blender curve object
        samples_per_segment: Number of samples per curve segment

    Returns:
        List of Vector world-space positions
    """
    curve_obj.data.resolution_u = samples_per_segment

    depsgraph = bpy.context.evaluated_depsgraph_get()
    curve_eval = curve_obj.evaluated_get(depsgraph)
    mesh_from_curve = curve_eval.to_mesh()

    points = [curve_obj.matrix_world @ v.co for v in mesh_from_curve.vertices]

    curve_eval.to_mesh_clear()

    return points


def compute_arc_lengths(points):
    """
    Compute cumulative arc length at each point along a curve.

    Args:
        points: List of Vector positions

    Returns:
        List of floats representing cumulative distance from curve start
    """
    if len(points) == 0:
        return []

    arc_lengths = [0.0]
    cumulative = 0.0

    for i in range(1, len(points)):
        segment_length = (points[i] - points[i-1]).length
        cumulative += segment_length
        arc_lengths.append(cumulative)

    return arc_lengths


def get_path_curves(collection_name="Paths"):
    """
    Get all curve objects from a collection, or all curves with 'path' in the name.

    Args:
        collection_name: Name of collection containing path curves

    Returns:
        List of curve objects
    """
    collection = bpy.data.collections.get(collection_name)

    if collection:
        curves = [obj for obj in collection.objects if obj.type == 'CURVE']
        print(f"Found {len(curves)} curves in collection '{collection_name}'")
        return curves

    print(f"Collection '{collection_name}' not found. Searching for curves with 'path' in name...")
    curves = [obj for obj in bpy.data.objects if obj.type == 'CURVE' and 'path' in obj.name.lower()]

    if not curves:
        print("No path curves found. Looking for any curve objects...")
        curves = [obj for obj in bpy.data.objects if obj.type == 'CURVE']

    return curves


def closest_point_on_segment_2d(point, seg_start, seg_end):
    """
    Find closest point on a 2D line segment to a given point.

    Args:
        point: Vector2 (x, y)
        seg_start: Vector2 segment start
        seg_end: Vector2 segment end

    Returns:
        Vector2 closest point on segment
    """
    segment = seg_end - seg_start
    point_vec = point - seg_start

    segment_len_sq = segment.length_squared

    if segment_len_sq < 1e-8:
        return seg_start

    t = max(0.0, min(1.0, point_vec.dot(segment) / segment_len_sq))

    return seg_start + segment * t


def process_row_chunk(args):
    """
    Process a chunk of rows for parallel computation.

    Args:
        args: Tuple of (start_row, end_row, width, height_total, min_x, max_x, min_y, max_y, path_segments_simple)

    Returns:
        Tuple of (flow_field_chunk, distance_field_chunk, arc_length_field_chunk, segment_id_chunk)
    """
    start_row, end_row, width, height_total, min_x, max_x, min_y, max_y, path_segments_simple = args

    height = end_row - start_row
    flow_field_chunk = np.zeros((height, width, 2), dtype=np.float32)
    distance_field_chunk = np.zeros((height, width), dtype=np.float32)
    arc_length_field_chunk = np.zeros((height, width), dtype=np.float32)
    segment_id_chunk = np.zeros((height, width), dtype=np.int32)

    for local_y in range(height):
        y = start_row + local_y

        for x in range(width):
            u = x / width
            v = y / height_total

            world_x = min_x + u * (max_x - min_x)
            world_y = min_y + v * (max_y - min_y)
            world_pos_2d = np.array([world_x, world_y])

            min_dist = float('inf')
            closest_point = None
            closest_arc_length = 0.0
            closest_segment_id = -1

            for seg_id, (seg_start, seg_end, arc_start, arc_end) in enumerate(path_segments_simple):
                segment = seg_end - seg_start
                point_vec = world_pos_2d - seg_start

                segment_len_sq = np.dot(segment, segment)

                if segment_len_sq < 1e-8:
                    closest_on_segment = seg_start
                    t = 0.0
                else:
                    t = max(0.0, min(1.0, np.dot(point_vec, segment) / segment_len_sq))
                    closest_on_segment = seg_start + segment * t

                dist = np.linalg.norm(closest_on_segment - world_pos_2d)

                if dist < min_dist:
                    min_dist = dist
                    closest_point = closest_on_segment
                    closest_arc_length = arc_start + t * (arc_end - arc_start)
                    closest_segment_id = seg_id

            if closest_point is not None:
                direction = closest_point - world_pos_2d
                flow_field_chunk[local_y, x, 0] = direction[0]
                flow_field_chunk[local_y, x, 1] = -direction[1]
                distance_field_chunk[local_y, x] = min_dist
                arc_length_field_chunk[local_y, x] = closest_arc_length
                segment_id_chunk[local_y, x] = closest_segment_id

    return (start_row, flow_field_chunk, distance_field_chunk, arc_length_field_chunk, segment_id_chunk)


def generate_flowmap_from_paths(terrain_obj, path_curves, resolution=1024, blur_iterations=2):
    """
    Generate a flowmap texture showing direction toward nearest path.

    Args:
        terrain_obj: Blender mesh object (the terrain plane)
        path_curves: List of Blender curve objects representing paths
        resolution: Output texture resolution (square)
        blur_iterations: Number of smoothing passes for flow field

    Returns:
        Blender image containing encoded flowmap
    """
    print(f"Generating flowmap from {len(path_curves)} path curves")
    print(f"Output resolution: {resolution}×{resolution}")

    mesh = terrain_obj.data
    bm = bmesh.new()
    bm.from_mesh(mesh)
    bm.verts.ensure_lookup_table()

    world_matrix = terrain_obj.matrix_world
    verts_world = [world_matrix @ v.co for v in bm.verts]

    if len(verts_world) == 0:
        raise ValueError("Terrain mesh has no vertices")

    xs = [v.x for v in verts_world]
    ys = [v.y for v in verts_world]

    min_x, max_x = min(xs), max(xs)
    min_y, max_y = min(ys), max(ys)

    print(f"Terrain bounds: X=[{min_x:.2f}, {max_x:.2f}], Y=[{min_y:.2f}, {max_y:.2f}]")

    print("Sampling path curves...")
    all_path_points = []
    for curve_obj in path_curves:
        points = sample_curve_points(curve_obj, samples_per_segment=50)
        all_path_points.extend(points)
        print(f"  {curve_obj.name}: {len(points)} points")

    print(f"Total path points: {len(all_path_points)}")

    if len(all_path_points) == 0:
        raise ValueError("No path points sampled. Check that path curves exist and have geometry.")

    print("Building line segments from path points with arc lengths...")
    path_segments = []
    current_segment_start = 0

    for curve_obj in path_curves:
        curve_points = sample_curve_points(curve_obj, samples_per_segment=50)
        arc_lengths = compute_arc_lengths(curve_points)
        for i in range(len(curve_points) - 1):
            path_segments.append((curve_points[i], curve_points[i + 1], arc_lengths[i], arc_lengths[i + 1]))

    print(f"Created {len(path_segments)} line segments with arc length data")

    print("Generating flow field toward nearest path...")
    h, w = resolution, resolution
    flow_field = np.zeros((h, w, 2), dtype=np.float32)
    distance_field = np.zeros((h, w), dtype=np.float32)
    arc_length_field = np.zeros((h, w), dtype=np.float32)
    segment_id_field = np.zeros((h, w), dtype=np.int32)

    path_segments_simple = [
        (np.array([seg_start.x, seg_start.y]), np.array([seg_end.x, seg_end.y]), arc_start, arc_end)
        for seg_start, seg_end, arc_start, arc_end in path_segments
    ]

    num_cores = cpu_count()
    chunk_size = max(1, h // num_cores)

    print(f"Using {num_cores} CPU cores with chunk size {chunk_size} rows")

    chunks = []
    for start_row in range(0, h, chunk_size):
        end_row = min(start_row + chunk_size, h)
        chunks.append((start_row, end_row, w, h, min_x, max_x, min_y, max_y, path_segments_simple))

    with Pool(processes=num_cores) as pool:
        total_chunks = len(chunks)
        print(f"Processing {total_chunks} chunks...")
        results = []
        for i, result in enumerate(pool.imap(process_row_chunk, chunks)):
            results.append(result)
            progress = (i + 1) / total_chunks * 100
            print(f"  Progress: {i+1}/{total_chunks} chunks ({progress:.1f}%)")

    print("Assembling results from parallel workers...")
    for start_row, flow_chunk, distance_chunk, arc_length_chunk, segment_id_chunk in results:
        end_row = start_row + flow_chunk.shape[0]
        flow_field[start_row:end_row, :, :] = flow_chunk
        distance_field[start_row:end_row, :] = distance_chunk
        arc_length_field[start_row:end_row, :] = arc_length_chunk
        segment_id_field[start_row:end_row, :] = segment_id_chunk

    print(f"Distance range: {distance_field.min():.2f} to {distance_field.max():.2f}")
    print(f"Arc length range: {arc_length_field.min():.2f} to {arc_length_field.max():.2f}")

    print("Normalizing flow vectors...")
    magnitudes = np.sqrt(flow_field[:, :, 0]**2 + flow_field[:, :, 1]**2)
    magnitudes = np.maximum(magnitudes, 1e-8)

    flow_field[:, :, 0] /= magnitudes
    flow_field[:, :, 1] /= magnitudes

    if blur_iterations > 0:
        print(f"Applying distance-based blur (max {blur_iterations} iterations)...")

        original_flow = flow_field.copy()

        print("  Creating maximally blurred version...")
        blurred_flow = simple_blur(flow_field, iterations=blur_iterations)

        magnitudes = np.sqrt(blurred_flow[:, :, 0]**2 + blurred_flow[:, :, 1]**2)
        magnitudes = np.maximum(magnitudes, 1e-8)
        blurred_flow[:, :, 0] /= magnitudes
        blurred_flow[:, :, 1] /= magnitudes

        print("  Blending based on distance to path...")
        max_distance = 60.0
        distance_normalized = np.clip(distance_field / max_distance, 0.0, 1.0)

        blend_factor = distance_normalized[:, :, np.newaxis]

        flow_field = original_flow * (1.0 - blend_factor) + blurred_flow * blend_factor

        magnitudes = np.sqrt(flow_field[:, :, 0]**2 + flow_field[:, :, 1]**2)
        magnitudes = np.maximum(magnitudes, 1e-8)
        flow_field[:, :, 0] /= magnitudes
        flow_field[:, :, 1] /= magnitudes

        print("  Distance-based blur complete!")

    print("Saving segment ID debug image...")
    num_segments = len(path_segments)
    segment_colors = np.random.RandomState(42).rand(num_segments, 3).astype(np.float32)

    segment_img = np.zeros((resolution, resolution, 4), dtype=np.float32)
    for y in range(resolution):
        for x in range(resolution):
            seg_id = segment_id_field[y, x]
            if seg_id >= 0 and seg_id < num_segments:
                segment_img[y, x, 0:3] = segment_colors[seg_id]
            segment_img[y, x, 3] = 1.0

    segment_debug_img = bpy.data.images.new(
        "Segment_Debug", width=resolution, height=resolution, alpha=True, float_buffer=True
    )
    segment_debug_img.pixels[:] = segment_img.flatten()
    segment_debug_img.filepath_raw = str(
        Path(bpy.data.filepath).parent.parent / "textures" / "path_segment_debug.png"
    )
    segment_debug_img.file_format = 'PNG'
    segment_debug_img.save()
    bpy.data.images.remove(segment_debug_img)
    print(f"  Saved to textures/path_segment_debug.png ({num_segments} segments)")

    print("Saving distance field debug image...")
    distance_normalized = np.clip(distance_field / 50.0, 0.0, 1.0)
    distance_img = np.zeros((resolution, resolution, 4), dtype=np.float32)
    distance_img[:, :, 0] = distance_normalized
    distance_img[:, :, 1] = distance_normalized
    distance_img[:, :, 2] = distance_normalized
    distance_img[:, :, 3] = 1.0

    debug_img = bpy.data.images.new(
        "Distance_Debug", width=resolution, height=resolution, alpha=True, float_buffer=True
    )
    debug_img.pixels[:] = distance_img.flatten()
    debug_img.filepath_raw = str(
        Path(bpy.data.filepath).parent.parent / "textures" / "path_distance_debug.png"
    )
    debug_img.file_format = 'PNG'
    debug_img.save()
    bpy.data.images.remove(debug_img)
    print("  Saved to textures/path_distance_debug.png")

    print("Saving direction field debug image...")
    direction_img = np.zeros((resolution, resolution, 4), dtype=np.float32)

    cardinal_dirs = np.array([
        [0.0, 1.0],
        [0.707, 0.707],
        [1.0, 0.0],
        [0.707, -0.707],
        [0.0, -1.0],
        [-0.707, -0.707],
        [-1.0, 0.0],
        [-0.707, 0.707],
    ])

    cardinal_colors = np.array([
        [76, 120, 168],
        [242, 142, 43],
        [225, 87, 89],
        [118, 183, 178],
        [89, 161, 79],
        [237, 201, 72],
        [176, 122, 161],
        [255, 157, 167],
    ]) / 255.0

    flow_flat = flow_field.reshape(-1, 2)
    dots = flow_flat @ cardinal_dirs.T
    closest_dir_indices = np.argmax(dots, axis=1)
    direction_img[:, :, 0:3] = cardinal_colors[closest_dir_indices].reshape(resolution, resolution, 3)
    direction_img[:, :, 3] = 1.0

    print("Drawing compass in lower right corner...")
    compass_radius = resolution // 8
    compass_center_x = resolution - compass_radius - 20
    compass_center_y = compass_radius + 20

    for y in range(resolution):
        for x in range(resolution):
            dx = x - compass_center_x
            dy = y - compass_center_y
            dist = np.sqrt(dx * dx + dy * dy)

            if dist <= compass_radius:
                angle = np.arctan2(dy, dx)
                sector = int(((angle + np.pi / 2) % (2 * np.pi)) / (np.pi / 4)) % 8

                direction_img[y, x, 0:3] = cardinal_colors[sector]

    direction_debug_img = bpy.data.images.new(
        "Direction_Debug", width=resolution, height=resolution, alpha=True, float_buffer=True
    )
    direction_debug_img.pixels[:] = direction_img.flatten()
    direction_debug_img.filepath_raw = str(
        Path(bpy.data.filepath).parent.parent / "textures" / "path_direction_debug.png"
    )
    direction_debug_img.file_format = 'PNG'
    direction_debug_img.save()
    bpy.data.images.remove(direction_debug_img)
    print("  Saved to textures/path_direction_debug.png (8-color cardinal directions)")

    print("\n=== DEBUG: Sample flow field values ===")
    print(f"Terrain bounds: X=[{min_x}, {max_x}], Y=[{min_y}, {max_y}]")

    sample_pixels = [
        (resolution // 4, resolution // 4),
        (resolution // 2, resolution // 2),
        (3 * resolution // 4, 3 * resolution // 4),
    ]

    for py, px in sample_pixels:
        u = px / resolution
        v = py / resolution
        world_x = min_x + u * (max_x - min_x)
        world_y = min_y + v * (max_y - min_y)

        flow_x = flow_field[py, px, 0]
        flow_y = flow_field[py, px, 1]

        dots = [np.dot([flow_x, flow_y], cardinal_dirs[i]) for i in range(8)]
        best_dir = np.argmax(dots)
        dir_names = ["North", "NE", "East", "SE", "South", "SW", "West", "NW"]

        print(f"  Pixel [{px}, {py}] -> World [{world_x:.1f}, {world_y:.1f}]")
        print(f"    Flow: [{flow_x:.3f}, {flow_y:.3f}] -> {dir_names[best_dir]}")
        print(f"    Encoded: R={flow_x * 0.5 + 0.5:.3f}, G={flow_y * 0.5 + 0.5:.3f}")

    print("Encoding to RGBA texture...")
    flow_encoded_x = flow_field[:, :, 0] * 0.5 + 0.5
    flow_encoded_y = flow_field[:, :, 1] * 0.5 + 0.5

    distance_normalized = np.clip(distance_field / max_distance, 0.0, 1.0)

    arc_length_repeat = 100.0
    arc_length_normalized = np.fmod(arc_length_field, arc_length_repeat) / arc_length_repeat

    print(f"Distance encoding: 0.0 = on path, 1.0 = {max_distance}+ units away")
    print(f"Arc length encoding: repeating pattern every {arc_length_repeat} world units")

    flowmap = np.zeros((resolution, resolution, 4), dtype=np.float32)
    flowmap[:, :, 0] = flow_encoded_x
    flowmap[:, :, 1] = flow_encoded_y
    flowmap[:, :, 2] = distance_normalized
    flowmap[:, :, 3] = arc_length_normalized

    print(f"Creating Blender image...")
    output_img = bpy.data.images.new(
        name="Path_Flowmap",
        width=resolution,
        height=resolution,
        alpha=True,
        float_buffer=True
    )

    output_img.pixels[:] = flowmap.flatten()

    bm.free()

    return output_img


def save_flowmap(output_path, resolution=1024, blur_iterations=3, terrain_name="TerrainPlane", path_collection="Paths"):
    """
    Main function to generate and save flowmap from paths in current Blender file.

    Args:
        output_path: Path to save PNG flowmap
        resolution: Output texture resolution
        blur_iterations: Smoothing iterations
        terrain_name: Name of terrain object
        path_collection: Name of collection or search pattern for path curves
    """
    terrain_obj = bpy.data.objects.get(terrain_name)

    if not terrain_obj:
        print(f"Object '{terrain_name}' not found. Searching for terrain-like objects...")
        for obj in bpy.data.objects:
            if obj.type == 'MESH':
                print(f"  Found mesh: {obj.name}")
                if 'terrain' in obj.name.lower() or 'plane' in obj.name.lower():
                    terrain_obj = obj
                    print(f"  -> Using: {obj.name}")
                    break

    if not terrain_obj:
        raise ValueError(
            f"Object '{terrain_name}' not found and no terrain mesh detected. "
            f"Available objects: {[obj.name for obj in bpy.data.objects if obj.type == 'MESH']}"
        )

    print(f"Using terrain object: {terrain_obj.name}")

    if terrain_obj.type != 'MESH':
        raise ValueError(f"Object '{terrain_obj.name}' is not a mesh")

    path_curves = get_path_curves(path_collection)

    if not path_curves:
        raise ValueError(
            f"No path curves found. Create curves and add them to a '{path_collection}' collection "
            f"or name them with 'path' in the name. Available curves: "
            f"{[obj.name for obj in bpy.data.objects if obj.type == 'CURVE']}"
        )

    print(f"Found {len(path_curves)} path curves:")
    for curve in path_curves:
        print(f"  - {curve.name}")

    flowmap_img = generate_flowmap_from_paths(
        terrain_obj,
        path_curves,
        resolution=resolution,
        blur_iterations=blur_iterations
    )

    flowmap_img.filepath_raw = str(output_path)
    flowmap_img.file_format = 'OPEN_EXR'
    flowmap_img.save()

    bpy.data.images.remove(flowmap_img)

    print(f"\n{'='*60}")
    print(f"Flowmap generation complete!")
    print(f"Output: {output_path}")
    print(f"Resolution: {resolution}×{resolution}")
    print(f"Paths used: {len(path_curves)}")
    print(f"Format: OpenEXR (32-bit float)")
    print(f"{'='*60}")
    print("\nChannel encoding:")
    print("  R channel: X direction toward nearest path (0.5=none, <0.5=left, >0.5=right)")
    print("  G channel: Y direction toward nearest path (0.5=none, <0.5=down, >0.5=up)")
    print("  B channel: Distance to nearest path (0.0=on path, 1.0=far away)")
    print("  A channel: Arc length along path (0.0-1.0, repeating every 100 units)")
    print("\nTo use in shader:")
    print("  vec4 flowmap_sample = texture(flowmap, uv);")
    print("  vec2 direction = flowmap_sample.rg * 2.0 - 1.0;  // Direction to path")
    print("  float distance = flowmap_sample.b;                // Distance to path")
    print("  float arc_length = flowmap_sample.a * 100.0;      // Arc length in world units")


if __name__ == "__main__":
    project_root = Path(bpy.data.filepath).parent.parent
    output_path = project_root / "textures" / "terrain_flowmap.exr"

    output_path.parent.mkdir(parents=True, exist_ok=True)

    save_flowmap(
        output_path=output_path,
        resolution=1024,
        blur_iterations=5,
        terrain_name="TerrainPlane",
        path_collection="Paths"
    )
