import { Vector2, Raycaster, Vector3, Box3 } from 'three';
import { GLTFLoader } from 'three/examples/jsm/loaders/GLTFLoader.js';
export { debounce, merge, throttle } from 'lodash-es';

/**
 * @desc 计算最佳线路时，需要转换数据
 */
function standardKeyPointsChanger(path) {
    // 转换 v1 的邻近点规则
    const keyPoints = [];
    const map = {};
    for (const item of path) {
        const node1 = map[JSON.stringify(item.begin)];
        const node2 = map[JSON.stringify(item.end)];
        if (!node1) {
            // 初始化
            map[JSON.stringify(item.begin)] = {
                id: JSON.stringify(item.begin),
                x: item.begin[0],
                y: item.begin[1],
                neighbours: [
                    {
                        id: JSON.stringify(item.end),
                        d: item.distance
                    }
                ]
            };
        }
        else {
            // 继续添加 neighbours
            node1.neighbours.push({
                id: JSON.stringify(item.end),
                d: item.distance
            });
        }
        if (!node2) {
            // 初始化
            map[JSON.stringify(item.end)] = {
                id: JSON.stringify(item.end),
                x: item.end[0],
                y: item.end[1],
                neighbours: [
                    {
                        id: JSON.stringify(item.begin),
                        d: item.distance
                    }
                ]
            };
        }
        else {
            // 继续添加 neighbours
            node2.neighbours.push({
                id: JSON.stringify(item.begin),
                d: item.distance
            });
        }
    }
    for (const key of Object.keys(map)) {
        keyPoints.push(map[key]);
    }
    return keyPoints;
}
/**
 * @desc 截取精度
 */
function myTruncate(point, precision = 3) {
    const [x, y] = point;
    if (isNaN(+x) || isNaN(+y)) {
        return [];
    }
    // 精度缩小
    return [
        parseFloat((+x).toFixed(precision)),
        parseFloat((+y).toFixed(precision))
    ];
}
function getDistance$1(p1, p2) {
    return Math.abs(Math.sqrt(Math.pow(p1.x - p2.x, 2) + Math.pow(p1.y - p2.y, 2)));
}
/**
 * @desc 把字符串转换成 float
 */
function parsePath(path) {
    const _parseArr = (arr) => {
        return [parseFloat(arr[0]), parseFloat(arr[1])];
    };
    for (const item of path) {
        Object.assign(item, {
            begin: _parseArr(item.begin),
            end: _parseArr(item.end),
            distance: parseFloat(item.distance)
        });
    }
    return path;
}

/**
 * A star
 * TODO:需要处理坐标为 threejs 规范的 Vector3 或 Vector2
 */
