
from ..extensions.extensionbase import ExtensionBase
from ..blender_export import getSceneExportContext
import bpy
from ..types.timeline.timeline_serializer import serializeTimeline
from ..settings_scene import tryGetMainSceneSettings
from ..panels_components import getComponentKey
from ..utils_debug import debug_log
from ..utils_blender import objectIsVisibleInViewport
from .skybox_utils import tryGetViewportSkyboxSettings

from ..component_utils import addComponentToObject, getComponent, getComponentGuid, hasComponent
from ..lightmapping.lightmapping import getLightmapScaleOffset, isLightmapped
from ..panels_object import hasInstancingEnabled

# NOTE: keep in mind that components might exist already because the glTF was imported from another Editor or Tool like Unity and has existing data that we want to keep

# called after a object has been processed by the components extension
# we handle here "special" core blender components like Cameras, Meshes, Lights
# where we still want to emit a needle component with useful runtime data 
# for example if an object should be instanced, or receive shadow, or is enabled at all
def postProcessComponents(extension, blender_object, components):
    # TODO: we need to handle this somewhere else for the camera 
    # because we *might* have multiple cameras in the scene and only one has a camera component (or none)
    # and then we want to e.g. make the one camera with the explicit camera component the MainCamera (if no other cameraobject is marked as "MainCamera")
    if blender_object.type == "CAMERA":
        __addCameraData(blender_object, components)
    if blender_object.type == "MESH":
        __addRenderer(blender_object, components)
    if blender_object.type == "LIGHT":
        __addLight(blender_object, components)
    pass

def __findExisting(components, list):
    for existing in components:
        name = existing["name"]
        if name in list:
            return existing
    return None


__knownLightNames = ["Light"]
def __addLight(blender_object, components):
    component = __findExisting(components, __knownLightNames)
    if not component: 
        component = {}
        components.append(component)
        component["name"] = "Light"
    
    if not "guid" in component:
        component["guid"] = getComponentGuid(blender_object, "Light")

    # is the light used for lightmapping?
    isUsedForBaking = isLightmapped(blender_object)
    if isUsedForBaking:
        component["lightmapBakeType"] = 2 # baked
    else:
        component["lightmapBakeType"] = 4 # realtime
    
    # write shadows settings
    if getattr(blender_object, "data"):
        useShadow = blender_object.data.use_shadow
        if useShadow:
            component["shadows"] = 1
        else:
            component["shadows"] = 0
    # write shadow bias settings
    blenderBias = 1
    if bpy.app.version >= (4, 3, 0):
        # TODO
        pass
    else:
        blenderBias = blender_object.data.shadow_buffer_bias
    component["shadowBias"] = -.00005 * blenderBias
    component["shadowNormalBias"] = -.00003 * blenderBias
    pass


__knownRendererNames = ["MeshRenderer", "Renderer", "SkinnedMeshRenderer", "SpriteRenderer"]
def __addRenderer(blender_object, components):
    component = __findExisting(components, __knownRendererNames)
    if component is None:
        component = {}
        armature = blender_object.find_armature()
        if armature:
            component["name"] = "SkinnedMeshRenderer"
        else:
            component["name"] = "MeshRenderer"
        components.append(component)
    
    if not "guid" in component:
        component["guid"] = getComponentGuid(blender_object, "MeshRenderer")
    # always write enabled state
    component["enabled"] = blender_object.hide_get() == False and blender_object.hide_viewport == False
    isVisibleInViewport = objectIsVisibleInViewport(blender_object)
    if not isVisibleInViewport:
        component["enabled"] = False

    # special handling for shadow catcher (it may be disabled in the viewport but we need an enabled renderer then)
    if hasComponent(blender_object, "ShadowCatcher"):
        component["enabled"] = True

    # only write shadow casting setting 
    if not "shadowCastingMode" in component:
        component["shadowCastingMode"] = 1
    if not "receiveShadows" in component:
        component["receiveShadows"] = True

    # override instancing settings
    if hasInstancingEnabled(blender_object):
        component["enableInstancing"] = [True]
    
    if isLightmapped(blender_object):
        offset = getLightmapScaleOffset(blender_object)
        if offset is not None:
            component["lightmapScaleOffset"] = {"x": offset[0], "y": offset[1], "z": offset[2], "w": offset[3]}
            component["lightmapIndex"] = 0


    
