import { ocRef } from './oc.js';

export function create3DModel(positions, config) {
    const oc = ocRef.instance;
    if (!oc || positions.length === 0) return null;

    const {
        cellSize,
        spacing,
        height,
        terminalDiameter,
        terminalDepth,
        coverThickness,
        ledgeWidth,
        roundedCorners,
        bmsHoles,
        useTabs,
        useFullCircles,
        filletBms,
        tabOverlapSide,
        layoutType = 'honeycomb',
    } = config;

    const r = cellSize / 2;

    const minX = Math.min(...positions.map(p => p[0])) - r - spacing;
    const minY = Math.min(...positions.map(p => p[1])) - r - spacing;
    const maxX = Math.max(...positions.map(p => p[0])) + r + spacing;
    const maxY = Math.max(...positions.map(p => p[1])) + r + spacing;

    const centerX = (minX + maxX) / 2;
    const centerY = (minY + maxY) / 2;

    const adjusted = positions.map(([x, y]) => [x - centerX, y - centerY]);

    const width = maxX - minX;
    const length = maxY - minY;

    const perfStart = performance.now();
    let perfLast = perfStart;
    const logTime = (label) => {
        const now = performance.now();
        perfLast = now;
    };

    const boxMaker = new oc.BRepPrimAPI_MakeBox(width, length, height);
    let base = boxMaker.Shape();
    const translation = new oc.gp_Trsf();
    translation.SetTranslation(new oc.gp_Vec(-width / 2, -length / 2, 0));
    const transform = new oc.BRepBuilderAPI_Transform(base, translation, false);
    base = transform.Shape();

    if (roundedCorners) {
        try {
            const cornerRadius = 5.0;

            const forEachEdge = (shape, callback) => {
                const edgeHashes = {};
                let edgeIndex = 0;
                const anExplorer = new oc.TopExp_Explorer(shape, oc.TopAbs_EDGE);
                for (anExplorer.Init(shape, oc.TopAbs_EDGE); anExplorer.More(); anExplorer.Next()) {
                    const edge = oc.TopoDS.prototype.Edge(anExplorer.Current());
                    const edgeHash = edge.HashCode(100000000);
                    if (!edgeHashes.hasOwnProperty(edgeHash)) {
                        edgeHashes[edgeHash] = edgeIndex;
                        callback(edgeIndex++, edge);
                    }
                }
                return edgeHashes;
            };

            const verticalEdgeIndices = [];
            forEachEdge(base, (index, edge) => {
                try {
                    const bbox = new oc.Bnd_Box();
                    oc.BRepBndLib.prototype.Add(edge, bbox, false);
                    const bboxMin = bbox.CornerMin();
                    const bboxMax = bbox.CornerMax();
                    const dx = Math.abs(bboxMax.X() - bboxMin.X());
                    const dy = Math.abs(bboxMax.Y() - bboxMin.Y());
                    const dz = Math.abs(bboxMax.Z() - bboxMin.Z());
                    if (dx < 1.0 && dy < 1.0 && dz > height * 0.8) {
                        verticalEdgeIndices.push(index);
                    }
                } catch (e) { /* ignore */ }
            });

            if (verticalEdgeIndices.length > 0) {
                const mkFillet = new oc.BRepFilletAPI_MakeFillet(base);
                let edgeCount = 0;
                forEachEdge(base, (index, edge) => {
                    if (verticalEdgeIndices.includes(index)) {
                        try {
                            mkFillet.Add(cornerRadius, edge);
                            edgeCount++;
                        } catch (e) {
                            console.error(`    Failed to add edge ${index}:`, e.message);
                        }
                    }
                });
                if (edgeCount > 0) {
                    base = new oc.TopoDS_Solid(mkFillet.Shape());
                    console.log('  Applied rounded corners');
                    logTime('Rounded corners');
                }
            } else {
                console.log('  No vertical edges found to fillet');
            }
        } catch (e) {
            console.error('  Fillet operation failed:', e.message);
            console.log('  Continuing without rounded corners');
        }
    }

    // Build cell hole cutters.
    // When a ledge is requested (ledgeWidth > 0 || coverThickness > 0) we use a
    // two-step bore so the ledge ring stays part of the base body:
    //   • outer bore  (radius r)               from z=coverThickness to z=height
    //   • inner bore  (radius r - ledgeWidth)  from z=0             to z=height
    const hasLedge = (ledgeWidth > 0 || coverThickness > 0);

    if (!hasLedge) {
        const cutters = adjusted.map(([x, y]) => {
            const ax = new oc.gp_Ax2(new oc.gp_Pnt(x, y, 0), oc.gp.prototype.DZ());
            return new oc.BRepPrimAPI_MakeCylinder(ax, r, height).Shape();
        });
        let allCutters = cutters[0];
        for (let i = 1; i < cutters.length; i++) {
            allCutters = new oc.BRepAlgoAPI_Fuse(allCutters, cutters[i]).Shape();
        }
        base = new oc.BRepAlgoAPI_Cut(base, allCutters).Shape();
    } else {
        const outerZ = coverThickness;
        const outerH = height - coverThickness;
        const innerR = Math.max(0.1, r - ledgeWidth);

        const outerCutters = adjusted.map(([x, y]) => {
            const ax = new oc.gp_Ax2(new oc.gp_Pnt(x, y, outerZ), oc.gp.prototype.DZ());
            return new oc.BRepPrimAPI_MakeCylinder(ax, r, outerH).Shape();
        });
        let allOuter = outerCutters[0];
        for (let i = 1; i < outerCutters.length; i++) {
            allOuter = new oc.BRepAlgoAPI_Fuse(allOuter, outerCutters[i]).Shape();
        }
        base = new oc.BRepAlgoAPI_Cut(base, allOuter).Shape();

        const innerCutters = adjusted.map(([x, y]) => {
            const ax = new oc.gp_Ax2(new oc.gp_Pnt(x, y, 0), oc.gp.prototype.DZ());
            return new oc.BRepPrimAPI_MakeCylinder(ax, innerR, height).Shape();
        });
        let allInner = innerCutters[0];
        for (let i = 1; i < innerCutters.length; i++) {
            allInner = new oc.BRepAlgoAPI_Fuse(allInner, innerCutters[i]).Shape();
        }
        base = new oc.BRepAlgoAPI_Cut(base, allInner).Shape();
    }
    console.log('  Cell holes cut' + (hasLedge ? ' (with integrated ledge)' : ''));
    logTime('Cell holes');

    if (bmsHoles) {
        const holeDiameter = config.bmsHoleDiameter || 4.0;

        let holeYTop, holeYBottom;
        if (useFullCircles) {
            holeYTop = null;
            holeYBottom = null;
        } else {
            holeYTop = length / 2;
            holeYBottom = -length / 2;
        }

        const rows = {};
        adjusted.forEach(([x, y]) => {
            const yKey = Math.round(y * 1000);
            if (!rows[yKey]) rows[yKey] = [];
            rows[yKey].push([x, y]);
        });

        const rowKeys = Object.keys(rows).map(k => parseInt(k)).sort((a, b) => b - a);
        const topYKey = rowKeys[0];
        const bottomYKey = rowKeys[rowKeys.length - 1];

        if (useFullCircles) {
            const topCellY = rows[topYKey][0][1];
            const bottomCellY = rows[bottomYKey][0][1];
            const wallTop = length / 2;
            const wallBottom = -length / 2;

            const solveEquilateral3D = (wallY, cellY, x1, x2) => {
                const xMid = (x1 + x2) / 2;
                const flip = cellY < wallY ? -1 : 1;
                let lo = flip > 0 ? -Math.PI / 2 : 0;
                let hi = flip > 0 ? 0 : Math.PI / 2;
                for (let i = 0; i < 80; i++) {
                    const alpha = (lo + hi) / 2;
                    const Bx = x1 + r * Math.cos(alpha);
                    const By = cellY + r * Math.sin(alpha);
                    const d = xMid - Bx;
                    const h = (By - wallY) * flip;
                    const diff = h - d * Math.sqrt(3);
                    if (Math.abs(diff) < 1e-8) break;
                    if (diff < 0) { flip > 0 ? (lo = alpha) : (hi = alpha); }
                    else          { flip > 0 ? (hi = alpha) : (lo = alpha); }
                }
                const alpha = (lo + hi) / 2;
                const By = cellY + r * Math.sin(alpha);
                return (wallY + 2 * By) / 3;
            };

            holeYTop    = solveEquilateral3D(wallTop,    topCellY,    rows[topYKey][0][0],    rows[topYKey][1][0]);
            holeYBottom = solveEquilateral3D(wallBottom, bottomCellY, rows[bottomYKey][0][0], rows[bottomYKey][1][0]);
        }

        const topRow = rows[topYKey].sort((a, b) => a[0] - b[0]);
        const bottomRow = rows[bottomYKey].sort((a, b) => a[0] - b[0]);

        const minAllX = Math.min(...positions.map(([x]) => x));

        // Vertical column pitch: minimum X delta between any two cells
        const _allXSorted = [...new Set(positions.map(([x]) => Math.round(x * 1000)))]
            .sort((a, b) => a - b).map(v => v / 1000);
        const vertColPitch = _allXSorted.length >= 2 ? _allXSorted[1] - _allXSorted[0] : 0;

        const topPitch = topRow.length >= 2 ? topRow[topRow.length - 1][0] - topRow[topRow.length - 2][0] : 0;
        const topHoles = [];
        for (let i = 0; i < topRow.length - 1; i++) {
            topHoles.push([(topRow[i][0] + topRow[i + 1][0]) / 2, holeYTop]);
        }
        if (layoutType === 'vertical') {
            const topIsEven = vertColPitch === 0 || (topRow[0][0] - minAllX) < vertColPitch / 2;
            if (!topIsEven) {
                topHoles.unshift([topRow[0][0] - vertColPitch / 2, holeYTop]);
                topHoles.push([topRow[topRow.length - 1][0] + vertColPitch / 2, holeYTop]);
            }
        } else if (layoutType !== 'grid') {
            const topExtraRight = topRow.length >= 2 && (topRow[0][0] - minAllX) < topPitch / 4;
            if (topExtraRight) {
                topHoles.push([topRow[topRow.length - 1][0] + topPitch / 2, holeYTop]);
            } else {
                topHoles.unshift([topRow[0][0] - topPitch / 2, holeYTop]);
            }
        }

        const bottomPitch = bottomRow.length >= 2 ? bottomRow[bottomRow.length - 1][0] - bottomRow[bottomRow.length - 2][0] : 0;
        const bottomHoles = [];
        for (let i = 0; i < bottomRow.length - 1; i++) {
            bottomHoles.push([(bottomRow[i][0] + bottomRow[i + 1][0]) / 2, holeYBottom]);
        }
        if (layoutType === 'vertical') {
            const bottomIsEven = vertColPitch === 0 || (bottomRow[0][0] - minAllX) < vertColPitch / 2;
            if (!bottomIsEven) {
                bottomHoles.unshift([bottomRow[0][0] - vertColPitch / 2, holeYBottom]);
                bottomHoles.push([bottomRow[bottomRow.length - 1][0] + vertColPitch / 2, holeYBottom]);
            }
        } else if (layoutType !== 'grid') {
            const bottomExtraRight = bottomRow.length >= 2 && (bottomRow[0][0] - minAllX) < bottomPitch / 4;
            if (bottomExtraRight) {
                bottomHoles.push([bottomRow[bottomRow.length - 1][0] + bottomPitch / 2, holeYBottom]);
            } else {
                bottomHoles.unshift([bottomRow[0][0] - bottomPitch / 2, holeYBottom]);
            }
        }

        const allBmsHoles = [...topHoles, ...bottomHoles];

        if (useTabs) {
            const slotWidth = config.tabWidth || holeDiameter;
            const slotInset = config.tabDepth || 1.0;
            const slotHeight = Number.isFinite(config.tabLength) && config.tabLength > 0
                ? Math.min(config.tabLength, height)
                : height;
            const topEdgeY = length / 2;
            const bottomEdgeY = -length / 2;

            const allSlots = [];

            topHoles.forEach(([xPos]) => {
                const slotBox = new oc.BRepPrimAPI_MakeBox(slotWidth, slotInset, slotHeight);
                const slot = slotBox.Shape();
                const trans = new oc.gp_Trsf();
                trans.SetTranslation(new oc.gp_Vec(xPos - slotWidth / 2, topEdgeY - slotInset, height - slotHeight));
                const slotTransform = new oc.BRepBuilderAPI_Transform(slot, trans, false);
                allSlots.push(slotTransform.Shape());
            });

            bottomHoles.forEach(([xPos]) => {
                const slotBox = new oc.BRepPrimAPI_MakeBox(slotWidth, slotInset, slotHeight);
                const slot = slotBox.Shape();
                const trans = new oc.gp_Trsf();
                trans.SetTranslation(new oc.gp_Vec(xPos - slotWidth / 2, bottomEdgeY, height - slotHeight));
                const slotTransform = new oc.BRepBuilderAPI_Transform(slot, trans, false);
                allSlots.push(slotTransform.Shape());
            });

            if (allSlots.length > 0) {
                if (allSlots.length === 1) {
                    base = new oc.BRepAlgoAPI_Cut(base, allSlots[0]).Shape();
                } else {
                    let compound = allSlots[0];
                    for (let i = 1; i < allSlots.length; i++) {
                        compound = new oc.BRepAlgoAPI_Fuse(compound, allSlots[i]).Shape();
                    }
                    base = new oc.BRepAlgoAPI_Cut(base, compound).Shape();
                }
            }
            logTime('Edge tabs');
        } else {
            if (allBmsHoles.length <= 10) {
                allBmsHoles.forEach(([x, y]) => {
                    const cylinderAxis = new oc.gp_Ax2(new oc.gp_Pnt(x, y, 0), oc.gp.prototype.DZ());
                    const cylinder = new oc.BRepPrimAPI_MakeCylinder(cylinderAxis, holeDiameter / 2, height).Shape();
                    base = new oc.BRepAlgoAPI_Cut(base, cylinder).Shape();
                });
            } else {
                const bmsCylinders = allBmsHoles.map(([x, y]) => {
                    const cylinderAxis = new oc.gp_Ax2(new oc.gp_Pnt(x, y, 0), oc.gp.prototype.DZ());
                    return new oc.BRepPrimAPI_MakeCylinder(cylinderAxis, holeDiameter / 2, height).Shape();
                });

                let compound = bmsCylinders[0];
                for (let i = 1; i < bmsCylinders.length; i++) {
                    compound = new oc.BRepAlgoAPI_Fuse(compound, bmsCylinders[i]).Shape();
                }
                base = new oc.BRepAlgoAPI_Cut(base, compound).Shape();
            }

            logTime('BMS holes');

            if (filletBms && allBmsHoles.length > 0) {
                try {
                    console.log('  Filleting BMS hole edges...');
                    const bmsFilletRadius = 0.5;

                    const forEachEdge = (shape, callback) => {
                        const edgeHashes = {};
                        let edgeIndex = 0;
                        const anExplorer = new oc.TopExp_Explorer(shape, oc.TopAbs_EDGE);
                        for (anExplorer.Init(shape, oc.TopAbs_EDGE); anExplorer.More(); anExplorer.Next()) {
                            const edge = oc.TopoDS.prototype.Edge(anExplorer.Current());
                            const edgeHash = edge.HashCode(100000000);
                            if (!edgeHashes.hasOwnProperty(edgeHash)) {
                                edgeHashes[edgeHash] = edgeIndex;
                                callback(edgeIndex++, edge);
                            }
                        }
                        return edgeHashes;
                    };

                    const bmsEdgeIndices = [];

                    forEachEdge(base, (index, edge) => {
                        try {
                            const bbox = new oc.Bnd_Box();
                            oc.BRepBndLib.prototype.Add(edge, bbox, false);
                            const bboxMin = bbox.CornerMin();
                            const bboxMax = bbox.CornerMax();
                            const centerX = (bboxMin.X() + bboxMax.X()) / 2;
                            const centerY = (bboxMin.Y() + bboxMax.Y()) / 2;
                            const centerZ = (bboxMin.Z() + bboxMax.Z()) / 2;

                            for (const [holeX, holeY] of allBmsHoles) {
                                const distXY = Math.hypot(centerX - holeX, centerY - holeY);
                                const isNearHole = distXY < holeDiameter;
                                const isAtTopOrBottom = Math.abs(centerZ - 0) < 1.0 || Math.abs(centerZ - height) < 1.0;
                                if (isNearHole && isAtTopOrBottom) {
                                    bmsEdgeIndices.push(index);
                                    break;
                                }
                            }
                        } catch (e) { /* ignore */ }
                    });

                    if (bmsEdgeIndices.length > 0) {
                        const mkFillet = new oc.BRepFilletAPI_MakeFillet(base);
                        let edgeCount = 0;
                        forEachEdge(base, (index, edge) => {
                            if (bmsEdgeIndices.includes(index)) {
                                try {
                                    mkFillet.Add(bmsFilletRadius, edge);
                                    edgeCount++;
                                } catch (e) {
                                    console.error(`    Failed to add BMS edge ${index}`);
                                }
                            }
                        });
                        if (edgeCount > 0) {
                            base = new oc.TopoDS_Solid(mkFillet.Shape());
                            console.log(`  Applied ${bmsFilletRadius}mm fillet to ${edgeCount} BMS hole edges`);
                        }
                    }
                } catch (e) {
                    console.error('  BMS fillet failed:', e.message);
                    console.log('  Continuing without BMS filleting');
                }
            }
        }
    }

    if (adjusted.length <= 10) {
        adjusted.forEach(([x, y]) => {
            const cylinderAxis = new oc.gp_Ax2(new oc.gp_Pnt(x, y, height - terminalDepth), oc.gp.prototype.DZ());
            const cylinder = new oc.BRepPrimAPI_MakeCylinder(cylinderAxis, terminalDiameter / 2, terminalDepth).Shape();
            base = new oc.BRepAlgoAPI_Cut(base, cylinder).Shape();
        });
    } else {
        const terminals = adjusted.map(([x, y]) => {
            const cylinderAxis = new oc.gp_Ax2(new oc.gp_Pnt(x, y, height - terminalDepth), oc.gp.prototype.DZ());
            return new oc.BRepPrimAPI_MakeCylinder(cylinderAxis, terminalDiameter / 2, terminalDepth).Shape();
        });

        const batchSize = 30;
        const batches = [];

        for (let i = 0; i < terminals.length; i += batchSize) {
            const end = Math.min(i + batchSize, terminals.length);
            let batch = terminals[i];
            for (let j = i + 1; j < end; j++) {
                batch = new oc.BRepAlgoAPI_Fuse(batch, terminals[j]).Shape();
            }
            batches.push(batch);
        }

        let allTerminals = batches[0];
        for (let i = 1; i < batches.length; i++) {
            allTerminals = new oc.BRepAlgoAPI_Fuse(allTerminals, batches[i]).Shape();
        }

        base = new oc.BRepAlgoAPI_Cut(base, allTerminals).Shape();
    }

    console.log('  Terminal recesses cut');
    logTime('Terminal recesses');

    if (hasLedge) {
        console.log('  Ledge integrated into cell holes');
    }
    logTime('Ledge rings');
    logTime('TOTAL GENERATION');

    return base;
}
