from direct.task.TaskManagerGlobal import taskMgr
from panda3d.core import (
    BitMask32,
    Camera,
    FrameBufferProperties,
    GraphicsOutput,
    GraphicsPipe,
    NodePath,
    Shader,
    Texture,
    Vec4,
    WindowProperties,
)


class SelectionOutlineManager:
    """Selection mask manager feeding RenderPipeline SelectionOutlineStage."""

    OUTLINE_PREFIX = "selectionOutline"

    def __init__(
        self,
        app,
        enabled=True,
        outline_color=Vec4(1.0, 0.55, 0.0, 1.0),
        outline_width_px=2.0,
        fill_alpha=0.0,
        max_targets=128,
    ):
        self.app = app
        self.enabled = bool(enabled)
        self.outline_color = Vec4(outline_color)
        self.outline_width_px = max(0.0, float(outline_width_px))
        self.fill_alpha = max(0.0, min(1.0, float(fill_alpha)))
        self.max_targets = max(1, int(max_targets))

        self._task_name = "selection_outline_mask_sync"
        self._tracked = []  # [(source_np, clone_np), ...]
        self._stage_missing_warned = False

        self._mask_root = NodePath(f"{self.OUTLINE_PREFIX}_mask_root")
        self._mask_buffer = None
        self._mask_texture = None
        self._mask_cam = None
        self._mask_cam_np = None
        self._mask_shader = self._build_mask_shader()
        self._buffer_size = (0, 0)

    @staticmethod
    def _is_empty(np):
        if not np:
            return True
        if hasattr(np, "isEmpty"):
            return np.isEmpty()
        if hasattr(np, "is_empty"):
            return np.is_empty()
        return False

    @classmethod
    def is_outline_node(cls, node_path):
        if not node_path or cls._is_empty(node_path):
            return False
        name = node_path.getName() if hasattr(node_path, "getName") else ""
        if name.startswith(cls.OUTLINE_PREFIX):
            return True
        try:
            if node_path.hasPythonTag("selection_outline"):
                return True
        except Exception:
            pass
        return False

    def set_enabled(self, enabled):
        self.enabled = bool(enabled)
        if not self.enabled:
            self.clear()
        self._apply_stage_inputs()

    def set_targets(self, targets):
        if not self.enabled:
            self.clear()
            self._apply_stage_inputs()
            return

        self._ensure_mask_resources()
        self.clear()

        if not targets:
            self._apply_stage_inputs()
            return

        seen = set()
        valid = []
        for target in targets:
            if self._is_empty(target) or self.is_outline_node(target):
                continue
            key = str(target)
            if key in seen:
                continue
            seen.add(key)
            valid.append(target)
            if len(valid) >= self.max_targets:
                break

        for source in valid:
            self._clone_target(source)

        if self._tracked:
            self._start_sync_task()
            self._sync_once()
            print(f"[SelectionOutline] targets={len(self._tracked)} active")
        else:
            print("[SelectionOutline] no valid targets for outline")

        self._apply_stage_inputs()

    def clear(self):
        self._stop_sync_task()
        for _, clone_np in self._tracked:
            if not self._is_empty(clone_np):
                clone_np.removeNode()
        self._tracked = []
        self._apply_stage_inputs()

    def cleanup(self):
        self.clear()
        self._destroy_mask_resources()

    def _build_mask_shader(self):
        return Shader.make(
            Shader.SL_GLSL,
            """
#version 430
in vec4 p3d_Vertex;
uniform mat4 p3d_ModelViewProjectionMatrix;
void main() {
    gl_Position = p3d_ModelViewProjectionMatrix * p3d_Vertex;
}
""",
            """
#version 430
out vec4 result;
void main() {
    result = vec4(1.0, 1.0, 1.0, 1.0);
}
""",
        )

    def _clone_target(self, source):
        try:
            clone_np = source.copyTo(self._mask_root)
            if self._is_empty(clone_np):
                return
            if not self._node_has_geom(clone_np):
                clone_np.removeNode()
                return
            clone_np.setName(f"{self.OUTLINE_PREFIX}_{source.getName()}")
            clone_np.setPythonTag("selection_outline", True)
            clone_np.setCollideMask(BitMask32.allOff())
            clone_np.setMat(self.app.render, source.getMat(self.app.render))
            self._tracked.append((source, clone_np))
        except Exception as exc:
            print(f"[SelectionOutline] clone failed: {exc}")

    def _node_has_geom(self, np):
        if self._is_empty(np):
            return False
        try:
            node = np.node()
            if node and hasattr(node, "isGeomNode") and node.isGeomNode():
                return True
        except Exception:
            pass
        try:
            return not np.find("**/+GeomNode").isEmpty()
        except Exception:
            return False

    def _start_sync_task(self):
        taskMgr.remove(self._task_name)
        taskMgr.add(self._sync_task, self._task_name)

    def _stop_sync_task(self):
        taskMgr.remove(self._task_name)

    def _sync_task(self, task):
        self._sync_once()
        return task.cont

    def _sync_once(self):
        if not self.enabled:
            self._apply_stage_inputs()
            return

        self._ensure_mask_resources()
        alive = []
        for source, clone_np in self._tracked:
            if self._is_empty(source) or self._is_empty(clone_np):
                if not self._is_empty(clone_np):
                    clone_np.removeNode()
                continue
            clone_np.setMat(self.app.render, source.getMat(self.app.render))
            alive.append((source, clone_np))
        self._tracked = alive
        self._apply_stage_inputs()

    def _get_stage(self):
        rp = getattr(self.app, "render_pipeline", None)
        if not rp or not getattr(rp, "stage_mgr", None):
            return None
        return rp.stage_mgr.get_stage("SelectionOutlineStage")

    def _apply_stage_inputs(self):
        stage = self._get_stage()
        if not stage:
            if not self._stage_missing_warned:
                print("[SelectionOutline] SelectionOutlineStage not found; plugin may be disabled.")
                self._stage_missing_warned = True
            return

        self._stage_missing_warned = False
        stage.set_outline_style(
            color=self.outline_color,
            width_px=self.outline_width_px,
            fill_alpha=self.fill_alpha,
        )
        stage.set_mask_texture(self._mask_texture)
        stage.set_enabled_outline(self.enabled and bool(self._tracked))

    def _get_window_size(self):
        if not getattr(self.app, "win", None):
            return 1, 1
        return max(1, self.app.win.getXSize()), max(1, self.app.win.getYSize())

    def _ensure_mask_resources(self):
        size = self._get_window_size()
        if size != self._buffer_size:
            self._destroy_mask_resources()
            self._buffer_size = size
        if self._mask_buffer:
            return

        if not getattr(self.app, "graphicsEngine", None) or not getattr(self.app, "win", None):
            return

        w, h = self._buffer_size
        win_props = WindowProperties()
        win_props.setSize(w, h)
        fb_props = FrameBufferProperties()
        fb_props.setRgbaBits(8, 8, 8, 8)
        fb_props.setDepthBits(24)

        self._mask_buffer = self.app.graphicsEngine.make_output(
            self.app.pipe,
            "selection_outline_mask",
            -80,
            fb_props,
            win_props,
            GraphicsPipe.BFRefuseWindow,
            self.app.win.getGsg(),
            self.app.win,
        )
        if not self._mask_buffer:
            print("[SelectionOutline] failed to create mask buffer")
            return

        self._mask_texture = Texture("selection_outline_mask_tex")
        self._mask_texture.setMinfilter(Texture.FTNearest)
        self._mask_texture.setMagfilter(Texture.FTNearest)
        self._mask_buffer.addRenderTexture(self._mask_texture, GraphicsOutput.RTMBindOrCopy)
        self._mask_buffer.setClearColor(Vec4(0, 0, 0, 0))
        self._mask_buffer.setClearColorActive(True)
        self._mask_buffer.setActive(True)

        self._mask_cam = Camera("selection_outline_mask_camera")
        self._mask_cam.setScene(self._mask_root)
        self._mask_cam.setLens(self.app.camLens)
        self._mask_cam_np = self.app.cam.attachNewNode(self._mask_cam)

        dr = self._mask_buffer.makeDisplayRegion()
        dr.setCamera(self._mask_cam_np)

        state_np = NodePath("selection_outline_mask_state")
        state_np.setShader(self._mask_shader, 10000)
        state_np.setLightOff(1)
        state_np.setMaterialOff(1)
        state_np.setTextureOff(1)
        state_np.setColorOff(1)
        self._mask_cam.setInitialState(state_np.getState())

    def _destroy_mask_resources(self):
        if self._mask_cam_np and not self._is_empty(self._mask_cam_np):
            self._mask_cam_np.removeNode()
        self._mask_cam_np = None
        self._mask_cam = None

        if self._mask_buffer and getattr(self.app, "graphicsEngine", None):
            try:
                self.app.graphicsEngine.removeWindow(self._mask_buffer)
            except Exception:
                pass
        self._mask_buffer = None
        self._mask_texture = None