class AStar {
    constructor(path) {
        Object.defineProperty(this, "path", {
            enumerable: true,
            configurable: true,
            writable: true,
            value: path
        });
        Object.defineProperty(this, "sc", {
            enumerable: true,
            configurable: true,
            writable: true,
            value: void 0
        });
        Object.defineProperty(this, "keyPoints", {
            enumerable: true,
            configurable: true,
            writable: true,
            value: void 0
        });
        Object.defineProperty(this, "keyPointMap", {
            enumerable: true,
            configurable: true,
            writable: true,
            value: void 0
        });
        Object.defineProperty(this, "openList", {
            enumerable: true,
            configurable: true,
            writable: true,
            value: void 0
        });
        Object.defineProperty(this, "closeList", {
            enumerable: true,
            configurable: true,
            writable: true,
            value: void 0
        });
        Object.defineProperty(this, "invalidList", {
            enumerable: true,
            configurable: true,
            writable: true,
            value: void 0
        });
        Object.defineProperty(this, "searchCount", {
            enumerable: true,
            configurable: true,
            writable: true,
            value: void 0
        });
        Object.defineProperty(this, "beginTime", {
            enumerable: true,
            configurable: true,
            writable: true,
            value: void 0
        });
        Object.defineProperty(this, "endTime", {
            enumerable: true,
            configurable: true,
            writable: true,
            value: void 0
        });
        Object.defineProperty(this, "beginNode", {
            enumerable: true,
            configurable: true,
            writable: true,
            value: void 0
        });
        Object.defineProperty(this, "endNode", {
            enumerable: true,
            configurable: true,
            writable: true,
            value: void 0
        });
        Object.defineProperty(this, "beginVertical", {
            enumerable: true,
            configurable: true,
            writable: true,
            value: void 0
        });
        Object.defineProperty(this, "endVertical", {
            enumerable: true,
            configurable: true,
            writable: true,
            value: void 0
        });
        // 转换 path 的字符串为数字
        this.path = parsePath(this.path);
        // 单节点预估成本
        this.sc = 1;
        this.keyPoints = standardKeyPointsChanger(this.path);
        this.keyPointMap = {};
        // 将列表转换map对象
        for (let i = 0; i < this.keyPoints.length; i++) {
            const p = this.keyPoints[i];
            this.keyPointMap[p.id] = p;
        }
        this.openList = [];
        this.closeList = [];
        this.invalidList = [];
        this.searchCount = 0;
        this.beginTime = new Date();
        this.endTime = new Date();
    }
    shortestPath(beginNode, endNode) {
        const result = [];
        this.openList = []; // 已经知道，但未走过的节点
        this.closeList = []; // 走过的节点
        this.invalidList = []; // 失效的点
        this.searchCount = 0;
        this.beginTime = new Date();
        beginNode.parent = null;
        beginNode.d = 0;
        this.beginNode = beginNode;
        this.endNode = endNode;
        this.closeList.push(this.beginNode);
        // eslint-disable-next-line no-constant-condition
        while (true) {
            if (this.closeList.length === 0) {
                break;
            }
            this.searchCount++;
            let currNode = this.closeList[this.closeList.length - 1];
            if (currNode.id === endNode.id) {
                // 已经抵达目的地, 按照parent往回追溯，返回结果
                while (currNode && currNode !== null) {
                    result.unshift(currNode);
                    currNode = currNode.parent;
                }
                break;
            }
            if (currNode.neighbours && currNode.neighbours.length > 0) {
                for (let i = 0; i < currNode.neighbours.length; i++) {
                    const nb = currNode.neighbours[i];
                    const tmpNode = this.keyPointMap[nb.id];
                    // 周围节点必须满足三个条件：
                    // 1.这个点是合法的，什么叫合法？这里简单的就是可以通行的，不能是障碍物
                    if (!tmpNode || this.contain(this.invalidList, tmpNode)) {
                        continue;
                    }
                    // 2.这个点不能在close表中
                    if (this.contain(this.closeList, tmpNode)) {
                        continue;
                    }
                    // 3.这个点不能在open表中
                    if (this.contain(this.openList, tmpNode)) {
                        continue;
                    }
                    tmpNode.d = nb.d; // 相邻点距离
                    tmpNode.parent = currNode;
                    tmpNode.cost = this.f(tmpNode);
                    this.openList.push(tmpNode);
                }
            }
            if (this.openList.length > 0) {
                // 计算open列表cost值最低的节点minNode
                let minNode = null;
                for (let i = 0; i < this.openList.length; i++) {
                    const tmp = this.openList[i];
                    if (minNode === null || minNode.cost > tmp.cost) {
                        minNode = tmp;
                    }
                }
                // minNode从open表中删除加入close表
                this.remove(this.openList, minNode);
                this.closeList.push(minNode);
            }
            else {
                // 如果openList没有节点了，那么取上一个closeList中的节点，继续循环
                // 该节点没有有效的邻近节点，移到失效列表
                this.invalidList.push(currNode);
                this.remove(this.closeList, currNode);
            }
            if (this.searchCount > 500000) {
                // 避免死循环
                break;
            }
        }
        this.endTime = new Date();
        return result;
    }
    contain(nodes, node) {
        let result = false;
        nodes.forEach(function (item) {
            if (item.id === node.id) {
                result = true;
            }
        });
        return result;
    }
    remove(nodes, node) {
        let move = false;
        for (let i = 0; i < nodes.length; i++) {
            if (move) {
                nodes[i - 1] = nodes[i];
            }
            if (node.id === nodes[i].id) {
                move = true;
            }
        }
        if (move) {
            nodes.length -= 1;
        }
    }
    // 节点代价
    f(n) {
        return this.g(n) + this.h(n);
    }
    // 初始节点到n节点的明确代价
    g(n) {
        let result = 0;
        let currNode = n;
        while (currNode && currNode !== null) {
            result += currNode.d;
            currNode = currNode.parent;
        }
        return result;
    }
    // 是n节点到目标点的预估代价；
    h(n) {
        return this.euclidianCost(n, this.endNode);
    }
    // //麦哈顿估算法
    // manhattanCost(begin, end) {
    //   return (
    //     Math.abs(begin.x - end.x) * this.sc + Math.abs(begin.y + end.y) * this.sc
    //   );
    // }
    // 几何估价法
    euclidianCost(begin, end) {
        const dx = begin.x - end.x;
        const dy = begin.y - end.y;
        return Math.sqrt(dx * dx + dy * dy) * this.sc;
    }
    // //对角线估价法
    // diagonalCost(begin, end) {
    //   var dx = Math.abs(begin.x - end.x);
    //   var dy = Math.abs(begin.y - end.y);
    //   var diag = Math.min(dx, dy);
    //   var straight = dx + dy;
    //   return this.sc * diag + this.sc * (straight - 2 * diag);
    // }
    getNearest(point, isBegin = true) {
        let tempItem;
        let nearPoint;
        let vertical;
        let dist = Infinity;
        for (let i = 0; i < this.path.length; i++) {
            tempItem = this.path[i];
            const { dist: tempDist, point: tempPoint, vertical: tempVertical } = this.nearestPointOnLine(tempItem.begin, tempItem.end, point);
            if (tempDist < dist) {
                dist = tempDist;
                nearPoint = tempPoint;
                vertical = tempVertical;
            }
        }
        // this.drawNearLine(nearItem.begin, nearItem.end, _name('rnFindLineNear'));
        // this.drawNearPoint(nearPoint as [number, number], _name('rnFindPointNear'));
        // vertical &&
        //   this.drawVertical(
        //     vertical as [number, number],
        //     _name('rnFindPointVertical')
        //   );
        return { point: nearPoint, vertical };
    }
    /**
     * @desc 最佳线路计算
     */
    calcShortestPath(begin, end) {
        const { point: beginPoint, vertical: beginVertical } = this.getNearest(begin, true);
        const { point: endPoint, vertical: endVertical } = this.getNearest(end, false);
        // 更新起点/终点垂点
        this.beginVertical = beginVertical;
        this.endVertical = endVertical;
        const standardBeginPoint = this.keyPoints.find(_ => _.id === JSON.stringify(beginPoint));
        const standardEndPoint = this.keyPoints.find(_ => _.id === JSON.stringify(endPoint));
        let result = this.shortestPath(standardBeginPoint, standardEndPoint);
        // 进行线路优化
        result = this.trackOptimize(result);
        return result;
    }
    /**
     * @desc 线路优化
     */
    trackOptimize(trackPoints) {
        if (trackPoints.length >= 2) {
            // 加工，判断头尾是否加上垂点
            // 有垂点才进行判断追加，没有垂点，就不进行添加
            if (this.beginVertical) {
                const beginV = new Vector2(this.beginVertical[0], this.beginVertical[1]);
                if (this.isInLine(trackPoints[0], trackPoints[1], beginV, 2)) {
                    console.log('起点垂点在导航线上，shift 去除连接起点，后续 unshift 起点垂点');
                    trackPoints.shift();
                }
                trackPoints.unshift(beginV);
            }
            if (this.endVertical) {
                const endV = new Vector2(this.endVertical[0], this.endVertical[1]);
                if (this.isInLine(trackPoints[trackPoints.length - 1], trackPoints[trackPoints.length - 2], endV, 2)) {
                    console.log('终点垂点在导航线上，pop 去除连接终点，后续 push 终点垂点');
                    trackPoints.pop();
                }
                trackPoints.push(endV);
            }
        }
        return trackPoints;
    }
    /**
     * @desc 获取点到各个线段上最近的一条线段和相关信息
     * 需要返回距离，来筛选最近的线段
     * dist：一般来说是点和垂点的距离 dist。但是需要判断如果垂点在直线外部，就计算点到线段两端的距离 min dist
     * 拿到 dist 循环筛选出最近的即可。
     * point：表示距离点最近的线段端点，主要用来a星算法
     * vertical：表示垂点，主要用来补充线段
     */
    nearestPointOnLine(begin, end, p) {
        const _getNearPointByLineBeginEnd = () => {
            const arr = [
                {
                    point: begin,
                    dist: getDistance$1(new Vector2(begin[0], begin[1]), new Vector2(p[0], p[1]))
                },
                {
                    point: end,
                    dist: getDistance$1(new Vector2(end[0], end[1]), new Vector2(p[0], p[1]))
                }
            ];
            arr.sort((a, b) => a.dist - b.dist);
            return arr[0];
        };
        let point = [0, 0];
        let vertical = null;
        let dist = 0;
        // 获取垂足
        const { x, y } = this.getVerticalPoint(new Vector2(begin[0], begin[1]), new Vector2(end[0], end[1]), new Vector2(p[0], p[1]));
        // 判断垂足是否在线上，在线上直接返回，不在线上，返回距离垂足最近的点
        const isInline = this.isInLine(new Vector2(begin[0], begin[1]), new Vector2(end[0], end[1]), new Vector2(x, y), 2);
        const { point: _point, dist: _dist } = _getNearPointByLineBeginEnd();
        point = _point;
        if (isInline) {
            vertical = [x, y];
            dist = getDistance$1(new Vector2(p[0], p[1]), new Vector2(x, y));
        }
        else {
            vertical = null;
            point = _point;
            dist = _dist;
        }
        return {
            vertical,
            point,
            dist
        };
    }
    /**
     * @desc 求点C在由点A、点B组成的直线的垂点
     */
    getVerticalPoint(p1, p2, p) {
        // 直线方程：Ax + By + C = 0
        const A = p2.y - p1.y;
        const B = p1.x - p2.x;
        const C = p2.x * p1.y - p1.x * p2.y;
        const tmp = A * A + B * B;
        let x = (B * B * p.x - A * B * p.y - A * C) / tmp; // 垂点的X坐标
        let y = (A * A * p.y - A * B * p.x - B * C) / tmp; // 垂点的Y坐标
        // 缩小精度
        const [x1, y1] = myTruncate([x, y]);
        x = x1;
        y = y1;
        return { x, y };
    }
    /**
     * @desc 计算两个坐标连线后的倾斜角度
     */
    getAngle(p1, p2) {
        const radian = Math.atan2(p2.y - p1.y, p2.x - p1.x);
        const angle = radian * (180 / Math.PI);
        return {
            radian,
            angle
        };
    }
    /**
     * @desc 判断p是否在p1和p2的线段内
     */
    isInLine(p1, p2, p, offsetAngle = 0) {
        if (!p1 || !p2 || !p) {
            return false;
        }
        // 允许一定的上下偏差，由于一些精度计算原因，可能点无法被识别在线段上
        const diffAngle = Math.floor(this.getAngle(p1, p).angle) -
            Math.floor(this.getAngle(p, p2).angle);
        if (offsetAngle === 0) {
            return diffAngle === 0;
        }
        else {
            // [-offsetAngle,offsetAngle]
            return diffAngle >= -offsetAngle && diffAngle <= offsetAngle;
        }
    }
}

