import cadquery as Cq
from dataclasses import dataclass, field
from typing import Tuple, Optional, Union, Callable
from nhf.build import Model, TargetKind, target
import nhf.utils

def box_with_centre_holes(
        length: float,
        width: float,
        height: float,
        hole_loc: list[float],
        hole_diam: float = 6.0,
        ) -> Cq.Workplane:
    """
    Creates a box with holes along the X axis, marked `conn0, conn1, ...`. The
    box's y axis is centred
    """
    result = (
        Cq.Workplane('XY')
        .box(length, width, height, centered=(False, True, False))
        .faces(">Z")
        .workplane()
    )
    plane = result
    for i, x in enumerate(hole_loc):
        result = result.moveTo(x, 0).hole(hole_diam)
        plane.moveTo(x, 0).tagPlane(f"conn{i}")
    return result

@dataclass
class Hole:
    x: float
    y: float = 0.0
    diam: Optional[float] = None
    tag: Optional[str] = None
    face: Optional[Cq.Face] = None

    @property
    def rev_tag(self) -> str:
        assert self.tag is not None
        return self.tag + "_rev"

    def cutting_geometry(self, default_diam: Optional[float] = None) -> Cq.Face:
        if self.face is not None:
            return self.face
        diam = self.diam if self.diam is not None else default_diam
        assert diam is not None
        return Cq.Face.makeFromWires(Cq.Wire.makeCircle(diam/2, Cq.Vector(), Cq.Vector(0,0,1)))

@dataclass
class MountingBox(Model):
    """
    Create a box with marked holes
    """
    length: float = 100.0
    width: float = 60.0
    thickness: float = 1.0

    # List of (x, y), diam
    holes: list[Hole] = field(default_factory=lambda: [])
    hole_diam: Optional[float] = None

    centred: Tuple[bool, bool] = (False, True)

    generate_side_tags: bool = True
    # Generate tags on the opposite side
    generate_reverse_tags: bool = False

    centre_bot_top_tags: bool = False
    centre_left_right_tags: bool = False

    # Determines the position of side tags
    flip_y: bool = False

    profile_callback: Optional[Callable[[Cq.Sketch], Cq.Sketch]] = None

    def __post_init__(self):
        assert self.thickness > 0
        for i, hole in enumerate(self.holes):
            if hole.tag is None:
                hole.tag = f"conn{i}"

    @target(kind=TargetKind.DXF)
    def profile(self) -> Cq.Sketch:
        bx, by = 0, 0
        if not self.centred[0]:
            bx = self.length / 2
        if not self.centred[1]:
            by = self.width / 2
        result = (
            Cq.Sketch()
            .push([(bx, by)])
            .rect(self.length, self.width)
        )
        for hole in self.holes:
            face = hole.cutting_geometry(default_diam=self.hole_diam)
            result.push([(hole.x, hole.y)]).each(lambda l:face.moved(l), mode='s')
        if self.profile_callback:
            result = self.profile_callback(result)
        return result

    def generate(self) -> Cq.Workplane:
        """
        Creates box shape with markers
        """
        result = (
            Cq.Workplane('XY')
            .placeSketch(self.profile())
            .extrude(self.thickness)
        )
        plane = result.copyWorkplane(Cq.Workplane('XY')).workplane(offset=self.thickness)
        reverse_plane = result.copyWorkplane(Cq.Workplane('XY'))
        for hole in self.holes:
            assert hole.tag
            plane.moveTo(hole.x, hole.y).tagPlane(hole.tag)
            if self.generate_reverse_tags:
                reverse_plane.moveTo(hole.x, hole.y).tagPlane(hole.rev_tag, '-Z')

        if self.generate_side_tags:
            xn, xp = 0, self.length
            if self.centred[0]:
                xn -= self.length/2
                xp -= self.length/2
            yn, yp = 0, self.width
            if self.centred[1]:
                yn -= self.width/2
                yp -= self.width/2

            tag_x = xn + (self.length/2 if self.centre_left_right_tags else 0)
            result.copyWorkplane(Cq.Workplane('XZ', origin=(tag_x, yn, self.thickness))).tagPlane("left")
            result.copyWorkplane(Cq.Workplane('ZX', origin=(tag_x, yp, self.thickness))).tagPlane("right")

            tag_y = yn + (self.width/2 if self.centre_bot_top_tags else 0)
            result.copyWorkplane(Cq.Workplane('ZY', origin=(xn, tag_y, self.thickness))).tagPlane("bot")
            result.copyWorkplane(Cq.Workplane('YZ', origin=(xp, tag_y, self.thickness))).tagPlane("top")
            result.faces(">Z").tag("dir")
        return result

    def marked_assembly(self) -> Cq.Assembly:
        result = (
            Cq.Assembly()
            .add(self.generate(), name="box")
        )
        for hole in self.holes:
            result.markPlane(f"box?{hole.tag}")
            if self.generate_reverse_tags:
                result.markPlane(f"box?{hole.rev_tag}")
        if self.generate_side_tags:
            (
                result
                .markPlane("box?left")
                .markPlane("box?right")
                .markPlane("box?dir")
                .markPlane("box?top")
                .markPlane("box?bot")
            )
        return result.solve()