import { Utils, getTransformOffset } from "../common/Utils";
import { Component, MathUtils } from "@uino/thing";

const __ = {
    private: Symbol('private'),
}

const WallType = {
    ModelWall: "ModelWall",
    TextureWall: "TextureWall"
}

let defCenter = [0, 0, 0];
let defHalfSize = [0, 0, 0];
let defRotation = [0, 0, 0];
let modelSizeMap = {};

/**
 * 墙组件,用于生成墙体
 * @extends THING.Component
 * @public
 * @example
 * // 模拟数据
 * const points = [[0, 0], [2, 0], [2, 2], [0, 2]]
 * const materialResource = { map: './images/camera.png' }
 * // 创建对象作为墙体
 * const wall = new THING.Object3D({
 *     tags: ['BuildingElement', 'Wall']
 * });
 * // 实例化平面组件
 * const wallComponent = new THING.WallComponent();
 * wall.addComponent(wallComponent, 'wallComponent');
 * wallComponent.load({
 *     type: 'TextureWall',
 *     walls: [
 *         {
 *             points: [...points[0], ...points[1]],
 *             leftMaterial: materialResource,
 *             rightMaterial: materialResource,
 *             edgeMaterial: materialResource
 *         },
 *         {
 *             points: [...points[1], ...points[2]],
 *             leftMaterial: materialResource,
 *             rightMaterial: materialResource,
 *             edgeMaterial: materialResource
 *         }, {
 *             points: [...points[2], ...points[3]],
 *             leftMaterial: materialResource,
 *             rightMaterial: materialResource,
 *             edgeMaterial: materialResource
 *         }, {
 *             points: [...points[3], ...points[0]],
 *             leftMaterial: materialResource,
 *             rightMaterial: materialResource,
 *             edgeMaterial: materialResource
 *         }
 *     ],
 * }).then(() => {
 *     console.log('墙体加载完成');
 * })
 */
class WallComponent extends Component {

