import FreeCAD
import FreeCADGui
import Part
from PySide import QtGui
import math

INCH_TO_MM = 25.4
MOVE_STEP_IN = 1.0  # movement step per arrow press (inches)

def inch_to_mm(v):
    return float(v) * INCH_TO_MM


def clear_old_walls(doc):
    """
    Delete all geometry belonging to Wall1, Wall2, Wall3 from the document.
    Uses Name/Label prefixes to find everything.
    """
    to_delete = []
    prefixes = ("Wall1", "Wall2", "Wall3")

    for obj in doc.Objects:
        name = getattr(obj, "Name", "")
        label = getattr(obj, "Label", "")
        if name.startswith(prefixes) or label.startswith(prefixes):
            to_delete.append(obj)

    # Remove from document
    for obj in to_delete:
        doc.removeObject(obj.Name)


def build_wall_module_2x6(name, spacing_in, height_in, width_in, base_vec):
    """
    Build a 2x6 wall named `name` at a given base_vec (FreeCAD.Vector).
    Returns the wall width in mm.
    """

    doc = FreeCAD.ActiveDocument
    if doc is None:
        doc = FreeCAD.newDocument("WallModuleDoc")

    # convert to mm
    spacing = inch_to_mm(spacing_in)
    height  = inch_to_mm(height_in)
    width   = inch_to_mm(width_in)

    stud_t = inch_to_mm(1.5)      # 2x6 actual thickness (1.5")
    stud_d = inch_to_mm(5.5)      # wall thickness (5.5")
    osb_t  = inch_to_mm(0.4375)   # 7/16" OSB

    grp = doc.addObject("App::DocumentObjectGroup", name)

    # bottom plate
    bottom = Part.makeBox(width, stud_d, stud_t)
    bottom.translate(base_vec)
    b = doc.addObject("Part::Feature", f"{name}_BottomPlate")
    b.Shape = bottom
    grp.addObject(b)

    # top plate
    top = Part.makeBox(width, stud_d, stud_t)
    top.translate(base_vec + FreeCAD.Vector(0, 0, height - stud_t))
    t = doc.addObject("Part::Feature", f"{name}_TopPlate")
    t.Shape = top
    grp.addObject(t)

    # studs
    stud_len = height - 2.0 * stud_t
    positions = [0.0]

    n_max = int(math.floor(width_in / spacing_in))
    for n in range(1, n_max + 1):
        x = inch_to_mm(n * spacing_in)
        if x <= (width - stud_t):
            positions.append(x)

    right = width - stud_t
    if all(abs(right - p) > 1.0 for p in positions):
        positions.append(right)

    positions = sorted(positions)

    for i, x in enumerate(positions):
        s = Part.makeBox(stud_t, stud_d, stud_len)
        s.translate(base_vec + FreeCAD.Vector(x, 0, stud_t))
        obj = doc.addObject("Part::Feature", f"{name}_Stud_{i:02d}")
        obj.Shape = s
        grp.addObject(obj)

    # OSB
    osb = Part.makeBox(width, osb_t, height)
    osb.translate(base_vec + FreeCAD.Vector(0, stud_d, 0))
    o = doc.addObject("Part::Feature", f"{name}_OSB")
    o.Shape = osb
    grp.addObject(o)

    doc.recompute()
    return width


