import { Utils } from '../common/Utils'
import { BasePoints } from './BasePoints';
import { PCDLoader } from '../loaders/PCDLoader';
import { PLYLoader } from '../loaders/PLYLoader';

const visibleFlag = 2;
/**
 * Points - 点云
 * 
 * 场景中的点物体,可展示点云数据,支持从文件加载(url)或直接传入坐标数组。
 * 
 * @class Points
 * @memberof THING
 * @extends THING.BasePoints
 * @public
 * 
 * @example
 * const points = new THING.Points({
 *   points: [[0, 0, 0], [1, 1, 0], [2, 0, 0]],
 *   size: 10,
 *   style: { color: '#ffffff' },
 * });
 */
class Points extends BasePoints {

	/**
	 * 场景中的点物体(可展示点云)
	 * @description 场景中的点物体(可展示点云),构造参数的优先级,url > points > selfPoints
	 * @param {object} param 初始化参数
	 * @param {string} [param.url] 点云文件路径
	 * @param {number[][]} [param.points] 点的坐标数组(世界坐标系).
	 * @param {number[][]} [param.selfPoints] 点的坐标数组(自身坐标系).
	 * @param {boolean} [param.autoAdjustPosition=false] 是否自动校正坐标
	 * @param {number[][]} [param.colors] 点的颜色数组.
	 * @param {number} [param.imageRatio=1] 贴图比例(如果点用贴图渲染的话)
	 * @param {number} [param.size=1] 单个点尺寸
	 * @param {number} [param.sizeAttenuation=false] 尺寸是否随着摄像机远近而变化
	 * @param {number[]} [param.pickedIds] 每个点的pickID的数组 长度与points长度一致
	 * @param {object} [param.styles] 点样式
	 * @example
		const points = [
			[0, 1, 0],
			[1, 1, 0],
			[2, 1, 0],
			[3, 1, 0],
			[0, 1, 4],
			[1, 1, 4],
			[2, 1, 4],
			[3, 1, 4],
			[0, 1, 0],
			[0, 1, 1],
			[0, 1, 2],
			[0, 1, 3],
			[4, 1, 0],
			[4, 1, 1],
			[4, 1, 2],
			[4, 1, 3]
			];
		const pointsObj = new THING.Points({
			points: points,
			size: 3,
			style: {
				color: "#ff9900",
			},
		});
	 * @constructor
	 * @public
	 */
	constructor(param = {}) {
		super(param);
	}

	// #region Private Functions

	_adjustPosition(points) {
		if (points instanceof Float32Array) {
			let center = [0, 0, 0];
			for (let i = 0; i < points.length; i += 3) {
				center[0] += points[i];
				center[1] += points[i + 1];
				center[2] += points[i + 2];
			}

			center[0] /= points.length / 3;
			center[1] /= points.length / 3;
			center[2] /= points.length / 3;
			this.position = center;
		}
		else {
			super._adjustPosition(points);
		}
	}

	_getResultByKeyType(key, array) {
		if (key === 'Float32Array') {
			return new Float32Array(array);
		}
		else {
			const result = [];
			for (let i = 0; i < array.length; i += this._colorStride) {
				const color = [];
				for (let j = 0; j < this._colorStride; j++) {
					color[j] = array[i + j];
				}
				result.push(color);
			}
			return result;
		}
	}

	_loadPoints(url, resolve, reject) {
		const ext = url._getExtension();

		let loader = null;
		if (ext === 'pcd') {
			loader = new PCDLoader();
		}
		else if (ext === 'ply') {
			loader = new PLYLoader();
		}
		else {
			console.error('Url is not pcd or ply');
			reject();
			return;
		}

		loader.load(url,
			(data) => {
				resolve({ positions: data.positions, colors: data.colors });
			},
			() => { },
			(error) => {
				reject(error);
			}
		)
	}

