from types import SimpleNamespace

import moderngl as mgl
import numpy as np

import topovec.mgl as topovec_mgl
from topovec.core import System
from topovec.mgl.axes import AxesRender
from topovec.mgl.charge import ChargeRender
from topovec.mgl.scene import Scene
from topovec.mgl.scene import ChargeScene, EmptyScene, FlowScene, LayerAndSurfaceScene, VectorScene
from topovec.mgl.target import RenderTarget


class DummyScene(Scene):
    def upload(self, state=None):
        return None

    def render(self):
        return None


def test_scene_clear_background_uses_current_framebuffer():
    clear_calls = []

    class DummyFramebuffer:
        def clear(self, *args, **kwargs):
            clear_calls.append((args, kwargs))

    ctx = SimpleNamespace(fbo=DummyFramebuffer())
    system = SimpleNamespace()

    scene = DummyScene.__new__(DummyScene)
    scene._ctx = ctx
    scene._system = system
    scene._background = (0.25, 0.5, 0.75)

    scene.clear_background()
    scene.clear_current_framebuffer(0.1, 0.2, 0.3, 0.4)

    assert clear_calls == [
        ((0.25, 0.5, 0.75, 1.0), {}),
        ((0.1, 0.2, 0.3, 0.4), {}),
    ]


def test_scene_render_compatibility_wrapper_routes_target_based_scene():
    class DummyFramebuffer:
        def __init__(self):
            self.size = (640, 320)
            self.use_calls = 0

        def use(self):
            self.use_calls += 1

    class TargetScene(Scene):
        def upload(self, state=None):
            return None

        def render_to(self, target):
            self.last_target = target

    framebuffer = DummyFramebuffer()
    ctx = SimpleNamespace(fbo=framebuffer, viewport=None)
    scene = TargetScene.__new__(TargetScene)
    scene._ctx = ctx
    scene._system = SimpleNamespace()
    scene._background = (1.0, 1.0, 1.0)

    scene.render(aspectratio=2.5)

    assert isinstance(scene.last_target, RenderTarget)
    assert scene.last_target.aspectratio == 2.5
    assert scene.last_target.size == (640, 320)


def test_scene_render_to_falls_back_to_legacy_render_override():
    class LegacyScene(Scene):
        def upload(self, state=None):
            return None

        def render(self, aspectratio=None):
            self.last_aspectratio = aspectratio

    scene = LegacyScene.__new__(LegacyScene)
    scene._ctx = SimpleNamespace(fbo=SimpleNamespace(size=(100, 50)))
    scene._system = SimpleNamespace()
    target = RenderTarget(
        ctx=SimpleNamespace(viewport=None),
        fbo=SimpleNamespace(size=(100, 50), color_mask=(True, True, True, True), depth_mask=True),
        size=(100, 50),
        aspectratio=3.0,
    )

    Scene.render_to(scene, target)

    assert scene.last_aspectratio == 3.0


def test_axes_render_release_does_not_invalidate_shared_context():
    system = System.cubic((4, 4, 4))
    state = np.zeros((4, 4, 4, 3), dtype=np.float32)
    state[..., 2] = 1.0

    image_container = topovec_mgl.render_prepare(
        scenecls="Layer",
        system=system,
        imgsize=32,
    )
    image_container.upload(state)

    assert image_container.save().size == image_container.size

    image_container.scene.axes_render.release()

    fresh_axes = AxesRender(mglctx=image_container._ctx)

    assert fresh_axes._prog is not None


def test_scene_release_keeps_shared_context_usable_for_new_scene():
    system = System.cubic((4, 4, 4))
    state = np.zeros((4, 4, 4, 3), dtype=np.float32)
    state[..., 2] = 1.0

    image_container = topovec_mgl.render_prepare(
        scenecls="Layer",
        system=system,
        imgsize=32,
    )
    image_container.upload(state)

    assert image_container.save().size == image_container.size

    image_container.scene.release()

    replacement = topovec_mgl.SCENES["Level surface"](
        mglctx=image_container._ctx,
        system=system,
    )
    image_container.attach_scene(replacement)
    image_container.upload(state)

    assert image_container.save().size == image_container.size


class DummyAxes:
    def list_properties(self):
        return []

    def render(self, **kwargs):
        return None


class DummyCamera:
    def get_view_matrix(self):
        return np.eye(4, dtype=np.float32)

    def get_projection_matrix(self, aspectratio=None):
        return np.eye(4, dtype=np.float32)

    def camera_position(self):
        return (0.0, 0.0, 1.0)


class DummyTarget:
    def __init__(self):
        self.aspectratio = 1.5
        self.clear_calls = []
        self.bind_calls = 0

    def bind(self):
        self.bind_calls += 1

    def clear_color(self, *rgba):
        if len(rgba) == 1 and not isinstance(rgba[0], (int, float)):
            rgba = tuple(rgba[0])
        self.clear_calls.append(tuple(float(value) for value in rgba))


