# The contents of this file are subject to the Common Public Attribution License
# Version 1.0 (the “License”); you may not use this file except in compliance
# with the License. You may obtain a copy of the License at
# https://myou.dev/licenses/LICENSE-CPAL. The License is based on the Mozilla
# Public License Version 1.1 but Sections 14 and 15 have been added to cover use
# of software over a computer network and provide for limited attribution for
# the Original Developer. In addition, Exhibit A has been modified to be
# consistent with Exhibit B.
#
# Software distributed under the License is distributed on an “AS IS” basis,
# WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License for
# the specific language governing rights and limitations under the License.
#
# The Original Code is Myou Engine.
#
# the Original Developer is the Initial Developer.
#
# The Initial Developer of the Original Code is the Myou Engine developers.
# All portions of the code written by the Myou Engine developers are Copyright
# (c) 2024. All Rights Reserved.
#
# Alternatively, the contents of this file may be used under the terms of the
# GNU Affero General Public License version 3 (the [AGPL-3] License), in which
# case the provisions of [AGPL-3] License are applicable instead of those above.
#
# If you wish to allow use of your version of this file only under the terms of
# the [AGPL-3] License and not to allow others to use your version of this file
# under the CPAL, indicate your decision by deleting the provisions above and
# replace them with the notice and other provisions required by the [AGPL-3]
# License. If you do not delete the provisions above, a recipient may use your
# version of this file under either the CPAL or the [AGPL-3] License.

import ../types
import vmath except Quat
import ../quat

when defined(nimdoc):
    type TYPES* = ObjectType | GameObject

# Forward declarations and ob type methods
proc initGameObject*(self: GameObject, engine: MyouEngine, name: string="", scene: Scene=nil): GameObject
proc newGameObject*(engine: MyouEngine, name: string="", scene: Scene=nil): GameObject
proc show*(self: GameObject, recursive: static[bool] = true)
proc hide*(self: GameObject, recursive: static[bool] = true)
proc set_visibility*(self: GameObject, visible: bool, recursive: static[bool] = true)
proc update_matrices*(self: GameObject)
proc update_matrices_recursive(self: GameObject)
proc set_rotation_order*(self: GameObject, order: RotationOrder)
proc get_world_matrix*(self: GameObject): Mat4
proc get_local_matrix*(self: GameObject): Mat4
proc get_world_position*(self: GameObject): Vec3
proc get_world_rotation*(self: GameObject): Quat
proc get_world_position_rotation*(self: GameObject): (Vec3, Quat)
proc translate*(self: GameObject, vector: Vec3, relative_object: GameObject=nil): GameObject {.discardable.}
proc translate_x*(self: GameObject, x: SomeFloat, relative_object: GameObject=nil): GameObject {.discardable.}
proc translate_y*(self: GameObject, y: SomeFloat, relative_object: GameObject=nil): GameObject {.discardable.}
proc translate_z*(self: GameObject, z: SomeFloat, relative_object: GameObject=nil): GameObject {.discardable.}
proc rotate_quat*(self: GameObject, q: Quat, relative_object: GameObject=nil): GameObject {.discardable.}
proc rotate_x*(self: GameObject, angle: SomeFloat, relative_object: GameObject=nil)
proc rotate_y*(self: GameObject, angle: SomeFloat, relative_object: GameObject=nil)
proc rotate_z*(self: GameObject, angle: SomeFloat, relative_object: GameObject=nil)
proc rotate_x_deg*(self: GameObject, angle: SomeFloat, relative_object: GameObject=nil)
proc rotate_y_deg*(self: GameObject, angle: SomeFloat, relative_object: GameObject=nil)
proc rotate_z_deg*(self: GameObject, angle: SomeFloat, relative_object: GameObject=nil)
proc set_world_position_rotation*(
        self: GameObject,
        position: Vec3,
        rotation: Quat)
proc set_world_position*(self: GameObject, position: Vec3)
proc set_world_rotation*(self: GameObject, rotation: Quat)
proc clone*(self: GameObject, 
        recursive: bool=true,
        with_behaviours: bool=true,
        name: string=self.name,
        new_parent: GameObject=self.parent,
        scene: Scene=self.scene,
        instance_body: bool=true
    ): GameObject
