#!/usr/bin/env python3
"""
Minimal reproducer for vtkSurfaceLICMapper viewport offset bug.
VTK version: 9.6 (build date 2026-04-20)

BUG
---
In a single vtkRenderWindow with multiple renderers, vtkSurfaceLICMapper
renders incorrectly in any viewport where y_min > 0. The LIC result is
either clipped, offset, or missing entirely.

vtkPolyDataMapper with a colormap is unaffected — only vtkSurfaceLICMapper
is broken, confirming the issue is internal to the LIC pipeline.

LAYOUT (1000 x 800 window)
---------------------------------------------------------------------------
  col →        LEFT (LIC mapper)      |     RIGHT (plain PolyData mapper)
  row ↓                               |
  TOP    viewport(0.0, 0.5, 0.5, 1.0) | viewport(0.5, 0.5, 1.0, 1.0)
  y_min=0.5    ** BUG HERE **         |     reference (always correct)
  --------------------------------------------------------------------------
  BOTTOM viewport(0.0, 0.0, 0.5, 0.5) | viewport(0.5, 0.0, 1.0, 0.5)
  y_min=0.0    correct                |     reference (always correct)
---------------------------------------------------------------------------

EXPECTED: within each column, top and bottom viewports look identical.
  - Left column:  both LIC viewports show the same swirling pattern.
  - Right column: both plain-mapper viewports show the same colored surface.
ACTUAL (unpatched): top-left (LIC, y_min=0.5) differs from bottom-left
  (LIC, y_min=0.0) — the LIC result is clipped, offset, or missing.
  The right column is unaffected, confirming the bug is LIC-specific.

ROOT CAUSE (identified)
-----------------------
Two locations in vtkSurfaceLICInterface.cxx ignore the viewport y origin:

1. PrepareForGeometry() — does not reset glViewport to (0,0,w,h) after
   binding the internal FBO. The camera's window-space viewport
   (x_origin, y_origin, w, h) is inherited, so geometry is rendered at
   y_origin pixels into a texture that is only h pixels tall.

2. CopyToScreen() — calls
       glViewport(0, 0, viewsize[0], viewsize[1])
   instead of
       glViewport(origin[0], origin[1], viewsize[0], viewsize[1])
   so the final LIC result is painted at the window's bottom-left corner
   instead of the viewport's actual position.

Neither vtkSurfaceLICHelper nor vtkSurfaceLICInterface stored the viewport
origin — only GetTiledSize() was called, not GetTiledSizeAndOrigin().

REPRODUCTION
------------
Run with stock (unpatched) VTK 9.6:
    python test_lic_viewport_bug.py

Python >= 3.9, VTK >= 9.2 required. No other dependencies.
"""

import math

# ── VTK imports ───────────────────────────────────────────────────────────────
from vtkmodules.vtkCommonCore import vtkFloatArray
from vtkmodules.vtkFiltersSources import vtkPlaneSource
from vtkmodules.vtkRenderingAnnotation import vtkCornerAnnotation
from vtkmodules.vtkRenderingCore import (
    vtkActor,
    vtkPolyDataMapper,
    vtkRenderWindow,
    vtkRenderWindowInteractor,
    vtkRenderer,
)
from vtkmodules.vtkRenderingLICOpenGL2 import vtkSurfaceLICMapper

import vtkmodules.vtkInteractionStyle    # noqa: F401 — registers vtkInteractorStyleTrackballCamera
import vtkmodules.vtkRenderingOpenGL2    # noqa: F401 — registers the OpenGL backend


