import math
from dataclasses import dataclass, field
from typing import Tuple
import cadquery as Cq
from nhf import Material, Role
from nhf.build import Model, target, assembly, TargetKind
import nhf.utils

@dataclass
class Rod(Model):

    width: float = 120.0
    length: float = 550.0
    length_tip: float = 100.0
    width_tail: float = 60.0
    margin: float = 10.0

    thickness_top: float = 25.4 / 8
    # The side which has mounted hinges must be thicker
    thickness_side: float = 25.4 / 4

    height_internal: float = 30.0

    material_shell: Material = Material.WOOD_BIRCH

    # Considering the glyph on the top ...

    # counted from middle to the bottom
    fac_bar_top: float = 0.1
    # counted from bottom to top
    fac_window_tsumi_bot: float = 0.63
    fac_window_tsumi_top: float = 0.88

    fac_window_footer_bot: float = 0.36
    fac_window_footer_top: float = 0.6

    # Considering the side ...
    hinge_plate_pos: list[float] = field(default_factory=lambda: [0.1, 0.9])
    hinge_plate_length: float = 30.0
    hinge_hole_diam: float = 2.5
    # Hole distance to axis
    hinge_hole_axis_dist: float = 12.5 / 2
    # Distance between holes
    hinge_hole_sep: float = 15.89

    # Consider the reference objects
    ref_object_width: float = 50.0
    ref_object_length: float = 50.0

    def __post_init__(self):
        super().__init__(name="rod")
        self.loc_core = Cq.Location.from2d(self.length - self.length_tip, 0)
        assert self.length_tip * 2 < self.length
        #assert self.fac_bar_top + self.fac_window_tsumi_top < 1
        assert self.fac_window_tsumi_bot < self.fac_window_tsumi_top

    @property
    def length_tail(self):
        return self.length - self.length_tip

    @property
    def _reduced_tip_x(self):
        return self.length_tip - self.margin
    @property
    def _reduced_y(self):
        return self.width / 2 - self.margin
    @property
    def _reduced_tail_y(self):
        return self.width_tail / 2 - self.margin

    def profile_points(self) -> list[Tuple[str, Tuple[float, float]]]:
        """
        Points in polygon line order, labaled
        """
        return [
            ("tip", (self.length, 0)),
            ("mid_r", (self.length - self.length_tip, self.width/2)),
            ("bot_r", (0, self.width_tail / 2)),
            ("bot_l", (0, -self.width_tail / 2)),
            ("mid_l", (self.length - self.length_tip, -self.width/2)),
        ]

    def _window_tip(self) -> Cq.Sketch:
        dxh = self._reduced_tip_x
        dy = self._reduced_y
        return (
            Cq.Sketch()
            .segment(
                (dxh, 0),
                (dxh / 2, dy / 2),
            )
            .bezier([
                (dxh / 2, dy / 2),
                (dxh * 0.6, dy * 0.4),
                (dxh * 0.6, -dy * 0.4),
                (dxh / 2, -dy / 2),
            ])
            .segment(
                (dxh, 0),
            )
            .assemble()
            .moved(self.loc_core.to2d_pos())
        )
    def _window_eye(self, refl: bool = False) -> Cq.Sketch:

        sign = -1 if refl else 1
        dxh = self._reduced_tip_x
        xm = dxh * 0.45
        dy = sign * self._reduced_y
        fac = 0.05

        p1 = Cq.Location.from2d(xm, sign * self.margin / 2)
        p2 = Cq.Location.from2d(dxh * 0.1, sign * self.margin / 2)
        p3 = Cq.Location.from2d(dxh * 0.15, dy * 0.55)
        p4 = Cq.Location.from2d(dxh * 0.4, dy * 0.45)
        d4 = Cq.Location.from2d(dxh * fac, -dy * fac)

        return (
            Cq.Sketch()
            .segment(
                p1.to2d_pos(),
                p2.to2d_pos(),
            )
            .bezier([
                p2.to2d_pos(),
                (p2 * Cq.Location.from2d(0, dy * fac)).to2d_pos(),
                (p3 * Cq.Location.from2d(-dxh * fac, -dy * fac)).to2d_pos(),
                p3.to2d_pos(),
            ])
            .bezier([
                p3.to2d_pos(),
                (p3 * Cq.Location.from2d(0, dy * fac)).to2d_pos(),
                (p4 * d4.inverse).to2d_pos(),
                p4.to2d_pos(),
            ])
            .bezier([
                p4.to2d_pos(),
                (p4 * d4).to2d_pos(),
                (p1 * Cq.Location.from2d(0, dy * fac)).to2d_pos(),
                p1.to2d_pos(),
            ])
            .assemble()
            .moved(self.loc_core.to2d_pos())
        )

    def _window_bar(self) -> Cq.Sketch():
        dxh = self._reduced_tip_x
        dy = self._reduced_y
        dyt = self._reduced_tail_y
        dxt = self.length_tail

        ext_fac = self.fac_bar_top

        p_corner = Cq.Location.from2d(0, dy)
        p_top = Cq.Location.from2d(0.3 * dxh, 0.7 * dy)
        p_bot = Cq.Location.from2d(-ext_fac * dxt, dy + ext_fac * (dyt - dy))
        p_top_int = p_corner * Cq.Location.from2d(.05 * dxh, -.2 * dy)
        p_top_ctrl = Cq.Location.from2d(0, .3 * dy)
        p_bot_int = p_corner * Cq.Location.from2d(-.15 * dxh, -.2 * dy)
        p_bot_ctrl = Cq.Location.from2d(-.25 * dxh, .3 * dy)

        return (
            Cq.Sketch()
            .segment(
                p_corner.to2d_pos(),
                p_top.to2d_pos(),
            )
            .segment(p_top_int.to2d_pos())
            .bezier([
                p_top_int.to2d_pos(),
                p_top_ctrl.to2d_pos(),
                p_top_ctrl.flip_y().to2d_pos(),
                p_top_int.flip_y().to2d_pos(),
            ])
            .segment(p_top.flip_y().to2d_pos())
            .segment(p_corner.flip_y().to2d_pos())
            .segment(p_bot.flip_y().to2d_pos())
            .segment(p_bot_int.flip_y().to2d_pos())
            .bezier([
                p_bot_int.flip_y().to2d_pos(),
                p_bot_ctrl.flip_y().to2d_pos(),
                p_bot_ctrl.to2d_pos(),
                p_bot_int.to2d_pos(),
            ])
            .segment(p_bot.to2d_pos())
            .segment(p_corner.to2d_pos())
            .assemble()
            .moved(self.loc_core.to2d_pos())
        )

    def _window_tsumi(self) -> Cq.Sketch:
        dx = (self.fac_window_tsumi_top - self.fac_window_tsumi_bot) * self.length_tail
        dy = 2 * self._reduced_y * 0.8
        loc = Cq.Location(self.fac_window_tsumi_bot * self.length_tail, 0)

        # Construction of the top part of the kanji

        dx_top = dx * 0.3
        x_top = dx - dx_top / 2
        dy_top = dy
        dy_eye = dy * 0.2
        dy_border = (dy_top - 3 * dy_eye) / 4
        # The skip must follow 3 * eye + 4 * border = dy_top
        y_skip = dy_eye + dy_border

        # Construction of the bottom part
        x_bot = dx * 0.65
        y3 = dy * 0.4
        y2 = dy * 0.2
        y1 = dy * 0.1
        # x/y-centers of the legs
        x_leg0 = x_bot / 14
        dx_leg = x_bot / 7
        y_leg = (y3 + y1) / 2

        return (
            Cq.Sketch()
            .push([(x_top, 0)])
            .rect(dx_top, dy_top)
            .push([
                (x_top, -y_skip),
                (x_top, 0),
                (x_top, y_skip),
            ])
            .rect(dx_top / 3, dy_eye, mode='s')

            # Construct the two sides
            .push([
                (x_bot / 2, (y2 + y1) / 2),
                (x_bot / 2, -(y2 + y1) / 2),
            ])
            .rect(x_bot, y2 - y1, mode='a')
            .push([
                (x_leg0 + dx_leg, y_leg),
                (x_leg0 + 3 * dx_leg, y_leg),
                (x_leg0 + 5 * dx_leg, y_leg),
                (x_leg0 + dx_leg, -y_leg),
                (x_leg0 + 3 * dx_leg, -y_leg),
                (x_leg0 + 5 * dx_leg, -y_leg),
            ])
            .rect(dx_leg, y3 - y1, mode='a')

            .moved(loc)
        )

    def _window_footer(self) -> Cq.Sketch:
        x_bot = self.fac_window_footer_bot * self.length_tail
        dx = (self.fac_window_footer_top - self.fac_window_footer_bot) * self.length_tail
        loc = Cq.Location(x_bot, 0)

        dy = self._reduced_y * 0.8

        # eyes
        eye_y2 = dy * .5
        eye_y1 = dy * .2
        eye_width = eye_y2 - eye_y1
        eye_x = dx - eye_width / 2

        # bar polygon
        bar_x0 = dx * 0.65
        bar_dx = dx * 0.1
        bar_x1 = bar_x0 + bar_dx
        bar_x2 = bar_x0 + bar_dx * 2
        bar_x3 = bar_x0 + bar_dx * 3
        bar_y1 = dy * .75
        assert bar_y1 > eye_y2
        bar_y2 = dy * .9
        assert bar_y1 < bar_y2

        # Construction of the cross
        cross_dx = dx * 0.7 / math.sqrt(2)
        cross_dy = dy * 0.2

        cross = (
            Cq.Sketch()
            .rect(cross_dx, cross_dy)
            .rect(cross_dy, cross_dx, mode='a')
            .moved(Cq.Location.from2d(dx * 0.5, 0, 45))
        )
        return (
            Cq.Sketch()
            # eyes
            .push([
                (eye_x,  (eye_y1 + eye_y2)/2),
                (eye_x, -(eye_y1 + eye_y2)/2),
            ])
            .rect(eye_width, eye_width, mode='a')
            # middle bar
            .push([(0,0)])
            .polygon([
                (bar_x1, bar_y1),
                (bar_x0, bar_y1),
                (bar_x0, bar_y2),
                (bar_x3, bar_y2),
                (bar_x3, bar_y1),
                (bar_x2, bar_y1),

                (bar_x2, -bar_y1),
                (bar_x3, -bar_y1),
                (bar_x3, -bar_y2),
                (bar_x0, -bar_y2),
                (bar_x0, -bar_y1),
                (bar_x1, -bar_y1),
            ], mode='a')
            # cross
            .boolean(cross, mode='a')

            #.push([(0,0)])
            #.rect(10, 10)

            .moved(loc)
        )

    @target(name="bottom", kind=TargetKind.DXF)
    def profile_bottom(self) -> Cq.Sketch:
        return (
            Cq.Sketch()
            .polygon([p for _, p in self.profile_points()])
        )

    @target(name="top", kind=TargetKind.DXF)
    def profile_top(self) -> Cq.Sketch:
        return (
            self.profile_bottom()
            .boolean(self._window_tip(), mode='s')
            .boolean(self._window_eye(True), mode='s')
            .boolean(self._window_eye(False), mode='s')
            .boolean(self._window_bar(), mode='s')
            .boolean(self._window_tsumi(), mode='s')
            .boolean(self._window_footer(), mode='s')
        )

    def surface_top(self) -> Cq.Workplane:
        return (
            Cq.Workplane('XY')
            .placeSketch(self.profile_top())
            .extrude(self.thickness_top)
        )

    def surface_bottom(self) -> Cq.Workplane:
        surface = (
            Cq.Workplane('XY')
            .placeSketch(self.profile_bottom())
            .extrude(self.thickness_top)
        )
        plane = surface.faces(">Z").workplane()

        for (name, p) in self.profile_points():
            plane.moveTo(*p).tagPlane(name)

        return surface

    # Properties of the side surfaces

    @property
    def length_edge_tip(self):
        return math.sqrt(self.length_tip ** 2 + (self.width / 2) ** 2)
    @property
    def length_edge_tail(self):
        dw = (self.width - self.width_tail) / 2
        return math.sqrt(self.length_tail ** 2 + dw ** 2)
    @property
    def tip_incident_angle(self):
        """
        Angle (measuring from vertical) at which the tip edge pieces must be
        sanded in order to make them not collide into each other.
        """
        return math.atan2(self.length_tip, self.width / 2)
    @property
    def shoulder_incident_angle(self) -> float:
        angle_tip = math.atan2(self.width / 2, self.length_tip)
        angle_tail = math.atan2((self.width - self.width_tail) / 2, self.length_tail)
        return (angle_tip + angle_tail) / 2

    @target(name="ref-tip")
    def ref_tip(self) -> Cq.Workplane:
        angle = self.tip_incident_angle
        w = self.ref_object_width
        drop = math.sin(angle) * w
        profile = (
            Cq.Sketch()
            .polygon([
                (0, 0),
                (0, w),
                (w, w),
                (w - drop, 0),
            ])
        )
        return (
            Cq.Workplane()
            .placeSketch(profile)
            .extrude(self.ref_object_length)
        )
    @target(name="ref-shoulder")
    def ref_shoulder(self) -> Cq.Workplane:
        angle = self.shoulder_incident_angle
        w = self.ref_object_width
        drop = math.sin(angle) * w
        profile = (
            Cq.Sketch()
            .polygon([
                (0, 0),
                (0, w),
                (w, w),
                (w - drop, 0),
            ])
        )
        return (
            Cq.Workplane()
            .placeSketch(profile)
            .extrude(self.ref_object_length)
        )

    @target(name="side-tip-2x", kind=TargetKind.DXF)
    def profile_side_tip(self):
        l = self.length_edge_tip
        w = self.height_internal
        return (
            Cq.Sketch()
            .push([(l/2, w/2)])
            .rect(l, w)
        )
    @target(name="side-tail", kind=TargetKind.DXF)
    def profile_side_tail(self):
        """
        Plain side 2 with no hinge
        """
        l = self.length_edge_tail
        w = self.height_internal
        return (
            Cq.Sketch()
            .push([(l/2, w/2)])
            .rect(l, w)
        )
    @target(name="side-hinge-plate", kind=TargetKind.DXF)
    def profile_side_hinge_plate(self):
        l = self.hinge_plate_length
        w = self.height_internal / 2
        return (
            Cq.Sketch()
            .push([(0, w/2)])
            .rect(l, w)
            .push([
                (self.hinge_hole_sep / 2, self.hinge_hole_axis_dist),
                (-self.hinge_hole_sep / 2, self.hinge_hole_axis_dist),
            ])
            .circle(self.hinge_hole_diam / 2, mode='s')
        )
    @target(name="side-tail-hinged", kind=TargetKind.DXF)
    def profile_side_tail_hinged(self):
        """
        Plain side 2 with no hinge
        """
        l = self.length_edge_tail
        w = self.height_internal

        # Holes for hinge
        plate_pos = [
            (t * l, w * 3/4) for t in self.hinge_plate_pos
        ]
        hole_pos = [
            (self.hinge_hole_sep / 2, self.hinge_hole_axis_dist),
            (-self.hinge_hole_sep / 2, self.hinge_hole_axis_dist),
        ]
        return (
            self.profile_side_tail()
            .push(plate_pos)
            .rect(self.hinge_plate_length, w/2, mode='s')
            .push([
                (hx + px, w/2 - hy)
                for hx, hy in hole_pos
                for px, _ in plate_pos
            ])
            .circle(self.hinge_hole_diam / 2, mode='s')
        )
    @target(name="side-bot", kind=TargetKind.DXF)
    def profile_side_bot(self):
        l = self.width_tail - self.thickness_side * 2
        w = self.height_internal
        return (
            Cq.Sketch()
            .rect(l, w)
        )

    def surface_side_tip(self):
        result = (
            Cq.Workplane('XY')
            .placeSketch(self.profile_side_tip())
            .extrude(self.thickness_side)
        )
        plane = result.faces(">Y").workplane()
        plane.moveTo(0, 0).tagPlane("bot")
        plane.moveTo(-self.length_edge_tip, 0).tagPlane("top")
        return result
    def surface_side_tail(self):
        result = (
            Cq.Workplane('XY')
            .placeSketch(self.profile_side_tail())
            .extrude(self.thickness_side)
        )
        plane = result.faces(">Y").workplane()
        plane.moveTo(0, 0).tagPlane("bot")
        plane.moveTo(-self.length_edge_tail, 0).tagPlane("top")
        return result
    def surface_side_tail_hinged(self):
        result = (
            Cq.Workplane('XY')
            .placeSketch(self.profile_side_tail_hinged())
            .extrude(self.thickness_side)
        )
        plane = result.faces(">Y").workplane()
        plane.moveTo(0, 0).tagPlane("bot")
        plane.moveTo(-self.length_edge_tail, 0).tagPlane("top")
        return result
    def surface_side_bot(self):
        result = (
            Cq.Workplane('XY')
            .placeSketch(self.profile_side_bot())
            .extrude(self.thickness_side)
        )
        plane = result.faces(">Y").workplane()
        plane.moveTo(self.width_tail / 2, 0).tagPlane("bot")
        plane.moveTo(-self.width_tail / 2, 0).tagPlane("top")
        return result

    @assembly()
    def assembly(self) -> Cq.Assembly:
        a = (
            Cq.Assembly()
            .addS(
                self.surface_top(),
                name="top",
                material=self.material_shell,
                role=Role.STRUCTURE | Role.DECORATION
            )
            .constrain("top", "Fixed")
            .addS(
                self.surface_bottom(),
                name="bottom",
                material=self.material_shell,
                role=Role.STRUCTURE,
                loc=Cq.Location(0, 0, -self.thickness_top - self.height_internal)
            )
            .constrain("bottom", "Fixed")
            .addS(
                self.surface_side_tip(),
                name="side_tip_l",
                material=self.material_shell,
                role=Role.STRUCTURE,
            )
            .constrain("bottom?tip", "side_tip_l?top", "Plane")
            .constrain("bottom?mid_l", "side_tip_l?bot", "Plane")
            .addS(
                self.surface_side_tip(),
                name="side_tip_r",
                material=self.material_shell,
                role=Role.STRUCTURE,
            )
            .constrain("bottom?tip", "side_tip_r?bot", "Plane")
            .constrain("bottom?mid_r", "side_tip_r?top", "Plane")
            .addS(
                self.surface_side_tail(),
                name="side_tail_l",
                material=self.material_shell,
                role=Role.STRUCTURE,
            )
            .constrain("bottom?mid_l", "side_tail_l?top", "Plane")
            .constrain("bottom?bot_l", "side_tail_l?bot", "Plane")
            .addS(
                self.surface_side_tail_hinged(),
                name="side_tail_r",
                material=self.material_shell,
                role=Role.STRUCTURE,
            )
            .constrain("bottom?mid_r", "side_tail_r?bot", "Plane")
            .constrain("bottom?bot_r", "side_tail_r?top", "Plane")
            .addS(
                self.surface_side_bot(),
                name="side_bot",
                material=self.material_shell,
                role=Role.STRUCTURE,
            )
            .constrain("bottom?bot_l", "side_bot?top", "Plane")
            .constrain("bottom?bot_r", "side_bot?bot", "Plane")
            .solve()
        )
        return a