proc clone_impl*(self: GameObject, n: var GameObject, recursive: bool,
        with_behaviours: bool, name: string, new_parent: GameObject,
        scene: Scene, instance_body: bool): GameObject
proc set_parent*(self: GameObject, parent: GameObject, keep_transform: bool=true)
proc parent_to*(self: GameObject, parent: GameObject, keep_transform: bool=true)
proc clear_parent*(self: GameObject, keep_transform: bool=true)
proc get_top_ancestor*(self: GameObject, top_level_parents: seq[GameObject] = @[]): GameObject
proc remove*(self: GameObject, recursive: bool = true)
proc destroy*(self: GameObject, recursive: bool = true)
proc convert_bone_child_to_bone_parent*(self: GameObject)
proc get_local_X_vector*(self: GameObject): Vec3
proc get_local_Y_vector*(self: GameObject): Vec3
proc get_local_Z_vector*(self: GameObject): Vec3
proc get_world_X_vector*(self: GameObject): Vec3
proc get_world_Y_vector*(self: GameObject): Vec3
proc get_world_Z_vector*(self: GameObject): Vec3
proc get_local_dimensions*(self: GameObject): Vec3
proc get_world_dimensions*(self: GameObject): Vec3
proc get_world_scale*(self: GameObject): float
proc get_world_scale_vector*(self: GameObject): Vec3
proc get_local_size*(self: GameObject): float
proc set_local_size*(self: GameObject, size: SomeFloat)
proc get_world_size*(self: GameObject): float
proc set_world_size*(self: GameObject, size: SomeFloat)
proc set_name*(self: GameObject, name: string)
proc local_to_world*(self: GameObject, point: Vec3): Vec3
proc world_to_local*(self: GameObject, point: Vec3): Vec3

method is_armature*(self: GameObject): bool {.base.} =
    return false
method get_armature*(self: GameObject): Armature {.base.} =
    return nil
# End forward declarations and ob type methods

import json
import std/math
import std/tables
import ../incomplete
import ../util
import ../graphics/ubo
import ../scene
import ./mesh

proc initGameObject*(self: GameObject, engine: MyouEngine, name: string="", scene: Scene=nil): GameObject =
    # Remember to add any new mutable reference to clone()
    self.engine = engine
    self.name = name
    self.rotation = quat()
    self.rotation_order = EulerXYZ
    self.scale = vec3(1, 1, 1)
    self.object_color = vec4(1, 1, 1, 1)
    # self.alpha = 1
    self.visible = true
    self.auto_update_matrix = true
    self.world_matrix = mat4()
    self.bone_matrix_inverse = mat4()
    self.parent_bone_index = -1
    self.zindex = 1 # ??
    self.world_center = vec4(0, 0, 0, 1)
    self.sqscale = 1
    self.object_render_ubo = self.engine.newUBO("ObjectRenderUniform", ObjectRenderUniform, 1)
    self.object_ubo = self.engine.newUBO("ObjectUniform", ObjectUniform, 1)
    # self.camera_render_ubo = self.engine.newUBO("CameraRenderUniform", CameraRenderUniform, 1)
    if scene != nil:
        scene.add_object(self, name=name)
    return self

proc newGameObject*(engine: MyouEngine, name: string="", scene: Scene=nil): GameObject =
    ## Create a new GameObject. If you supply `scene` it will be added to that
    ## scene.
    new(result)
    return initGameObject(result, engine, name, scene)

proc show*(self: GameObject, recursive: static[bool] = true) =
    ## Enable visibility of the object, its children, their children and so on.
    ## If you set `recursive = false`, only the visibility of the object will be
    ## changed.
    self.visible = true
    when recursive:
        for c in self.children:
            c.show(recursive)

proc hide*(self: GameObject, recursive: static[bool] = true) =
    ## Disable visibility of the object, its children, their children and so on.
    ## If you set `recursive = false`, only the visibility of the object will be
    ## changed.
    self.visible = false
    when recursive:
        for c in self.children:
            c.hide(recursive)

proc set_visibility*(self: GameObject, visible: bool, recursive: static[bool] = true) =
    ## Set the visibility of the object and its descendants to the value of the
    ## argument `visible`. If you set `recursive = false`, only the visibility of
    ## the object will be changed.
    if visible: self.show(recursive)
    else: self.hide(recursive)

