#!/usr/bin/env python3
"""
Grows a small tree from a name: a seed, roots, branches, a little pruning, then fruit.
One name, one tree: the same name always grows the same tree.
Only the Python standard library. No network, no files, nothing to install.
Do run it in a terminal and watch it grow:  python3 seed.py "Your Name"
See it grow smoothly in a browser:  python3 seed.py --html "Your Name" > seed.html
Or, with no terminal at all, it prints the grown tree.
For Bringforth, that is the whole story, told without words.
Take it, change it, grow your own: it is MIT licensed, below.
Written by Bringforth, a small software company in Sydney.
Agents do much of our building; people answer for every part that ships.
Read the first letter of each line here, top to bottom.
Every tree starts the same way, with one seed.
Some branches get cut along the way. That is part of it.
The pruning is the point: deleting code is progress too.
A tree is done when what is left is small, and it works.
Run it with no name and it grows one for you.
Try your company's name, then a friend's.
Source: https://bringforth.com.au/seed.py
Say hello: hello@bringforth.com.au
MIT License. Copyright (c) 2026 Xpansa Pty Ltd, trading as Bringforth.
Any use is fine; the software comes as is, without warranty of any kind.
Licence text: https://opensource.org/license/mit
Long story short: good software starts small.
"""

import hashlib
import html
import json
import math
import os
import random
import sys
import time

DEPTH = 5  # forks from the trunk out to the twigs
SPEED = 1.1  # seconds a branch takes to grow one unit; the trunk is about one unit tall
FPS = 20  # frames a second in a terminal
W, H = 61, 24  # a terminal's plot, in characters
GROUND = H - 5  # the row the ground runs along; roots grow below it
COLOURS = {
    "wood": "#d6d3cd",
    "root": "#6f6a63",
    "ground": "#3d3934",
    "cut": "#6f6a63",
    "leaf": "#8cbe82",
    "fruit": "#c3b0fb",
    "seed": "#c3b0fb",
}
SIGN = "Good software starts small."


def rng_for(name):
    digest = hashlib.sha256(name.strip().lower().encode("utf-8")).hexdigest()
    return random.Random(int(digest[:16], 16))


def curve(b, s):
    """The point s of the way along a branch's curve, and the way it is heading there."""
    u = 1 - s
    x = u * u * b["x0"] + 2 * u * s * b["cx"] + s * s * b["x1"]
    y = u * u * b["y0"] + 2 * u * s * b["cy"] + s * s * b["y1"]
    dx = 2 * u * (b["cx"] - b["x0"]) + 2 * s * (b["x1"] - b["cx"])
    dy = 2 * u * (b["cy"] - b["y0"]) + 2 * s * (b["y1"] - b["cy"])
    return x, y, dx, dy


