# cube_sphere.gd
@tool
extends MeshInstance3D

@export_group("sphere")
@export_range(2, 256) var resolution: int = 16:
    set(value):
        resolution = value
        if is_inside_tree():
            _generate_surface()
@export_range(1, 12) var subdivisions: int = 1:
    set(value):
        subdivisions = value
        if is_inside_tree():
            _generate_surface()
@export_range (0.1, 100.0) var radius: float = 1.0:
    set(value):
        radius = value
        if is_inside_tree():
            _generate_surface()
@export var debug_mode = false:
    set(value):
        debug_mode = value
        if is_inside_tree():
            _generate_surface()
@export var wireframe_mode = false:
    set(value):
        wireframe_mode = value
        _update_material()
@export var use_terrain_shader = false:
    set(value):
        use_terrain_shader = value
        _update_material()
@export var use_world_shader = true:
    set(value):
        use_world_shader = value
        _update_material()
@export var heightmap: Texture2D:
    set(value):
        heightmap = value
        if is_inside_tree():
            _update_material()
@export var earth_texture: Texture2D:
    set(value):
        earth_texture = value
        if is_inside_tree():
            _update_material()
@export_range(0.0, 0.2) var height_scale: float = 0.02:
    set(value):
        height_scale = value
        if is_inside_tree():
            _update_material()
@export var show_height_debug: bool = false:
    set(value):
        show_height_debug = value
        if is_inside_tree():
            _update_material()
        
func _ready():
    # generate mesh on game start
    _generate_surface()
    _update_material()
    
func _generate_surface():
    # if new mesh > create arraymesh for it
    if not mesh:
        mesh = ArrayMesh.new()
    # start with clean slate
    mesh.clear_surfaces()
    # arrays for mesh construction
    var verts = PackedVector3Array()
    var uvs = PackedVector2Array()
    var normals = PackedVector3Array()
    var indices = PackedInt32Array()
    var colors = PackedColorArray()
    # mesh generation code : 6 faces of cube
    var face_normals = [
        Vector3.UP,
        Vector3.DOWN,
        Vector3.LEFT,
        Vector3.RIGHT,
        Vector3.FORWARD,
        Vector3.BACK,
    ]
    var face_colors = [
        Color.CORAL,
        Color.DARK_OLIVE_GREEN,
        Color.AQUAMARINE,
        Color.DODGER_BLUE,
        Color.LIGHT_SALMON,
        Color.LIGHT_GREEN,
    ]
    var face_coverage_per_subface = 1.0 / subdivisions
    # debug
    if debug_mode:
        print("\n--- mesh generation start ---")
        print("resolution : ", resolution)
        print("subdivisions : ", subdivisions)
        print("radius : ", radius)
        print("facecoveragepersubface : ", face_coverage_per_subface)
    # generate 6 faces
    for face_index in range(face_normals.size()):
        var normal_dir = face_normals[face_index]
        var current_face_color = face_colors[face_index]
        if debug_mode:
            print("\n--- face ", face_index,
            " | normal : ", normal_dir,
            " ---")
        for y in range(subdivisions):
            for x in range(subdivisions):
                var start_uv = Vector2(x, y) * face_coverage_per_subface
                var end_uv = start_uv + Vector2.ONE * face_coverage_per_subface
                #breakpoint
                if debug_mode and face_index == 0 and x == 0 and y == 0:
                    print(
                        "  1st subface - startuv : ", start_uv,
                        " | enduv : ", end_uv)
                _create_face(
                    normal_dir,
                    start_uv, 
                    end_uv,
                    verts,
                    normals,
                    uvs,
                    indices,
                    colors, 
                    current_face_color,
                    #face_index == 0 and x == 0 and y == 0 # debug 1st subface only
                    )
    # assemble final mesh data 
    var surface_array = []
    surface_array.resize(Mesh.ARRAY_MAX)
    # assign arrays to surface array
    surface_array[Mesh.ARRAY_VERTEX] = verts
    surface_array[Mesh.ARRAY_TEX_UV] = uvs
    surface_array[Mesh.ARRAY_NORMAL] = normals
    surface_array[Mesh.ARRAY_INDEX] = indices
    surface_array[Mesh.ARRAY_COLOR] = colors
    # add surface to mesh 
    mesh.add_surface_from_arrays(Mesh.PRIMITIVE_TRIANGLES, surface_array)
    if debug_mode:
        print("\n--- mesh generation done ---")
        print("totalvertices : ", verts.size())
        print("totalindices : ", indices.size())
    _update_material()
    # save mesh to .tres file to be reused (need be not re-generated, like
    # global resource ? ie world globe map as game ui entry into game levels /
    # individual world countries ...
    ResourceSaver.save(mesh, "res://sphere.tres", ResourceSaver.FLAG_COMPRESS)

