import { Frustum, CameraFrustum } from '@uino/base-thing';
import { MathUtils, Utils, BaseTickableObject3D } from "@uino/thing";
import { ViewProbeInnerPlane } from "./ViewProbeInnerPlane";
import { ViewProbeOutline } from "./ViewProbeOutline";

const mat4_1 = MathUtils.createMat4();
const _ray = {
	origin: undefined,
	direction: undefined,
}
/**
 * @class ViewProbe
 * @summary 视图探针对象,用于模拟摄像机视锥体并在三维场景中进行视锥体裁剪与遮挡检测。
 * 支持自定义视锥体参数(FOV、近远平面、宽高比),可检测对象在视锥体内的可见性状态,
 * 以及对象间的相互遮挡关系(遮挡剔除)。同时提供可视化辅助功能,
 * 以轮廓线、扫描面和扫描锥体形式呈现视锥体与被检测对象的相交情况。
 * 注意:这里视椎的方向是自身z轴反向,因此如果lookAt的话,结果和Projector正好相反
 * @memberof THING
 * @extends THING.BaseTickableObject3D
 * @public
 * @example
 * // 创建视图探针并检测对象可见性
	const probe = new THING.EXTEND.ViewProbe({
		fov: 60,
		aspect: 16 / 9,
		near: 1,
		far: 100,
		outerColor: [1, 0, 0],
		castShadow:false,
		innerScanning: true
	});
	probe.position = [20, 15, 0];
	probe.rotateX(-90);
	const result = probe.intersectObjects(app.query('.Entity'), true);
	console.log(result)
 */
class ViewProbe extends BaseTickableObject3D {

	static defaultTagArray = ['Dummy'];

	/**
	 * 创建一个视图探针对象
	 * @param {object} param 选项
	 * @param {number} [param.fov] 视锥体的视场角值
	 * @param {number} [param.aspect] 视锥体的宽高比值
	 * @param {number} [param.near] 视锥体的近平面值
	 * @param {number} [param.far] 视锥体的远平面值
	 * @param {number|string|Array} [param.outerColor] 轮廓线颜色
	 * @param {boolean} [param.innerScanning] 是否生成扫描平面
	 * @param {boolean} [param.coneScanning] 是否生成扫描锥体
	 * @param {Array} [param.scanningNum] 扫描区域的射线数量,扫描区域的数量
	 * @param {number|string|Array} [param.innerPlaneColor] 扫描面颜色
	 * @param {number} [param.innerPlaneOpacity] 扫描面透明度
	 */
	constructor(param = {}) {
		super(param);

		this._cameraFrustum = new Frustum();
		this._cameraFrustum.setProjectionMatrix(30, 16 / 9, 1, 20);
		this._helpFrustum = new CameraFrustum();

		// outerline mesh
		this._outMesh = new ViewProbeOutline(this._cameraFrustum);
		this._outMesh.style.color = 0xffffff;
		this._outMesh.visible = false;
		this._outMesh.castShadow = false;
		this.add(this._outMesh, { attachMode: false });

		// scanning num = horizontalScanningNum * verticalScanningNum
		this._horizontalScanningNum = 15;
		this._verticalScanningNum = 20;

		this._innerPlanes = [];
		this._innerScanning = true;
		this._coneScanning = false;
		this._innerCounter = 0;
		this._scanning = false;
		this._scanningObject = null;

		this._innerPlaneColor = undefined;
		this._innerPlaneOpacity = undefined;

		this._cache = {
			outer: [],
			inner: []
		};
		this._useCache = false;

		this._isIntersect = true;

		this.onSetup(param);
	}

	_updateProjectionMatrixAndFrustum() {
		const cameraFrustum = this._cameraFrustum;
		const matrix = [];
		this.getMatrixWorld(matrix, true);
		cameraFrustum.matrixWorld = matrix;

		if (cameraFrustum.near === 0) {
			cameraFrustum.near += Number.EPSILON;
		}

		if (cameraFrustum.far === 0 || cameraFrustum.far === cameraFrustum.near) {
			cameraFrustum.far = cameraFrustum.near + Number.EPSILON;
		}

		cameraFrustum.updateProjectionMatrix();

		const matrixWorldInverse = MathUtils.createMat4();
		MathUtils.mat4.invert(matrixWorldInverse, this.matrixWorld);
		MathUtils.mat4.multiply(mat4_1, cameraFrustum.projectionMatrix, matrixWorldInverse);

		cameraFrustum.setFromProjectionMatrix(mat4_1);

		const vertices = this._helpFrustum.setWorldFromProjectionMatrix(cameraFrustum.projectionMatrix, cameraFrustum.far, this.transform);
		cameraFrustum.setFrustumVertical(vertices);
	}