	_setPoints(value) {
		let selfPoints = []
		if (value instanceof Float32Array) {
			this._originalPointType = 'Float32Array';

			if (this._autoAdjustPosition) {
				this._adjustPosition(value);
			}

			for (let i = 0; i < value.length; i += 3) {
				const selfPoint = this.worldToSelf([value[i], value[i + 1], value[i + 2]])
				selfPoints.push(selfPoint[0], selfPoint[1], selfPoint[2]);
			}
		}
		else {
			this._originalPointType = 'Array';

			if (this._autoAdjustPosition) {
				this._adjustPosition(value);
			}

			selfPoints = value.map(point => {
				return this.worldToSelf(point);
			}).flat(2);
		}

		this.bodyNode.setPoints(selfPoints);
		this.bodyNode.setStates([]);
		this.bodyNode.setColors([]);

		this.onRefreshPoints();
	}

	_setSelfPoints(value) {
		this._originalPointType = value instanceof Float32Array ? 'Float32Array' : 'Array';
		const selfPoints = this._originalPointType === 'Float32Array' ? Array.from(value) : value.flat(2);

		this.bodyNode.setPoints(selfPoints);
		this.onRefreshPoints();
	}

	/**
	 * 初始化颜色数组(2维)的步长
	 * @param {Array<Array<number>>} colors 颜色数组。
	 */
	_initColorsStride(colors) {
		if (!colors || colors.length === 0) {
			return;
		}

		this._colorStride = colors[0].length === 4 ? 4 : 3;
	}

	/**
	 * 初始化颜色数组的类型
	 * @param {Array<number> | Array<Array<number>>} colors 颜色数组。
	 */
	_initColorsType(colors) {
		if (!colors || colors.length === 0) {
			return;
		}

		this._originalColorType = colors instanceof Float32Array ? 'Float32Array' : 'Array';
	}

	/**
	 * 根据点位数组(1维数组)以及颜色数组(1维数组)初始化颜色步长
	 * @param {Array<number>} points 点位数组。
	 * @param {Array<number>} colors 颜色数组。
	 */
	_initColorsStrideByFlatArray(points, colors) {
		if (points.length === colors.length) {
			this._colorStride = 3;
			return;
		}
		this._colorStride = 4;
	}

	_parseColor(color) {
		if (!color) {
			return this._colorStride === 4 ? [1, 1, 1, 1] : [1, 1, 1];
		}
		if (color.length === this._colorStride) {
			return color;
		}

		if (this._colorStride === 4) {
			return [color[0], color[1], color[2], 1];
		}

		return [color[0], color[1], color[2]];
	}

	// #endregion

	// #region Overrides

	onCreateBodyNode(param) {
		const node = Utils.createObject('Points', { app: this.app });
		return node;
	}

	hasResource() {
		return true;
	}

	onSetupResource(param) {
		this.onSetupURL(param);

		this._originalColorType = 'Array';
		if (param['colors'] instanceof Float32Array) {
			this._originalColorType = 'Float32Array';
		}

		this._size = Utils.parseNumber(param['size'], 1);
		this._imageRatio = Utils.parseNumber(param['imageRatio'], 1);
		this._pickedIds = Utils.parseValue(param['pickedIds'], []);
		this._sizeAttenuation = false;

		super.onSetupResource(param);
	}

	onSetupPoints() {

	}