func _update_material():
    if not mesh or mesh.get_surface_count() == 0:
        return
    
    var shader_mat = ShaderMaterial.new()
    
    if wireframe_mode:
        # wireframe material
        shader_mat.shader = load("res://shaders/wireframe.gdshader")
    elif use_world_shader:
        # world shader with heightmap and earth texture
        shader_mat.shader = load("res://shaders/world.gdshader")
        if heightmap:
            shader_mat.set_shader_parameter("height_map", heightmap)
        if earth_texture:
            shader_mat.set_shader_parameter("earth_texture", earth_texture)
        shader_mat.set_shader_parameter("height_scale", height_scale)
        shader_mat.set_shader_parameter("apply_height", true)
        shader_mat.set_shader_parameter("show_height_debug", show_height_debug)
    elif use_terrain_shader:
        # cartoon shader : color per height
        shader_mat.shader = load("res://shaders/terrain.gdshader")
        if heightmap:
            shader_mat.set_shader_parameter("height_map", heightmap)
        shader_mat.set_shader_parameter("height_scale", height_scale)
        shader_mat.set_shader_parameter("apply_height", true)
        shader_mat.set_shader_parameter("show_height_debug", show_height_debug)
    else:
        # default: color faces
        shader_mat.shader = load("res://shaders/color_faces.gdshader")
    
    set_surface_override_material(0, shader_mat)

func _create_face(
    p_normal: Vector3,
    p_start_uv: Vector2,
    p_end_uv: Vector2,
    p_verts: PackedVector3Array,
    p_normals: PackedVector3Array,
    p_uvs: PackedVector2Array,
    p_indices: PackedInt32Array,
    p_colors: PackedColorArray,
    p_face_color: Color,
    ):
        # create 2 axes perpendicular to face normal
        var axis_a = Vector3(p_normal.y, p_normal.z, p_normal.x)
        var axis_b = p_normal.cross(axis_a)
        if debug_mode:
            print("\n createface called")
            print("\n normal : ", p_normal)
            print("\n axisa : ", axis_a)
            print("\n axisb : ", axis_b)
            print("\n startuv : ", p_start_uv)
            print("\n enduv : ", p_end_uv)
        # get number of vertices we have
        var base_vertex_index = p_verts.size()
        # calculate step sizes
        var dx = (p_end_uv.x - p_start_uv.x) / (resolution - 1)
        var dy = (p_end_uv.y - p_start_uv.y) / (resolution - 1)
#        breakpoint
        if debug_mode:
            print(" dx : ", dx, " dy : ", dy)
            print(" basevertexindex : ", base_vertex_index)
        # start position
        var ty = p_start_uv.y
        # loop through grid of vertices
        for y in range(resolution):
            var tx = p_start_uv.x
            for x in range(resolution):
                var i = x + y * resolution
                #breakpoint
                # map cube point onto a unit sphere
                var point_on_unit_cube = p_normal \
                    + (tx - 0.5) * 2.0 * axis_a \
                    + (ty - 0.5) * 2.0 * axis_b
                # map to sphere
                var point_on_unit_sphere = _cube_point_to_sphere_point(
                    point_on_unit_cube)
                # final vertex position
                var final_vertex = point_on_unit_sphere * radius
                #breakpoint
                # debug 1st 4 corners & middle, prevent known result warning
                @warning_ignore("integer_division")
                var mid_point := resolution / 2
                if debug_mode and (
                    (x == 0 and y == 0) or # top-left
                    (x == resolution - 1 and y ==0) or # top-right
                    (x == 0 and y == resolution - 1) or # bottom-left
                    (x == resolution - 1 and y == resolution - 1) or # bottom-right
                    (x == mid_point and y == mid_point) # middle
                ):
                    print("\n vertex ", x, "-", y, " i=", i)
                    print("\n tx : ", tx, " ty : ", ty)
                    print("\n pointoncube : ", point_on_unit_cube)
                    print("\n pointonsphere : ", point_on_unit_sphere)
                    print("\n finalvertex : ", final_vertex)
                # add vertex and normal
                p_verts.push_back(final_vertex)
                #p_verts.push_back(point_on_unit_sphere * radius)
                p_normals.push_back(point_on_unit_sphere)
                p_colors.push_back(p_face_color)
                # uv-based on grid position
                #var uv = Vector2(
                    #float(x) / (resolution - 1.0), 
                    #float(y) / (resolution - 1.0)
                #)
                var uv = GeoMaths.point_to_uv(point_on_unit_cube)
                # image origin is top-left : flip vertical & horizontal
                #uv.x = 1.0 - uv.x
                #uv.y = 1.0 - uv.y
                # wrap horizontal to prevent image seam todo not working
                #uv.x = uv.x - floor(uv.x)
                p_uvs.push_back(uv)
                # create 2 tris for quad
                if x != resolution - 1 and y != resolution - 1:
                    # 1st tris
                    p_indices.push_back(base_vertex_index + i)
                    p_indices.push_back(base_vertex_index + i + resolution)
                    p_indices.push_back(base_vertex_index + i + resolution + 1)
                    # 2nd tris
                    p_indices.push_back(base_vertex_index + i)
                    p_indices.push_back(base_vertex_index + i + resolution + 1)
                    p_indices.push_back(base_vertex_index + i + 1)
                tx += dx
            ty += dy
## maps a point on a unit cube to a point on a unit sphere
# http://mathproofs.blogspot.com/2005/ 07/mapping-cube-to-sphere.html
func _cube_point_to_sphere_point(p: Vector3) -> Vector3:
    var x2 = p.x * p.x / 2.0
    var y2 = p.y * p.y / 2.0
    var z2 = p.z * p.z / 2.0
    var x = p.x * sqrt(1.0 - y2 - z2 + (p.y * p.y * p.z * p.z) / 3.0)
    var y = p.y * sqrt(1.0 - z2 - x2 + (p.x * p.x * p.z * p.z) / 3.0)
    var z = p.z * sqrt(1.0 - x2 - y2 + (p.x * p.x * p.y * p.y) / 3.0)
    
    return Vector3(x, y, z)
