import FreeCAD as App
import FreeCADGui as Gui
from fractions import Fraction

doc = App.ActiveDocument

# Conversion factor mm -> inches
MM_TO_INCH = 1.0 / 25.4

# Standard lumber sizes in inches
LUMBER_SIZES = {
    (1.50, 3.50): "2x4",
    (1.50, 5.50): "2x6",
    (1.50, 7.25): "2x8",
    (1.50, 9.25): "2x10",
    (1.50, 11.25): "2x12"
}

# Standard sheet thicknesses in inches
SHEET_THICKNESSES = {
    0.344: ("OSB", "11/32 OSB"),
    0.438: ("OSB", "7/16 OSB"),
    0.500: ("PLYWOOD", "1/2 Plywood"),
    0.625: ("OSB", "5/8 OSB"),
    0.750: ("PLYWOOD", "3/4 Plywood")
}

# Convert decimal inches to fractional string
def to_fraction_string(value):
    whole = int(value)
    frac = Fraction(value - whole).limit_denominator(16)
    if frac.numerator == 0:
        return str(whole)
    return "{} {}/{}".format(whole, frac.numerator, frac.denominator)

# Detect sheet material by name or thickness
def detect_sheet_material(name, thickness):
    name = name.lower()
    if "osb" in name:
        return True, "OSB", None
    if "ply" in name or "plywood" in name:
        return True, "PLYWOOD", None
    for t, (mat_type, disp) in SHEET_THICKNESSES.items():
        if abs(thickness - t) < 0.03:
            return True, mat_type, disp
    return False, None, None

# Check if object is a container (Compound / MultiFuse / MultiCommon / assembly)
def is_container(obj):
    t = obj.TypeId
    if "Compound" in t:
        return True
    if "MultiFuse" in t:
        return True
    if "MultiCommon" in t:
        return True
    if "FeaturePython" in t:  # possible assembly
        return True
    return False

# Collect only standalone parts
parts = []
for obj in doc.Objects:
    if not hasattr(obj, "Shape"):
        continue
    # Skip container objects
    if is_container(obj):
        continue
    # Skip children of containers
    if hasattr(obj, "InList"):
        skip = False
        for parent in obj.InList:
            if is_container(parent):
                skip = True
                break
        if skip:
            continue
    # Otherwise include
    parts.append(obj)

# Create or clear spreadsheet
if not hasattr(doc, "CutList"):
    sheet = doc.addObject("Spreadsheet::Sheet", "CutList")
else:
    sheet = doc.CutList
    sheet.clearAll()

# Header row
sheet.set("A1", "Part Names")
sheet.set("B1", "Type / Size")
sheet.set("C1", "Dimensions (in)")
sheet.set("D1", "Qty")

# Collect parts into dictionary
cut_dict = {}

for obj in parts:
    bb = obj.Shape.BoundBox
    dims = sorted([bb.XLength * MM_TO_INCH, bb.YLength * MM_TO_INCH, bb.ZLength * MM_TO_INCH])
    thickness = round(dims[0], 3)
    width = round(dims[1], 3)
    length = round(dims[2], 3)

    # Sheet material detection
    is_sheet, mat_type, disp_name = detect_sheet_material(obj.Label, thickness)

    if is_sheet:
        typename = disp_name if disp_name else mat_type
        dim_text = "{} x {}".format(to_fraction_string(width), to_fraction_string(length))
        key = (typename, dim_text)
    else:
        t_key = round(thickness, 2)
        w_key = round(width, 2)
        if (t_key, w_key) in LUMBER_SIZES:
            typename = LUMBER_SIZES[(t_key, w_key)]
            dim_text = to_fraction_string(length)
        else:
            typename = "NON-STANDARD (DO NOT CUT)"
            dim_text = "{} x {}".format(to_fraction_string(width), to_fraction_string(length))
        key = (typename, dim_text)

    # Store names and quantity
    if key not in cut_dict:
        cut_dict[key] = {"Qty": 0, "Names": []}
    cut_dict[key]["Qty"] += 1
    cut_dict[key]["Names"].append(obj.Name)

# Write to spreadsheet
row = 2
for (typename, dim_text), data in sorted(cut_dict.items(), key=lambda x: x[0][0]):
    name_list = ", ".join(data["Names"])
    sheet.set("A{}".format(row), name_list)
    sheet.set("B{}".format(row), typename)
    sheet.set("C{}".format(row), dim_text)
    sheet.set("D{}".format(row), str(data["Qty"]))
    row += 1

doc.recompute()
print("Cut list updated with grouped duplicates, part names, and non-standard marking.")
