# 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.



## This module containst the functions to start using Myou Engine.

# TODO: runnableExamples is broken because we can't pass compile options

## ```nim
## let engine = newMyouEngine(1024, 768, "My Game")
## 
## # Create or load your initial scene here
## 
## engine.run()
## ```

import ./types
import std/tables

when defined(android):
    const default_gl_version = 320
    const default_gl_es = true
elif defined(emscripten) or defined(ios):
    const default_gl_version = 300
    const default_gl_es = true
else:
    const default_gl_version = 330
    const default_gl_es = false

# Forward declarations
proc newMyouEngine*(
        width, height: int32,
        title = "Myou Engine window",
        opengl_version = default_gl_version,
        opengl_es = default_gl_es,
        glsl_version = "",
        use_glsl_tone_mapping = true,
        context_msaa_samples = 1,
    ): MyouEngine
proc get_builtin_shader_library*(use_cubemap_prefiltering = true): string
proc get_builtin_shader_textures*(): Table[string, Texture]
proc detect_common_issues*(self: MyouEngine)
# End forward declarations

import std/strutils
import std/monotimes
import ./graphics/render
import ./screen
import ./platform/platform
import ./loaders/blend
import ./util
when compileOption("threads"):
    from loadable import updateLoadableWorkerThreads
    from ./gpu_formats/texture_optimize import updateTextureWorkerThreads
import arr_ref

export arr_ref
export tables

proc newMyouEngine*(
        width, height: int32,
        title = "Myou Engine window",
        opengl_version = default_gl_version,
        opengl_es = default_gl_es,
        glsl_version = "",
        use_glsl_tone_mapping = true,
        context_msaa_samples = 1,
    ): MyouEngine =
    ## Creates a Myou Engine instance. You need to call this before you can use
    ## the engine. You also need to call `run <#run,MyouEngine>`_ at the end of
    ## your main project file.
    result = new MyouEngine
    result.glsl_version = if glsl_version != "":
        glsl_version
    elif opengl_es:
        $opengl_version & " es"
    else:
        $opengl_version
    
    # TODO: move/copy to camera or to scene?
    # to override it with per-camera exposure settings
    if opengl_es:
        assert opengl_version >= 300, "Minimum supported OpenGL ES version is 3.0"
    else:
        assert opengl_version >= 330, "Minimum supported OpenGL version is 3.3"

    if opengl_es or use_glsl_tone_mapping:
        result.tone_mapping_library = dedent """
            float linearrgb_to_srgb(float c){
                if (c < 0.0031308) return (c < 0.0) ? 0.0 : c * 12.92;
                else return 1.055 * pow(c, 1.0 / 2.4) - 0.055;
            }
            vec4 linearrgb_to_srgb(vec4 col){
                return vec4(
                    linearrgb_to_srgb(col.r),
                    linearrgb_to_srgb(col.g),
                    linearrgb_to_srgb(col.b),
                    col.a
                );
            }
            #define MYOU_TONE_MAP(x) linearrgb_to_srgb(x)
        """
        result.tone_mapping_function = "MYOU_TONE_MAP"
        result.use_glsl_tone_mapping = true
    else:
        result.tone_mapping_library = "\n#define MYOU_TONE_MAP(x) x\n"
    
    echo "assigning renderer"
    result.renderer = result.newRenderManager
    # this will call result.renderer.initialize() now or later
    # but first it will ensure a screen can be created
    when not defined(nimdoc):
        init_graphics(result, width, height, title, opengl_version, opengl_es, context_msaa_samples)
        discard result.newScreen(width, height, title)

    registerBlendLoader(result)