const GLTFLoaderPromise = (url, onProgress) => {
    const loader = new GLTFLoader();
    return new Promise((resolve, reject) => {
        // 📝 [Three.js gltf模型加载后是黑色的](https://zhuanlan.zhihu.com/p/350136476)
        loader.load(
        // TODO:参考 axios 的 combine url，保证路径不会出错
        url, ({ scene }) => resolve(scene), onProgress, reject);
    });
};

const warn = (msg, ...args) => {
    console.warn(`[KTMap warn] ${msg}`, ...args);
};

/**
 * 射线拾取
 */
const getIntersects = ({ event, $map, scene, camera, objects }) => {
    const { top, left } = $map.getBoundingClientRect();
    // 声明raycaster和mouse变量
    const raycaster = new Raycaster();
    const mouse = new Vector2();
    // 通过鼠标点击的位置计算出raycaster所需要的点的位置，以屏幕中心为原点，值的范围为-1到1.
    mouse.x = ((event.clientX - left) / $map.clientWidth) * 2 - 1;
    mouse.y = -((event.clientY - top) / $map.clientHeight) * 2 + 1;
    // 通过鼠标点的位置和当前相机的矩阵计算出raycaster
    raycaster.setFromCamera(mouse, camera);
    // 获取raycaster直线和所有模型相交的数组集合，注意：true表示返回的值会递归，找到更深层次的模型
    const intersects = raycaster.intersectObjects(objects || scene.children, true);
    // 2d 场景下会存在bug，就是选择点的时候，跑到中心点去了，数组第一个是对象 AxesHelper 导致的，这里进行过滤
    // intersects = intersects.filter(item => !(item.object instanceof AxesHelper))
    // if (filter) {
    //   intersects = filter(intersects)
    // }
    return intersects;
};
/**
 * 获取世界坐标
 * 用于射线拾取对象
 * objects 传入需要被射线拾取的物体。精准判断情况，不传入 objects， 获取被射线拾取的所有对象，然后再判断第一个对象是否被命中，即可判断当前是否点击了想要的对象。
 * @return 返回第一个点和所有被射线拾取的对象
 */
