"""Fuzz target for grid Field-of-View computation (#283).

Exercises ``UIGrid.compute_fov`` / ``UIGrid.is_in_fov`` and the underlying
TCOD FOV state. The mutation surface intentionally hammers:

- transparency / walkability toggles between consecutive computes
  (catches stale FOV state bugs)
- in-bounds and out-of-bounds origins (must raise, not crash)
- radius edge cases: 0, normal, far larger than the grid
- ``light_walls`` flag flipping
- every value of the ``mcrfpy.FOV`` enum (BASIC, DIAMOND, SHADOW,
  PERMISSIVE_0..8, RESTRICTIVE, SYMMETRIC_SHADOWCAST) and raw int values
  including out-of-range integers (must raise ValueError, not crash)
- alternating compute/query loops where the origin walks across the grid

Grid sizes are kept small (2x2 .. 32x32) because compute_fov is the
per-iteration hot path here.
"""

import mcrfpy

from fuzz_common import ByteStream, EXPECTED_EXCEPTIONS, safe_reset


def _get_fov_enum_members():
    """Snapshot mcrfpy.FOV enum members once at import time.

    Returns a list of FOV enum instances. Falls back to integers if the
    enum module is unavailable for any reason — the C++ binding accepts
    raw ints too, so the fuzz target stays functional either way.
    """
    fov = getattr(mcrfpy, "FOV", None)
    if fov is None:
        return list(range(15))  # FOV_BASIC..NB_FOV_ALGORITHMS-1
    members = []
    for name in (
        "BASIC", "DIAMOND", "SHADOW",
        "PERMISSIVE_0", "PERMISSIVE_1", "PERMISSIVE_2", "PERMISSIVE_3",
        "PERMISSIVE_4", "PERMISSIVE_5", "PERMISSIVE_6", "PERMISSIVE_7",
        "PERMISSIVE_8",
        "RESTRICTIVE", "SYMMETRIC_SHADOWCAST",
    ):
        m = getattr(fov, name, None)
        if m is not None:
            members.append(m)
    if not members:
        return list(range(15))
    return members


_FOV_MEMBERS = _get_fov_enum_members()


def _make_grid(stream):
    """Create a fresh small grid and return (grid, w, h)."""
    w = stream.int_in_range(2, 32)
    h = stream.int_in_range(2, 32)
    grid = mcrfpy.Grid(grid_size=(w, h))
    return grid, w, h


def _color_or_none(stream):
    """Return a (possibly out-of-range) RGBA tuple, or None, for color args."""
    if stream.bool():
        return None
    return (stream.int_in_range(-20, 300), stream.int_in_range(-20, 300),
            stream.int_in_range(-20, 300), stream.int_in_range(-20, 300))


def _grid_with_color_layer(stream, name):
    """Build a small grid with one named ColorLayer. Returns (grid, layer, w, h)
    or (None, None, 0, 0) if construction failed."""
    w = stream.int_in_range(2, 16)
    h = stream.int_in_range(2, 16)
    try:
        grid = mcrfpy.Grid(grid_size=(w, h))
        layer = mcrfpy.ColorLayer(name=name, z_index=0)
        grid.add_layer(layer)
    except EXPECTED_EXCEPTIONS:
        return None, None, 0, 0
    return grid, layer, w, h


def _fuzz_perspective(stream):
    """ColorLayer.apply_perspective / update_perspective / clear_perspective."""
    grid, layer, w, h = _grid_with_color_layer(stream, "persp")
    if layer is None:
        return
    ent = None
    try:
        ent = mcrfpy.Entity(grid_pos=(stream.int_in_range(0, w - 1),
                                      stream.int_in_range(0, h - 1)), grid=grid)
        ent.sight_radius = stream.int_in_range(-2, 20)
    except EXPECTED_EXCEPTIONS:
        pass
    # Sometimes pass a non-entity to hit the type-check path.
    target = ent if (ent is not None and stream.bool()) else stream.pick_one((None, "bad", ent))
    try:
        layer.apply_perspective(target, _color_or_none(stream),
                                _color_or_none(stream), _color_or_none(stream))
    except EXPECTED_EXCEPTIONS:
        pass
    try:
        layer.update_perspective()
    except EXPECTED_EXCEPTIONS:
        pass
    try:
        layer.clear_perspective()
    except EXPECTED_EXCEPTIONS:
        pass


