"""A component publishing ports with an attached route profile.

The trick that makes a well-curated PDK feel effortless: every
component publishes ports that already know their cross-section. End
users call ``c.Route(a.ports["o1"], b.ports["i1"])`` with no kwargs and
the route automatically stamps the right layers, claddings, and
periodic features.
"""
import lumicron as lm
import lumicron_pdk as lpdk

LAYER = lpdk.LayerTable(
    SILC = lpdk.Layer(9, 0, color="#008000", min_width=0.2, min_radius=5.0),
    OXID = lpdk.Layer(1, 0, color="#87ceeb"),
)


@lpdk.route_profile(port_width=0.5, radius=10.0, radius_min=5.0)
def silc_strip(p):
    p.layer(LAYER.SILC, width=p.port_width)
    p.layer(LAYER.OXID, width=p.port_width + 4.0)


@lm.pcell
def EdgeCouplerSi(length: float = 100.0, tip_width: float = 0.1):
    """A linear taper from a 0.1 µm tip to a 0.5 µm body, with an
    OXID cladding region around it. The output port carries
    `silc_strip` so any route off it inherits the profile."""
    c = lm.CELL("EdgeCouplerSi")

    # The taper polygon: tip → body, 4 vertices.
    body_w = 0.5
    taper = lm.Polygon(
        vertices=[
            (0, -tip_width / 2),
            (length, -body_w / 2),
            (length, body_w / 2),
            (0, tip_width / 2),
        ],
        layer=LAYER.SILC,
    )
    c.add(taper)
    c.Place(taper).at((0, 0))

    # OXID cladding rectangle hugging the taper.
    clad = lm.Rectangle(x_dim=length, y_dim=body_w + 4, layer=LAYER.OXID)
    h = c.add(clad)
    c.Place(h).using("SW").at((0, -(body_w + 4) / 2))

    # Tip port (incoming from chip edge) and body port (publishes profile).
    c.add(lm.PORT("o_tip", position=(0, 0), direction=180,
                  width=tip_width, layer=LAYER.SILC))
    c.add(lm.PORT("o1", position=(length, 0), direction=0,
                  width=body_w, route_profile=silc_strip))
    return c


@lm.pcell
def CouplerDemo():
    c = lm.CELL("CouplerDemo")
    cpl = c.add(EdgeCouplerSi(length=100))
    c.Place(cpl).at((0, 0))

    # Publish a destination port on the chip and route from coupler.o1.
    # No `radius=` / `profile=` — both come from the port's profile.
    c.add(lm.PORT("out", position=(500, 100), direction=180,
                  route_profile=silc_strip))
    c.Route(cpl.ports["o1"], c.ports["out"])
    return c


if __name__ == "__main__":
    CouplerDemo().to_gds("coupler_demo.gds")