const getWorldPosition = ({ event, $map, scene, camera, objects }) => {
    const intersects = getIntersects({ event, $map, scene, camera, objects });
    const points = intersects.map(item => item.point);
    const worldPosition = new Vector3();
    if (points.length > 0) {
        worldPosition.set(points[0].x, points[0].y, points[0].z);
        return { worldPosition, intersects };
    }
    return {};
};
/**
 * 获取屏幕坐标
 */
const getScreenPostion = ({ worldPosition, $map, scene, camera }) => {
    const { top, left, width, height } = $map.getBoundingClientRect();
    const standardVector = new Vector3().copy(worldPosition).project(camera);
    const halfWidth = width / 2;
    const halfHeight = height / 2;
    const x = Math.round(standardVector.x * halfWidth + halfWidth);
    const y = Math.round(-standardVector.y * halfHeight + halfHeight);
    return {
        x: x + left,
        y: y + top
    };
};
/**
 * 根据 name 获取对应的对象集合
 */
const getObjectsByName = (name, parent) => {
    // ❌ 方法1
    // scene.getObjectByName 只会返回第一个
    // return scene.getObjectByName(name);
    // ❌ 方法2
    // 不能深层遍历
    // return scene.children.filter(item => item.name === name);
    // ✅ 较优算法
    const arr = [];
    parent.traverse(item => {
        if (item.name === name)
            arr.push(item);
    });
    return arr;
};
/**
 * 根据 name 获取对应的对象
 */