def build_header_for_wall(wall_name,
                          header_height_in,
                          header_length_in,
                          wall_height_in,
                          wall_base_vec,
                          wall_width_mm):
    """
    Build a header module adjacent to the wall, in the same group as the wall.

    - Header total height selectable (header_height_in).
    - Header length selectable (header_length_in).
    - Top of header aligns with top of wall.
    - Header is located along +X, directly after the wall.
    - Jack studs:
        * 2x6 studs from floor (z=0) to bottom of header bottom plate.
        * First bearing zone: continuous studs from x = 0" to 4.5" of header.
        * Last bearing zone: continuous studs from x = (L - 4.5") to L of header.
        * Mid of header has no support.
        * Implemented as 3 studs per bearing zone (total 6).

    Returns header length in mm.
    """
    doc = FreeCAD.ActiveDocument
    if doc is None:
        doc = FreeCAD.newDocument("WallModuleDoc")

    header_h = inch_to_mm(header_height_in)
    header_len = inch_to_mm(header_length_in)
    wall_height_mm = inch_to_mm(wall_height_in)

    stud_t = inch_to_mm(1.5)      # plate/stud thickness in X
    stud_d = inch_to_mm(5.5)      # wall/header thickness in Y (5.5")

    grp = doc.getObject(wall_name)
    if grp is None:
        return 0.0

    # Base of header: start after the wall in X,
    # and place bottom so that header top = wall top.
    header_base = wall_base_vec + FreeCAD.Vector(
        wall_width_mm,   # start right after wall
        0,
        wall_height_mm - header_h
    )

    # Bottom plate
    bottom = Part.makeBox(header_len, stud_d, stud_t)
    bottom.translate(header_base)
    b = doc.addObject("Part::Feature", f"{wall_name}_Header_BottomPlate")
    b.Shape = bottom
    grp.addObject(b)

    # Top plate
    top = Part.makeBox(header_len, stud_d, stud_t)
    top.translate(header_base + FreeCAD.Vector(0, 0, header_h - stud_t))
    t = doc.addObject("Part::Feature", f"{wall_name}_Header_TopPlate")
    t.Shape = top
    grp.addObject(t)

    # Solid block between plates
    block_h = header_h - 2.0 * stud_t
    if block_h > 0:
        block = Part.makeBox(header_len, stud_d, block_h)
        block.translate(header_base + FreeCAD.Vector(0, 0, stud_t))
        blk = doc.addObject("Part::Feature", f"{wall_name}_Header_Block")
        blk.Shape = block
        grp.addObject(blk)

    # ------------- Jack studs -------------
    # Jack studs go from floor (z=0) to bottom of header bottom plate (z = header_base.z)
    jack_len = header_base.z
    if jack_len > 0:
        bearing_len = inch_to_mm(4.5)

        start_x = header_base.x
        end_x = header_base.x + header_len

        # LEFT bearing zone: from x = start_x to start_x + 4.5"
        # Use 3 studs: [0-1.5], [1.5-3.0], [3.0-4.5] inches
        left_positions = [
            start_x + 0.0 * stud_t,
            start_x + 1.0 * stud_t,
            start_x + 2.0 * stud_t,
        ]

        # RIGHT bearing zone: from x = end_x - 4.5" to end_x
        # Use 3 studs: [L-4.5,L-3.0], [L-3.0,L-1.5], [L-1.5,L]
        right_zone_start = end_x - bearing_len
        right_positions = [
            right_zone_start + 0.0 * stud_t,
            right_zone_start + 1.0 * stud_t,
            right_zone_start + 2.0 * stud_t,
        ]

        # Create studs for both bearing zones (no mid support)
        all_positions = []
        for x_jack in left_positions:
            all_positions.append(("L", x_jack))
        for x_jack in right_positions:
            all_positions.append(("R", x_jack))

        for i, (side, x_jack) in enumerate(all_positions):
            jack = Part.makeBox(stud_t, stud_d, jack_len)
            # jacks sit at floor (z=0), same Y as wall/header base (wall_base_vec.y)
            jack.translate(FreeCAD.Vector(x_jack, wall_base_vec.y, 0))
            j = doc.addObject("Part::Feature", f"{wall_name}_JackStud_{side}_{i:02d}")
            j.Shape = jack
            grp.addObject(j)

    doc.recompute()
    return header_len


