"""Headless execution shell for scenes on a standalone ModernGL context."""

# import struct
from typing import Callable
import moderngl as mgl
from PIL import Image, ImageOps

# from .buffer import Field3DBuffer
# from ..core import System
from .scene import Scene
from .target import OwnedRenderTarget


class ImageContainer(object):
    """Bind one scene to an offscreen framebuffer and render it to images.

    This is the reusable headless primitive underneath notebook adapters and
    sugar helpers. It owns the GL context and framebuffer, while the attached
    scene owns the actual rendering semantics.
    """

    def __init__(self, size=(1024, 1024), scene: Callable[[mgl.Context], Scene] | Scene = None):
        self._ctx = mgl.create_standalone_context()
        self._size = size
        self._target = OwnedRenderTarget(self._ctx, self._size)
        self._fbo = self._target.fbo
        self._scene = None
        if not scene is None:
            self.attach_scene(scene)

    @property
    def scene(self):
        return self._scene

    @property
    def size(self):
        return self._size

    def attach_scene(self, scene: Callable[[mgl.Context], Scene] | Scene):
        """Attach a scene factory or a prebuilt scene for this GL context."""
        if isinstance(scene, Scene):
            if getattr(scene, "_ctx", None) is not self._ctx:
                raise ValueError("Scene was created for a different OpenGL context.")
            self._scene = scene
            return self
        self._scene = scene(self._ctx)
        return self

    def upload(self, state):
        """Forward state upload to the attached scene."""
        if self._scene is None:
            raise Exception("Scene should be attached first")
        self._scene.upload(state)
        return self

    def _render(self):
        """Render the current scene into the owned framebuffer."""
        self._target.bind()
        self._scene.render_to(self._target)

    def save(self, filename=None, crop=False):
        """Render the current frame and return or save a PIL image."""
        self._render()

        img = Image.frombytes("RGB", self._size, self._target.read_rgb())
        img = img.transpose(Image.FLIP_TOP_BOTTOM)

        if crop:
            inverted = ImageOps.invert(img)
            imageBox = inverted.getbbox()
            img = img.crop(imageBox)

        if filename is not None:
            img.save(filename)
            return self
        else:
            return img