def __addCameraData(blender_object, components):
    component = __findExisting(components, ["Camera"])
    
    settings = tryGetMainSceneSettings()

    hasExplicitCameraComponent = component is not None
    
    # if there's no explicit camera component in your blender scene
    if component is None:
        # make sure we have a camera component
        component = {}
        component["name"] = "Camera"
        component["tag"] = "MainCamera"
        components.append(component)

        # we also want to add OrbitControls if we don't have an explicit component
        orbitControls = __findExisting(components, ["OrbitControls"])
        if settings.orbitControls:
            # if the scene doesnt have an orbit controls component we add one
            if not orbitControls:
                debug_log("Adding default OrbitControls")
                # autofit if the camera is in the scene position 0 0 0 
                autofit = settings.autoFit
                # if blender_object.location.length < 0.0001:
                #     autofit = True
                orbit_controls = {}
                orbit_controls["name"] = "OrbitControls"
                orbit_controls["enabled"] = True
                orbit_controls["autoFit"] = autofit
                components.append(orbit_controls)
            else:
                # if the orbit controls has a look at object assigned we emit a LookAtConstraint component
                currentlyAssignedLookat = orbitControls.get("lookAtTarget", None)
                if currentlyAssignedLookat is not None:
                    # orbitControlsComponent = getComponent(blender_object, getComponentKey("OrbitControls"))
                    lookAtConstraint = {}
                    lookAtConstraint["name"] = "LookAtConstraint"
                    lookAtConstraint["enabled"] = True
                    lookAtConstraint["locked"] = False
                    lookAtConstraint["sources"] = [currentlyAssignedLookat]
                    lookAtConstraint["guid"] = getComponentGuid(blender_object, "LookAtConstraint")
                    orbitControls["lookAtConstraint"] = { "guid": lookAtConstraint["guid"] }
                    components.append(lookAtConstraint)
                    pass

                if "lookAtTarget" in orbitControls:
                    del orbitControls["lookAtTarget"]
                pass
    
    component["guid"] = getComponentGuid(blender_object, "Camera")

    
    viewport_settings = tryGetViewportSkyboxSettings()
    if viewport_settings:
        # set the clear flags to skybox if the viewport opacity is > 0.5
        if not "clearFlags" in component and viewport_settings.opacity is not None and viewport_settings.opacity > 0.5:
            component["clearFlags"] = 1
            
        if viewport_settings.blur != None:
            blur = viewport_settings.blur
            # the three blur is very sensitive so we square it
            component["backgroundBlurriness"] = blur * blur * blur

        if viewport_settings.intensity != None:
            intensity = viewport_settings.intensity
            if intensity is not None:
                # the blender intensity
                if not "backgroundIntensity" in component:
                    component["backgroundIntensity"] = intensity
                if not "environmentIntensity" in component:
                    component["environmentIntensity"] = intensity
        
        if not "backgroundColor" in component and viewport_settings.background_color != None:
            col = viewport_settings.background_color
            component["backgroundColor"] = {"r": col[0], "g": col[1], "b": col[2], "a": 1 }
            # if it's an array
            if len(col) == 4:
                component["backgroundColor"]["a"] = col[3]

        if viewport_settings.rotation != None:
            euler = {"x": 0, "y": -viewport_settings.rotation, "z": 0}
            component["backgroundRotation"] = euler
            pass


    # defaults that might be overriden by a camera component
    # if we didnt write any clearFlags options then set it to solidcoor
    if not "clearFlags" in component:
        component["clearFlags"] = 2
    # if no specific backgroundcolor was set then use a default grey
    if not "backgroundColor" in component:
        component["backgroundColor"] = {"r": 0.5, "g": 0.5, "b": 0.5}
    
    # https://docs.blender.org/api/current/bpy.types.Camera.html
    data = bpy.data.objects[blender_object.name].data
    if data:
        component["nearClipPlane"] = data.clip_start
        component["farClipPlane"] = data.clip_end


