import bpy
from os import path
from collections import defaultdict
import bmesh

triangulate_meshes = True

def sprint(*text):
    screen = bpy.data.screens['Scripting']
    for area in screen.areas:
        if area.type != "CONSOLE":
            continue
        override = {'screen': screen, 'area': area}
        with bpy.context.temp_override(**override):
            bpy.ops.console.scrollback_append(text=" ".join(map(str, text)))

print = sprint

def get_material_image(material):
    assert material.use_nodes, material.name
    for node in material.node_tree.nodes:
        if node.type == "TEX_IMAGE":
            return node.image

_offset = 0
texture_ids = {}
prefix = "textures_"

def get_image_id(image):
    global _offset
    global texture_ids
    if image.name in texture_ids:
        value = texture_ids[image.name]
        return value
    value = _offset
    texture_ids[image.name] = value
    width, height = image.size
    #_offset += width * height * 2
    _offset += 1
    return value

def render_vec3(v):
    return f"{{{v.x:.6f}, {v.y:.6f}, {v.z:.6f}}}"

def render_vec2(v):
    return f"{{{v.x:.6f}, {v.y:.6f}}}"

def render_mesh_vertices(f, name, vertices):
    f.write(f"static const vec3 {name}_position[] = {{\n")
    for vertex in vertices:
        f.write(f"  {render_vec3(vertex.co)},\n")
    f.write("};\n\n")

def render_vertex_normals(f, name, vertices):
    f.write(f"static const vec3 {name}_normal[] = {{\n")
    for vertex in vertices:
        f.write(f"  {render_vec3(vertex.normal)},\n")
    f.write("};\n\n")

def render_polygon_normals(f, name, polygon_normals):
    f.write(f"static const vec3 {name}_polygon_normal[] = {{\n")
    for normal in polygon_normals:
        f.write(f"  {render_vec3(normal.vector)},\n")
    f.write("};\n\n")

def sort_by_material(polygons):
    return sorted(polygons, key=lambda p: p.material_index)

def render_polygons(f, name, mesh):
    f.write(f"static const struct polygon {name}_polygons[] = {{\n")

    uv_ix = 0
    for i, polygon in enumerate(mesh.polygons):
        if triangulate_meshes:
            assert len(polygon.vertices) == 3

        if len(polygon.vertices) not in {3, 4}:
            f.write(f"  {{-1, -1, -1, -1, -1, -1}}, // {{{s}}}\n")
            continue
        
        material = mesh.materials[polygon.material_index]
        image = get_material_image(material)
        image_id = get_image_id(image)
        
        if triangulate_meshes:
            indices = [*polygon.vertices, image_id]
        elif len(polygon.vertices) == 4:
            indices = [*polygon.vertices, image_id, uv_ix]
        else:
            indices = [*polygon.vertices, -1, image_id, uv_ix]
                    
        uv_ix += len(polygon.vertices)
        s = ", ".join(map(str, indices))
        f.write(f"  {{{s}}},\n")
        
    f.write("};\n\n")

def render_polygon_edge_pairs(f, name, polygons):
    by_edge = defaultdict(list)
    for i, polygon in enumerate(polygons):
        for edge in polygon.edge_keys:
            by_edge[frozenset(edge)].append(i)

    f.write(f"static const struct edge_polygon {name}_edge_polygons[] = {{\n")
    if all(len(p) == 2 for p in by_edge.values()):
        for edge, polygons in by_edge.items():
            edges = sorted(list(edge))
            assert len(edges) == 2, edges
            assert len(polygons) == 2, polygons
            f.write(f"  {{{{{edges[0]}, {edges[1]}}}, {{{polygons[0]}, {polygons[1]}}}}},\n")
    else:
        f.write("// non-solid polygon\n")
    f.write("};\n\n")

def render_uv_map(f, name, name2, uvm):
    f.write(f"static const vec2 {name}_{name2}_uvmap[] = {{\n")
    for uv in uvm:
        s = render_vec2(uv.vector)
        f.write(f"  {s},\n")
    f.write("};\n\n")

def render_location(f, location):
    s = render_vec3(location)
    f.write(f"  .location = {s},\n")

def render_scale(f, scale):
    s = render_vec3(scale)
    f.write(f"  .scale = {s},\n")

def render_rotation_axis_angle(f, r):
    r = f"{{{r[1]:.6f}, {r[2]:.6f}, {r[3]:.6f}, {r[0]:.6f}}}"
    f.write(f"  .rotation = {r}, // rotation_axis_angle (XYZ T)\n")

def render_rotation_quaternion(f, r):
    r = f"{{{r[1]:.6f}, {r[2]:.6f}, {r[3]:.6f}, {r[0]:.6f}}}"
    f.write(f"  .rotation = {r}, // quaternion (XYZW)\n")