	_updateInnerPlane(idx, probeObjects) {
		let y = 2 * idx / this._verticalScanningNum - 1;

		let scanningCoords = [];
		for (let i = 0; i <= this._horizontalScanningNum; i++) {
			let x = 2 * i / this._horizontalScanningNum - 1;
			scanningCoords.push([x, y]);
		}

		let distArray;
		if (this._useCache && this._cache.inner[idx]) {
			distArray = this._cache.inner[idx];
		}
		else {
			distArray = scanningCoords.map(coord => {
				const ray = this._cameraFrustum.getRayFromCamera(coord);
				const intersects = Frustum.intersectObjects(ray, probeObjects);
				return intersects[0] ? intersects[0].distance : Infinity;
			});
			this._cache.inner[idx] = distArray;
		}

		if (this._innerPlanes[idx] && this._innerPlanes[idx].isViewProbeInnerPlane) {
			this._innerPlanes[idx].destroy();
			this._innerPlanes[idx] = null;
		}

		this._innerPlanes[idx] = new ViewProbeInnerPlane({
			frustum: this._cameraFrustum,
			vertexNum: this._horizontalScanningNum + 2,
			scanningCoords,
			distArray,
			color: this._innerPlaneColor,
			opacity: this._innerPlaneOpacity
		});

		this._innerPlanes[idx].visible = true;
		this._innerPlanes[idx].castShadow = this.castShadow;
		this.add(this._innerPlanes[idx], { attachMode: false });

		this._innerPlanes[idx].waitForComplete().then(() => {
			this._innerPlanes[idx].node.setRenderLayer(this.node.getRenderLayer());
		});
	}

	_updateConePlane(idx, probeObjects) {
		const isSide = idx % 2 === 0;
		let scanningCoords = [];
		if (isSide) {
			const x = idx - 1;
			for (let i = 0; i <= this._verticalScanningNum; i++) {
				const y = 2 * i / this._verticalScanningNum - 1;
				scanningCoords.push([x, y]);
			}
		}
		else {
			const y = idx - 2;
			for (let i = 0; i <= this._horizontalScanningNum; i++) {
				const x = 2 * i / this._horizontalScanningNum - 1;
				scanningCoords.push([x, y]);
			}
		}

		let distArray;
		if (this._useCache && this._cache.inner[idx]) {
			distArray = this._cache.inner[idx];
		}
		else {
			distArray = scanningCoords.map(coord => {
				const ray = this._cameraFrustum.getRayFromCamera(coord);
				const intersects = Frustum.intersectObjects(ray, probeObjects);
				return intersects[0] ? intersects[0].distance : Infinity;
			});
			this._cache.inner[idx] = distArray;
		}

		if (this._innerPlanes[idx] && this._innerPlanes[idx].isViewProbeInnerPlane) {
			this._innerPlanes[idx].destroy();
			this._innerPlanes[idx] = null;
		}

		this._innerPlanes[idx] = new ViewProbeInnerPlane({
			frustum: this._cameraFrustum,
			vertexNum: isSide ? this._verticalScanningNum + 2 : this._horizontalScanningNum + 2,
			scanningCoords,
			distArray,
			color: this._innerPlaneColor,
			opacity: this._innerPlaneOpacity
		});
		this._innerPlanes[idx].visible = true;
		this.add(this._innerPlanes[idx], { attachMode: false });

		this._innerPlanes[idx].waitForComplete().then(() => {
			this._innerPlanes[idx].node.setRenderLayer(this.node.getRenderLayer());
		});
	}

	onSetup(param) {
		if (param.fov !== undefined) {
			this._cameraFrustum.fov = param.fov;
		}
		if (param.aspect !== undefined) {
			this._cameraFrustum.aspect = param.aspect;
		}
		if (param.near !== undefined) {
			this._cameraFrustum.near = param.near;
		}
		if (param.far !== undefined) {
			this._cameraFrustum.far = param.far;
		}
		if (param.outerColor !== undefined) {
			this._outMesh.style.color = Utils.parseColor(param.outerColor);
		}
		if (param.innerScanning !== undefined) {
			this._innerScanning = !!param.innerScanning;
		}
		if (param.coneScanning !== undefined) {
			this._coneScanning = !!param.coneScanning;
		}
		if (param.scanningNum !== undefined) {
			this._horizontalScanningNum = param.scanningNum[0];
			this._verticalScanningNum = param.scanningNum[1];
		}

		this._innerPlaneColor = Utils.parseColor(param.innerPlaneColor);
		this._innerPlaneOpacity = param.innerPlaneOpacity;

		this._updateProjectionMatrixAndFrustum();
	}