    constructor(param) {
        super(param);
        let _private = this[__.private] = {};

        _private.data = null;
        _private.dataType = null;
        _private.pickIds = null;

        /**
         * Sets the wall data and options.
         * @param {Object} data - The wall data.
         * @param {Object} options - The options.
         */
        _private.setData = (data, options) => {
            if (!data) {
                return;
            }
            _private.toolkit = options;

            _private.data = data;
            _private.dataType = data.type;
            _private.pickIds = data.pickIds;
            _private.walls = data.walls;
            _private.materialExtendOptions = {
                defineUsePickId: !!_private.pickIds
            };

            if (data.type === WallType.ModelWall) {
                _private._parseModelWalls(data.walls);
            }
        }

        /**
         * Returns the wall data.
         * @returns {Object} The wall data.
         */
        _private.getData = (options) => {
            let exportData = {
                type: _private.dataType
            };

            if (_private.pickIds) {
                exportData.pickIds = _private.pickIds;
            }

            if (_private.walls) {
                // 浅拷贝wall,防止多次导出时 数据源被修改
                const walls = [];
                _private.walls.forEach(wall => {
                    walls.push(Object.assign({}, wall));
                });
                exportData.walls = walls;
            }

            return exportData;
        }

        /**
         * Loads the wall object.
         * @param {Object} wallObject - The wall object.
         * @returns {Promise} A promise that resolves when the loading is complete.
         */
        _private.load = (wallObject) => {
            if (_private.dataType === WallType.ModelWall) {
                return _private._loadModelWall(wallObject);
            }

            if (_private.dataType === WallType.TextureWall) {
                return _private._loadTextureWall(wallObject);
            }

            return Promise.resolve();
        }

        _private._getParserManager = () => {
            if (!_private._parserManager) {
                _private._parserManager = Utils.createObject('ParserManager');
            }
            return _private._parserManager;
        }

        //#region load texture wall

        _private._loadTextureWall = (wallObject) => {
            let promiseArr = [];
            let newWalls = [];
            _private.walls.forEach(element => {
                if (element.visible !== false) {
                    element.height = Utils.parseNumber(element.height, 3);
                    element.thickness = Utils.parseNumber(element.thickness, 0.25);
                    element = Object.assign({}, element);
                    element.leftMaterial = _private._loadMaterialResource(element.leftMaterial, promiseArr);
                    element.rightMaterial = _private._loadMaterialResource(element.rightMaterial, promiseArr);
                    element.edgeMaterial = _private._loadMaterialResource(element.edgeMaterial, promiseArr);
                    newWalls.push(element);
                }
            });

            return Promise.all(promiseArr).then(
                () => {
                    _private._parseWall(wallObject, newWalls)
                },
                () => {
                    _private._parseWall(wallObject, newWalls)
                }
            )
        }

        _private._loadMaterialResource = (data, promiseArr) => {
            if (!data) {
                return null
            }

            const { material, promise } = this.app._materialResourceLoader.createMaterialResource(data);
            promiseArr.push(promise);

            return material;
        }

        _private._parseWall = (wallObject, newWalls) => {
            if (wallObject.destroyed) {
                return;
            }

            const wallNode = _private._getParserManager().parseWall(newWalls, { pickIds: _private.pickIds });
            wallObject.body.setNode(wallNode);
        }

        //#endregion

        //#region load model wall 

        _private._parseModelWalls = (walls) => {
            walls.forEach(wallData => {
                const modelIndex = wallData['model'];
                if (typeof modelIndex !== 'number') {
                    return;
                }

                if (_private.toolkit.getURIByIndex) {
                    wallData['model'] = _private.toolkit.resolveURL(_private.toolkit.getURIByIndex(modelIndex))
                }

                if (_private.toolkit.getModelDataByIndex) {
                    const _data = _private.toolkit.getModelDataByIndex(modelIndex);
                    wallData['model'] = _data.url;
                }
            });
        }

        _private._loadModelWall = async (wallObject) => {
            const walls = _private.walls;

            let mergeDataMap = {};
            let pickidMap = {};
            for (let i = 0; i < walls.length; i++) {
                const wallData = walls[i];
                const modelUrl = wallData['model'];
                if (wallData.visible === false || !modelUrl) {
                    continue;
                }

                // Get size of the model
                let modelSize = modelSizeMap[modelUrl];
                if (!modelSize) {
                    modelSize = await _private._getModelSize(modelUrl, wallObject);
                }
                modelSizeMap[modelUrl] = modelSize;

                // Generate matrix data of the wall
                let transfromData = _private._generateModelWallTransfrom(wallData, modelSize);

                // Generate pick id of the wall
                if (_private.pickIds) {
                    let pickId = _private.pickIds[i];
                    let tempPickids = [];
                    transfromData.forEach(element => {
                        tempPickids.push(pickId);
                    });
                    pickidMap[modelUrl] = pickidMap[modelUrl] ? pickidMap[modelUrl].concat(tempPickids) : tempPickids;
                }
                mergeDataMap[modelUrl] = mergeDataMap[modelUrl] ? mergeDataMap[modelUrl].concat(transfromData) : transfromData;
            }

            // Create wall
            const keys = Object.keys(mergeDataMap);
            let promises = [];
            for (let i = 0; i < keys.length; i++) {
                const url = keys[i];
                const transfromData = mergeDataMap[url];

                promises[i] = _private._mergeCreate(url, transfromData, pickidMap[url], wallObject);
            }
            return Promise.all(promises);
        }

        _private._generateModelWallTransfrom = (wallData, modelSize) => {
            let result = [];
            // 1. Ready param
            const points = wallData.points;
            const startPosition = [points[0], 0, points[1] + getTransformOffset()];
            const endPosition = [points[2] + getTransformOffset(), 0, points[3]];
            const holes = wallData['holes'];
            const height = wallData['height'];
            const thickness = wallData['thickness'];

            // 2. Generate lines data
            const lines = _private._generateLines(startPosition, endPosition, holes);


            // 3. Calc public transform property
            const forward = MathUtils.normalizeVector(MathUtils.subVector(endPosition, startPosition));
            const quat = MathUtils.getQuatFromUnitVectors([-1, 0, 0], forward);
            const euler = MathUtils.getEulerFromQuat(quat);
            const angles = [MathUtils.radToDeg(euler[0]), MathUtils.radToDeg(euler[1]), MathUtils.radToDeg(euler[2])];
            const modelDistanceHalf = MathUtils.scaleVector(forward.concat(), modelSize[0] / 2);
            const modelDistance = MathUtils.scaleVector(forward.concat(), modelSize[0]);
            const scaleY = height == 0 ? 0.001 : height / modelSize[1];
            const scaleZ = thickness == 0 ? 0.001 : thickness / modelSize[2];

            // 4. Generate transform data
            for (let j = 0; j < lines.length; j++) {
                const line = lines[j];
                // calc create count
                let count = MathUtils.getDistance(line.start, line.end) / modelSize[0];
                const decimal = count % 1; // get decimals
                count = Math.floor(count); // get integer
                count += 1;
                // create
                if (count == 1) {
                    const position = MathUtils.getPointOnLine(line.start, line.end, 0.5);
                    const mat4 = MathUtils.createMat4(
                        [position[0], 0, position[2]],
                        angles,
                        [decimal, scaleY, scaleZ]
                    );
                    result.push(mat4);
                }
                else {
                    let position;
                    let scaleX;
                    for (let k = 0; k < count; k++) {
                        let point = MathUtils.getPointOnLine(line.start, line.end, k);
                        let pos = [point[0], 0, point[2]];
                        if (k == 0) {
                            // first
                            position = MathUtils.addVector(pos, modelDistanceHalf);
                            scaleX = 1;
                        }
                        else if (k == count - 1) {
                            // last
                            position = MathUtils.addVector(position, MathUtils.scaleVector(forward.concat(), modelSize[0] / 2 + modelSize[0] * decimal / 2));
                            scaleX = decimal;
                        }
                        else {
                            // middle
                            position = MathUtils.addVector(position, modelDistance);
                            scaleX = 1;
                        }
                        const mat4 = MathUtils.createMat4(
                            [position[0], 0, position[2]],
                            angles,
                            [scaleX, scaleY, scaleZ]
                        );
                        result.push(mat4);
                    }
                }
            }

            // 5. Generate Hole transform data
            const fillHole = Utils.parseBoolean(wallData.fillHole, false);
            if (fillHole && holes) {
                for (let i = 0; i < holes.length; i++) {
                    const hole = holes[i];
                    const size = hole.size;
                    const loc = hole.loc;
                    const yPercent = loc[1];
                    let pos = MathUtils.getPointOnLine(startPosition, endPosition, loc[0]);
                    const scaleX = size[0] / modelSize[0];
                    // fill bottom
                    if (yPercent >= 0.001) {
                        // calc scale
                        let _scaleY = height * yPercent / modelSize[1];
                        const mat4 = MathUtils.createMat4(
                            [pos[0], 0, pos[2]],
                            angles,
                            [scaleX, _scaleY, scaleZ]
                        );
                        result.push(mat4);
                    }
                    // fill top
                    if (yPercent < 1) {
                        const topHeight = (1 - yPercent) * height - size[1];
                        if (topHeight > 0) {
                            let _scaleY = topHeight / modelSize[1];
                            const mat4 = MathUtils.createMat4(
                                [pos[0], height - topHeight, pos[2]],
                                angles,
                                [scaleX, _scaleY, scaleZ]
                            );
                            result.push(mat4);
                        }
                    }
                }
            }

            return result;
        }

        _private._generateLines = (startPosition, endPosition, holes) => {
            let lines = [];
            const rootLineLength = MathUtils.getDistance(startPosition, endPosition);

            // if the data contains holes
            if (holes && holes.length > 0) {
                // Sort holes
                for (let j = 0; j < holes.length - 1; j++) {
                    for (let k = 0; k < holes.length - 1 - j; k++) {
                        if (holes[k].loc[0] > holes[k + 1].loc[0]) {
                            const temp = holes[k];
                            holes[k] = holes[k + 1];
                            holes[k + 1] = temp;
                        }
                    }
                }

                // Split root line
                let preHolePercent = null;
                holes.forEach(hole => {
                    const loc = hole['loc'];
                    const size = hole['size'];
                    const sizexPercent = size[0] / rootLineLength;
                    const subLineEndPercent = loc[0] - sizexPercent / 2;
                    // the ratio of the hole to the wall
                    let subLineStart, subLineEnd;
                    if (!preHolePercent) {
                        subLineStart = MathUtils.getPointOnLine(startPosition, endPosition, 0);
                        subLineEnd = MathUtils.getPointOnLine(startPosition, endPosition, subLineEndPercent);
                    }
                    else {
                        subLineStart = MathUtils.getPointOnLine(startPosition, endPosition, preHolePercent);
                        subLineEnd = MathUtils.getPointOnLine(startPosition, endPosition, subLineEndPercent);
                    }
                    lines.push({ start: subLineStart, end: subLineEnd });
                    preHolePercent = subLineEndPercent + sizexPercent;
                });

                // add last line
                if (preHolePercent < 1) {
                    lines.push({
                        start: MathUtils.getPointOnLine(startPosition, endPosition, preHolePercent),
                        end: MathUtils.getPointOnLine(startPosition, endPosition, 1)
                    });
                }
            }
            else {
                // direct use root line
                lines.push({ start: startPosition, end: endPosition });
            }

            return lines;
        }

        _private._mergeCreate = (url, matrices, pickIds, wallObject) => {
            let options = { matrices, lights: true, pickIds };
            return new Promise(function (resolve, reject) {
                if (matrices && matrices.length) {
                    const resourceManager = wallObject.app.resourceManager;
                    resourceManager.loadModel(url, result => {
                        if (!wallObject.destroyed) {
                            result.node.setStyle(wallObject.bodyNode.getStyle());
                            wallObject.bodyNode.add(result.node);
                        }
                        resolve();
                    }, undefined, e => {
                        reject(e);
                        console.error(e);
                    }, options);
                }
            });
        }

        _private._getModelSize = async (url, wallObject) => {
            return new Promise((resolve, reject) => {
                wallObject.app.resourceManager.loadModel(url, function (e) {
                    const box = e.node.getOBB({ center: defCenter, halfSize: defHalfSize, rotation: defRotation });
                    const halfSize = box.halfSize;
                    const modelSize = [halfSize[0] * 2, halfSize[1] * 2, halfSize[2] * 2];
                    e.node.dispose();
                    resolve(modelSize);
                }, function () { }, reject);
            })
        }

        //#endregion
    }

    /**
     * 加载墙体
     * @param {Object} data - 墙体数据 (数据格式参考: https://wiki.uino.com/book/6538d1e88ea0ba892398fdf2/6523a7adae83a4dfb2c2cba4.html)
     * @returns {Promise}
     * @public
     */
    load(data) {
        let _private = this[__.private];
        _private.setData(data);
        return _private.load(this.object);
    }

    /**
     * 导入墙体数据
     * @param {Object} data
     */
    onImport(data, options) {
        if (!data) {
            return;
        }
        super.onImport(data, options);
        let _private = this[__.private];
        _private.setData(data, options);
    }

    /**
     * 导出墙体数据
     * @returns {Object}
     */
    onExport(options) {
        super.onExport(options);
        let _private = this[__.private];
        return _private.getData(options);
    }

    /**
     * Handles the loading of resources for the wall component.
     */
    onLoadResource() {
        let _private = this[__.private];
        return _private.load(this.object);
    }
}

Utils.registerClass("WallComponent", WallComponent);

export { WallComponent };