#!/usr/bin/env python3
import sys
from collections import defaultdict
import random


def close_scaled(a, b, scale):
    """
    Adjacency test in *scaled* coordinate space (TikZ units we output).
    a, b = (x, y, diam) already scaled by `scale`.
    Two models are adjacent if centers are <= 2 inches apart + their radii.
    """
    # 1 inch = 25.4 mm; in the scaled space, millimeters were multiplied by `scale`
    dist_in = 25.4 * scale
    dx = a[0] - b[0]
    dy = a[1] - b[1]
    # 'a[2]' and 'b[2]' are diameters already scaled
    return dx * dx + dy * dy <= (2 * dist_in + a[2] / 2 + b[2] / 2) ** 2


def components(models):
    """
    Connected components over the *unscaled* millimeter coordinates, using a
    bucket grid of size 211 mm to prune pair checks.
    """
    def mm_close(u, v):
        # Same threshold logic but directly in mm (no 'scale').
        # Allowance matches original: <= (2*254 + 5*(du+dv)) with some scaling slack.
        return ((u[0] - v[0]) ** 2 + (u[1] - v[1]) ** 2) * 100 <= (2 * 254 + 5 * (u[2] + v[2])) ** 2

    GRANULARITY = 211  # mm
    buckets = defaultdict(list)

    for i, (x, y, d) in enumerate(models):
        buckets[(int(x) // GRANULARITY, int(y) // GRANULARITY)].append(i)

    edges = defaultdict(set)
    for (gx, gy), idxs in buckets.items():
        candidates = set()
        for dx in (-1, 0, 1):
            for dy in (-1, 0, 1):
                candidates.update(buckets.get((gx + dx, gy + dy), []))
        for a in idxs:
            ua = models[a]
            for b in candidates:
                if b == a:
                    continue
                if mm_close(ua, models[b]):
                    edges[a].add(b)
                    edges[b].add(a)

    comp = {}
    cid = 0
    for s in range(len(models)):
        if s in comp:
            continue
        comp[s] = cid
        q = [s]
        while q:
            nq = []
            for u in q:
                for v in edges[u]:
                    if v in comp:
                        continue
                    comp[v] = cid
                    nq.append(v)
            q = nq
        cid += 1
    return comp


def main():
    if len(sys.argv) < 2:
        print("Usage: draw_board.py INPUT.txt [--grid|--grid211] [--no-models|--no-disks|--no-visuals] [--no-fluff|--minimal]")
        sys.exit(2)

    with open(sys.argv[1], "r") as infile, sys.stdout as texfile:
        # Flags
        draw_grid = any(arg in ("--grid", "--grid211") for arg in sys.argv[2:])
        hide_models = any(arg in ("--no-models", "--no-disks", "--no-visuals") for arg in sys.argv[2:])
        hide_fluff = any(arg in ("--no-fluff", "--minimal") for arg in sys.argv[2:])

        # Read input
        N = int(infile.readline())
        text = None
        if N > 5000:
            text = f"Too many models ({N})"

        models_mm = []
        for _ in range(N):
            # each line: x y diameter   (in millimeters)
            models_mm.append(tuple(map(float, infile.readline().split())))

        # Connected components (unscaled)
        comp = components(models_mm)

        # Colour palette per component index
        colours = {}
        for i in range(len(models_mm)):
            c = comp[i]
            if c == 0:
                colours[i] = "blue"
            elif c == 1:
                colours[i] = "cyan"
            elif c == 2:
                colours[i] = "brown"
            elif c == 3:
                colours[i] = "green"
            elif c == 4:
                colours[i] = "lime"
            elif c == 5:
                colours[i] = "magenta"
            elif c == 6:
                colours[i] = "olive"
            elif c == 7:
                colours[i] = "orange"
            else:
                colours[i] = random.choice(["pink", "purple", "red", "teal", "violet", "yellow"])

        # Compute bounding box (mm) with a small margin of 25.4/2 around disks
        xs = set()
        ys = set()
        for (x, y, d) in models_mm:
            xs.add(x - d / 2 - 25.4 / 2)
            xs.add(x + d / 2 + 25.4 / 2)
            ys.add(y - d / 2 - 25.4 / 2)
            ys.add(y + d / 2 + 25.4 / 2)

        xmin, xmax = min(xs), max(xs)
        ymin, ymax = min(ys), max(ys)
        size_mm = max(xmax - xmin, ymax - ymin)

        # Scale so the figure fits roughly 5x5 cm in TikZ units (1 unit = 1 cm)
        IMAGE_SIZE_CM = 5.0
        scale = IMAGE_SIZE_CM / size_mm  # multiply mm by this to get cm in TikZ

        # Prepare scaled models for drawing
        models = []
        for (x, y, d) in models_mm:
            models.append((x * scale, y * scale, d * scale))

        # Neighbours only needed if we might draw adjacency lines
        if text is None and (not hide_models) and (not hide_fluff):
            neighbours = [[] for _ in range(N)]
            for i, a in enumerate(models):
                neighbours[i] = [b for b in models if a is not b and close_scaled(a, b, scale)]

        # Preamble
        print(
            r"""\documentclass[varwidth]{standalone}
\pdfinfoomitdate=1
\pdftrailerid{}
\pdfsuppressptexinfo=-1
\usepackage{tikz}
\usetikzlibrary{matrix, fit}
\tikzset{every node/.style={font=\sffamily}}
\begin{document}
""",
            file=texfile,
        )

        if text is not None:
            print(text, file=texfile)
            print(r"\end{document}", file=texfile)
            return

        # Begin picture
        print(r"\begin{tikzpicture}", file=texfile)

        # Optional 211x211 mm grid overlay (scaled to TikZ units)
        if draw_grid:
            step_cm = 211 * scale
            x0 = xmin * scale
            x1 = xmax * scale
            y0 = ymin * scale
            y1 = ymax * scale
            xv = x0
            # vertical lines
            while xv <= x1 + 1e-9:
                print(rf"\draw[black!50, thin] ({xv}, {y0}) -- ({xv}, {y1});")
                xv += step_cm
            # horizontal lines
            yh = y0
            while yh <= y1 + 1e-9:
                print(rf"\draw[black!50, thin] ({x0}, {yh}) -- ({x1}, {yh});")
                yh += step_cm
        else:
            # Background light grid(s) for orientation (10 mm and 100 mm in scaled units)
            print(
                rf"\draw[step={scale * 100},gray, thin] ({xmin * scale}, {ymin * scale}) grid ({xmax * scale}, {ymax * scale});",
                file=texfile,
            )
            if scale > 0.04:
                print(
                    rf"\draw[step={scale * 10},gray, very thin] ({xmin * scale}, {ymin * scale}) grid ({xmax * scale}, {ymax * scale});",
                    file=texfile,
                )

        if not hide_models:
            # --- Optional fluff (extra radii + adjacency lines) ---
            if not hide_fluff:
                # 2-inch adjacency aura around each model
                for i, (x, y, diam) in enumerate(models):
                    col = colours[i]
                    print(
                        rf"\draw[fill={col}!10, semitransparent, draw={col}!20] "
                        rf"({x}, {y}) circle ({diam / 2 + 25.4 * scale});"
                    )
                # adjacency lines
                for i, a in enumerate(models):
                    for b in neighbours[i]:
                        print(rf"\draw [blue] ({a[0]}, {a[1]})--({b[0]}, {b[1]});")

            # --- Always draw the disks themselves ---
            for i, (x, y, diam) in enumerate(models):
                # If neighbours are not available (because fluff was hidden), default to 'good'
                if "neighbours" in locals():
                    good = len(neighbours[i]) >= (1 + int(N > 6))
                else:
                    good = True
                colour = "black!20" if good else "red!50"
                print(rf"\draw[fill={colour}, draw] ({x}, {y}) circle ({diam / 2});")

        print(r"\end{tikzpicture}", file=texfile)
        print(r"\end{document}", file=texfile)


if __name__ == "__main__":
    main()
