"""
Collection of useful shortcuts for 3D modelling with build123d.
"""

from __future__ import annotations

from build123d import Shape,Align,Axis,BuildLine,Rot,Matrix,Intrinsic,JernArc,Location,RegularPolygon,Part,Plane,export_step,export_stl,import_step,import_stl,Cylinder,loft
from pathlib import Path
import math
from moat.util import InexactFloat

#from bd_warehouse.gear import InvoluteToothProfile, SpurGear, SpurGearPlan

from typing import TYPE_CHECKING
if TYPE_CHECKING:
    from collection.abc import Sequence

import logging
log=logging.getLogger("moat.d3")

__all__=["AMIN","ACCM","Loc","LocR","Cyl","Up","D",
         "ArcAt","RotAxis","RotateTo","PolyCap","sin","cos","tan","atan","stack","show","quarter","gear_turn","read_step","read_stl","pos","li","lto",
         "RX","RX1","RX2","RX3",
         "RY","RY1","RY2","RY3",
         "RZ","RZ1","RZ2","RZ3",
         "R0","R1","R2","R3",
         ]

AMIN={"align":(Align.MIN,Align.MIN,Align.MIN)}
ACCM={"align":(Align.CENTER,Align.CENTER,Align.MIN)}

def D(n, **kw):
    """
    A wrapper for `moat.util.InexactFloat`. Saves on typing.
    """
    return InexactFloat(n, **kw)

def Loc(a,b,c):
    """
    Shorthand for Location.
    """
    return Location((a,b,c))

def LocR(d,a,h=0):
    """
    Locate radially, distance d, angle a, clockwise from top.
    """
    a=a*math.pi/180
    return Loc(d*math.sin(a),d*math.cos(a),h)

def Cyl(d,h):
    """
    A cylinder with diameter d and height h, centered but with +z.
    """
    return Cylinder(d/2,h,align=(Align.CENTER,Align.CENTER,Align.MIN))

def Up(z):
    """
    Move something up. Shorthand for Location(0,0,z).
    """
    return Location((0,0,z))

def ArcAt(line, radius, angle=90, twist=0, at_end=True):
    """
    Add an arc to the end of a path. @twist modifies the direction the arc
    bends towards.
    """
    res = JernArc((0,0,0),(0,1,0),radius,angle)
    res=RX1*Rot(0,twist,0)*res

    if at_end:
        return Location(line@1)*RotateTo(res,line%1)
    else:
        return Location(line@0)*RotateTo(res,-(line%0))
    
    # Location(line@.9)*Rot(line%1)*JernArc((0,0,0),(1,0,0),radius,angle)
    # Rot(0,0,twist)*

def PolyCap(d, n=6):
    """
    Given a polygonal rod, this returns its end cap.
    """
    al=(Align.CENTER,Align.CENTER)
    return stack(RegularPolygon(d/2,n, align=al),d/4,RegularPolygon(d/4,n, align=al))

def RotateTo(thing, direction):
    ax = Axis((0,0,0),(0,0,1)+direction.normalized())
    return thing.rotate(ax,180)

def RotAxis(axis, angle):
    from transforms3d.euler import mat2euler
    rtm = Matrix()
    breakpoint()
    rtm.rotate(axis,angle)
    rtm = [[rtm[x,y] for y in range(3)] for x in range(3)]
    rtm = [ a*180/math.pi for a in mat2euler(rtm, "szyx") ]
    return Rot(*rtm, Intrinsic.XYZ)

def sin(a):
    "sin (angle in degrees)"
    return math.sin(a*math.pi/180)

def cos(a):
    "cos (angle in degrees)"
    return math.cos(a*math.pi/180)

def tan(a):
    "tan (angle in degrees)"
    return math.tan(a*math.pi/180)

def atan(a,b=None):
    "tan⁻¹ (angle in degrees)"
    return (math.atan(a) if b is None else math.atan2(a,b))*180/math.pi


