"""A brush-width-aware Eiffel Tower and Seine stroke plan.

Coordinates are normalized canvas coordinates: u=0 is the left edge and v=0
is the top edge. These are desired strokes, not observations or a claim that
the simulator has painted them. The default plan has 19 strokes.
"""
from __future__ import annotations

import math
from pathlib import Path


COLORS = {
    "brown": (112, 73, 47),
    "blue": (35, 112, 201),
    "green": (39, 148, 93),
    "orange": (240, 140, 48),
}

# New versions should be added after inspecting an actual camera result.
# Version 1 is a credible initial design, not an intentionally weak baseline.
VERSIONS = {
    1: {"tower_width": .40, "crossbrace": True, "detail": 1, "river_spacing": .044},
}


def _linspace(start, stop, count):
    return [start + (stop-start)*i/(count-1) for i in range(count)]


def _quadratic(start, control, end, count=13):
    return [[(1-t)**2*start[axis] + 2*(1-t)*t*control[axis] + t*t*end[axis]
             for axis in (0, 1)] for t in _linspace(0, 1, count)]


def design(version=1, *, tower_width=None, crossbrace=None, detail=None, river_spacing=None):
    """Return [{color, label, points:[[u,v], ...]}, ...] for physical strokes.

    ``tower_width`` is the normalized width between the outer tower feet
    (default .40). ``crossbrace`` enables the large middle X. ``detail`` is
    0 for that X only, or 1 to include a second, smaller upper X. A larger
    ``river_spacing`` separates the three blue curves. All overrides are
    explicit; the function does not infer improvements from unseen results.

    Use one continuous contact for each list of points. Lift between strokes.
    Reload brown paint as needed during the tower; the planner does not read
    paint load or any other simulator state.
    """
    if version not in VERSIONS:
        raise ValueError(f"Unknown design version {version!r}; available: {tuple(VERSIONS)}")
    options = dict(VERSIONS[version])
    for name, value in (("tower_width", tower_width), ("crossbrace", crossbrace),
                        ("detail", detail), ("river_spacing", river_spacing)):
        if value is not None:
            options[name] = value
    width = float(options["tower_width"])
    spacing = float(options["river_spacing"])
    if not .34 <= width <= .50:
        raise ValueError("tower_width must be between .34 and .50")
    if not .034 <= spacing <= .057:
        raise ValueError("river_spacing must be between .034 and .057")
    if options["detail"] not in (0, 1):
        raise ValueError("detail must be 0 or 1")

    strokes = []

    def stroke(color, label, points):
        points = [[round(float(u), 6), round(float(v), 6)] for u, v in points]
        if len(points) < 2 or any(not (0 <= u <= 1 and 0 <= v <= 1) for u, v in points):
            raise ValueError(f"Invalid stroke {label}")
        strokes.append({"color": color, "label": label, "points": points})

    def tower(label, points):
        # Construction coordinates use an outer-foot span of .43.
        stroke("brown", label, [[.5 + (u-.5)*width/.43, v] for u, v in points])

    # Curved, flaring legs and a short antenna are the primary silhouette.
    outline = [[.5,.205], [.478,.305], [.450,.404], [.407,.510], [.350,.604], [.285,.690]]
    tower("tower-left-outline", list(reversed(outline)))
    tower("tower-right-outline", [[1-u,v] for u,v in outline])
    tower("tower-antenna", [[.5,.120],[.5,.217]])
    # The observation decks extend past the sloping legs, like the real tower.
    tower("tower-lower-deck", [[.351,.550],[.649,.550]])
    tower("tower-middle-deck", [[.429,.415],[.571,.415]])
    tower("tower-upper-deck", [[.462,.285],[.538,.285]])
    # Leave a large white opening below the lower deck. The broad arch reads
    # clearly with a 3.6-5.5mm brush; small lattice details would merge here.
    arch = _quadratic([.365,.690],[.393,.593],[.5,.590])
    arch += _quadratic([.5,.590],[.607,.593],[.635,.690])[1:]
    tower("tower-base-arch", arch)
    tower("tower-left-foot", [[.268,.695],[.394,.695]])
    tower("tower-right-foot", [[.606,.695],[.732,.695]])
    if options["crossbrace"]:
        tower("tower-middle-brace-rising", [[.401,.529],[.557,.434]])
        tower("tower-middle-brace-falling", [[.443,.434],[.599,.529]])
        if options["detail"]:
            tower("tower-upper-brace-rising", [[.451,.394],[.526,.319]])
            tower("tower-upper-brace-falling", [[.474,.319],[.549,.394]])

    # A single spiral fills the sun by overlapping real brush passes.
    sun = []
    for t in _linspace(0, 1, 68):
        angle = t*math.tau*2.4
        radius = .060*t
        sun.append([.790 + radius*math.cos(angle), .218 + .9*radius*math.sin(angle)])
    stroke("orange", "sun", sun)

    # Two disconnected banks keep the tower base open and uncluttered.
    stroke("green", "left-river-bank", _quadratic([.100,.754],[.225,.710],[.352,.748]))
    stroke("green", "right-river-bank", _quadratic([.654,.748],[.770,.714],[.900,.750]))
    # Three separated, gently curving blue strokes suggest a broad river and
    # reflected light. The lowest remains safely inside the paper margin.
    for index in range(3):
        river = [[u, .795 + index*spacing + .020*math.sin((u-.10)/.80*math.tau-.5)]
                 for u in _linspace(.105,.895,31)]
        stroke("blue", f"seine-curve-{index+1}", river)
    return strokes


