"""Convenience factories for common scene/camera/container combinations.

These helpers do not define a separate rendering model. They only build
standard :mod:`topovec.mgl.scene` objects, choose an initial camera, and wrap
everything into an :class:`topovec.mgl.image.ImageContainer`.
"""

import numpy as np

from ..core import System, log
from .image import ImageContainer
from .camera import Camera
from .scene import LayerScene, FlowScene, IsolinesScene, SCENES, Scene


def _resolve_scene_class(scenecls: str | type[Scene]) -> type[Scene]:
    """Resolve a built-in scene label or pass through an explicit scene class."""
    if isinstance(scenecls, str):
        return SCENES[scenecls]
    return scenecls


def _scene_camera_and_size(
    scenecls: type[Scene],
    system: System,
    *,
    scene_state: dict[str, object] | None = None,
    camera_preset: int | None = None,
    scalex: bool = False,
    imgsize: int = 1024,
) -> tuple[Camera, tuple[int, int]]:
    """Compute the default camera and recommended image size for a scene."""
    state = {} if scene_state is None else dict(scene_state)
    camera = scenecls.make_default_camera_for(system, state=state)
    if camera_preset is not None:
        camera.set_predefined_direction(preset=camera_preset)
        camera.adjust_fov(system)
    aspectratio = camera.adjust_scale(system)
    size = (
        (int(imgsize * aspectratio), imgsize)
        if scalex
        else (imgsize, int(imgsize / aspectratio))
    )
    return camera, size


def _attach_prepared_scene(
    scenecls: type[Scene],
    system: System,
    *,
    camera: Camera,
    size: tuple[int, int],
    scene_state: dict[str, object] | None = None,
) -> ImageContainer:
    """Create an :class:`ImageContainer` with one prepared scene instance."""
    state = {} if scene_state is None else dict(scene_state)
    return ImageContainer(
        size=size,
        scene=lambda mglctx: scenecls(
            mglctx=mglctx,
            system=system,
            camera=camera,
            state=state,
        ),
    )


def render_prepare(
    scenecls: str,
    system: System,
    camera_preset: int | None = 2,
    scalex: bool = False,
    imgsize: int = 1024,
):
    """Prepare a built-in scene for headless rendering.

    Arguments:
        scenecls: Scene class or built-in scene name.
        camera_preset: optional predefined camera direction override. Use `None`
            to keep the scene default camera orientation.

    Example:

    ic = render_layer(system) # Create render.
    ic.upload(director) # Specify director field to render.
    ic.save() # Render.
    """

    scenecls = _resolve_scene_class(scenecls)
    camera, size = _scene_camera_and_size(
        scenecls,
        system,
        camera_preset=camera_preset,
        scalex=scalex,
        imgsize=imgsize,
    )
    return _attach_prepared_scene(scenecls, system, camera=camera, size=size)


#############################################################################################################


def render_layer(
    system: System,
    scalex: bool = False,
    imgsize: int = 1024,
    show_axes: bool = False,
    layer: int = 0,
    axis: int = 2,
    mesh: str = "Cylinder",
):
    """Prepare the standard orthographic single-layer scene.

    Example:

    ic = render_layer(system) # Create render.
    ic.upload(director) # Specify director field to render.
    ic.save() # Render.
    """

    # Check parameters.
    if not 0 <= axis < 3:
        raise ValueError(f"Axis should be 0 (x-axis), 1 (y-axis) or 2 (z-axis).")

    if not 0 <= layer < system.size[axis]:
        raise ValueError(
            f"Layer {layer} is outside of allowed range 0 .. {system.size[axis]}"
        )

    scene_state = {
        "axis": int(axis),
        "layer": int(layer),
        "show_axes": bool(show_axes),
    }
    camera, size = _scene_camera_and_size(
        LayerScene,
        system,
        scene_state=scene_state,
        camera_preset=None,
        scalex=scalex,
        imgsize=imgsize,
    )
    # log.debug(f"Aspect ratio {size[0] / size[1]:.2f}")
    ic = _attach_prepared_scene(
        LayerScene,
        system,
        camera=camera,
        size=size,
        scene_state=scene_state,
    )

    ic.scene.vector_render.mesh_shape = mesh

    return ic


################################################################################################################################


def render_isosurface(
    system: System,
    scalex: bool = False,
    imgsize: int = 1024,
    show_axes: bool = False,
    preset: int = 2,
    levels: list[float] = [0],
    lighting: bool = True,
    neglect_z: bool = True,
):
    """Prepare the standard flow/isosurface scene.

    Example:

    ic = render_layer(system) # Create render.
    ic.upload(director) # Specify director field to render.
    ic.save() # Render.
    """

    camera, size = _scene_camera_and_size(
        FlowScene,
        system,
        camera_preset=preset,
        scalex=scalex,
        imgsize=imgsize,
    )
    ic = _attach_prepared_scene(FlowScene, system, camera=camera, size=size)

    ic.scene.surface_render.neglect_z = False
    ic.scene.surface_render.levels = levels
    # ic.scene.surface_render.cutx = lx
    # ic.scene.surface_render.cuty = ly
    # ic.scene.surface_render.cutphi = np.pi/2
    ic.scene.surface_render.lighting = lighting
    ic.scene.surface_render.neglect_z = neglect_z

    return ic


################################################################################################################################


def render_isolines(
    system: System,
    scalex: bool = False,
    imgsize: int = 1024,
    show_axes: bool = False,
    preset: int = 2,
    levels: list[float] = [0],
    lighting: bool = True,
    neglect_z: bool = True,
    thickness: float = 0.002,
    nlines: int = 5,
    z0: float = 0.0,
    phi0: float = 0.0,
):
    """Prepare the standard isolines scene.

    Arguments:
        thickness # Isoline thickness
        nlines # Number of isolines
        phi0   # Orientation of xy-projection of projector for isolines:
        z0     # z-projection of director for isolines.
               # S=(sqrt(1-z0^2) cos phi0, sqrt(1-z0^2) sin phi0, z0)

    Example:

    ic = render_layer(system) # Create render.
    ic.upload(director) # Specify director field to render.
    ic.save() # Render.
    """

    scene_state = {"show_axes": bool(show_axes)}
    camera, size = _scene_camera_and_size(
        IsolinesScene,
        system,
        scene_state=scene_state,
        camera_preset=preset,
        scalex=scalex,
        imgsize=imgsize,
    )
    ic = _attach_prepared_scene(
        IsolinesScene,
        system,
        camera=camera,
        size=size,
        scene_state=scene_state,
    )

    ic.scene.isolines_render.neglect_z = False
    ic.scene.isolines_render.levels = levels
    ic.scene.isolines_render.lighting = lighting
    ic.scene.isolines_render.neglect_z = neglect_z
    ic.scene.isolines_render.width = thickness
    # k = np.round( (nlines-1) )#/np.sqrt(1-z0**2) )
    ic.scene.isolines_render.npetals = nlines - 1
    ic.scene.isolines_render.phi_shift = phi0 / (2 * np.pi) * nlines
    ic.scene.isolines_render.theta_shift = np.arcsin(z0)

    ic.scene.axes_render.location = (-0.9, -0.6)

    return ic