	onLoadRenderableResource(param, resolve, reject) {
		if (this.resource.url) {
			this._loadPoints(this.resource.url, (data) => {
				const bodyNode = Utils.createObject('Points', { app: this.app });
				this.body.setNode(bodyNode);

				if (this._autoAdjustPosition) {
					this._adjustPosition(data.positions);
				}

				this._originalPointType = 'Float32Array'
				this._originalColorType = 'Float32Array'
				this._initColorsStrideByFlatArray(data.positions, data.colors);

				this.bodyNode.begin()
				{
					this.bodyNode.setPoints(data.positions);
					this.bodyNode.setColors(data.colors);
					this.onRefreshPoints();
				}
				this.bodyNode.end()

				if (this._pickedIds) {
					this.bodyNode.setPickedIds(this._pickedIds);
				}
				if (this._imageRatio) {
					this.bodyNode.setImageRatio(this._imageRatio);
				}

				resolve && resolve();
			}, reject)
		}
		else {
			const bodyNode = Utils.createObject('Points', { app: this.app });
			this.body.setNode(bodyNode);

			bodyNode.begin();

			if (this.options['points']) {
				this._setPoints(this.options['points']);
			}
			else if (this.options['selfPoints']) {
				this._setSelfPoints(this.options['selfPoints']);
			}

			if (this.options['colors']) {
				this.colors = this.options['colors'];
			}

			if (this.options['pickedIds']) {
				this.pickedIds = this.options['pickedIds'];
			}

			if (this.options['imageRatio']) {
				this.imageRatio = this.options['imageRatio'];
			}

			bodyNode.end();
			resolve && resolve();
		}
	}

	onSetupPosition(param) {
		if (param['points']) {
			this._adjustPosition(param['points'])
		}
		else {
			super.onSetupPosition(param);
		}
	}

	onSetupStyle(param) {
		let style = param['style'];
		if (style) {
			// Enable transparent mode as default
			if (style.transparent === undefined) {
				style.transparent = true;
			}
		}
		else {
			param['style'] = {
				transparent: true
			}
		}

		super.onSetupStyle(param);
	}

	onRefreshPoints() {
		this.bodyNode.setSize(this._size);
		this.bodyNode.setSizeAttenuation(this._sizeAttenuation);

		this.onBeforeSetBodyPoints();

		this.onAfterSetBodyPoints();
	}

	onExportExternalData() {
		let external = Object.assign({}, super.onExportExternalData());
		if (!this._url) {
			Utils.setAttributeIfExist(external, 'size', this);
			Utils.setAttributeIfExist(external, 'imageRatio', this);
			Utils.setAttributeIfExist(external, 'pickedIds', this);
			Utils.setAttributeIfExist(external, 'colors', this);
		}
		return external;
	}

	clearPoints() {
		const bodyNode = this.bodyNode;
		bodyNode.begin();

		bodyNode.setColors([]);
		bodyNode.setPoints([]);
		bodyNode.setStates([]);

		bodyNode.end();
	}

	removePoint(index) {
		const bodyNode = this.bodyNode;
		bodyNode.begin();

		const colors = bodyNode.getColors();
		if (colors.length > 0) {
			colors.splice(index * this._colorStride, this._colorStride);
		}

		const selfPoints = bodyNode.getPoints();
		selfPoints.splice(index * 3, 3);

		const states = bodyNode.getStates();
		if (states.length > 0) {
			states.splice(index, 1);
		}

		this.onRefreshPoints();

		bodyNode.end();
	}

	insertPoint(index, position, color) {
		const bodyNode = this.bodyNode;
		bodyNode.begin();

		const colors = bodyNode.getColors();
		if (colors.length > 0) {
			if (!color) {
				color = Utils.getSimilarColor(colors, index, this._colorStride);
			}

			colors.splice(index * this._colorStride, 0, ...color);
		}

		const selfPoints = bodyNode.getPoints();
		const selfPoint = this.worldToSelf(position)
		selfPoints.splice(index * 3, 0, selfPoint[0], selfPoint[1], selfPoint[2]);

		const states = bodyNode.getStates();
		if (states.length > 0) {
			states.splice(index, 0, visibleFlag);
		}

		this.onRefreshPoints();
		bodyNode.end();
	}

	addPoint(position, color) {
		const bodyNode = this.bodyNode;
		bodyNode.begin();

		const colors = bodyNode.getColors();
		if (colors.length > 0) {
			if (!color) {
				color = Utils.getSimilarColor(colors, null, this._colorStride);
			}

			colors.push(...color);
		}

		const selfPoints = bodyNode.getPoints();
		const selfPoint = this.worldToSelf(position)
		selfPoints.push(selfPoint[0], selfPoint[1], selfPoint[2]);

		const states = bodyNode.getStates();
		if (states.length > 0) {
			states.push(visibleFlag);
		}

		this.onRefreshPoints();
		bodyNode.end();
	}