def _fuzz_draw_fov(stream):
    """ColorLayer.draw_fov(source, radius, fov, visible, discovered, unknown)."""
    grid, layer, w, h = _grid_with_color_layer(stream, "fov")
    if layer is None:
        return
    source = (stream.int_in_range(-3, w + 3), stream.int_in_range(-3, h + 3))
    kw = {}
    if stream.bool():
        kw["radius"] = stream.int_in_range(-2, 40)
    if stream.bool():
        kw["fov"] = stream.pick_one(_FOV_MEMBERS)
    if stream.bool():
        kw["visible"] = _color_or_none(stream)
    if stream.bool():
        kw["discovered"] = _color_or_none(stream)
    try:
        layer.draw_fov(source, **kw)
    except EXPECTED_EXCEPTIONS:
        pass


def fuzz_one_input(data):
    stream = ByteStream(data)
    try:
        grid, w, h = _make_grid(stream)

        n_ops = stream.int_in_range(1, 30)
        for _ in range(n_ops):
            if stream.remaining < 1:
                break
            op = stream.u8() % 18
            try:
                if op == 0:
                    # Replace the active grid (drop the old one).
                    grid, w, h = _make_grid(stream)

                elif op == 1:
                    # Set transparent on a possibly-OOB cell.
                    x = stream.int_in_range(-5, w + 5)
                    y = stream.int_in_range(-5, h + 5)
                    grid.at(x, y).transparent = stream.bool()

                elif op == 2:
                    # Set walkable on a possibly-OOB cell.
                    x = stream.int_in_range(-5, w + 5)
                    y = stream.int_in_range(-5, h + 5)
                    grid.at(x, y).walkable = stream.bool()

                elif op == 3:
                    # In-bounds compute_fov with reasonable radius.
                    x = stream.int_in_range(0, max(0, w - 1))
                    y = stream.int_in_range(0, max(0, h - 1))
                    radius = stream.int_in_range(0, 20)
                    grid.compute_fov((x, y), radius=radius)

                elif op == 4:
                    # Out-of-bounds origin: must raise, not crash.
                    sign_x = -1 if stream.bool() else 1
                    sign_y = -1 if stream.bool() else 1
                    x = sign_x * stream.int_in_range(0, w + 5)
                    y = sign_y * stream.int_in_range(0, h + 5)
                    if 0 <= x < w and 0 <= y < h:
                        # Force OOB even if randomness lined up in-bounds.
                        x = w + 1
                    radius = stream.int_in_range(0, 10)
                    grid.compute_fov((x, y), radius=radius)

                elif op == 5:
                    # Radius 0 — degenerate case.
                    x = stream.int_in_range(0, max(0, w - 1))
                    y = stream.int_in_range(0, max(0, h - 1))
                    grid.compute_fov((x, y), radius=0)

                elif op == 6:
                    # Extreme radius (far larger than grid).
                    x = stream.int_in_range(0, max(0, w - 1))
                    y = stream.int_in_range(0, max(0, h - 1))
                    radius = stream.int_in_range(50, 1000)
                    grid.compute_fov((x, y), radius=radius)

                elif op == 7:
                    # light_walls toggle.
                    x = stream.int_in_range(0, max(0, w - 1))
                    y = stream.int_in_range(0, max(0, h - 1))
                    radius = stream.int_in_range(0, 15)
                    grid.compute_fov(
                        (x, y),
                        radius=radius,
                        light_walls=stream.bool(),
                    )

                elif op == 8:
                    # Random algorithm selection (valid enum or int).
                    x = stream.int_in_range(0, max(0, w - 1))
                    y = stream.int_in_range(0, max(0, h - 1))
                    radius = stream.int_in_range(0, 15)
                    algo = stream.pick_one(_FOV_MEMBERS)
                    grid.compute_fov(
                        (x, y),
                        radius=radius,
                        light_walls=stream.bool(),
                        algorithm=algo,
                    )

                elif op == 9:
                    # Out-of-range raw int algorithm — must raise ValueError.
                    x = stream.int_in_range(0, max(0, w - 1))
                    y = stream.int_in_range(0, max(0, h - 1))
                    bad_algo = stream.int_in_range(-50, 200)
                    grid.compute_fov(
                        (x, y), radius=5, algorithm=bad_algo
                    )

                elif op == 10:
                    # is_in_fov with possibly-OOB coords.
                    x = stream.int_in_range(-5, w + 5)
                    y = stream.int_in_range(-5, h + 5)
                    _ = grid.is_in_fov(x, y)

                elif op == 11:
                    # Tight loop: 20x compute + query against a moving origin.
                    base_x = stream.int_in_range(0, max(0, w - 1))
                    base_y = stream.int_in_range(0, max(0, h - 1))
                    radius = stream.int_in_range(1, 8)
                    for i in range(20):
                        ox = (base_x + i) % w
                        oy = (base_y + i) % h
                        try:
                            grid.compute_fov((ox, oy), radius=radius)
                            _ = grid.is_in_fov(ox, oy)
                            if w > 1:
                                _ = grid.is_in_fov((ox + 1) % w, oy)
                        except EXPECTED_EXCEPTIONS:
                            pass

                elif op == 12:
                    # Toggle ~20 cells' transparency, recompute, query.
                    n_toggles = stream.int_in_range(1, 20)
                    for _ in range(n_toggles):
                        tx = stream.int_in_range(0, max(0, w - 1))
                        ty = stream.int_in_range(0, max(0, h - 1))
                        try:
                            grid.at(tx, ty).transparent = stream.bool()
                        except EXPECTED_EXCEPTIONS:
                            pass
                    ox = stream.int_in_range(0, max(0, w - 1))
                    oy = stream.int_in_range(0, max(0, h - 1))
                    grid.compute_fov((ox, oy), radius=stream.int_in_range(1, 15))
                    # Query a handful of cells, including OOB.
                    for _ in range(5):
                        qx = stream.int_in_range(-2, w + 2)
                        qy = stream.int_in_range(-2, h + 2)
                        try:
                            _ = grid.is_in_fov(qx, qy)
                        except EXPECTED_EXCEPTIONS:
                            pass
                    # More toggles, recompute, requery.
                    for _ in range(stream.int_in_range(1, 10)):
                        tx = stream.int_in_range(0, max(0, w - 1))
                        ty = stream.int_in_range(0, max(0, h - 1))
                        try:
                            grid.at(tx, ty).transparent = stream.bool()
                        except EXPECTED_EXCEPTIONS:
                            pass
                    ox = stream.int_in_range(0, max(0, w - 1))
                    oy = stream.int_in_range(0, max(0, h - 1))
                    grid.compute_fov((ox, oy), radius=stream.int_in_range(0, 25))
                    for _ in range(5):
                        qx = stream.int_in_range(-2, w + 2)
                        qy = stream.int_in_range(-2, h + 2)
                        try:
                            _ = grid.is_in_fov(qx, qy)
                        except EXPECTED_EXCEPTIONS:
                            pass

                elif op == 13:
                    # Read-only properties: fov_radius, perspective.
                    try:
                        _ = grid.fov_radius
                    except EXPECTED_EXCEPTIONS:
                        pass
                    try:
                        grid.fov_radius = stream.int_in_range(-5, 50)
                    except EXPECTED_EXCEPTIONS:
                        pass
                    try:
                        _ = grid.perspective
                    except EXPECTED_EXCEPTIONS:
                        pass

                elif op == 14:
                    # Type-confusion: pass garbage as pos / radius / algorithm.
                    bad_choice = stream.u8() % 5
                    if bad_choice == 0:
                        grid.compute_fov(None, radius=5)
                    elif bad_choice == 1:
                        grid.compute_fov("not a tuple", radius=5)
                    elif bad_choice == 2:
                        grid.compute_fov((1, 2, 3), radius=5)
                    elif bad_choice == 3:
                        x = stream.int_in_range(0, max(0, w - 1))
                        y = stream.int_in_range(0, max(0, h - 1))
                        grid.compute_fov((x, y), radius="five")
                    else:
                        x = stream.int_in_range(0, max(0, w - 1))
                        y = stream.int_in_range(0, max(0, h - 1))
                        grid.compute_fov((x, y), radius=5, algorithm="basic")

                elif op == 15:
                    # is_in_fov garbage args.
                    bad_choice = stream.u8() % 3
                    if bad_choice == 0:
                        _ = grid.is_in_fov(None, None)
                    elif bad_choice == 1:
                        _ = grid.is_in_fov("a", "b")
                    else:
                        _ = grid.is_in_fov((1, 2, 3))

                elif op == 16:
                    # Tier C (#312): ColorLayer perspective system. Build a
                    # self-contained grid + ColorLayer + Entity so the entity
                    # visibility-perspective path resolves to real objects.
                    _fuzz_perspective(stream)

                else:  # op == 17
                    # Tier C (#312): ColorLayer.draw_fov from a source cell.
                    _fuzz_draw_fov(stream)

            except EXPECTED_EXCEPTIONS:
                pass
    except EXPECTED_EXCEPTIONS:
        pass


# When invoked directly via --exec (smoke test path), run a single iteration
# against a tiny canned input so the script is self-contained.
if __name__ == "__main__":
    import sys

    safe_reset()
    fuzz_one_input(b"\x05\x05\x10\x03\x02\x02\x05\x07\x01\x01\x01\x0c\x03\x03\x05")
    print("PASS")
    sys.exit(0)