const getObjectByName = (name, parent) => {
    return getObjectsByName(name, parent)[0];
};
const getPointBetweenPoints = ({ p1, p2, 
// +1 把终点加入
segment = Math.abs(Math.sqrt(Math.pow(p1.x - p2.x, 2) + Math.pow(p1.y - p2.y, 2))) + 1, 
// 从 0 开始，把起点加入
i = 0 }) => {
    return {
        point: new Vector2(((p2.x - p1.x) / segment) * i + p1.x, ((p2.y - p1.y) / segment) * i + p1.y),
        segment
    };
};
/**
 * 计算两个坐标连线后的倾斜角度
 */
const getAngle = (p1, p2) => {
    const radian = Math.atan2(p2.y - p1.y, p2.x - p1.x);
    const angle = radian * (180 / Math.PI);
    return {
        radian,
        angle
    };
};
const CLS_PREFIX = 'ktmap';
/**
 * 截取精度
 */
const truncateBase = (n, precision = 3) => {
    if (isNaN(+n))
        return n;
    return parseFloat((+n).toFixed(precision));
};
const truncateNumber = (data, precision = 3) => {
    return (Array.isArray(data)
        ? data.map(_ => truncateBase(_, precision))
        : truncateBase(data, precision));
};
const truncateVector2 = (data, precision = 3) => {
    return (Array.isArray(data)
        ? data.map(_ => new Vector2(truncateBase(_.x, precision), truncateBase(_.y, precision)))
        : new Vector2(truncateBase(data.x, precision), truncateBase(data.y, precision)));
};
const truncateVector3 = (data, precision = 3) => {
    return (Array.isArray(data)
        ? data.map(_ => new Vector3(truncateBase(_.x, precision), truncateBase(_.y, precision), truncateBase(_.z, precision)))
        : new Vector3(truncateBase(data.x, precision), truncateBase(data.y, precision), truncateBase(data.z, precision)));
};
/**
 * 获取两点间距离
 */