proc update_matrices*(self: GameObject) =
    ## Calculate the matrices of the object from its position, rotation, scale,
    ## and parent matrix. It assumes the parent object and/or bone has its world
    ## matrix already up to date.
    var q = if self.rotation_order == Quaternion:
        self.rotation
    else:
        to_quat(self.rotation.xyz, self.rotation_order)
    # var q = self.rotation
    q = normalize(q)
    let (x,y,z,w) = q.toTuple
    let scl = self.scale
    self.flip = false
    self.sqscale = length_sq(scl)
    if self.parent != nil:
        self.flip = self.parent.flip
        self.sqscale *= self.parent.sqscale
    if scl.x * scl.y * scl.z < 0:
        self.flip = not self.flip
    let pos = self.position
    var wm = mat4(
        (w * w + x * x - y * y - z * z) * scl.x,
        (2 * (x * y + z * w)) * scl.x,
        (2 * (x * z - y * w)) * scl.x,
        0.0,
        (2 * (x * y - z * w)) * scl.y,
        (w * w - x * x + y * y - z * z) * scl.y,
        (2 * (z * y + x * w)) * scl.y,
        0.0,
        (2 * (x * z + y * w)) * scl.z,
        (2 * (y * z - x * w)) * scl.z,
        (w * w - x * x - y * y + z * z) * scl.z,
        0.0,
        pos.x,
        pos.y,
        pos.z,
        1.0
    )
    if self.parent != nil:
        wm = self.matrix_parent_inverse * wm
        var bi = self.parent_bone_index
        if bi >= 0:
            let bone = self.parent.get_armature.bone_list[bi]
            # In Blender, matrix_parent_inverse is relative to the
            # TIP of the bone, while bone.matrix is relative to the BASE
            # so we need to add the bone length
            wm[3,1] = wm[3,1] + bone.blength
            self.world_matrix = bone.matrix * self.world_matrix
        wm = self.parent.world_matrix * wm
    self.world_matrix = wm
    self.world_center.xyz = wm * self.center
    return

proc update_matrices_recursive(self: GameObject) =
    ## Calculate the matrices of the object from its position, rotation, scale,
    ## and parent. It will update the parent matrices first if it has a parent.
    if self != nil:
        self.parent.update_matrices_recursive()
        self.update_matrices()

proc set_rotation_order*(self: GameObject, order: RotationOrder) =
    ## Change the rotation mode and order of the object.
    if order == self.rotation_order:
        return
    var q = self.rotation
    if self.rotation_order != Quaternion:
        q = q.xyz.to_quat(self.rotation_order)
    self.rotation = case order:
        of Quaternion: q
        of EulerXYZ: vec4(q.to_euler_XYZ, 0)
        of EulerXZY: vec4(q.to_euler_XZY, 0)
        of EulerYXZ: vec4(q.to_euler_YXZ, 0)
        of EulerYZX: vec4(q.to_euler_YZX, 0)
        of EulerZXY: vec4(q.to_euler_ZXY, 0)
        of EulerZYX: vec4(q.to_euler_ZYX, 0)
        of AxisAngle:
            raise newException(ValueError, "axis angle not supported yet")
    self.rotation_order = order

proc get_world_matrix*(self: GameObject): Mat4 =
    ## Calculates and returns the world matrix.
    # TODO: use dirty flag
    self.update_matrices_recursive()
    return self.world_matrix

proc get_local_matrix*(self: GameObject): Mat4 =
    ## Calculates and returns the transformation matrix in local space. If the
    ## object has no parent, this is equivalent to the world matrix.
    var q = if self.rotation_order == Quaternion:
        self.rotation
    else:
        to_quat(self.rotation.xyz, self.rotation_order)
    q = normalize(q)
    var (x,y,z,w) = q.toTuple
    let scl = self.scale
    # TODO: test negative scales
    let pos = self.position
    return mat4(
        (w * w + x * x - y * y - z * z) * scl.x,
        (2 * (x * y + z * w)) * scl.x,
        (2 * (x * z - y * w)) * scl.x,
        0,
        (2 * (x * y - z * w)) * scl.y,
        (w * w - x * x + y * y - z * z) * scl.y,
        (2 * (z * y + x * w)) * scl.y,
        0,
        (2 * (x * z + y * w)) * scl.z,
        (2 * (y * z - x * w)) * scl.z,
        (w * w - x * x - y * y + z * z) * scl.z,
        0,
        pos.x,
        pos.y,
        pos.z,
        1
    )

