import math
from collections import defaultdict
from typing import TextIO, Tuple

import numpy  # type: ignore


def read_asteroids(data: TextIO) -> Tuple[numpy.array, numpy.array]:
    xs = []
    ys = []
    for y, line in enumerate(data):
        for x, c in enumerate(line):
            if c == '#':
                xs.append(x)
                ys.append(y)

    return numpy.array(xs), -numpy.array(ys)


def asteroids_visible(x: int, y: int, xs: numpy.array, ys: numpy.array) -> int:
    dx = xs - x
    dy = ys - y

    div = numpy.abs(numpy.gcd(dx, dy))

    mask = div != 0

    dx[mask] //= div[mask]
    dy[mask] //= div[mask]

    unique = set(zip(dx, dy))

    return len(unique) - 1  # need to ignore the point itself


def part1(data: TextIO) -> int:
    xs, ys = read_asteroids(data)

    return max(asteroids_visible(x, y, xs, ys) for x, y in zip(xs, ys))


def part2(data: TextIO) -> int:
    xs, ys = read_asteroids(data)

    cx, cy = max(zip(xs, ys), key=lambda c: asteroids_visible(c[0], c[1], xs, ys))

    dx = xs - cx
    dy = ys - cy

    angles = numpy.arctan2(dy, dx)
    distances = numpy.abs(dx) + numpy.abs(dy)

    to_shoot = defaultdict(list)

    for angle, distance, dx, dy in zip(angles, distances, dx, dy):
        if distance == 0:
            # The point itself
            continue

        to_shoot[angle].append((distance, dx, dy))

    for distances in to_shoot.values():
        distances.sort(reverse=True)

    unique_angles = sorted(set(angles), reverse=True)

    shot = 0

    # First shoot from up clockwise
    for angle in unique_angles:
        if angle > math.pi / 2:
            continue

        shot += 1

        _, dx, dy = to_shoot[angle].pop()

    # Repeatedly shoot until you reach #200
    while True:
        for angle in unique_angles:
            if not to_shoot[angle]:
                # Nothing left to shoot
                continue

            shot += 1

            _, dx, dy = to_shoot[angle].pop()

            if shot == 200:
                x = cx + dx
                y = -(cy + dy)
                return 100 * x + y

