#!/usr/bin/env python3
"""
Generate a parametric eyeliner strip mask for `region: 'eyeLidUpper'` or
'eyeLidLower', for when there is no art to bake and you just want a clean stroke
to work with.

Output is a MASK: the stroke goes in RGB (the shader reads alphaMap on the GREEN
channel) and the image itself is opaque, so the layer's `color` drives the tint.
Use it as `alphaMapDir`, leaving `colorMapDir` unset.

Strip space, matching src/eye_strip_geometry.ts: x is arc length along the lash
line, inner canthus at x=0 and outer canthus at --corner-u, wing beyond it. y is
offset across the lid, and v=0 — the lash margin — is the BOTTOM row of the
image, because the shader reads tUV = uv and three's default flipY = true puts
uv.v = 0 on the last row.

    python3 tools/make_liner_strip.py --region lower \
        --out demos/assets/imgs/face/eye_liner_lower_mask.png
"""

import argparse
import numpy as np
from PIL import Image


def smoothstep(a, b, x):
    t = np.clip((x - a) / (b - a), 0.0, 1.0)
    return t * t * (3.0 - 2.0 * t)


def main():
    ap = argparse.ArgumentParser()
    ap.add_argument('--out', required=True)
    ap.add_argument('--region', choices=('upper', 'lower'), default='lower')
    ap.add_argument('--size', nargs=2, type=int, default=[1024, 128])
    ap.add_argument('--corner-u', type=float, default=0.7447,
                    help='where the outer canthus lands; the geometry snaps '
                         '0.75 to this for the default 48 columns')
    # thickness is a fraction of the strip height, i.e. of eyeStrip.strokeWidth
    ap.add_argument('--thick-inner', type=float, default=0.35)
    ap.add_argument('--thick-outer', type=float, default=0.85)
    ap.add_argument('--start-u', type=float, default=0.02,
                    help='the stroke fades in from here. 0.02 runs the full lash '
                         'line; raise it to leave the inner canthus bare.')
    ap.add_argument('--end-u', type=float, default=None,
                    help='where the stroke tapers out. Defaults to the outer '
                         'canthus for the lower lid and to the wing tip for the '
                         'upper one — because with wingLength 0, which is the '
                         'lower-lid default, every column past the canthus '
                         'collapses onto it and any ink there is simply never '
                         'rendered.')
    ap.add_argument('--wing-taper', type=float, default=0.55,
                    help='fraction of the remaining span over which the tip '
                         'tapers out')
    ap.add_argument('--edge', type=float, default=0.22,
                    help='softness of the far edge, as a fraction of thickness')
    args = ap.parse_args()

    if args.end_u is None:
        args.end_u = args.corner_u if args.region == 'lower' else 1.0

    W, H = args.size
    u = (np.arange(W) + 0.5) / W
    # v = 0 at the lash margin, which is the bottom row
    v = 1.0 - (np.arange(H) + 0.5) / H
    U, Vv = np.meshgrid(u, v, indexing='xy')

    cu, eu = args.corner_u, args.end_u
    # fade in from start_u, thicken toward the outer canthus
    onEye = smoothstep(args.start_u, args.start_u + 0.12, U)
    ramp = smoothstep(args.start_u, cu, U)
    thick = args.thick_inner + (args.thick_outer - args.thick_inner) * ramp
    thick = thick * onEye
    # taper out over the span between end_u and the strip's end
    if eu < 1.0:
        tail = np.clip((U - eu) / (1.0 - eu), 0.0, 1.0)
        thick = thick * (1.0 - smoothstep(0.0, args.wing_taper, tail))
    thick = np.where(U > eu + args.wing_taper * (1.0 - eu), 0.0, thick)

    # an upper liner sits tight to the lashes and thickens outward; a lower one
    # is thinner overall and softer, so it reads as definition rather than a line
    if args.region == 'lower':
        thick = thick * 0.55

    edge = np.maximum(1e-4, thick * args.edge)
    mask = 1.0 - smoothstep(thick - edge, thick + edge, Vv)
    mask = np.where(thick <= 1e-4, 0.0, mask)

    m = np.clip(np.rint(mask * 255.0), 0, 255).astype(np.uint8)
    img = np.dstack([m, m, m, np.full_like(m, 255)])
    Image.fromarray(img, 'RGBA').save(args.out)

    rows = np.where(m.max(axis=1) > 8)[0]
    cols = np.where(m.max(axis=0) > 8)[0]
    vlo = 1.0 - (rows.max() + 1) / H
    vhi = 1.0 - rows.min() / H
    print(f'wrote {args.out}  ({W}x{H}, mask, {args.region} lid)')
    print(f'  ink v {vlo:.3f}..{vhi:.3f}   u {cols.min()/W:.3f}..{(cols.max()+1)/W:.3f}'
          f'   (outer canthus u={cu:.3f})')
    print(f'  peak thickness {thick.max():.3f} of the strip height, so '
          f'{thick.max():.3f} * eyeStrip.strokeWidth of eye width')


if __name__ == '__main__':
    main()