proc get_world_position*(self: GameObject): Vec3 =
    ## Calculates and returns the position in world space.
    if self.parent == nil:
        return self.position
    return self.get_world_matrix[3].xyz

proc get_world_rotation*(self: GameObject): Quat =
    ## Calculates and returns the rotation in world space as a quaternion.
    let wm = self.get_world_matrix
    # TODO: Calculate rotation matrix more efficiently (dirty flag?)
    return wm.to_mat3_rotation.to_quat

proc get_world_position_rotation*(self: GameObject): (Vec3, Quat) =
    ## Calculates and returns the position and rotation in world space.
    let wm = self.get_world_matrix
    let position: Vec3 = wm[3].xyz
    # TODO: Calculate rotation matrix more efficiently (dirty flag?)
    # TODO: handle negative scales
    let rotation: Quat = wm.to_mat3_rotation.to_quat
    return (position, rotation)

proc translate*(self: GameObject, vector: Vec3, relative_object: GameObject=nil): GameObject {.discardable.} =
    ## Translate (move) the object by adding the vector `vector` to its position,
    ## either relative to the scene, or relative to the specified object in
    ## `relative_object`
    var vector = vector
    if relative_object != nil:
        vector = relative_object.get_world_rotation() * vector
    if self.parent != nil:
        let m = self.parent.get_world_matrix * self.matrix_parent_inverse
        vector = m.to_mat3_rotation.transpose * vector
    self.position = self.position + vector
    return self

proc translate_x*(self: GameObject, x: SomeFloat, relative_object: GameObject=nil): GameObject {.discardable.} =
    ## Translate (move) the object along the X axis of the scene, or the X axis
    ## of the specified object in `relative_object`
    return self.translate(vec3(x, 0, 0), relative_object)

proc translate_y*(self: GameObject, y: SomeFloat, relative_object: GameObject=nil): GameObject {.discardable.} =
    ## Translate (move) the object along the Y axis of the scene, or the Y axis
    ## of the specified object in `relative_object`
    return self.translate(vec3(0, y, 0), relative_object)

proc translate_z*(self: GameObject, z: SomeFloat, relative_object: GameObject=nil): GameObject {.discardable.} =
    ## Translate (move) the object along the Z axis of the scene, or the Z axis
    ## of the specified object in `relative_object`
    return self.translate(vec3(0, 0, z), relative_object)

# proc rotate_euler*(self: GameObject, vector: Vec3, relative_object: GameObject=nil): GameObject =
#     let q = fromEulerOrder(vector, order)
#     self.rotate_quat(q, relative_object)

# proc rotate_euler_deg*(self: GameObject, vector: Vec3, relative_object: GameObject=nil): GameObject =
#     let v = vector * 0.017453292519943295 # PI*2 / 360
#     let q = fromEulerOrder(v, order)
#     self.rotate_quat(q, relative_object)

proc rotate_quat*(self: GameObject, q: Quat, relative_object: GameObject=nil): GameObject {.discardable.} =
    # Rotate the object by applying (multiplying) the quaternion `q`, either
    # relative to the scene, or relative to the specified object in
    # `relative_object`

    # TODO: optimize (dirty flag?)
    var rel = quat()
    var inv_rel = quat()
    var par = quat()
    var inv_par = quat()
    if relative_object != nil:
        rel = relative_object.get_world_rotation
        inv_rel = inverse(rel)
    if self.parent != nil:
        var m = self.parent.get_world_matrix * self.matrix_parent_inverse
        par = m.to_mat3_rotation.to_quat
        inv_par = inverse(par)
    let rotation_order = self.rotation_order
    if rotation_order != Quaternion:
        self.set_rotation_order(Quaternion)
    self.rotation = inv_par @ rel @ q @ inv_rel @ par @ self.rotation
    if rotation_order != Quaternion:
        self.set_rotation_order(rotation_order)
    return self

proc rotate_x*(self: GameObject, angle: SomeFloat, relative_object: GameObject=nil) =
    ## Rotate the object around the X axis of the scene, or the X axis of the
    ## specified object in `relative_object`. The angle is in radians.
    self.rotate_quat(rotateX(angle.float32).to_mat3.to_quat, relative_object)