def exportExtraComponents(extension:ExtensionBase, blender_object, components):

    if extension.exportContext.exportOnlyComponentsInScene:
        return
    
    settings = tryGetMainSceneSettings()
    context = bpy.context
    
    # needle menu
    menuComponent = {
        "name": "NeedleMenu",
        "enabled": True,
        "position": "bottom",
        "showNeedleLogo": settings.showNeedleMenuLogo,
        "createFullscreenButton": settings.showFullscreenButton,
        "createMuteButton": settings.showMuteButton,
        "createQRCodeButton": settings.showQRCodeButton,
    }
    components.append(menuComponent)

    def remap(value, in_min, in_max, out_min, out_max):
        return (value - in_min) * (out_max - out_min) / (in_max - in_min) + out_min

    if settings.vr or settings.ar or settings.quicklook:
        scale = settings.arScale
        if scale < 1:
            scale = remap(scale, 0.1, 1, 10, 1)
        elif scale > 1:
            scale = remap(scale, 1, 10, 1, 0.1)
        webxr_component = {
            "name": "WebXR",
            "enabled": True,
            "createVRButton": settings.vr,
            "createARButton": settings.ar,
            "useQuicklookExport": settings.quicklook,
            "createSendToQuestButton": settings.vr or settings.ar,
            "createQRCode": False, # done by needle menu
            "autoPlace": False,
            "autoCenter": True,
            "arScale": scale,
            "useDepthSensing": settings.depthSensing,
            "defaultAvatar": True,
        }
        components.append(webxr_component)

    if settings.useNetworking:
        blender_scene_name = context.scene.name
        synedRoom = {
            "name": "SyncedRoom",
            "enabled": True,
            "roomName": blender_scene_name,
            "createJoinButton": True,
            "joinRandomRoom": False, # settings so that the user isn't automatically joined a room immediatey
            "requireRoomParameter": True,
        }
        components.append(synedRoom)
    
    if settings.voip:
        voip = {
            "name": "Voip",
            "enabled": True,
            "autoConnect": True,
            "runInBackground": True,
        }
        components.append(voip)
    
    # contact shadows
    if settings.contactShadows == True:
        contactShadows = {
            "name": "ContactShadows",
            "enabled": True,
            "autoFit": True,
            "darkness": settings.contactShadowsDarkness,
            "opacity": settings.contactShadowsDarkness,
        }
        components.append(contactShadows)
    
    if settings.groundProjection == True:
        cameraInScene = bpy.context.scene.camera
        farClip = (cameraInScene and cameraInScene.data.clip_end * .3) or 50
        groundProjection = {
            "name": "GroundProjectedEnv",
            "enabled": True,
            "applyOnAwake": True,
            "autoFit": True,
            "height": settings.groundProjectionHeight,
            "radius": farClip,
            "arBlending": 1, # immersive-ar blending
        }
        components.append(groundProjection)
    
    # tonemapping
    import math
    current_colorspace = context.scene.view_settings.view_transform
    current_exposure = context.scene.view_settings.exposure

    current_exposure = math.pow(2, current_exposure)

    tonemapping_mode = 0;
    if current_colorspace == "AgX":
        tonemapping_mode = 3 # NEToneMappingMode.AgX
    elif current_colorspace == 'Khronos PBR Neutral':
        tonemapping_mode = 4 # NEToneMappingMode.KhronosNeutral

    
    if tonemapping_mode >= 0:
        tonemapping = {
            "name": "ToneMappingEffect",
            "enabled": True,
            "mode": tonemapping_mode,
            "exposure": current_exposure
        }
        components.append(tonemapping)


    if settings.nlatracks == True:
        playable_director = {
            "name": "PlayableDirector",
            "enabled": True,
            "playableAsset": serializeTimeline(extension, blender_object, None),
            "playOnAwake": True,
        }
        components.append(playable_director)
    

    if settings.remoteSkybox:
        remoteSkybox = {
            "name": "RemoteSkybox",
            "enabled": True,
        }
        components.append(remoteSkybox)
        
    if settings.dropListener:
        dropListener = {
            "name": "DropListener",
            "enabled": True,
            "fitIntoVolume": True,
            "placeAtHitPosition": True,
        }
        components.append(dropListener)

    pass


def exportPostprocessing(blender_object, components:list):
    import math

    if not bpy.context:
        return
    if not bpy.context.scene:
        return
    engine = bpy.context.engine
    scene = bpy.context.scene

    effectComponents = []
    
    if engine == "BLENDER_EEVEE":
        evee_settings = scene.eevee

        if evee_settings.use_bloom:
            bloomEffect = {
                "name": "BloomEffect",
                "enabled": evee_settings.use_bloom,
                "intensity": evee_settings.bloom_intensity * 10,
                "threshold": evee_settings.bloom_threshold * .5,
                "scatter": evee_settings.bloom_radius / 10
            }
            effectComponents.append(bloomEffect)

        if evee_settings.use_gtao:
            ao_radius = 1 #evee_settings.gtao_distance / 2
            gtaoEffect = {
                "name": "ScreenSpaceAmbientOcclusionN8",
                "enabled": evee_settings.use_gtao,
                "intensity": evee_settings.gtao_factor * 2,
                "aoRadius": ao_radius,
                "falloff": ao_radius / 5,
                "screenspaceRadius": False,
                "quality": int(round(evee_settings.gtao_quality * 4))
            }
            effectComponents.append(gtaoEffect)
        
    elif engine == "CYCLES":
        cycles_settings = scene.cycles
        bloomEffect = {
            "name": "BloomEffect",
            "enabled": True,
            "intensity": .5,
            "threshold": 10,
            "scatter": .01
        }
        effectComponents.append(bloomEffect)
        gtaoEffect = {
            "name": "ScreenSpaceAmbientOcclusionN8",
            "enabled": True,
            "intensity": 2,
            "aoRadius": 1,
            "falloff": 1 / 5,
            "screenspaceRadius": False,
            "quality": 3
        }
        effectComponents.append(gtaoEffect)



    if len(effectComponents) > 0:
        volume = { 
            "name": "Volume",
        }
        components.append(volume)
        for effect in effectComponents:
            components.append(effect)
    