def estimated_seconds(strokes=None, *, speed=.035, reloads=6):
    """Planning estimate for the current controller; not measured runtime.

    Uses a 135x150mm canvas, 2.45s approach/touch/lift per stroke, a minimum
    .8s contact, and 3.8s per palette visit. It excludes calibration and video
    holds. Six visits allow three brown reloads plus orange, green, and blue.
    """
    if strokes is None:
        strokes = design()
    seconds = 3.8*reloads
    for item in strokes:
        length = sum(math.hypot((b[0]-a[0])*.135, (b[1]-a[1])*.150)
                     for a,b in zip(item["points"],item["points"][1:]))
        seconds += 2.45 + max(.8,length/speed)
    return seconds


def render_target(path, version=1, *, size=800, brush_width_mm=4.5, **overrides):
    """Save an explicitly labeled reference PNG, not simulated brush output.

    The drawing panel preserves the real canvas's 135:150 aspect ratio.
    The solid lines indicate intended paths and nominal width; actual paint
    appearance depends on physical contact, pigment load, and camera lighting.
    """
    from PIL import Image, ImageDraw, ImageFont

    strokes = design(version, **overrides)
    height, width = int(size), round(size*.135/.150)
    header, footer = 68, 42
    image = Image.new("RGB", (width,height+header+footer), (241,239,233))
    draw = ImageDraw.Draw(image)
    title_font = ImageFont.load_default(size=max(14,round(size*.024)))
    note_font = ImageFont.load_default(size=max(11,round(size*.016)))
    draw.text((18,12), "EIFFEL TOWER + SEINE", fill=(55,51,44), font=title_font)
    draw.text((18,40), f"TARGET PLAN v{version} / REFERENCE ONLY", fill=(97,90,77), font=note_font)
    draw.rectangle((0,header,width-1,header+height-1), fill=(255,253,247))
    line_width = max(1,round(brush_width_mm/150*height))
    for item in strokes:
        points = [(round(u*(width-1)), header+round(v*(height-1))) for u,v in item["points"]]
        color = COLORS[item["color"]]
        draw.line(points, fill=color, width=line_width, joint="curve")
        radius = line_width/2
        for x,y in (points[0],points[-1]):
            draw.ellipse((x-radius,y-radius,x+radius,y+radius), fill=color)
    draw.text((18,header+height+12), "Desired strokes - not a simulator result or camera image.",
              fill=(97,90,77), font=note_font)
    destination = Path(path)
    destination.parent.mkdir(parents=True, exist_ok=True)
    image.save(destination)
    return destination


if __name__ == "__main__":
    import argparse
    import json

    parser = argparse.ArgumentParser(description=__doc__)
    parser.add_argument("--render", type=Path, help="Write a labeled target-plan PNG.")
    args = parser.parse_args()
    if args.render:
        print(render_target(args.render))
    print(json.dumps({"strokes":len(design()), "estimated_sim_seconds":round(estimated_seconds(),1)}))
