# 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 std/strutils
import ../types
import vmath except Quat, quat
import ../graphics/framebuffer
import ../graphics/material
import ../graphics/render
import ../graphics/ubo
# import ../objects/light
import ../objects/mesh
import ../objects/gameobject
import ../quat
import ../util
import ./shadow_common

when defined(nimdoc):
    type TYPES* = SimpleShadowManager

when USE_SHADOW_SAMPLERS:
    const LIGHT_PROJ_TO_DEPTH = mat4(0.5, 0, 0, 0,  0, 0.5, 0, 0, 
                                    0, 0, 0.5, 0,  0.5, 0.5, 0.5, 1)
else:
    # Z depth is not modified, only X and Y for UVs
    const LIGHT_PROJ_TO_DEPTH = mat4(0.5, 0, 0, 0,  0, 0.5, 0, 0, 
                                    0, 0, 1, 0,  0.5, 0.5, 0, 1)

when defined(myouDebugShadows):
    var debug_mesh: Mesh

proc newSimpleShadowManager*(light: Light;
                             use_camera: bool;
                             near, far: float32 = 0.0;
                            ): SimpleShadowManager {.discardable.} =
    
    new(result)
    result.engine = light.engine
    result.light = light
    result.depth_range = (near, far)
    result.auto_render = true
    result.required_texture_count = 1
    result.shadow_index = -1
    result.use_camera = use_camera
    light.shadows.add result
    let self = result
    result.engine.renderer.enqueue proc()=
        when USE_SHADOW_SAMPLERS:
            # depth-only pass
            self.material = light.engine.newMaterial("simple_shadow", nil,
                fragment=dedent "",
            )
            # TODO: Disable color texture
            self.uses_color_channels = 0
            self.uses_depth = true
        else:
            self.material = light.engine.newMaterial("simple_shadow", nil,
                fragment=dedent """
                in vec4 ppos;
                out float color;
                void main(){
                    color = ppos.z/ppos.w;
                }""",
                varyings = @[Varying(vtype: ProjPosition, varname: "ppos")]
            )
            self.uses_color_channels = 1
            self.uses_depth = false

        when defined(myouDebugShadows):
            if debug_mesh != nil:
                debug_mesh.destroy()
            debug_mesh = self.engine.newMesh(vertex_count = 100, draw_method = Lines)
            debug_mesh.materials.add self.engine.newSolidMaterial("green", vec4(0,1,0,1))
            debug_mesh.visible = false
            light.scene.add_object debug_mesh

method destroy*(self: SimpleShadowManager) {.locks:"unknown".} =
    if self.material != nil:
        self.material.destroy()
        when defined(myouDebugShadows):
            debug_mesh.destroy()

when defined(myouDebugShadows):
    proc show_mat(mat: Mat4) =
        var points = newSeqOfCap[Vec3](8)
        for z in [-1'f32,1'f32]:
            for y in [-1'f32,1'f32]:
                for x in [-1'f32,1'f32]:
                    let v = mat * vec4(x,y,z,1)
                    points.add v.xyz/v.w
        for i in [0,1,1,3,3,2,2,0,4,5,5,7,7,6,6,4,0,4,1,5,2,6,3,7]:
            debug_mesh.add_vertex points[i], vec4(1)
        debug_mesh.data.update_varray
        debug_mesh.visible = true