def render_mesh(f, name, mesh):
    f.write(f"static const vec2 * {name}_uv_layers[] = {{\n")
    for layer_name in mesh.uv_layers.keys():
        f.write(f"  {name}_{translate_name(layer_name)}_uvmap,\n");
    f.write( "};\n\n")

    f.write(f"static const struct mesh {name} = {{\n")
    f.write(f"  .position = {name}_position,\n")
    f.write(f"  .position_length = (sizeof ({name}_position)) / (sizeof ({name}_position[0])),\n")
    f.write(f"  .normal = {name}_normal,\n")
    f.write(f"  .normal_length = (sizeof ({name}_normal)) / (sizeof ({name}_normal[0])),\n")
    f.write(f"  .polygon_normal = {name}_polygon_normal,\n")
    f.write(f"  .polygon_normal_length = (sizeof ({name}_polygon_normal)) / (sizeof ({name}_polygon_normal[0])),\n")
    f.write(f"  .polygons = {name}_polygons,\n")
    f.write(f"  .polygons_length = (sizeof ({name}_polygons)) / (sizeof ({name}_polygons[0])),\n")
    f.write(f"  .uv_layers = {name}_uv_layers,\n")
    f.write(f"  .uv_layers_length = (sizeof ({name}_uv_layers)) / (sizeof ({name}_uv_layers[0])),\n")
    f.write(f"  .materials = {name}_materials,\n")
    f.write(f"  .materials_length = (sizeof ({name}_materials)) / (sizeof ({name}_materials[0])),\n")
    f.write(f"  .edge_polygons = {name}_edge_polygons,\n");
    f.write(f"  .edge_polygons_length = (sizeof ({name}_edge_polygons)) / (sizeof ({name}_edge_polygons[0])),\n")
    f.write( "};\n\n")

def translate_name(name):
    return name.replace(".", "_").replace("-", "_").replace(" ", "_")

def mesh_objects(collections):
    objects = set()
    for collection in collections:
        if collection.hide_render:
            continue
        for object in collection.objects:
            assert object.name not in objects, object.name
            objects.add(object.name)
            if object.hide_render:
                continue
            if object.type == "MESH":
                yield object

def mesh_meshes(collections):
    mesh_names = set()
    for object in mesh_objects(collections):
        mesh = object.data
        if mesh.name in mesh_names:
            continue
        mesh_names.add(mesh.name)
        
        if not triangulate_meshes:
            yield mesh.name, mesh
        else:
            # triangulate
            tri_mesh = mesh.copy()
            bm = bmesh.new()
            bm.from_mesh(mesh)
            bmesh.ops.triangulate(bm, faces=bm.faces[:])
            bm.to_mesh(tri_mesh)
            bm.free()
            yield mesh.name, tri_mesh
            bpy.data.meshes.remove(tri_mesh)

def texture_data_name(name):
    name = path.splitext(name)[0]
    name = translate_name(name)
    return f"{prefix}{name}_data"

def render_mesh_materials(f, name, materials):
    f.write(f"static const struct mesh_material {name}_materials[] = {{\n")
    print("materials", materials)
    for material in materials:
        image = get_material_image(material)
        print("image", material, image)
        if image is not None:
            f.write(f"  {{ // {material.name} {image.name}\n")
            width, height = image.size
            texture_id = get_image_id(image)
            f.write(f"    .width = {width},\n")
            f.write(f"    .height = {height},\n")
            f.write(f"    .texture_id = {texture_id},\n")
            f.write("  },\n")
        else:
            f.write("  {},\n")
    f.write("};\n")

def render_materials(f):
    f.write("static const struct material materials[] = {\n")
    for image_name, texture_id in sorted(texture_ids.items(), key=lambda i: i[1]):
        name = texture_data_name(image_name)
        f.write("  {\n")
        #f.write(f"    .start = (void *)&_binary_{name}_start,\n")
        #f.write(f"    .size = (int)&_binary_{name}_size,\n")
        f.write(f"    .name = \"{image_name}\",\n")
        f.write(f"    .texture_id = {texture_id},\n")
        f.write("  },\n")
    f.write("};\n\n");

def export_meshes(f):
    for mesh_name, mesh in mesh_meshes(bpy.data.collections):
        #mesh.vertex_normals
        #mesh.vertex_colors
        #mesh.vertices
        #mesh.uv_layers
        #mesh.polygons
        #mesh.polygon_normals
        #mesh.name

        mesh_name = "mesh_" + translate_name(mesh_name)

        render_mesh_vertices(f, mesh_name, mesh.vertices)
        for layer_name, layer in mesh.uv_layers.items():
            render_uv_map(f, mesh_name, translate_name(layer_name), layer.uv)
        render_vertex_normals(f, mesh_name, mesh.vertices)
        render_polygon_normals(f, mesh_name, mesh.polygon_normals)
        render_polygons(f, mesh_name, mesh)
        render_polygon_edge_pairs(f, mesh_name, mesh.polygons)
        render_mesh_materials(f, mesh_name, mesh.materials)

        render_mesh(f, mesh_name, mesh)

        #mesh.polygons[0].vertices
        # [0, 1, 3, 2]

        # v = mesh.vertices[0]
        # v.normal
        # v.index

def mesh_objects_sorted(objects):
    def key(o):
        return (o.data.name, o.name)
    return sorted(mesh_objects(objects), key=key)

def export_objects(f):
    f.write("static const struct object objects[] = {\n")
    for object in mesh_objects_sorted(bpy.data.collections):

        #object.rotation_mode = 'AXIS_ANGLE'
        #object.name
        #object.rotation_axis_angle
        #object.rotation_euler
        #object.location

        obj_name = "object_" + translate_name(object.name)

        f.write(f"  {{ // {obj_name}\n")
        
        obj_mesh_name = "mesh_" + translate_name(object.data.name)

        f.write("  ")
        f.write(f"  .mesh = &{obj_mesh_name},\n")

        location, rotation, scale = object.matrix_world.decompose()
        f.write("  ")
        render_scale(f, scale)
        f.write("  ")
        render_rotation_quaternion(f, rotation)
        f.write("  ")
        render_location(f, location)

        f.write("  },\n")
    f.write("};\n\n")

def export_scene(f):
    export_meshes(f)
    export_objects(f)
    render_materials(f)

home = path.expanduser('~')
with open(path.join(home, "output.h"), "w") as f:
    offset = 0
    export_scene(f)