def transform_wall_group(doc, group_name, origin_vec, angle_deg, offset_vec):
    """
    Rotate all objects in group `group_name` around Z axis by `angle_deg`
    about the given origin_vec (FreeCAD.Vector), then translate by offset_vec.
    offset_vec is in global coordinates (e.g., +/-X, +/-Y from arrow keys).
    """
    grp = doc.getObject(group_name)
    if grp is None:
        return

    apply_rotation = abs(angle_deg) >= 1e-6
    if apply_rotation:
        rot = FreeCAD.Rotation(FreeCAD.Vector(0, 0, 1), angle_deg)

    for obj in getattr(grp, "Group", []):
        pl = obj.Placement
        base = pl.Base
        rel = base - origin_vec

        if apply_rotation:
            rel = rot.multVec(rel)
            new_base = origin_vec + rel + offset_vec
            pl.Rotation = rot * pl.Rotation
        else:
            new_base = origin_vec + rel + offset_vec

        pl.Base = new_base
        obj.Placement = pl

    doc.recompute()


class ThreeWallDialog(QtGui.QDialog):
    """
    Live-update UI for up to 3 walls.
    Any parameter change:
      - deletes existing walls
      - rebuilds the current configuration

    Walls are placed side-by-side along +X, with optional headers per wall.
    Each wall (including its header and jack studs, if present) is transformed as a unit:
      - rotation about the wall base point
      - then translation in X/Y via arrow buttons
    """

    def __init__(self, parent=None):
        super(ThreeWallDialog, self).__init__(parent)
        self.setWindowTitle("Three Wall Modules – Live (Delete & Redraw)")

        # per-wall offsets (mm)
        self.w1_dx = 0.0
        self.w1_dy = 0.0
        self.w2_dx = 0.0
        self.w2_dy = 0.0
        self.w3_dx = 0.0
        self.w3_dy = 0.0

        layout = QtGui.QFormLayout(self)

        # ---- Wall 1 ----
        layout.addRow(QtGui.QLabel("------ WALL 1 ------"))
        self.w1enable = QtGui.QCheckBox("Enable Wall 1"); self.w1enable.setChecked(True)
        layout.addRow(self.w1enable)

        self.w1spacing = QtGui.QComboBox()
        self.w1spacing.addItem("16 in", 16.0)
        self.w1spacing.addItem("24 in", 24.0)
        layout.addRow("Wall 1 Spacing", self.w1spacing)

        self.w1height = QtGui.QDoubleSpinBox()
        self.w1height.setRange(24.0, 168.0)
        self.w1height.setValue(96.0)   # default 96"
        self.w1height.setSuffix(" in")
        layout.addRow("Wall 1 Height", self.w1height)

        self.w1width = QtGui.QDoubleSpinBox()
        self.w1width.setRange(12.0, 600.0)
        self.w1width.setValue(48.0)    # default 48"
        self.w1width.setSuffix(" in")
        layout.addRow("Wall 1 Width", self.w1width)

        self.w1angle = QtGui.QDoubleSpinBox()
        self.w1angle.setRange(-180.0, 180.0)
        self.w1angle.setValue(0.0)
        self.w1angle.setSuffix(" deg")
        layout.addRow("Wall 1 Rotation", self.w1angle)

        self.w1_header_enable = QtGui.QCheckBox("Enable Header after Wall 1")
        self.w1_header_enable.setChecked(False)
        layout.addRow(self.w1_header_enable)

        self.w1_header_height = QtGui.QDoubleSpinBox()
        self.w1_header_height.setRange(6.0, 48.0)
        self.w1_header_height.setValue(19.0)   # default 19"
        self.w1_header_height.setSuffix(" in")
        layout.addRow("Wall 1 Header Height", self.w1_header_height)

        self.w1_header_length = QtGui.QDoubleSpinBox()
        self.w1_header_length.setRange(12.0, 600.0)
        self.w1_header_length.setValue(144.0)  # default 12 ft
        self.w1_header_length.setSuffix(" in")
        layout.addRow("Wall 1 Header Length", self.w1_header_length)

        # Wall 1 move arrows
        w1_move_layout = QtGui.QHBoxLayout()
        self.w1_up_btn = QtGui.QPushButton("↑")
        self.w1_down_btn = QtGui.QPushButton("↓")
        self.w1_left_btn = QtGui.QPushButton("←")
        self.w1_right_btn = QtGui.QPushButton("→")
        w1_move_layout.addWidget(self.w1_left_btn)
        w1_move_layout.addWidget(self.w1_right_btn)
        w1_move_layout.addWidget(self.w1_up_btn)
        w1_move_layout.addWidget(self.w1_down_btn)
        layout.addRow("Wall 1 Move", w1_move_layout)

        # ---- Wall 2 ----
        layout.addRow(QtGui.QLabel("------ WALL 2 ------"))
        self.w2enable = QtGui.QCheckBox("Enable Wall 2"); self.w2enable.setChecked(True)
        layout.addRow(self.w2enable)

        self.w2spacing = QtGui.QComboBox()
        self.w2spacing.addItem("16 in", 16.0)
        self.w2spacing.addItem("24 in", 24.0)
        layout.addRow("Wall 2 Spacing", self.w2spacing)

        self.w2height = QtGui.QDoubleSpinBox()
        self.w2height.setRange(24.0, 168.0)
        self.w2height.setValue(96.0)
        self.w2height.setSuffix(" in")
        layout.addRow("Wall 2 Height", self.w2height)

        self.w2width = QtGui.QDoubleSpinBox()
        self.w2width.setRange(12.0, 600.0)
        self.w2width.setValue(48.0)
        self.w2width.setSuffix(" in")
        layout.addRow("Wall 2 Width", self.w2width)

        self.w2angle = QtGui.QDoubleSpinBox()
        self.w2angle.setRange(-180.0, 180.0)
        self.w2angle.setValue(0.0)
        self.w2angle.setSuffix(" deg")
        layout.addRow("Wall 2 Rotation", self.w2angle)

        self.w2_header_enable = QtGui.QCheckBox("Enable Header after Wall 2")
        self.w2_header_enable.setChecked(False)
        layout.addRow(self.w2_header_enable)

        self.w2_header_height = QtGui.QDoubleSpinBox()
        self.w2_header_height.setRange(6.0, 48.0)
        self.w2_header_height.setValue(19.0)
        self.w2_header_height.setSuffix(" in")
        layout.addRow("Wall 2 Header Height", self.w2_header_height)

        self.w2_header_length = QtGui.QDoubleSpinBox()
        self.w2_header_length.setRange(12.0, 600.0)
        self.w2_header_length.setValue(144.0)
        self.w2_header_length.setSuffix(" in")
        layout.addRow("Wall 2 Header Length", self.w2_header_length)

        # Wall 2 move arrows
        w2_move_layout = QtGui.QHBoxLayout()
        self.w2_up_btn = QtGui.QPushButton("↑")
        self.w2_down_btn = QtGui.QPushButton("↓")
        self.w2_left_btn = QtGui.QPushButton("←")
        self.w2_right_btn = QtGui.QPushButton("→")
        w2_move_layout.addWidget(self.w2_left_btn)
        w2_move_layout.addWidget(self.w2_right_btn)
        w2_move_layout.addWidget(self.w2_up_btn)
        w2_move_layout.addWidget(self.w2_down_btn)
        layout.addRow("Wall 2 Move", w2_move_layout)

        # ---- Wall 3 ----
        layout.addRow(QtGui.QLabel("------ WALL 3 ------"))
        self.w3enable = QtGui.QCheckBox("Enable Wall 3"); self.w3enable.setChecked(True)
        layout.addRow(self.w3enable)

        self.w3spacing = QtGui.QComboBox()
        self.w3spacing.addItem("16 in", 16.0)
        self.w3spacing.addItem("24 in", 24.0)
        layout.addRow("Wall 3 Spacing", self.w3spacing)

        self.w3height = QtGui.QDoubleSpinBox()
        self.w3height.setRange(24.0, 168.0)
        self.w3height.setValue(96.0)
        self.w3height.setSuffix(" in")
        layout.addRow("Wall 3 Height", self.w3height)

        self.w3width = QtGui.QDoubleSpinBox()
        self.w3width.setRange(12.0, 600.0)
        self.w3width.setValue(48.0)
        self.w3width.setSuffix(" in")
        layout.addRow("Wall 3 Width", self.w3width)

        self.w3angle = QtGui.QDoubleSpinBox()
        self.w3angle.setRange(-180.0, 180.0)
        self.w3angle.setValue(0.0)
        self.w3angle.setSuffix(" deg")
        layout.addRow("Wall 3 Rotation", self.w3angle)

        self.w3_header_enable = QtGui.QCheckBox("Enable Header after Wall 3")
        self.w3_header_enable.setChecked(False)
        layout.addRow(self.w3_header_enable)

        self.w3_header_height = QtGui.QDoubleSpinBox()
        self.w3_header_height.setRange(6.0, 48.0)
        self.w3_header_height.setValue(19.0)
        self.w3_header_height.setSuffix(" in")
        layout.addRow("Wall 3 Header Height", self.w3_header_height)

        self.w3_header_length = QtGui.QDoubleSpinBox()
        self.w3_header_length.setRange(12.0, 600.0)
        self.w3_header_length.setValue(144.0)
        self.w3_header_length.setSuffix(" in")
        layout.addRow("Wall 3 Header Length", self.w3_header_length)

        # Wall 3 move arrows
        w3_move_layout = QtGui.QHBoxLayout()
        self.w3_up_btn = QtGui.QPushButton("↑")
        self.w3_down_btn = QtGui.QPushButton("↓")
        self.w3_left_btn = QtGui.QPushButton("←")
        self.w3_right_btn = QtGui.QPushButton("→")
        w3_move_layout.addWidget(self.w3_left_btn)
        w3_move_layout.addWidget(self.w3_right_btn)
        w3_move_layout.addWidget(self.w3_up_btn)
        w3_move_layout.addWidget(self.w3_down_btn)
        layout.addRow("Wall 3 Move", w3_move_layout)

        # Close button
        closebtn = QtGui.QPushButton("Close")
        closebtn.clicked.connect(self.close)
        layout.addRow(closebtn)

        # Wire up live updates and movement handlers
        self._connect_live()
        self._connect_moves()

        self.update_model()

    def _connect_live(self):
        controls = [
            self.w1enable, self.w1spacing, self.w1height, self.w1width, self.w1angle,
            self.w1_header_enable, self.w1_header_height, self.w1_header_length,

            self.w2enable, self.w2spacing, self.w2height, self.w2width, self.w2angle,
            self.w2_header_enable, self.w2_header_height, self.w2_header_length,

            self.w3enable, self.w3spacing, self.w3height, self.w3width, self.w3angle,
            self.w3_header_enable, self.w3_header_height, self.w3_header_length,
        ]

        for c in controls:
            if hasattr(c, "stateChanged"):
                c.stateChanged.connect(self.update_model)
            elif hasattr(c, "currentIndexChanged"):
                c.currentIndexChanged.connect(self.update_model)
            elif hasattr(c, "valueChanged"):
                c.valueChanged.connect(self.update_model)

    def _connect_moves(self):
        step_mm = inch_to_mm(MOVE_STEP_IN)

        # Wall 1
        self.w1_left_btn.clicked.connect(lambda: self._move_wall(1, -step_mm, 0.0))
        self.w1_right_btn.clicked.connect(lambda: self._move_wall(1, step_mm, 0.0))
        self.w1_up_btn.clicked.connect(lambda: self._move_wall(1, 0.0, step_mm))
        self.w1_down_btn.clicked.connect(lambda: self._move_wall(1, 0.0, -step_mm))

        # Wall 2
        self.w2_left_btn.clicked.connect(lambda: self._move_wall(2, -step_mm, 0.0))
        self.w2_right_btn.clicked.connect(lambda: self._move_wall(2, step_mm, 0.0))
        self.w2_up_btn.clicked.connect(lambda: self._move_wall(2, 0.0, step_mm))
        self.w2_down_btn.clicked.connect(lambda: self._move_wall(2, 0.0, -step_mm))

        # Wall 3
        self.w3_left_btn.clicked.connect(lambda: self._move_wall(3, -step_mm, 0.0))
        self.w3_right_btn.clicked.connect(lambda: self._move_wall(3, step_mm, 0.0))
        self.w3_up_btn.clicked.connect(lambda: self._move_wall(3, 0.0, step_mm))
        self.w3_down_btn.clicked.connect(lambda: self._move_wall(3, 0.0, -step_mm))

    def _move_wall(self, idx, dx, dy):
        if idx == 1:
            self.w1_dx += dx
            self.w1_dy += dy
        elif idx == 2:
            self.w2_dx += dx
            self.w2_dy += dy
        elif idx == 3:
            self.w3_dx += dx
            self.w3_dy += dy
        self.update_model()

    def update_model(self, *args):
        doc = FreeCAD.ActiveDocument
        if doc is None:
            doc = FreeCAD.newDocument("WallModuleDoc")

        # Delete all existing wall geometry
        clear_old_walls(doc)

        current_x = 0.0

        # Track bases for transformation per wall index
        # bases[idx] = (name, base_vector, width_mm, wall_height_in)
        bases = {}

        def maybe_build(idx,
                        enable,
                        spacing,
                        height,
                        width,
                        header_enable,
                        header_height_ctrl,
                        header_length_ctrl):
            nonlocal current_x
            if not enable:
                return
            s = spacing.itemData(spacing.currentIndex())
            h = height.value()
            w = width.value()
            name = f"Wall{idx}"
            base = FreeCAD.Vector(current_x, 0, 0)
            width_mm = build_wall_module_2x6(name, s, h, w, base)
            bases[idx] = (name, base, width_mm, h)
            current_x += width_mm

            # Optional header for this wall
            if header_enable:
                header_h_in = header_height_ctrl.value()
                header_len_in = header_length_ctrl.value()
                header_len_mm = build_header_for_wall(
                    name,
                    header_h_in,
                    header_len_in,
                    h,
                    base,
                    width_mm
                )
                current_x += header_len_mm

        # Build walls + optional headers in sequence (unrotated, un-offset)
        maybe_build(
            1,
            self.w1enable.isChecked(),
            self.w1spacing,
            self.w1height,
            self.w1width,
            self.w1_header_enable.isChecked(),
            self.w1_header_height,
            self.w1_header_length
        )
        maybe_build(
            2,
            self.w2enable.isChecked(),
            self.w2spacing,
            self.w2height,
            self.w2width,
            self.w2_header_enable.isChecked(),
            self.w2_header_height,
            self.w2_header_length
        )
        maybe_build(
            3,
            self.w3enable.isChecked(),
            self.w3spacing,
            self.w3height,
            self.w3width,
            self.w3_header_enable.isChecked(),
            self.w3_header_height,
            self.w3_header_length
        )

        # Now apply per-wall rotation + translation about each wall's base point
        if 1 in bases:
            name, base, _, _ = bases[1]
            offset_vec = FreeCAD.Vector(self.w1_dx, self.w1_dy, 0)
            transform_wall_group(doc, name, base, self.w1angle.value(), offset_vec)

        if 2 in bases:
            name, base, _, _ = bases[2]
            offset_vec = FreeCAD.Vector(self.w2_dx, self.w2_dy, 0)
            transform_wall_group(doc, name, base, self.w2angle.value(), offset_vec)

        if 3 in bases:
            name, base, _, _ = bases[3]
            offset_vec = FreeCAD.Vector(self.w3_dx, self.w3_dy, 0)
            transform_wall_group(doc, name, base, self.w3angle.value(), offset_vec)


def main():
    mw = FreeCADGui.getMainWindow()
    dlg = ThreeWallDialog(mw)
    dlg.show()

main()