proc renderShadow*(self: SimpleShadowManager, scene: Scene,
                    bounding_points: seq[Vec3],
                    sphere_center: Vec3 = vec3(0),
                    sphere_radius: float32 = 0,
                    min_z: float32 = 0) =
    # Find the projection matrix that fits all bounding points and sphere
    # TODO: option to rotate proj to favor vertical lines?
    let rotm = self.light.world_matrix.to_mat3_rotation
    let rotmi = rotm.inverse
    var pmin = vec3(Inf)
    var pmax = vec3(-Inf)
    if sphere_radius != 0:
        pmin = sphere_center - vec3(sphere_radius)
        pmax = sphere_center + vec3(sphere_radius)
    for p in bounding_points:
        let p = rotmi * p
        pmin = min(pmin, p)
        pmax = max(pmax, p)
    pmax.z = max(pmin.z+min_z, pmax.z)
    let mid = mix(pmin, pmax, 0.5)
    var scale = (pmax-pmin) * vec3(0.5, 0.5, -0.5)
    scale.xy = vec2(max(scale.x, scale.y)) # make it square
    # TODO: grow 1 px and snap the box to increments of pixels
    let proj = scale(1'f32/scale)
    let light_matrix = rotm.to_mat4 * translate(mid)
    # calculate the bias = the diagonal of half a pixel
    # multiplied by twice the radius in the shader (1.5*2 at the moment)
    # plus 1 because of texture filtering (GL_COMPARE_REF_TO_TEXTURE)
    # TODO: test at multiple resolutions and different filtering radii
    let resolution = scene.shadow_maps.width.float32
    let bias = 4.0 * sqrt(2.0f) * scale.x/resolution
     
    when defined(myouDebugShadows):
        show_mat inverse(proj * inverse(light_matrix))

    scene.shadow_maps.enable(layer = self.shadow_index)
    scene.shadow_maps.clear(color=vec4(1), layer = self.shadow_index)
    let cd = newRenderCameraData(light_matrix,
        proj, proj.get_culling_planes(), vec2(scene.shadow_maps.width.float32))
    
    let rm = self.engine.renderer
    if not rm.initialized: # TODO: enqueue but only once
        return
    let material = self.material
    for ob in scene.mesh_passes[0]:
        if ob.visible and ob.draw_method != Lines:
            rm.draw_mesh(ob, ob.world_matrix, cd, 0, material)

    scene.shadow_maps.disable()

    var depth_matrix = LIGHT_PROJ_TO_DEPTH * proj * inverse(light_matrix)
    scene.shadow_maps_ubo.storage(ShadowMapUniform)[self.shadow_index] =
        ShadowMapUniform(depth_matrix: depth_matrix,
                        tex_size: resolution, bias: bias)

proc renderShadowWithCamera*(self: SimpleShadowManager, camera: Camera) =
    # first we'll make a list of points that matches the corners of the camera
    # frustum and given depth range

    # it assumes that all world matrices are already up to date
    # TODO: accept a cameradata instead?
    
    template depth_to_screen(d: float32, proj: Mat4): float32 =
        # TODO: make more efficient and put in Camera
        let v = proj * vec4(0,0,-d,1)
        clamp(v.z/v.w,-1.0, 1.0)

    # get frustum points, in world space
    let screen_depth = if self.depth_range == (0'f32,0'f32):
        [-1'f32, 1'f32]
    else: [
        depth_to_screen(self.depth_range[0], camera.projection_matrix),
        depth_to_screen(self.depth_range[1], camera.projection_matrix),
    ]
    let mat = camera.world_matrix.remove_scale_skew * camera.projection_matrix_inverse
    when defined(myouDebugShadows):
        show_mat mat
    var points = newSeqOfCap[Vec3](8)
    for z in screen_depth:
        for y in [-1'f32,1'f32]:
            for x in [-1'f32,1'f32]:
                let v = mat * vec4(x,y,z,1)
                points.add v.xyz/v.w
    when defined(myouDebugShadows):
        for i in [0,1,1,3,3,2,2,0,4,5,5,7,7,6,6,4,0,4,1,5,2,6,3,7]:
            debug_mesh.add_vertex points[i], vec4(1)

    var zrange = self.depth_range[1] - self.depth_range[0]
    if zrange == 0.0:
        zrange = camera.far_plane - camera.near_plane
    # extend z range an arbitrary amount
    # TODO: make it configurable
    # or better: clamp polygons in the shader!
    self.renderShadow(camera.scene, points, min_z=zrange*4)

method renderShadow*(self: SimpleShadowManager, camera: Camera): bool {.locks:"unknown".} =
    when defined(myouDebugShadows):
        debug_mesh.clear_vertices()
    if self.use_camera:
        self.renderShadowWithCamera(
            camera = camera)
        return true
    else:
        let v = get_world_Z_vector(self.light)
        let angle = angle(v, self.last_light_dir)
        # We use < and not <= so it always renders when min delta is 0
        if angle < self.min_light_angle_delta:
            return
        self.last_light_dir = v
        self.renderShadow(
            scene = camera.scene,
            bounding_points = self.caster_bounding_points,
            min_z = -Inf)
        return true