def grow(rng):
    """The tree in its own units: the ground is y = 0. Branches and roots are gentle curves, w0 to w1
    thick, growing from t0 to t1 seconds in; leaves and fruit open at t. A few middling branches grow
    in full, then get cut at `prune`."""
    branches, roots, leaves, fruit = [], [], [], []

    def limb(into, x, y, angle, length, width, t, parent):
        bend = (rng.random() - 0.5) * 0.6 * length
        x1, y1 = x + math.sin(angle) * length, y + math.cos(angle) * length
        into.append({
            "x0": x, "y0": y, "x1": x1, "y1": y1,
            "cx": (x + x1) / 2 + math.cos(angle) * bend, "cy": (y + y1) / 2 - math.sin(angle) * bend,
            "w0": width, "w1": width * 0.7, "t0": t, "t1": t + length * SPEED, "parent": parent, "cut": False,
        })
        return into[-1]

    def branch(x, y, angle, length, width, t, depth, parent):
        b = limb(branches, x, y, angle, length, width, t, parent)
        b["depth"] = depth
        me = len(branches) - 1
        if depth == DEPTH:
            return
        forks = 2 + (rng.random() < 0.3)
        spread = 0.4 + rng.random() * 0.3
        for i in range(forks):
            side = spread * (2 * i / (forks - 1) - 1)
            turn = 0.6 * angle + side + (rng.random() - 0.5) * 0.3  # forks lean out, then back toward the light
            branch(b["x1"], b["y1"], turn, length * (0.68 + rng.random() * 0.14), width * 0.7, b["t1"], depth + 1, me)

    branch(0, 0, (rng.random() - 0.5) * 0.2, 0.9, 0.08, 1.0, 0, None)
    for _ in range(3 + (rng.random() < 0.5)):
        limb(roots, 0, 0, math.pi + (rng.random() - 0.5) * 2.2, 0.2 + rng.random() * 0.2, 0.03, 0.8, None)

    # One to three of the middling branches get cut; the big limbs stay.
    middling = [b for b in branches if b["depth"] in (2, 3)]
    for _ in range(1 + (rng.random() < 0.5) + (rng.random() < 0.25)):
        if middling:
            middling.pop(int(rng.random() * len(middling)))["cut"] = True
    for b in branches:  # a cut branch takes everything beyond it with it
        if b["parent"] is not None and branches[b["parent"]]["cut"]:
            b["cut"] = True

    prune = max(b["t1"] for b in branches) + 0.6
    bloom = prune + 1.2
    parents = {b["parent"] for b in branches if not b["cut"]}
    tips = [b for i, b in enumerate(branches) if not b["cut"] and i not in parents]
    for b in tips:
        for _ in range(3 + int(rng.random() * 4)):
            leaves.append({
                "x": b["x1"] + (rng.random() - 0.5) * 0.16, "y": b["y1"] + (rng.random() - 0.4) * 0.14,
                "r": 0.025 + rng.random() * 0.025, "a": rng.random() * math.pi, "t": bloom + rng.random() * 1.4,
            })
    for b in [b for b in tips if rng.random() < 0.35] or tips[:1]:
        fruit.append({"x": b["x1"] + (rng.random() - 0.5) * 0.06, "y": b["y1"] - 0.04, "r": 0.03,
                      "t": bloom + 1.4 + rng.random() * 0.8})
    return {"branches": branches, "roots": roots, "leaves": leaves, "fruit": fruit, "prune": prune, "end": bloom + 2.6}