	addPoints(points, colors) {
		const bodyNode = this.bodyNode;
		bodyNode.begin();

		const bodyColors = bodyNode.getColors();
		if (bodyColors.length > 0) {
			if (colors) {
				colors.forEach(color => {
					color = this._parseColor(color);
					bodyColors.push(...color);
				})
			}
			else {
				const color = Utils.getSimilarColor(bodyColors, null, this._colorStride);
				for (let i = 0; i < points.length; i++) {
					bodyColors.push(...color);
				}
			}
		}

		const selfPoints = bodyNode.getPoints();
		const states = bodyNode.getStates();
		points.forEach(point => {
			const selfPoint = this.worldToSelf(point)
			selfPoints.push(selfPoint[0], selfPoint[1], selfPoint[2]);

			if (states.length > 0) {
				states.push(visibleFlag);
			}
		});

		this.onRefreshPoints();
		bodyNode.end();
	}

	setPoint(index, position, color) {
		const bodyNode = this.bodyNode;
		bodyNode.begin();

		const colors = bodyNode.getColors();
		if (colors.length > 0) {
			if (color) {
				color = this._parseColor(color);
			}
			else {
				color = Utils.getSimilarColor(colors, index, this._colorStride);
			}

			for (let i = 0; i < color; i++) {
				colors[index * this._colorStride + i] = color[i];
			}
		}

		const selfPoints = bodyNode.getPoints();

		const selfPoint = this.worldToSelf(position)
		selfPoints[index * 3] = selfPoint[0];
		selfPoints[index * 3 + 1] = selfPoint[1];
		selfPoints[index * 3 + 2] = selfPoint[2];

		this.onRefreshPoints();
		bodyNode.end();
	}
	// #endregion

	// #region Common

	/**
	 * Set the visibility of the specified index point.
	 * @param {number|Array} index 索引。
	 * @param {boolean} visible 可见性。
	 */
	setVisibleByIndex(index, visible) {
		const bodyNode = this.bodyNode;

		const states = bodyNode.getStates();
		if (states.length === 0) {
			const points = bodyNode.getPoints();

			bodyNode.setStates(new Array(points.length / 3).fill(visibleFlag));
		}

		if (Utils.isArray(index)) {
			bodyNode.begin();
			index.forEach(i => {
				bodyNode.setVisibleByIndex(i, visible);
			})
			bodyNode.end();
		}
		else {
			bodyNode.setVisibleByIndex(index, visible);
		}
	}
	/**
	 * Get the visibility of the specified index point.
	 * @param {number|Array} index 索引。
	 * @returns {Array<boolean>} 返回可见性。
	 */
	getVisibleByIndex(index) {
		const bodyNode = this.bodyNode;
		if (Utils.isArray(index)) {
			let visibles = [];
			index.forEach(i => {
				visibles.push(bodyNode.getVisibleByIndex(i));
			})
			return visibles;
		}
		else {
			return bodyNode.getVisibleByIndex(index);
		}
	}

	/**
	 * Sets the color of the specified index point.
	 * @param {number|Array} index 索引。
	 * @param {Array<number>} color 颜色。
	 */
	setColorByIndex(index, color) {
		color = this._parseColor(color);

		const bodyNode = this.bodyNode;
		if (Utils.isArray(index)) {
			bodyNode.begin();
			index.forEach(i => {
				bodyNode.setColorByIndex(i, color);
			})
			bodyNode.end();
		}
		else {
			bodyNode.setColorByIndex(index, color);
		}
	}
	/**
	 * Get the color of the specified index point.
	 * @param {number|Array} index 索引。
	 * @returns {Array<number>} 返回颜色。
	 */
	getColorByIndex(index) {
		const bodyNode = this.bodyNode;

		if (Utils.isArray(index)) {
			let colors = [];
			index.forEach(i => {
				colors.push(bodyNode.getColorByIndex(i));
			})
			return colors;
		}
		else {
			return bodyNode.getColorByIndex(index);
		}
	}