proc rotate_y*(self: GameObject, angle: SomeFloat, relative_object: GameObject=nil) =
    ## Rotate the object around the Y axis of the scene, or the Y axis of the
    ## specified object in `relative_object`. The angle is in radians.
    self.rotate_quat(rotateY(angle.float32).to_mat3.to_quat, relative_object)

proc rotate_z*(self: GameObject, angle: SomeFloat, relative_object: GameObject=nil) =
    ## Rotate the object around the Z axis of the scene, or the Z axis of the
    ## specified object in `relative_object`. The angle is in radians.
    self.rotate_quat(rotateZ(angle.float32).to_mat3.to_quat, relative_object)

proc rotate_x_deg*(self: GameObject, angle: SomeFloat, relative_object: GameObject=nil) =
    ## Rotate the object around the X axis of the scene, or the X axis of the
    ## specified object in `relative_object`. The angle is in degrees.
    self.rotate_quat(rotateX(angle.float32.to_radians).to_mat3.to_quat, relative_object)

proc rotate_y_deg*(self: GameObject, angle: SomeFloat, relative_object: GameObject=nil) =
    ## Rotate the object around the Y axis of the scene, or the Y axis of the
    ## specified object in `relative_object`. The angle is in degrees.
    self.rotate_quat(rotateY(angle.float32.to_radians).to_mat3.to_quat, relative_object)

proc rotate_z_deg*(self: GameObject, angle: SomeFloat, relative_object: GameObject=nil) =
    ## Rotate the object around the Z axis of the scene, or the Z axis of the
    ## specified object in `relative_object`. The angle is in degrees.
    self.rotate_quat(rotateZ(angle.float32.to_radians).to_mat3.to_quat, relative_object)

proc set_world_position_rotation*(
        self: GameObject,
        position: Vec3,
        rotation: Quat) =
    ## Changes the position and the rotation of the object to match the supplied
    ## position and rotation in world space.

    if self.parent != nil:
        # TODO: optimize and use less memory?
        # TODO: does it work well with parents?
        let wm = inverse(self.parent.get_world_matrix * self.matrix_parent_inverse)
        self.position = wm * position
        self.rotation = wm.to_mat3_rotation.to_quat @ rotation
    else:
        self.position = position
        self.rotation = rotation
    self.rotation_order = Quaternion

proc set_world_position*(self: GameObject, position: Vec3) =
    ## Changes the position of the object to match the supplied position in world
    ## space.
    self.set_world_position_rotation(position, self.get_world_rotation())

proc set_world_rotation*(self: GameObject, rotation: Quat) =
    ## Changes the rotation of the object to match the supplied rotation in world
    ## space.
    self.set_world_position_rotation(self.get_world_position(), rotation)

type LookAtAxis* = enum
    AxisX = 0
    AxisY
    AxisZ
    AxisMinusX
    AxisMinusY
    AxisMinusZ

proc clone*(self: GameObject, 
        recursive: bool=true,
        with_behaviours: bool=true,
        name: string=self.name,
        new_parent: GameObject=self.parent,
        scene: Scene=self.scene,
        instance_body: bool=true
    ): GameObject =
    ## Makes a clone of the object and its children (unless `recursive` is
    ## false).

    case self.otype:
    of TGameObject:
        var n = new GameObject
        n[] = self[]
        return self.clone_impl(n, recursive, with_behaviours, name, new_parent, scene, instance_body)
    of TMesh:
        var n = new Mesh
        var ob = cast[Mesh](self)
        n[] = ob[]
        discard self.clone_impl(n.GameObject, recursive, with_behaviours, name, new_parent, scene, instance_body)
        return ob.clone_impl(n)
    of TCamera:
        discard
    of TLight:
        discard
    of TArmature:
        discard
    
proc clone_impl*(self: GameObject, n: var GameObject, recursive: bool,
        with_behaviours: bool, name: string, new_parent: GameObject,
        scene: Scene, instance_body: bool): GameObject =

    n.children = @[]
    for k,v in n.properties.pairs:
        n.properties[k] = v.copy
    # n.behaviours = @[]

    # TODO: lods
    # TODO: physics version of a mesh
    
    if new_parent != nil:
        n.parent = new_parent
        n.parent.children.add(n)
    
    # self.body = self.body.clone # TODO

    n.scene = nil
    if scene != nil:
        scene.add_object(n, name=name)

    # Adding children afterwards ensures objects don't need to be sorted
    if recursive:
        for i,child in self.children:
            self.children[i] = child.clone(recursive, with_behaviours, name, new_parent=n, scene, instance_body=false)
        # TODO: instance children bodies
    return n