	onUpdate(deltaTime) {
		super.onUpdate(deltaTime);

		if (!this.visible || !this._isIntersect) return true;

		this._updateProjectionMatrixAndFrustum();

		if (this._scanning) {
			if (this._innerCounter > this._verticalScanningNum) {
				this._scanning = false;
				this._innerCounter = 0;
				this._scanningObject = null;
				if (this._saveCache) {
					localStorage.setItem(this.name + '_scanning_data', JSON.stringify(this._cache));
				}
			}
			else {
				this._updateInnerPlane(this._innerCounter, this._scanningObject);
				this._innerCounter++;
			}
		}
		else if (this._scanningConePlane) {
			if (this._innerCounter > 3) {
				this._scanningConePlane = false;
				this._innerCounter = 0;
				this._scanningObject = null;
				if (this._saveCache) {
					localStorage.setItem(this.name + '_scanning_data', JSON.stringify(this._cache));
				}
			}
			else {
				this._updateConePlane(this._innerCounter, this._scanningObject);
				this._innerCounter++;
			}
		}

		return true;
	}

	/**
	 * 开始生成扫描区域,每帧扫描一条水平线
	 * @param {Array<Object3D>} scanningObject 要扫描的对象
	 * @param {boolean} [update=true] 场景矩阵是否需要更新
	 * @param {boolean} [useCache=false] 是否尝试使用存储在浏览器缓存(localStorage)中的扫描值,可以提高性能
	 * @param {boolean} [saveCache=false] 扫描结束后是否自动将扫描结果存储在浏览器缓存(localStorage)中
	 * 浏览器缓存的键为 name + '_scanning_data',所以如果场景中有多个需要缓存的视锥体扫描结果,
	 * 注意设置不同的名称
	 * @public
	 */
	start(scanningObject = [], update = true, useCache = false, saveCache = false) {
		if (!this.visible) {
			// Utils.warn('ViewProbe: .start() must accept an array of Object3D parameter!');
			return;
		}

		this._isIntersect = true;
		if (update) {
			this._updateProjectionMatrixAndFrustum();
		}

		this._useCache = useCache;
		this._saveCache = saveCache;
		if (useCache) {
			let cache = localStorage.getItem(this.name + '_scanning_data');
			if (cache) {
				this._cache = JSON.parse(cache, function (key, value) {
					return value === null ? Infinity : value;
				});
			}
		}

		const scanningCoords = [
			[-1, 1],
			[-1, -1],
			[1, -1],
			[1, 1]
		];

		let distArray;
		if (this._useCache && this._cache.outer.length > 0) {
			distArray = this._cache.outer;
		}
		else {
			distArray = scanningCoords.map(coord => {
				const ray = this._cameraFrustum.getRayFromCamera(coord);
				const intersects = Frustum.intersectObjects(ray, scanningObject);
				return intersects[0] ? intersects[0].distance : Infinity;
			});
			this._cache.outer = distArray;
			if (this._saveCache) {
				localStorage.setItem(this.name + '_scanning_data', JSON.stringify(this._cache));
			}
		}

		this._outMesh.update(scanningCoords, distArray);
		this._outMesh.visible = true;

		if (this._innerScanning) {
			this._innerCounter = 0;
			this._scanning = true;
			this._scanningObject = scanningObject || this.app.query('.Ground');
		}
		else if (this._coneScanning) {
			this._innerCounter = 0;
			this._scanningConePlane = true;
			this._scanningObject = scanningObject || this.app.query('.Ground');
		}

		return distArray;
	}

	/**
	 * 暂停任务
	 * @public
	 */
	pause() {
		if (this._scanning) {
			this._scanning = false;
			this._innerCounter = 0;
			this._scanningObject = null;
		}
		this._isIntersect = false;
	}

	/**
	 * 停止计时
	 * @public
	 */
	stop() {
		this._outMesh.visible = false;
		for (let i = 0; i <= this._verticalScanningNum; i++) {
			if (this._innerPlanes[i]) {
				this._innerPlanes[i].visible = false;
			}
		}
		this._scanning = false;
		this._scanningConePlane = false;
		this._innerCounter = 0;
		this._scanningObject = null;
		this._isIntersect = false;
	}