def stack(*x: Sequence[int|float|Shape]):
    """
    A sequence of 2d things, loft-ed.

    A z1 B z2 C: place shape A at z=0, shape B at z=z1, shape C at z=z1+z2. Loft them.
    A B: complete the object with shape A and start another with shape B.
    A z None: shorthand for A z A.
    """
    obj=Part()
    d=None
    p=0
    i=iter(x)
    d = last_shape = next(i)
    did_num=False
    for w in i:
        if isinstance(w,(int,float)):
            if did_num:
                raise ValueError("two consecutive numbers?")
            p += w
            did_num=True
        elif did_num:
            if w is None:
                w = last_shape
            else:
                last_shape = w
            nd = Up(p)*w
            obj += loft([d,nd])
            d = nd
            did_num=False
        elif w is None:
            raise ValueError("Real shape wanted")
        else:
            d = Up(p)*w
            last_shape = w
            did_num=False
    if did_num:
        raise ValueError("Trailing number")
    return obj

s_o = ...
def show(obj,name=None,dest=None):
    """
    Display this thing.

    If "dest" is a file or directory, write a STEP file there.
    """
    if name is None:
        name = f"{obj}_{(id(obj)/99)%10000}"

    global s_o
    if s_o is Ellipsis:
        import inspect
        f = inspect.currentframe()
        while f is not None and "show_object" not in f.f_globals:
            f = f.f_back
        s_o = None if f is None else f.f_globals["show_object"]
    if s_o is not None:
        s_o(getattr(obj, "wrapped", obj),name)

    if dest is not None:
        dest = Path(dest)
        if dest.is_dir():
            export_stl(obj,str(dest / f"{name}.stl"))
            dest /= f"{name}.step"
        export_step(obj,str(dest))

def read_step(name):
    """
    Read this STEP file.
    """
    return import_step(name)

def read_stl(name):
    """
    Read this STL file.
    """
    return import_stl(name)

def quarter(res):
    """
    Given an object in the +x/+y quarter, mirror it twice.
    """
    res_xz=mirror(res,about=Plane.XZ)
    return (res
      +res_xz
      +mirror(res_xz+res,about=Plane.YZ)
      )

    
def gear_turn(n1, a1, n2, a2, minimize=True):
    """
    Given gear1 with n1 teeth, turned to angle a1, and gear2 with n2 teeth positioned at angle a2 relative to gear 1:
    how far do we turn gear2 so that its teeth mesh with gear1?

    This code assumes that 0° means the same thing for both gears (e.g. center of a tooth).

    If @minimize is true (the default), the resulting angle will be between 0 and 360/n2.
    """
    # step 1, assume that gear 2 sits on top of gear 1: turn gear 2 by 180° plus half a tooth.
    res = 180*(1+1/n2)  # 180+360/n2/2

    # step 2, turn gear 1 counterclockwise by a1, which turns gear 2 clockwise by the gear ratio.
    res += a1 * (n1/n2)

    # step 3, physically move gear2 around clockwise
    res += a2 * (1+n1/n2)

    # step 4, gear 2 is rotationally symmetric
    if minimize:
        res %= 360/n2

    return res

def pos(x=None,y=None):
    "Line drawing et al. Start position."
    ctx = BuildLine._get_context(log=False)  # noqa:SLF001
    try:
        res = ctx._mt_pos  # noqa:SLF001
    except AttributeError:
        if x is None:
            raise
        res = None
    if y is not None:
        ctx._mt_pos = (x,y)
    return res

def lto(x=None,y=None):
    "Line to absolute position"
    ctx = BuildLine._get_context(log=False)  # noqa:SLF001
    p = ctx._mt_pos  # noqa:SLF001
    if x is None:
        x=p[0]
    if y is None:
        y=p[1]
    p2 = (x,y)
    ctx._mt_pos = p2  # noqa:SLF001
    Line(p,p2)

def li(x=0,y=0):
    "Relative line"
    ctx = BuildLine._get_context(log=False)  # noqa:SLF001
    p = ctx._mt_pos  # noqa:SLF001
    p2 = (p[0]+x,p[1]+y)
    ctx._mt_pos = p2  # noqa:SLF001
    Line(p,p2)

def RX(x):
    "Rotate 90°*x"
    return Rot(90*x,0,0)
RX1=RX(1)
RX2=RX(2)
RX3=RX(3)
def RY(y):
    "Rotate 90°*y"
    return Rot(0,90*y,0)
RY1=RY(1)
RY2=RY(2)
RY3=RY(3)
def RZ(z):
    "Rotate 90°*z"
    return Rot(0,0,90*z)
RZ0=R0=RZ(0)
RZ1=R1=RZ(1)
RZ2=R2=RZ(2)
RZ3=R3=RZ(3)

AL=(Align.CENTER,Align.CENTER,Align.MIN)