# ── geometry ──────────────────────────────────────────────────────────────────
def make_plane_with_vectors(resolution=50):
    """Flat plane [-1,1]^2 with a circular (swirling) vector field."""
    src = vtkPlaneSource()
    src.SetResolution(resolution, resolution)
    src.SetOrigin(-1.0, -1.0, 0.0)
    src.SetPoint1( 1.0, -1.0, 0.0)
    src.SetPoint2(-1.0,  1.0, 0.0)
    src.Update()

    pd = src.GetOutput()
    n  = pd.GetNumberOfPoints()

    vectors = vtkFloatArray()
    vectors.SetNumberOfComponents(3)
    vectors.SetNumberOfTuples(n)
    vectors.SetName("vectors")

    for i in range(n):
        x, y, _ = pd.GetPoint(i)
        r = math.sqrt(x * x + y * y)
        if r > 1e-6:
            vx, vy = -y / r, x / r   # tangent to circles → swirling LIC
        else:
            vx, vy = 0.0, 0.0
        vectors.SetTuple3(i, vx, vy, 0.0)

    pd.GetPointData().SetVectors(vectors)
    return pd


# ── actor factories ───────────────────────────────────────────────────────────
def make_lic_actor(polydata):
    mapper = vtkSurfaceLICMapper()
    mapper.SetInputData(polydata)
    lic = mapper.GetLICInterface()
    lic.SetNumberOfSteps(40)
    lic.SetStepSize(0.35)
    lic.SetEnhancedLIC(1)
    lic.SetEnhanceContrast(0)
    actor = vtkActor()
    actor.SetMapper(mapper)
    return actor


def make_poly_actor(polydata):
    mapper = vtkPolyDataMapper()
    mapper.SetInputData(polydata)
    # color by x-coordinate so the surface isn't a flat grey
    mapper.SetScalarModeToUsePointFieldData()
    actor = vtkActor()
    actor.SetMapper(mapper)
    return actor


# ── label ─────────────────────────────────────────────────────────────────────
def make_label(top_text, bottom_text=""):
    ann = vtkCornerAnnotation()
    ann.SetText(2, top_text)    # top-left corner of this viewport
    ann.SetText(0, bottom_text) # bottom-left corner
    ann.GetTextProperty().SetFontSize(14)
    ann.GetTextProperty().SetColor(1, 1, 0.3)
    return ann


# ── main ──────────────────────────────────────────────────────────────────────
def main():
    polydata = make_plane_with_vectors(resolution=60)

    renWin = vtkRenderWindow()
    renWin.SetSize(1000, 800)
    renWin.SetWindowName("vtkSurfaceLICMapper viewport offset bug (VTK 9.6)")
    renWin.SetMultiSamples(0)   # disable MSAA — required for LIC

    # (x_min, y_min, x_max, y_max) in normalised window coordinates
    panes = [
        # viewport                   mapper   label (top)                    label (bottom)
        ((0.0, 0.0, 0.5, 0.5), "lic",  "LIC mapper",                  "y_min=0.0  [correct]"),
        ((0.5, 0.0, 1.0, 0.5), "poly", "Plain PolyData mapper",        "y_min=0.0  [reference]"),
        ((0.0, 0.5, 0.5, 1.0), "lic",  "LIC mapper",                   "y_min=0.5  [BUG — broken on unpatched VTK]"),
        ((0.5, 0.5, 1.0, 1.0), "poly", "Plain PolyData mapper",        "y_min=0.5  [reference — always correct]"),
    ]

    for viewport, mapper_type, label_top, label_bottom in panes:
        if mapper_type == "lic":
            actor = make_lic_actor(polydata)
            bg = (0.08, 0.08, 0.15)
        else:
            actor = make_poly_actor(polydata)
            bg = (0.05, 0.12, 0.08)

        ren = vtkRenderer()
        ren.AddActor(actor)
        ren.AddViewProp(make_label(label_top, label_bottom))
        ren.SetViewport(*viewport)
        ren.SetBackground(*bg)
        ren.ResetCamera()
        renWin.AddRenderer(ren)

    iren = vtkRenderWindowInteractor()
    iren.SetRenderWindow(renWin)
    renWin.Render()
    iren.Start()


if __name__ == "__main__":
    main()
