"""
Functions related to line of sight calculations
"""
from clubsandwich.geom import Point

MULT = [
    [1,  0,  0, -1, -1,  0,  0,  1],
    [0,  1, -1,  0,  0, -1,  1,  0],
    [0,  1,  1,  0,  0, -1, -1,  0],
    [1,  0,  0,  1, -1,  0,  0, -1],
]


def get_visible_points(vantage_point, get_allows_light, max_distance=30):
    """
    :param clubsandwich.geom.Point vantage_point:
    :param function get_allows_light: ``get_allows_light(point) -> bool``
    :param int max_distance:

    Returns a set of all points visible from the given vantage point.

    Adapted from `this RogueBasin article <http://www.roguebasin.com/index.php?title=Python_shadowcasting_implementation>`_.
    """
    los_cache = set()
    los_cache.add(vantage_point)
    for region in range(8):
        _cast_light(
            los_cache, get_allows_light,
            vantage_point.x, vantage_point.y, 1, 1.0, 0.0, max_distance,
            MULT[0][region], MULT[1][region],
            MULT[2][region], MULT[3][region])
    return los_cache


def _cast_light(los_cache, get_allows_light, cx, cy, row, start, end, radius, xx, xy, yx, yy):
    if start < end:
        return

    radius_squared = radius * radius

    for j in range(row, radius + 1):
        dx, dy = -j - 1, -j
        blocked = False
        while dx <= 0:
            dx += 1
            # Translate the dx, dy coordinates into map coordinates:
            X, Y = cx + dx * xx + dy * xy, cy + dx * yx + dy * yy
            point = Point(X, Y)
            # l_slope and r_slope store the slopes of the left and right
            # extremities of the square we're considering:
            l_slope, r_slope = (dx - 0.5) / (dy + 0.5), (dx + 0.5) / (dy - 0.5)
            if start < r_slope:
                continue
            elif end > l_slope:
                break
            else:
                # Our light beam is touching this square; light it:
                if dx * dx + dy * dy < radius_squared:
                    los_cache.add(point)
                if blocked:
                    # we're scanning a row of blocked squares:
                    if not get_allows_light(point):
                        new_start = r_slope
                        continue
                    else:
                        blocked = False
                        start = new_start
                else:
                    if not get_allows_light(point) and j < radius:
                        # This is a blocking square, start a child scan:
                        blocked = True
                        _cast_light(
                            los_cache, get_allows_light,
                            cx, cy, j + 1, start, l_slope,
                            radius, xx, xy, yx, yy)
                        new_start = r_slope
        # Row is scanned; do next row unless last square was blocked:
        if blocked:
            break