proc get_builtin_shader_library*(use_cubemap_prefiltering = true): string =
    ## Returns a string with the code of the default shader library of the
    ## engine. If you use this, you may want to also add the textures given by
    ## `get_builtin_shader_textures<#get_builtin_shader_textures>`_.
    return @[
        if use_cubemap_prefiltering:
            "#define PREFILTERED_CUBEMAPS"
        else: "",
        # staticOrDebugRead "shaders/debug_text.glsl",
        staticOrDebugRead "shaders/cube_prefilter.glsl",
        staticOrDebugRead "shaders/spherical_harmonics.glsl",
        staticOrDebugRead "shaders/shader_library.glsl",
    ].join("\n")

proc get_builtin_shader_textures*(): Table[string, Texture] =
    ## Returns a table of textures to be used with the library that is returned
    ## by `get_builtin_shader_library<#get_builtin_shader_library,bool>`_.
    discard

var last_time: float

proc myou_main_loop*(self: MyouEngine) =
    ## Runs one iteration of the engine main loop. It doesn't swap buffers.
    ##
    ## You usually don't need to call this. Use `run <#run,MyouEngine>`_
    ## instead.
    when compileOption("threads"):
        updateTextureWorkerThreads()
        updateLoadableWorkerThreads()
    # TODO: make a table object that can be iterated while changing, e.g. with a
    # seq and a dirty flag to update the seq
    if self.new_del_scenes.len != 0:
        for name,scene in self.new_del_scenes.pairs:
            if scene.nonNil:
                self.scenes[name] = scene
            else: # nil means that the scene is meant to be deleted
                self.scenes.del(name)
        self.new_del_scenes.clear()
    let time = getmonotime().ticks.float/1_000_000_000
    let delta_seconds = time - last_time
    for _,scene in self.scenes.pairs:
        if not scene.enabled:
            continue
        scene.time += delta_seconds
        for f in scene.pre_draw_callbacks:
            f(scene, delta_seconds)
    self.renderer.draw_all()
    for _,scene in self.scenes.pairs:
        if not scene.enabled:
            continue
        for f in scene.post_draw_callbacks:
            f(scene, delta_seconds)
    last_time = time

proc run*(self: MyouEngine) =
    ## Starts the main loop of the engine. You should call it at the end of your
    ## main file. In mobile platforms and on web, this function doesn't block,
    ## and instead it just configures the main loop function. Therefore you
    ## shouldn't run any code after calling it.
    last_time = getmonotime().ticks.float/1000000000
    when not defined(nimdoc):
        start_platform_main_loop(self, myou_main_loop)

proc loadScene*(self: MyouEngine, uri: string, callback: proc(err: string, scene: Scene), name = "", ext = "") =
    let ext = if ext == "":
        uri.rsplit('.', 1)[1]
    else:
        ext
    
    if ext notin self.loaders_by_ext:
        raise ValueError.newException "File extension not supported: " & ext
    
    # TODO: use the next loader if the first one fails
    let loaders = self.loaders_by_ext[ext]
    let loader = loaders[0](self)
    loader.openAssetFile(uri)
    loader.loadScene(name, nil, callback)

proc detect_common_issues*(self: MyouEngine) =
    # TODO: use logging
    template check(cond: bool, msg: string) =
        if not cond:
            echo msg
            return

    check self.screen.nonNil, "Warning: there is no screen"
    check self.screen.enabled, "Warning: main screen is not enabled"
    check self.screen.viewports.len != 0, "Warning: main screen has no viewports"
    var any_scene_enabled = false
    for vp in self.screen.viewports:
        if vp.camera.scene.enabled:
            any_scene_enabled = true
    check any_scene_enabled, "Warning: no visible scene is enabled"
    var any_object_visible = false
    block ob_vis:
        for vp in self.screen.viewports:
            if vp.camera.scene.enabled:
                for pass in vp.camera.scene.mesh_passes:
                    for mesh in pass:
                        if mesh.visible and mesh.materials.len != 0:
                            # TODO: check culling
                            # TODO: separate flags to specify why nothing is visible
                            any_object_visible = true
                            break ob_vis
    check any_object_visible, "Warning: no visible object"