/**
 * FolderManager — папки/группы в иерархии сцены.
 *
 * Папка — логическое объединение объектов (блоков/моделей/примитивов).
 * Объект помещается в папку записью `folderId` в его metadata
 * (через FolderManager.assignToFolder).
 *
 * Структура папки:
 *   { id, name, parentId, visible }
 *
 * parentId === null → папка в корне.
 * folderId === null → объект в корне сцены.
 *
 * Public API:
 *   createFolder(name, parentId?)        → id
 *   renameFolder(id, name)
 *   removeFolder(id, deleteContent?)     → удаляет папку (и контент опционально)
 *   setVisible(id, visible)              — скрыть/показать вместе с объектами
 *   assignToFolder(kind, ref, folderId)  — переместить блок/модель/прим в папку
 *   getFolder(id) / getAll()
 *   getChildrenOf(folderId)              — { folders, blocks, models, primitives }
 *   getRootChildren()                    — что в корне (folderId === null)
 *   serialize() / loadFromArray(arr)
 */

export class FolderManager {
    constructor(blockManager, modelManager, primitiveManager) {
        this.blockManager = blockManager;
        this.modelManager = modelManager;
        this.primitiveManager = primitiveManager;
        this.folders = new Map();   // id → folder data
        this._nextId = 1;
        this._onChange = null;
    }

    setOnChange(cb) { this._onChange = cb; }
    _notifyChange() { if (this._onChange) this._onChange(); }

    createFolder(name = 'Новая папка', parentId = null) {
        const id = this._nextId++;
        this.folders.set(id, { id, name, parentId, visible: true });
        this._notifyChange();
        return id;
    }

    getFolder(id) {
        return this.folders.get(id) || null;
    }

    getAll() {
        return Array.from(this.folders.values());
    }

    renameFolder(id, name) {
        const f = this.folders.get(id);
        if (!f) return;
        f.name = name;
        this._notifyChange();
    }

    /**
     * Удалить папку. Если deleteContent=true — содержимое стирается со сцены,
     * иначе содержимое поднимается в parent папку (или в корень).
     */
    removeFolder(id, deleteContent = false) {
        const f = this.folders.get(id);
        if (!f) return;

        // Сначала рекурсивно: подпапки.
        const childFolders = this.getAll().filter(x => x.parentId === id);
        for (const cf of childFolders) {
            if (deleteContent) this.removeFolder(cf.id, true);
            else this._setFolderParent(cf.id, f.parentId);
        }

        // Объекты в папке — в parent или удалить.
        if (this.blockManager) {
            for (const mesh of this.blockManager.blocks.values()) {
                if (mesh.metadata?.folderId === id) {
                    if (deleteContent) {
                        this.blockManager.removeBlock(mesh.metadata.gridX, mesh.metadata.gridY, mesh.metadata.gridZ);
                    } else {
                        mesh.metadata.folderId = f.parentId;
                    }
                }
            }
        }
        if (this.modelManager) {
            for (const data of this.modelManager.instances.values()) {
                if (data.folderId === id) {
                    if (deleteContent) this.modelManager.removeInstance(data.instanceId);
                    else data.folderId = f.parentId;
                }
            }
        }
        if (this.primitiveManager) {
            for (const data of this.primitiveManager.instances.values()) {
                if (data.folderId === id) {
                    if (deleteContent) this.primitiveManager.removeInstance(data.id);
                    else data.folderId = f.parentId;
                }
            }
        }

        this.folders.delete(id);
        this._notifyChange();
    }

    _setFolderParent(id, parentId) {
        const f = this.folders.get(id);
        if (!f) return;
        f.parentId = parentId;
    }

    /** Поменять видимость папки (рекурсивно вместе с подпапками и контентом). */
    setVisible(id, visible) {
        const f = this.folders.get(id);
        if (!f) return;
        f.visible = visible;
        this._applyVisibility(id, this._effectiveVisible(id));
        this._notifyChange();
    }

    /** Эффективная видимость = AND по цепочке родителей. */
    _effectiveVisible(folderId) {
        let cur = this.folders.get(folderId);
        while (cur) {
            if (!cur.visible) return false;
            cur = cur.parentId != null ? this.folders.get(cur.parentId) : null;
        }
        return true;
    }

    /** Применить видимость к содержимому (включая вложенные папки). */
    _applyVisibility(folderId, visible) {
        // Объекты в этой папке
        if (this.blockManager) {
            for (const mesh of this.blockManager.blocks.values()) {
                if (mesh.metadata?.folderId === folderId) {
                    // Для proxy-блоков (thin instance) — через setBlockProps
                    if (mesh._isBlockProxy) {
                        this.blockManager.setBlockProps(
                            mesh.metadata.gridX, mesh.metadata.gridY, mesh.metadata.gridZ,
                            { visible }
                        );
                    } else if (typeof mesh.setEnabled === 'function') {
                        mesh.setEnabled(visible);
                    }
                }
            }
        }
        if (this.modelManager) {
            for (const data of this.modelManager.instances.values()) {
                if (data.folderId === folderId && data.rootMesh) {
                    data.rootMesh.setEnabled(visible);
                }
            }
        }
        if (this.primitiveManager) {
            for (const data of this.primitiveManager.instances.values()) {
                if (data.folderId === folderId && data.mesh) {
                    // Учитываем собственный visible-флаг примитива
                    data.mesh.setEnabled(visible && data.visible !== false);
                }
            }
        }
        // Подпапки
        for (const cf of this.getAll()) {
            if (cf.parentId === folderId) {
                this._applyVisibility(cf.id, visible && cf.visible);
            }
        }
    }