	// #endregion

	// #region Accessor

	/**
	 * 获取/设置点大小
	 * @type {number}
	 * @default 1.0
	 * @public
	 */
	get size() {
		return this._size;
	}
	set size(value) {
		this._size = value;

		this.bodyNode.setSize(value);
	}

	/**
	 * 获取/设置贴图比例
	 * @type {number}
	 * @default 1.0
	 * @public
	 */
	get imageRatio() {
		return this._imageRatio;
	}
	set imageRatio(value) {
		this._imageRatio = value;

		this.bodyNode.setImageRatio(value);
	}

	/**
	 * 开启/关闭尺寸是否随着摄像机远近而变化
	 * @default false
	 * @type {boolean}
	 * @public
	 */
	get sizeAttenuation() {
		return this._sizeAttenuation;
	}
	set sizeAttenuation(value) {
		this._sizeAttenuation = value;

		this.bodyNode.setSizeAttenuation(value);
	}

	/**
	 * 获取/设置 pickId的数组
	 * @type {Array<number>}
	 * @public
	 */
	get pickedIds() {
		return this.bodyNode.getPickedIds().slice(0);
	}
	set pickedIds(value) {
		this.bodyNode.setPickedIds(value);
	}

	/**
	 * 获取/设置colors数组
	 * @type {Array<Array<number>> | Float32Array<number>}
	 * @public
	 */
	get colors() {
		const colors = this.bodyNode.getColors();

		return this._getResultByKeyType(this._originalColorType, colors)
	}
	set colors(value) {
		// 初始化颜色数组类型
		this._initColorsType(value);

		// 根据数组类型设置颜色
		if (this._originalColorType === 'Float32Array') {
			// float32数组需要根据点位数组和颜色数组一起判断rgb还是rgba
			this._initColorsStrideByFlatArray(this.bodyNode.getPoints(), value);
			this.bodyNode.setColors(Array.from(value), this._colorStride === 4);
		}
		else {
			this._initColorsStride(value);
			this.bodyNode.setColors(value.flat(2), this._colorStride === 4);
		}
	}

	/**
	 * Get/Set Url
	 * @type {string}
	 * @public
	 */
	get url() {
		return this.resource.url;
	}
	set url(value) {
		const resource = THING.Utils.cloneObject(this.resource)
		resource.url = value;

		this.resource = resource;
	}

	get selfPoints() {
		const selfPoints = this.bodyNode.getPoints();

		return this._getResultByKeyType(this._originalPointType, selfPoints)
	}
	set selfPoints(value) {
		if (value) {
			this.bodyNode.begin();
			this._setSelfPoints(value);
			this.bodyNode.end();
		}
		else {
			this.clearPoints();
		}
	}

	/**
	 * 获取/设置顶点数组
	 * 注意,每次设置Points之后会清空颜色和显隐状态,需要先设置points,在设置colors和visible
	 * @type {Array<Array<number>>}
	 * @public
	 */
	get points() {
		let points = this.bodyNode.getPoints();
		if (this._originalPointType === 'Float32Array') {
			const tempPoints = [];
			for (let i = 0; i < points.length; i += 3) {
				const point = this.selfToWorld([points[i], points[i + 1], points[i + 2]]);
				tempPoints.push(point[0], point[1], point[2]);
			}
			return new Float32Array(tempPoints);
		}
		else {
			const result = [];
			for (let i = 0; i < points.length; i += 3) {
				result.push(this.selfToWorld([points[i], points[i + 1], points[i + 2]]));
			}

			return result;
		}
	}
	set points(value) {
		if (value) {
			this.bodyNode.begin();
			this._setPoints(value);
			this.bodyNode.end();
		}
		else {
			this.clearPoints();
		}
	}

	get pointsLength() {
		const points = this.bodyNode.getPoints();

		return points.length / 3;
	}

	// #endregion

	get isPoints() {
		return true;
	}

}

export { Points }