from types import SimpleNamespace

import numpy as np
import pytest

from topovec.mgl.solid import SolidOfRevolution
from topovec.mgl.vector import VectorRender


class _Releasable:
    def __init__(self, name="resource"):
        self.name = name
        self.release_calls = 0

    def release(self):
        self.release_calls += 1


class _DummyMesh:
    created: list["_DummyMesh"] = []

    def __init__(self, mglctx=None, count=None):
        self.mglctx = mglctx
        self.count = count
        self.pos = SimpleNamespace(vbo=_Releasable("pos"))
        self.normal = SimpleNamespace(vbo=_Releasable("normal"))
        self.idx = SimpleNamespace(ibo=_Releasable("idx"))
        self.release_calls = 0
        type(self).created.append(self)

    def release(self):
        self.release_calls += 1


class _DummyConeMesh(_DummyMesh):
    created = []


class _DummyDoubleConeMesh(_DummyMesh):
    created = []


class _DummyCylinderMesh(_DummyMesh):
    created = []


class _DummyArrowMesh(_DummyMesh):
    created = []


class _DummyVao:
    def __init__(self, call_no):
        self.call_no = call_no
        self.release_calls = 0
        self.render_calls = []

    def release(self):
        self.release_calls += 1

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


class _DummyContext:
    def __init__(self, fail_on_vertex_array_calls=None):
        self.fail_on_vertex_array_calls = set(fail_on_vertex_array_calls or ())
        self.vertex_array_calls = 0
        self.created_vaos = []

    def vertex_array(self, _prog, _bindings, _ibo=None):
        self.vertex_array_calls += 1
        if self.vertex_array_calls in self.fail_on_vertex_array_calls:
            raise RuntimeError("vao boom")
        vao = _DummyVao(self.vertex_array_calls)
        self.created_vaos.append(vao)
        return vao


def _make_vector_render(monkeypatch, *, context=None):
    context = _DummyContext() if context is None else context
    for mesh_cls in (
        _DummyConeMesh,
        _DummyDoubleConeMesh,
        _DummyCylinderMesh,
        _DummyArrowMesh,
    ):
        mesh_cls.created.clear()
    monkeypatch.setattr(
        VectorRender,
        "MESH_NAMES",
        {
            "Cone": _DummyConeMesh,
            "Double cone": _DummyDoubleConeMesh,
            "Cylinder": _DummyCylinderMesh,
            "Arrow": _DummyArrowMesh,
        },
    )

    render = VectorRender.__new__(VectorRender)
    render._ctx = context
    render._prog = object()
    render._system = SimpleNamespace(number_of_spins=8, size=(4, 5, 6), dim=3)
    render._state = SimpleNamespace(vbo=object())
    render._coordinatesBuffer = SimpleNamespace(vbo=object())
    render._maskBuffer = SimpleNamespace(vbo=object())
    render._scalarMaskBuffer = SimpleNamespace(vbo=object())
    render._mesh_shape = "Cone"
    render._detalization = 1.0
    render.use_mask = False
    render.use_scalar_mask = False
    render.director_mode = False
    render.base_size = 1.0
    render.min_size = 0.0
    render.max_size = 1.2
    render.min_scalar = 0.0
    render.max_scalar = 1.0
    render.power_size = 1.0
    render.min_threshold = 0.75
    render.size = render._system.size
    render.clip_min = (0, 0, 0)
    render.clip_max = tuple(render.size)
    render.clip_invert = False
    render.color_shift = 0.81
    render.width = 0.75
    render.saturation_mag = 1.0
    render.lighting = True
    render.negate = False
    for uniform in (
        "u_view",
        "u_projection",
        "u_camera",
        "u_use_mask",
        "u_min_size",
        "u_max_size",
        "u_use_scalar_mask",
        "u_min_scalar",
        "u_max_scalar",
        "u_power_size",
        "u_min_threshold",
        "u_size",
        "u_clip_min",
        "u_clip_max",
        "u_clip_invert",
        "u_color_shift",
        "u_director_mode",
        "u_width",
        "u_saturation_mag",
        "u_lighting",
        "u_negate",
    ):
        setattr(render, uniform, SimpleNamespace(value=None))
    render.init_vao()
    return render, context


def test_solid_of_revolution_release_is_idempotent():
    solid = SolidOfRevolution.__new__(SolidOfRevolution)
    solid.pos = _Releasable("pos")
    solid.normal = _Releasable("normal")
    solid.idx = _Releasable("idx")

    solid.release()
    solid.release()

    assert solid.pos is None
    assert solid.normal is None
    assert solid.idx is None


def test_vector_render_rebuilds_mesh_shape_safely(monkeypatch):
    render, context = _make_vector_render(monkeypatch)

    for shape in ("Double cone", "Cylinder", "Arrow", "Cone"):
        render.mesh_shape = shape
        render.render(
            modelview=np.eye(4, dtype=np.float32),
            projection=np.eye(4, dtype=np.float32),
            camera_position=(0.0, 0.0, 1.0),
        )
        assert render.mesh_shape == shape
        assert render._vao.render_calls

    current_mesh = render.mesh
    current_vao = render._vao
    all_meshes = (
        _DummyConeMesh.created
        + _DummyDoubleConeMesh.created
        + _DummyCylinderMesh.created
        + _DummyArrowMesh.created
    )
    for mesh in all_meshes:
        if mesh is current_mesh:
            assert mesh.release_calls == 0
        else:
            assert mesh.release_calls == 1
    for vao in context.created_vaos:
        if vao is current_vao:
            assert vao.release_calls == 0
        else:
            assert vao.release_calls == 1


def test_vector_render_rebuilds_detalization_safely(monkeypatch):
    render, _context = _make_vector_render(monkeypatch)

    render.detalization = 1.5
    assert render._detalization == 1.5
    assert render.mesh.count == 15

    render.detalization = 2.0
    assert render._detalization == 2.0
    assert render.mesh.count == 20

    render.render(
        modelview=np.eye(4, dtype=np.float32),
        projection=np.eye(4, dtype=np.float32),
        camera_position=(0.0, 0.0, 1.0),
    )
    assert render._vao.render_calls


def test_vector_render_failed_rebuild_keeps_previous_geometry(monkeypatch):
    context = _DummyContext(fail_on_vertex_array_calls={2})
    render, _ = _make_vector_render(monkeypatch, context=context)

    previous_mesh = render.mesh
    previous_vao = render._vao

    with pytest.raises(RuntimeError, match="vao boom"):
        render.mesh_shape = "Arrow"

    assert render.mesh is previous_mesh
    assert render._vao is previous_vao
    assert render.mesh_shape == "Cone"
    assert len(_DummyArrowMesh.created) == 1
    assert _DummyArrowMesh.created[0].release_calls == 1
    assert previous_mesh.release_calls == 0
    assert previous_vao.release_calls == 0