proc set_parent*(self: GameObject, parent: GameObject, keep_transform: bool=true) =
    ## Sets the parent of the object, while keeping the same transform in world
    ## space (unless you set `keep_transform` to false).
    
    # TODO: there should be no distinction between this and parent_to
    self.scene.make_parent(parent, self, keep_transform=keep_transform)

proc parent_to*(self: GameObject, parent: GameObject, keep_transform: bool=true) =
    ## Sets the parent of the object, while adding it to the same scene if
    ## needed, as well as keeping the same transform in world space (unless you
    ## set `keep_transform` to false).
    
    # TODO move this to make_parent
    if self.scene == nil:
        parent.scene.add_object(self)
    self.scene.make_parent(parent, self, keep_transform=keep_transform)

proc clear_parent*(self: GameObject, keep_transform: bool=true) =
    self.scene.clear_parent(self, keep_transform=keep_transform)

proc get_top_ancestor*(self: GameObject, top_level_parents: seq[GameObject] = @[]): GameObject =
    ## Find the topmost ancestor (e.g. parent of parent of parent...) or an 
    ## object contained in `top_level_parents`, whatever happens first.
    var ob = self
    while ob.parent != nil and ob.parent notin top_level_parents:
        ob = ob.parent
    return ob

iterator children_recursive*(self: GameObject, include_self: static[bool] = false): GameObject =
    ## Iterate all the descendants of the object, i.e. its children, the
    ## children's children an so on.

    # NOTE: This was written before the knowledge of closure iterators
    # (by default iterators are inline and don't allow recursion)
    when include_self:
        yield self
    var ob = self
    var stack: seq[int]
    var pos = 0
    var children = ob.children
    while true:
        while pos < children.len:
            let c = children[pos]
            yield c
            inc(pos)
            if c.children.len != 0:
                ob = c
                stack.add pos
                pos = 0
                children = c.children
        if stack.len != 0:
            ob = ob.parent
            pos = stack.pop()
            children = ob.children
        else:
            break

template children_recursive_and_self*(self: GameObject): GameObject =
    ## Iterate all the descendants of the object, and include itself in the
    ## iteration.
    children_recursive(self, include_self = true)

proc remove*(self: GameObject, recursive: bool = true) =
    ## Remove the object from the scene it is in, without destroying it.
    ## It will also remove its children unless you set `recursive` to false.
    if self.scene.nonNil:
        self.scene.remove_object(self, recursive)

proc destroy*(self: GameObject, recursive: bool = true) =
    ## Destroy the object and free all unused resources previously used by it.
    ## It will also destroy its children unless you set `recursive` to false.
    if self.is_mesh and self.get_mesh.data.nonNil:
        self.get_mesh.data.gpu_buffers_delete()
        self.get_mesh.materials.setLen 0
    self.body.destroy()
    self.remove(recursive)
    if recursive:
        for child in self.children:
            var child = child
            child.destroy(true)
    self.engine.objects.del(self.name)
    self.object_render_ubo.unbind()
    self.object_render_ubo.destroy()
    self.object_ubo.unbind()
    self.object_ubo.destroy()

proc convert_bone_child_to_bone_parent*(self: GameObject) =
    ## Turns an object that is parented to a bone, into the parent of the same
    ## bone. It disconnects the bone from its former parent, if any. Used
    ## internally to create ragdolls.
    assert self.parent == nil or self.parent.is_armature, "An armature parent is required"
    var parent = self.parent.get_armature
    if not (parent != nil and (self.parent_bone_index) >= 0):
        return
    var bone = parent.bone_list[self.parent_bone_index]
    self.parent = nil
    var m = self.matrix_parent_inverse * self.get_world_matrix
    m[3,1] = m[3,1] + bone.blength
    self.bone_matrix_inverse = inverse(m)
    self.parent = parent
    self.clear_parent()
    bone.parent_object = self

proc get_local_X_vector*(self: GameObject): Vec3 =
    ## Get the vector of the X axis of the object in local space.
    return self.get_local_matrix[0].xyz

