# %% [markdown]
# # topologize — getting started
#
# `topologize` takes a set of polylines, inflates them by a `buffer_distance`,
# and returns the medial axis as maximal non-branching chains.
#
# **Typical use case:** clean up hand-drawn or over-sampled strokes into a
# single-pixel-wide centerline graph.

# %%
import sys
from pathlib import Path

import numpy as np
import matplotlib.pyplot as plt

sys.path.insert(0, str(Path(__file__).parent.parent))
from topologize import topologize

# %% [markdown]
# ## Helper: plot curves and chains

# %%
COLORS = [
    "#e41a1c", "#377eb8", "#4daf4a", "#ff7f00",
    "#984ea3", "#a65628", "#f781bf", "#333333",
]

def plot(curves, chains, buffer_distance, title=""):
    fig, ax = plt.subplots(figsize=(8, 6))
    ax.set_aspect("equal")
    ax.set_title(title or f"buffer_distance={buffer_distance}")

    for c in curves:
        ax.plot(c[:, 0], c[:, 1], color="lightgray", lw=6, solid_capstyle="round")

    for i, chain in enumerate(chains):
        ax.plot(chain[:, 0], chain[:, 1], color=COLORS[i % len(COLORS)], lw=2)

    plt.tight_layout()
    plt.show()

# %% [markdown]
# ## Example 1: a few simple polylines
#
# Four curves — two open polylines, a circle, and a star — placed close enough
# that their inflated buffers overlap and merge.

# %%
# open polyline
p1 = np.array([[0.0, 2.0], [10.0, 0.0], [5.0, 0.1], [5.0, 5.0]])

# open polyline resampled to 24 points
import shapely  # noqa: E402
_p2 = shapely.LineString([[0, 6], [1, 4], [2, 0]])
p2 = np.array(
    shapely.LineString(
        _p2.interpolate(np.linspace(0, 1, 24), normalized=True)
    ).coords
)

# circle (closed ring)
_cc, _cr = np.array([13.0, 1.0]), np.linalg.norm(np.array([13.0, 1.0]) - [10.0, 0.0])
_a = np.linspace(0, 2 * np.pi, 64, endpoint=False)
_circle = _cc + _cr * np.column_stack([np.cos(_a), np.sin(_a)])
circle = np.vstack([_circle, _circle[:1]])

# 5-point star (closed ring)
_sc = np.array([5.0, -5.0])
_sa = np.linspace(np.pi / 2, np.pi / 2 + 2 * np.pi, 10, endpoint=False)
_sr = np.tile([8.0, 1.8], 5)
_star = _sc + np.column_stack([_sr * np.cos(_sa), _sr * np.sin(_sa)])
star = np.vstack([_star, _star[:1]])

curves = [p1, p2, circle, star]

# %%
buffer_distance = 0.6
result = topologize(curves, buffer_distance)
chains = result.chains
print(f"{len(chains)} chains, {sum(len(c) for c in chains)} total points")
plot(curves, chains, buffer_distance, title="Simple shapes")

# %% [markdown]
# ## Example 2: effect of buffer_distance
#
# A larger `buffer_distance` merges nearby strokes more aggressively,
# producing fewer but smoother chains.

# %%
fig, axes = plt.subplots(1, 3, figsize=(15, 5))
for ax, bd in zip(axes, [0.3, 0.6, 1.2]):
    ch = topologize(curves, bd).chains
    ax.set_aspect("equal")
    ax.set_title(f"buffer_distance={bd}  ({len(ch)} chains)")
    for c in curves:
        ax.plot(c[:, 0], c[:, 1], color="lightgray", lw=6, solid_capstyle="round")
    for i, chain in enumerate(ch):
        ax.plot(chain[:, 0], chain[:, 1], color=COLORS[i % len(COLORS)], lw=2)
plt.tight_layout()
plt.show()

# %% [markdown]
# ## Example 3: parallel lines merging into one
#
# Two nearly-parallel lines become a single centerline when the buffer
# is large enough to bridge the gap between them.

# %%
line_a = np.column_stack([np.linspace(0, 10, 50), np.zeros(50)])
line_b = np.column_stack([np.linspace(0, 10, 50), np.ones(50) * 0.8])

fig, axes = plt.subplots(1, 2, figsize=(12, 4))
for ax, bd in zip(axes, [0.3, 0.6]):
    ch = topologize([line_a, line_b], bd).chains
    ax.set_aspect("equal")
    ax.set_title(f"buffer_distance={bd}  ({len(ch)} chain{'s' if len(ch) != 1 else ''})")
    for c in [line_a, line_b]:
        ax.plot(c[:, 0], c[:, 1], color="lightgray", lw=10, solid_capstyle="round")
    for i, chain in enumerate(ch):
        ax.plot(chain[:, 0], chain[:, 1], color=COLORS[i % len(COLORS)], lw=2.5)
plt.tight_layout()
plt.show()

# %% [markdown]
# ## Example 4: inspecting chain geometry
#
# Each chain is a plain `(N, 2)` numpy array — easy to work with downstream.

# %%
chains = topologize(curves, buffer_distance=0.6).chains

for i, chain in enumerate(chains):
    length = np.sum(np.linalg.norm(np.diff(chain, axis=0), axis=1))
    print(f"chain {i:2d}:  {len(chain):4d} pts,  length={length:.3f}")