	/**
	 * 检测一组对象与视锥体的关系,
	 * 返回包围盒在视锥体内的对象。
	 * 如果 occlusionCulling = true,则返回视锥体内未被遮挡的对象。
	 * (注意:设置 occlusionCulling = true 后,
	 * 将使用对象的几何体而不是包围盒进行精确检测。)
	 * @param {Array<Object3D>} testObjects - 检测对象范围
	 * @param {boolean} [occlusionCulling=false] - 遮挡剔除
	 * @return {Array<Object3D>}
	 * @public
	 */
	intersectObjects(testObjects, occlusionCulling = false) {
		if (!this.visible || !this._isIntersect || !testObjects) {
			return [];
		}

		const that = this;
		let result = testObjects.filter(object => this._cameraFrustum.intersectsBox(object.boundingBox));
		if (occlusionCulling) {
			const objects = result;
			result = objects.filter(object => {
				const cameraPosition = MathUtils.getVec3FromMatrixColumn(this._cameraFrustum.matrixWorld, 3);
				const objectCenter = object.boundingBox.center;
				_ray.origin = cameraPosition;
				_ray.direction = MathUtils.normalizeVector(MathUtils.subVector(objectCenter, cameraPosition));
				const intersects = Frustum.intersectObjects(_ray, objects);
				if (intersects.length > 0) {
					if (that.app.objectManager.getBaseObjectFromNode(intersects[0].node) === object) {
						return true;
					}
				}
				return false;
			});
		}
		return result;
	}

	/**
	 * 检测单个对象与视锥体的关系,
	 * 返回当前对象包围盒与视锥体的空间状态(OUT | VISIBLE | OBSCURED)。
	 * 如果设置 occlusionCulling = true,并设置了 testObjects,
	 * 则会考虑对象与视锥体内其他对象(testObjects)的遮挡关系。
	 * (注意:设置 occlusionCulling = true 后,
	 * 将使用对象的几何体而不是包围盒进行精确检测。)
	 * @param {Object3D} object - 要检测的对象
	 * @param {Array<Object3D>} [testObjects] - 检测对象范围
	 * @param {boolean} [occlusionCulling=false] - 遮挡剔除
	 * @return {number} - 0 表示 OUT,1 表示 VISIBLE,2 表示 OBSCURED,-1 表示检查错误
	 * @public
	 */
	intersectObject(object, testObjects, occlusionCulling = false) {
		if (!this.visible || !this._isIntersect || !Utils.isObject(object) || (occlusionCulling && !testObjects)) {
			return -1;
		}

		const that = this;
		const intersectState = this._cameraFrustum.intersectsBox(object.boundingBox);

		if (intersectState && occlusionCulling) {
			const objects = testObjects.filter(object => that._cameraFrustum.intersectsBox(object.boundingBox));
			const cameraPosition = MathUtils.getVec3FromMatrixColumn(that._cameraFrustum.matrixWorld, 3);
			const objectCenter = object.boundingBox.center;
			_ray.origin = cameraPosition;
			_ray.direction = MathUtils.normalizeVector(MathUtils.subVector(objectCenter, cameraPosition));
			const intersects = Frustum.intersectObjects(_ray, objects);

			if (intersects.length > 0) {
				if (that.app.objectManager.getBaseObjectFromNode(intersects[0].node) === object) {
					return ViewProbe.VISIBLE;
				}
				else {
					return ViewProbe.OBSCURED;
				}
			}
		}
		else if (intersectState) {
			return ViewProbe.VISIBLE;
		}

		return ViewProbe.OUT;
	}

	/**
	 * 获取/设置视场角
	 * @type {number}
	 * @public
	 */
	get fov() {
		return this._cameraFrustum.fov;
	}
	set fov(val) {
		this._cameraFrustum.fov = val;
		this._updateProjectionMatrixAndFrustum();
	}

	/**
	 * 获取/设置宽高比
	 * @type {number}
	 * @public
	 */
	set aspect(val) {
		this._cameraFrustum.aspect = val;
		this._updateProjectionMatrixAndFrustum();
	}
	get aspect() {
		return this._cameraFrustum.aspect;
	}

	/**
	 * 获取/设置近平面距离
	 * @type {number}
	 * @public
	 */
	get near() {
		return this._cameraFrustum.near;
	}
	set near(val) {
		this._cameraFrustum.near = val;
		this._updateProjectionMatrixAndFrustum();
	}

	/**
	 * 获取/设置远平面距离
	 * @type {number}
	 * @public
	 */
	get far() {
		return this._cameraFrustum.far;
	}
	set far(val) {
		this._cameraFrustum.far = val;
		this._updateProjectionMatrixAndFrustum();
	}

	get isViewProbe() {
		return true;
	}

}

ViewProbe.OUT = 0;		// outside the frustum
ViewProbe.VISIBLE = 1;	// Inside the frustum, visible
ViewProbe.OBSCURED = 2;	// Inside the frustum, invisible

export {
	ViewProbe
}