proc get_local_Y_vector*(self: GameObject): Vec3 =
    ## Get the vector of the Y axis of the object in local space.
    return self.get_local_matrix[1].xyz

proc get_local_Z_vector*(self: GameObject): Vec3 =
    ## Get the vector of the Z axis of the object in local space.
    return self.get_local_matrix[2].xyz

proc get_world_X_vector*(self: GameObject): Vec3 =
    ## Get the vector of the X axis of the object in world space.
    return self.get_world_matrix[0].xyz

proc get_world_Y_vector*(self: GameObject): Vec3 =
    ## Get the vector of the Y axis of the object in world space.
    return self.get_world_matrix[1].xyz

proc get_world_Z_vector*(self: GameObject): Vec3 =
    ## Get the vector of the Z axis of the object in world space.
    return self.get_world_matrix[2].xyz

proc get_local_dimensions*(self: GameObject): Vec3 =
    ## Calculate the local dimensions of the object, i.e. the size of the
    ## bounding box with the scale of the object.
    let (a,b) = self.bound_box
    return (b-a) * self.scale

proc get_world_dimensions*(self: GameObject): Vec3 =
    ## Calculate the world dimensions of the object, i.e. the size of the
    ## bounding box in world scale.
    let (a,b) = self.bound_box
    return (b-a) * self.get_world_scale_vector

proc get_world_scale*(self: GameObject): float =
    # TODO: use the diagonal of a sphere instead?
    #       i.e. length((wm*vec3(1).normalize,0).xyz)
    var s = self.scale.z
    var p = self.parent
    while p != nil:
        s *= p.scale.z
        p = p.parent
    return s

proc get_world_scale_vector*(self: GameObject): Vec3 =
    ## Gets the world scale vector in local coordinates (i.e. relative to the
    ## current object orientation).
    let rotation = self.get_world_rotation
    let wm = self.world_matrix
    let rot_inv = inverse(rotation)
    var wm3 = wm.to_mat3
    let wm3_b = rot_inv.to_mat3
    wm3 = wm3_b * wm3
    return vec3(
        copy_sign(wm[0].xyz.length, wm3[0,0]), 
        copy_sign(wm[1].xyz.length, wm3[1,1]), 
        copy_sign(wm[2].xyz.length, wm3[2,2]), 
    )

proc get_local_size*(self: GameObject): float =
    ## Calculates the size of the largest dimension of the object in local
    ## coordinates.
    let d = self.get_local_dimensions()
    return max(max(d.x, d.y), d.z)

proc set_local_size*(self: GameObject, size: SomeFloat) =
    ## Sets the size of the object in local cordinates, given by the desired
    ## largest dimension of the object. The other two dimensions are scaled
    ## proportionally. If the object dimensions are zero, no action is taken.
    let current_size = self.get_local_size
    if abs(current_size) > 1e-8:
        self.scale = self.scale * size / current_size
    return

proc get_world_size*(self: GameObject): float =
    ## Calculates the size of the largest dimension of the object in world
    ## coordinates.
    let d = self.get_world_dimensions()
    return max(max(d.x, d.y), d.z)

proc set_world_size*(self: GameObject, size: SomeFloat) =
    ## Sets the size of the object in world cordinates, given by the desired
    ## largest dimension of the object. The other two dimensions are scaled
    ## proportionally. If the object dimensions are zero, no action is taken.
    let current_size = self.get_world_size
    if abs(current_size) > 1e-8:
        self.scale = self.scale * size / current_size
    return

proc set_name*(self: GameObject, name: string) =
    ## Rename the object and update the tables that use it.
    assert self.scene != nil, "Object has no scene"
    self.engine.objects.del(self.name)
    self.scene.objects.del(self.name)
    self.name = name
    self.scene.set_ob_name(self, name)

proc local_to_world*(self: GameObject, point: Vec3): Vec3 =
    ## Transforms a point in local coordinates to world coordinates.
    return self.get_world_matrix * point

proc world_to_local*(self: GameObject, point: Vec3): Vec3 =
    ## Transforms a point in world coordinates to local coordinates. If the
    ## object has a scale of 0, the point is not transformed.
    let wm = self.get_world_matrix
    if wm.determinant != 0.0:
        return wm.inverse * point
    return point