const getDistance = (p1, p2) => {
    return Math.abs(Math.sqrt(Math.pow(p1.x - p2.x, 2) + Math.pow(p1.y - p2.y, 2)));
};
/**
 * 获取包围盒的中心点
 */
const getBox3Center = (mesh) => {
    return new Box3().setFromObject(mesh).getCenter(new Vector3());
};
/**
 * sleep
 */
const sleep = (delay = 1000) => new Promise(_ => setTimeout(_, delay));
const getDollys = (targetMesh, controls, offset = 5) => {
    if (!targetMesh) {
        return { fitDistance: 0, dollys: [0] };
    }
    const bb = new Box3().setFromObject(targetMesh);
    const fitDistance = controls.getDistanceToFitBox(bb.max.x - bb.min.x, bb.max.y - bb.min.y, bb.max.z - bb.min.z);
    const minDollys = [...new Array(offset)]
        .map((_, i) => fitDistance / (i + 2))
        .reverse();
    const maxDollys = [...new Array(offset)].map((_, i) => fitDistance * (i + 2));
    const dollys = [...minDollys, fitDistance, ...maxDollys];
    return { fitDistance, dollys };
};
const NOOP = () => { };
const extend = Object.assign;
const objectToString = Object.prototype.toString;
const toTypeString = (value) => objectToString.call(value);
// is
const isArray = Array.isArray;
const isMap = (val) => toTypeString(val) === '[object Map]';
const isSet = (val) => toTypeString(val) === '[object Set]';
const isDate = (val) => toTypeString(val) === '[object Date]';
const isFunction = (val) => typeof val === 'function';
const isString = (val) => typeof val === 'string';
const isSymbol = (val) => typeof val === 'symbol';
const isObject = (val) => val !== null && typeof val === 'object';
const isPromise = (val) => {
    return isObject(val) && isFunction(val.then) && isFunction(val.catch);
};

export { AStar, CLS_PREFIX, GLTFLoaderPromise, NOOP, extend, getAngle, getBox3Center, getDistance, getDollys, getIntersects, getObjectByName, getObjectsByName, getPointBetweenPoints, getScreenPostion, getWorldPosition, isArray, isDate, isFunction, isMap, isObject, isPromise, isSet, isString, isSymbol, objectToString, sleep, toTypeString, truncateNumber, truncateVector2, truncateVector3, warn };