class DummyRender:
    MESH_NAMES = {"Cone": object(), "Arrow": object()}
    ANGLENAMES = ["polar angle", "azimuthal angle"]

    def __init__(self, **attrs):
        self.render_calls = []
        self.color_shift = 0.81
        self.lighting = True
        self.negate = False
        self.width = 0.5
        self.max_size = 1.0
        self.neglect_z = False
        self.level = 0.0
        self.angle_name = self.ANGLENAMES[0]
        self.mesh_shape = "Cone"
        self.subtract_blend = True
        for name, value in attrs.items():
            setattr(self, name, value)

    def render(self, **kwargs):
        self.render_calls.append(dict(kwargs))


class DummyContext:
    def __init__(self):
        self.enable_only_calls = []
        self.blend_func = None
        self.blend_equation = None
        self.line_width = 0.0

    def enable_only(self, flag):
        self.enable_only_calls.append(flag)


class DummyUniform:
    def __init__(self):
        self.value = None


class DummyVao:
    def __init__(self):
        self.calls = []

    def render(self, **kwargs):
        self.calls.append(dict(kwargs))


def _empty_scene_stub(scene_cls):
    scene = scene_cls.__new__(scene_cls)
    scene._background = (1.0, 1.0, 1.0)
    scene._white_background = True
    scene._ctx = DummyContext()
    scene._axes = DummyAxes()
    scene.show_axes = False
    scene._camera = DummyCamera()
    return scene


def test_empty_scene_style_group_exposes_white_background():
    scene = _empty_scene_stub(EmptyScene)

    groups = EmptyScene.list_properties(scene)

    assert list(groups)[:2] == ["Style", "Axes"]
    style_prop = groups["Style"][0]
    assert style_prop.field == "white_background"
    assert style_prop.title == "White background"
    assert style_prop.param_key == "style.white_background"

    scene.white_background = False
    assert scene._background == (0.0, 0.0, 0.0)
    scene.white_background = True
    assert scene._background == (1.0, 1.0, 1.0)


def test_vector_scene_render_uses_common_white_background_toggle():
    scene = _empty_scene_stub(VectorScene)
    scene._render = DummyRender()
    target = DummyTarget()

    VectorScene.render_to(scene, target)
    scene.white_background = False
    VectorScene.render_to(scene, target)

    assert target.clear_calls == [
        (1.0, 1.0, 1.0),
        (0.0, 0.0, 0.0),
    ]
    assert len(scene._render.render_calls) == 2


def test_flow_scene_render_uses_common_white_background_toggle():
    scene = _empty_scene_stub(FlowScene)
    scene._render = DummyRender()
    target = DummyTarget()

    scene.white_background = False
    FlowScene.render_to(scene, target)
    scene.white_background = True
    FlowScene.render_to(scene, target)

    assert target.clear_calls == [
        (0.0, 0.0, 0.0),
        (1.0, 1.0, 1.0),
    ]
    assert len(scene._render.render_calls) == 2


def test_layer_and_surface_scene_uses_style_group_and_common_background():
    scene = _empty_scene_stub(LayerAndSurfaceScene)
    scene._render = DummyRender()
    scene._level_render = DummyRender()
    scene.layers_count = 4
    target = DummyTarget()

    groups = LayerAndSurfaceScene.list_properties(scene)
    LayerAndSurfaceScene.render_to(scene, target)
    scene.white_background = False
    LayerAndSurfaceScene.render_to(scene, target)

    assert "Style" in groups
    assert "Shared" not in groups
    assert groups["Style"][0].field == "white_background"
    assert target.clear_calls == [
        (1.0, 1.0, 1.0),
        (0.0, 0.0, 0.0),
    ]
    assert len(scene._render.render_calls) == 2
    assert len(scene._level_render.render_calls) == 2


def test_charge_scene_background_follows_white_background_only():
    scene = _empty_scene_stub(ChargeScene)
    scene._render = DummyRender(subtract_blend=True)
    target = DummyTarget()

    scene.white_background = False
    ChargeScene.render_to(scene, target)
    scene.white_background = True
    ChargeScene.render_to(scene, target)

    assert target.clear_calls == [
        (0.0, 0.0, 0.0),
        (1.0, 1.0, 1.0),
    ]


def test_charge_render_uses_subtract_blend_property_for_blend_equation():
    render = ChargeRender.__new__(ChargeRender)
    render._ctx = DummyContext()
    render._vao = DummyVao()
    render.u_view = DummyUniform()
    render.u_projection = DummyUniform()
    render.u_brightness = DummyUniform()
    render.u_subtract_blend = DummyUniform()
    render.brightness = 2.0
    modelview = np.eye(4, dtype=np.float32)
    projection = np.eye(4, dtype=np.float32)

    render.subtract_blend = True
    ChargeRender.render(
        render,
        modelview=modelview,
        projection=projection,
        camera_position=(0.0, 0.0, 1.0),
    )
    min_equation = render._ctx.blend_equation

    render.subtract_blend = False
    ChargeRender.render(
        render,
        modelview=modelview,
        projection=projection,
        camera_position=(0.0, 0.0, 1.0),
    )

    props = ChargeRender.list_properties(render)

    assert min_equation == mgl.MIN
    assert render._ctx.blend_equation == mgl.MAX
    assert props[0].field == "subtract_blend"
    assert props[0].title == "Subtract blend"