    /**
     * Поместить объект в папку (или вынуть в корень при folderId=null).
     * kind: 'block' | 'model' | 'primitive'
     * ref:
     *   block     → {x,y,z}
     *   model     → instanceId
     *   primitive → primitive id
     */
    assignToFolder(kind, ref, folderId) {
        if (kind === 'block' && this.blockManager) {
            const mesh = this.blockManager.blocks.get(`${ref.x},${ref.y},${ref.z}`);
            if (mesh) {
                if (!mesh.metadata) mesh.metadata = {};
                mesh.metadata.folderId = folderId;
                mesh.setEnabled(folderId == null ? true : this._effectiveVisible(folderId));
            }
        } else if (kind === 'model' && this.modelManager) {
            const data = this.modelManager.instances.get(ref);
            if (data) {
                data.folderId = folderId;
                if (data.rootMesh) data.rootMesh.setEnabled(folderId == null ? true : this._effectiveVisible(folderId));
            }
        } else if (kind === 'primitive' && this.primitiveManager) {
            const data = this.primitiveManager.instances.get(ref);
            if (data) {
                data.folderId = folderId;
                if (data.mesh) {
                    const folderVisible = folderId == null ? true : this._effectiveVisible(folderId);
                    data.mesh.setEnabled(folderVisible && data.visible !== false);
                }
            }
        }
        this._notifyChange();
    }

    /**
     * Повернуть все примитивы папки вокруг точки pivot на угол angle (рад) по Y.
     * Только примитивы (блоки и модели не вращаем — у блоков нет ротации,
     * для моделей удобнее делать через ModelManager напрямую).
     * pivot: {x, z} — точка вокруг которой вращаем (обычно центр группы).
     * Возвращает количество повёрнутых примитивов.
     */
    rotateFolderY(folderId, angle, pivot) {
        if (!pivot) return 0;
        const cosA = Math.cos(angle);
        const sinA = Math.sin(angle);
        let count = 0;
        // Примитивы папки.
        if (this.primitiveManager) {
            for (const data of this.primitiveManager.instances.values()) {
                if (data.folderId !== folderId) continue;
                const dx = data.x - pivot.x;
                const dz = data.z - pivot.z;
                data.x = pivot.x + dx * cosA - dz * sinA;
                data.z = pivot.z + dx * sinA + dz * cosA;
                data.rotationY = (data.rotationY || 0) + angle;
                if (data.mesh) {
                    data.mesh.position.set(data.x, data.y, data.z);
                    data.mesh.rotation.y = data.rotationY;
                    if (data._worldMatrixFrozen) {
                        try { data.mesh.unfreezeWorldMatrix?.(); } catch (e) {}
                        data._worldMatrixFrozen = false;
                    }
                }
                count++;
            }
        }
        // Модели папки (позиция вокруг pivot + собственный поворот).
        if (this.modelManager) {
            const Vec = this.modelManager._Vector3 || null;
            for (const data of this.modelManager.instances.values()) {
                if (data.folderId !== folderId) continue;
                const dx = data.x - pivot.x;
                const dz = data.z - pivot.z;
                data.x = pivot.x + dx * cosA - dz * sinA;
                data.z = pivot.z + dx * sinA + dz * cosA;
                data.rotationY = (data.rotationY || 0) + angle;
                const root = data.rootMesh || data.rootNode;
                if (root) {
                    if (data._worldMatrixFrozen) { try { root.unfreezeWorldMatrix?.(); } catch (e) {} data._worldMatrixFrozen = false; }
                    root.position.set(data.x, data.y, data.z);
                    if (root.rotation) root.rotation.y = data.rotationY;
                }
                count++;
            }
        }
        this._notifyChange();
        return count;
    }

    /**
     * Установить АБСОЛЮТНЫЙ угол поворота папки (вокруг pivot).
     * Папка хранит свой текущий yaw в `_yaw` (по умолчанию 0).
     * Вращает на разницу angle - _yaw.
     */
    setFolderYawY(folderId, angle, pivot) {
        const f = this.folders.get(folderId);
        if (!f) return 0;
        const prev = f._yaw || 0;
        const delta = angle - prev;
        if (Math.abs(delta) < 0.0001) return 0;
        const count = this.rotateFolderY(folderId, delta, pivot);
        f._yaw = angle;
        return count;
    }