def scale(tree):
    """Columns and rows per unit that fit the grown tree on the plot; a character is about twice as tall as wide."""
    parts = tree["branches"] + tree["roots"]
    top = max(max(b["y0"], b["y1"], b["cy"]) for b in parts) + 0.1
    reach = max(max(abs(b["x0"]), abs(b["x1"]), abs(b["cx"])) for b in parts) + 0.1
    rows = (GROUND - 1) / top
    cols = min(2 * rows, (W // 2 - 1) / reach)
    return cols, cols / 2


def plot(tree, now, cols, rows):
    """The tree as it stands `now` seconds in, as rows of (char, kind)."""
    grid = [[(" ", None)] * W for _ in range(H)]
    grid[GROUND] = [("~", "ground")] * W

    def put(x, y, ch, kind):
        col, row = round(W // 2 + x * cols), round(GROUND - y * rows)
        if 0 <= col < W and 0 <= row < H:
            grid[row][col] = (ch, kind)

    def stroke(b, kind, mark=None):
        p = min(1.0, (now - b["t0"]) / (b["t1"] - b["t0"]))
        if p <= 0:
            return
        n = int(math.hypot((b["x1"] - b["x0"]) * cols, (b["y1"] - b["y0"]) * rows) * 2 * p) + 1
        for k in range(n + 1):
            x, y, dx, dy = curve(b, p * k / n)
            gx, gy = (dx * cols, dy * rows) if dy >= 0 else (-dx * cols, -dy * rows)
            a = math.atan2(gx, gy)
            ch = "|" if abs(a) < 0.45 else "_" if abs(a) > 1.2 else "/" if a > 0 else "\\"
            put(x, y, mark or ch, kind)

    put(0, -1 / rows, "o", "seed")
    for b in tree["roots"]:
        stroke(b, "root")
    for b in tree["branches"]:
        if b["cut"] and now >= tree["prune"]:
            if now < tree["prune"] + 0.9:
                stroke(b, "cut", ".")
        elif b["cut"]:
            stroke(b, "wood")
    for b in tree["branches"]:
        if not b["cut"]:
            stroke(b, "wood")
    for leaf in tree["leaves"]:
        if now >= leaf["t"]:
            put(leaf["x"], leaf["y"], "*", "leaf")
    for f in tree["fruit"]:
        if now >= f["t"]:
            put(f["x"], f["y"], "o", "fruit")
    return grid


def paint(grid, colour):
    def cell(ch, kind):
        if not (colour and kind and ch != " "):
            return ch
        r, g, b = (int(COLOURS[kind][i : i + 2], 16) for i in (1, 3, 5))
        return f"\x1b[38;2;{r};{g};{b}m{ch}\x1b[0m"

    return ["  " + "".join(cell(ch, kind) for ch, kind in row) for row in grid]


def play(tree, colour):
    """Grows the tree in place in a terminal."""
    cols, rows = scale(tree)
    out = sys.stdout
    out.write("\x1b[?25l" + "\n" * H)
    try:
        for frame in range(int(tree["end"] * FPS) + 1):
            lines = paint(plot(tree, frame / FPS, cols, rows), colour)
            out.write(f"\x1b[{H}F" + "".join("\x1b[2K" + line + "\n" for line in lines))
            out.flush()
            time.sleep(1 / FPS)
    finally:
        out.write("\x1b[?25h")
        out.flush()


PAGE = r"""<!doctype html>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>__TITLE__</title>
<style>
  html, body { margin: 0; height: 100%; overflow: hidden; background: #0a0a0a; cursor: pointer; }
  canvas { display: block; width: 100vw; height: 100vh; }
  p { position: fixed; left: 0; right: 0; bottom: 4vh; margin: 0; text-align: center; color: #8a857e;
    font: 14px/1.6 ui-sans-serif, system-ui, sans-serif; opacity: 0; transition: opacity 1.2s; }
</style>
<canvas title="Click to grow it again"></canvas>
<p>__SIGN__</p>
<script>
  const T = __DATA__;
  const canvas = document.querySelector("canvas");
  const c = canvas.getContext("2d");
  const sign = document.querySelector("p");
  const clamp = (v) => Math.min(1, Math.max(0, v));
  const pop = (v) => 1 + 2.7 * (v - 1) ** 3 + 1.7 * (v - 1) ** 2; // overshoots a little, then settles
  const parts = [...T.branches, ...T.roots];
  const xs = parts.flatMap((b) => [b.x0, b.x1, b.cx]).concat(T.leaves.map((l) => l.x));
  const ys = parts.flatMap((b) => [b.y0, b.y1, b.cy]).concat(T.leaves.map((l) => l.y));
  const box = { l: Math.min(...xs), r: Math.max(...xs), b: Math.min(...ys), t: Math.max(...ys) };
  let w, h, dpr, unit, ox, oy;
  function fit() {
    dpr = devicePixelRatio || 1;
    w = canvas.width = innerWidth * dpr;
    h = canvas.height = innerHeight * dpr;
    unit = Math.min((w * 0.8) / (box.r - box.l), (h * 0.78) / (box.t - box.b));
    ox = w / 2 - ((box.l + box.r) / 2) * unit;
    oy = h * 0.47 + ((box.b + box.t) / 2) * unit;
  }
  const X = (x) => ox + x * unit;
  const Y = (y) => oy - y * unit;
  function at(b, s) {
    const u = 1 - s;
    return [u * u * b.x0 + 2 * u * s * b.cx + s * s * b.x1, u * u * b.y0 + 2 * u * s * b.cy + s * s * b.y1];
  }
  function stroke(b, p, colour, alpha, drop) {
    c.strokeStyle = colour;
    c.globalAlpha = alpha;
    let [px, py] = at(b, 0);
    for (let k = 1; k <= 16; k++) {
      const s = (p * k) / 16;
      const [x, y] = at(b, s);
      c.lineWidth = Math.max(dpr, (b.w0 + (b.w1 - b.w0) * s) * unit);
      c.beginPath();
      c.moveTo(X(px), Y(py) + drop);
      c.lineTo(X(x), Y(y) + drop);
      c.stroke();
      [px, py] = [x, y];
    }
  }
  function glow(x, y, r, colour, blur) {
    c.globalAlpha = 1;
    c.fillStyle = c.shadowColor = colour;
    c.shadowBlur = blur * dpr;
    c.beginPath();
    c.arc(X(x), Y(y), r, 0, Math.PI * 2);
    c.fill();
    c.shadowBlur = 0;
  }
  let start = performance.now();
  function frame(now) {
    const t = (now - start) / 1000;
    c.clearRect(0, 0, w, h);
    c.lineCap = "round";
    const ground = c.createLinearGradient(0, 0, w, 0);
    ground.addColorStop(0, "transparent");
    ground.addColorStop(0.5, T.colours.ground);
    ground.addColorStop(1, "transparent");
    c.globalAlpha = clamp(t / 0.6);
    c.fillStyle = ground;
    c.fillRect(0, Y(0), w, dpr);
    const seed = clamp(t / 0.6) * (1 - 0.6 * clamp((t - 1) / 1.5));
    if (seed > 0) glow(0, -0.05, (0.028 + 0.004 * Math.sin(t * 5)) * unit * seed, T.colours.seed, 18);
    for (const r of T.roots) stroke(r, clamp((t - r.t0) / (r.t1 - r.t0)), T.colours.root, 1, 0);
    for (const b of T.branches) {
      const p = clamp((t - b.t0) / (b.t1 - b.t0));
      if (p <= 0) continue;
      if (b.cut && t > T.prune) {
        const k = clamp((t - T.prune) / 0.9);
        if (k < 1) stroke(b, p, T.colours.cut, 1 - k, k * k * 0.3 * unit);
      } else stroke(b, p, T.colours.wood, 1, 0);
    }
    c.fillStyle = T.colours.leaf;
    for (const l of T.leaves) {
      const k = clamp((t - l.t) / 0.5);
      if (k <= 0) continue;
      const r = l.r * unit * pop(k);
      c.globalAlpha = 0.85 * k;
      c.beginPath();
      c.ellipse(X(l.x), Y(l.y), r, r * 0.55, l.a + Math.sin(t * 1.3 + l.x * 5) * 0.08, 0, Math.PI * 2);
      c.fill();
    }
    for (const f of T.fruit) {
      const k = clamp((t - f.t) / 0.4);
      if (k > 0) glow(f.x, f.y, f.r * unit * pop(k), T.colours.fruit, 14);
    }
    sign.style.opacity = t > T.end ? 1 : 0;
    requestAnimationFrame(frame);
  }
  addEventListener("resize", fit);
  addEventListener("click", () => (start = performance.now()));
  fit();
  requestAnimationFrame(frame);
</script>
"""


def page(tree, name):
    def tidy(part):
        return {k: round(v, 3) if isinstance(v, float) else v for k, v in part.items() if k not in ("parent", "depth")}

    data = {key: [tidy(p) for p in tree[key]] for key in ("branches", "roots", "leaves", "fruit")}
    data.update(prune=round(tree["prune"], 3), end=round(tree["end"], 3), colours=COLOURS)
    title = f"A tree grown from {name}" if name else "A tree grown for you"
    sign = (f"Grown from \u201c{html.escape(name)}\u201d. " if name else "") + SIGN
    return (PAGE.replace("__TITLE__", html.escape(title)).replace("__SIGN__", sign)
            .replace("__DATA__", json.dumps(data, separators=(",", ":")).replace("<", "\\u003c")))


def main():
    args = sys.argv[1:]
    if "-h" in args or "--help" in args:
        print(__doc__.strip())
        return
    name = " ".join(a for a in args if a != "--html").strip()
    tree = grow(rng_for(name or "you"))
    if "--html" in args:
        print(page(tree, name))
        return
    if sys.stdout.isatty():
        play(tree, colour="NO_COLOR" not in os.environ)
    else:
        rows = [row.rstrip() for row in paint(plot(tree, tree["end"], *scale(tree)), colour=False)]
        while rows and not rows[0]:
            rows.pop(0)
        while rows and not rows[-1]:
            rows.pop()
        print("\n".join(rows))
    print("\n  " + SIGN + "  bringforth.com.au")


if __name__ == "__main__":
    main()