    /**
     * Собрать все объекты папки (рекурсивно по подпапкам) с их мешами.
     * Возвращает { models:[{data}], primitives:[{data}], blocks:[mesh],
     *   meshes:[meshes для подсветки], center:{x,y,z}, count }.
     */
    getFolderObjects(folderId) {
        const out = { models: [], primitives: [], blocks: [], meshes: [] };
        const ids = new Set([folderId]);
        // Собираем id всех вложенных подпапок.
        let added = true;
        while (added) {
            added = false;
            for (const f of this.getAll()) {
                if (f.parentId != null && ids.has(f.parentId) && !ids.has(f.id)) {
                    ids.add(f.id); added = true;
                }
            }
        }
        if (this.modelManager) {
            for (const d of this.modelManager.instances.values()) {
                if (ids.has(d.folderId)) {
                    out.models.push(d);
                    const root = d.rootMesh || d.rootNode;
                    if (root) out.meshes.push(root);
                }
            }
        }
        if (this.primitiveManager) {
            for (const d of this.primitiveManager.instances.values()) {
                if (ids.has(d.folderId)) {
                    out.primitives.push(d);
                    if (d.mesh) out.meshes.push(d.mesh);
                }
            }
        }
        if (this.blockManager) {
            for (const mesh of this.blockManager.blocks.values()) {
                if (ids.has(mesh.metadata?.folderId)) out.blocks.push(mesh);
            }
        }
        // Центр группы (по позициям моделей/примитивов).
        let sx = 0, sy = 0, sz = 0, n = 0;
        for (const d of out.models) { sx += d.x || 0; sy += d.y || 0; sz += d.z || 0; n++; }
        for (const d of out.primitives) { sx += d.x || 0; sy += d.y || 0; sz += d.z || 0; n++; }
        out.center = n > 0 ? { x: sx / n, y: sy / n, z: sz / n } : { x: 0, y: 0, z: 0 };
        out.count = out.models.length + out.primitives.length + out.blocks.length;
        return out;
    }

    /** Сдвинуть все объекты папки на (dx,dy,dz). */
    moveFolderBy(folderId, dx, dy, dz) {
        const g = this.getFolderObjects(folderId);
        const apply = (d, mesh) => {
            d.x = (d.x || 0) + dx; d.y = (d.y || 0) + dy; d.z = (d.z || 0) + dz;
            if (mesh) {
                if (d._worldMatrixFrozen) { try { mesh.unfreezeWorldMatrix?.(); } catch (e) {} d._worldMatrixFrozen = false; }
                mesh.position.set(d.x, d.y, d.z);
            }
        };
        for (const d of g.models) apply(d, d.rootMesh || d.rootNode);
        for (const d of g.primitives) apply(d, d.mesh);
        this._notifyChange();
    }

    /**
     * Масштабировать папку относительно центра pivot на коэффициент factor.
     * Позиции расходятся/сходятся от центра + размеры примитивов меняются.
     */
    scaleFolder(folderId, factor, pivot) {
        if (!Number.isFinite(factor) || factor <= 0) return;
        const g = this.getFolderObjects(folderId);
        const p = pivot || g.center;
        const sc = (d, mesh, isPrim) => {
            d.x = p.x + ((d.x || 0) - p.x) * factor;
            d.y = p.y + ((d.y || 0) - p.y) * factor;
            d.z = p.z + ((d.z || 0) - p.z) * factor;
            if (isPrim) {
                d.sx = (d.sx || 1) * factor; d.sy = (d.sy || 1) * factor; d.sz = (d.sz || 1) * factor;
            } else {
                d.scale = (d.scale || 1) * factor;
            }
            if (mesh) {
                if (d._worldMatrixFrozen) { try { mesh.unfreezeWorldMatrix?.(); } catch (e) {} d._worldMatrixFrozen = false; }
                mesh.position.set(d.x, d.y, d.z);
                if (isPrim && mesh.scaling) mesh.scaling.set(d.sx, d.sy, d.sz);
                else if (mesh.scaling) mesh.scaling.scaleInPlace(factor);
            }
        };
        for (const d of g.models) sc(d, d.rootMesh || d.rootNode, false);
        for (const d of g.primitives) sc(d, d.mesh, true);
        this._notifyChange();
    }

    /** Найти папку по имени (regex/exact). */
    findByName(name) {
        const n = String(name || '').toLowerCase();
        for (const f of this.folders.values()) {
            if (String(f.name).toLowerCase() === n) return f;
        }
        return null;
    }

    serialize() {
        return Array.from(this.folders.values()).map(f => ({
            id: f.id, name: f.name, parentId: f.parentId, visible: f.visible,
        }));
    }

    loadFromArray(arr) {
        this.clear();
        let maxId = 0;
        for (const f of arr || []) {
            this.folders.set(f.id, {
                id: f.id, name: f.name,
                parentId: f.parentId ?? null,
                visible: f.visible !== false,
            });
            if (f.id > maxId) maxId = f.id;
        }
        this._nextId = maxId + 1;
        this._notifyChange();
    }

    clear() {
        this.folders.clear();
        this._nextId = 1;
    }

    dispose() {
        this.clear();
        this._onChange = null;
    }
}
