import { MathUtils } from '../math/MathUtils';

/**
 * @class MeshBuilder
 * The mesh builder.
 * @memberof THING
 */
class MeshBuilder {

	/**
	 * @typedef {object} MeshResult
	 * @property {Array<number>} position The position.
	 * @property {Array<number>} normal The normal.
	 * @property {Array<number>} uv The uv.
	 * @property {Array<number>} index The index of position.
	 */


	static createPlane(width = 1, height = 1, widthSegments = 1, heightSegments = 1) {
		const plane = {};
		plane.position = [];
		plane.normal = [];
		plane.uv = [];
		plane.index = [];

		const segmentWidth = width / widthSegments;
		const segmentHeight = height / heightSegments;

		for (let j = 0; j <= heightSegments; j++) {
			const yPos = j * segmentHeight - height / 2;
			for (let i = 0; i <= widthSegments; i++) {
				const xPos = i * segmentWidth - width / 2;

				plane.position.push(xPos, yPos, 0);
				plane.normal.push(0, 0, 1);
				plane.uv.push(i / widthSegments, j / heightSegments);
			}
		}

		for (let j = 0; j < heightSegments; j++) {
			for (let i = 0; i < widthSegments; i++) {
				const a = i + (widthSegments + 1) * j;
				const b = i + (widthSegments + 1) * (j + 1);
				const c = a + 1;
				const d = b + 1;

				plane.index.push(a, b, c);
				plane.index.push(b, d, c);
			}
		}

		return plane;
	}

	/**
	 * @public
	 * @typedef {object} CircleParam 构造圆形需要的参数
	 * @property {number} [radius=1] 半径
	 * @property {number} [segments=64] 模拟圆形使用的多边形的边数
	 * @property {number} [startRad=0] 开始位置(弧度)
	 * @property {number} [arc=Math.PI * 2] 所形成的扇形的角度(弧度)
	 * @example
	 * let cirecle = THING.MeshBuilder.createCircle();
	 * // @expect(cirecle.index.length == 192 );
	 */

	static createCircle({ radius = 1, segments = 64, startRad = 0, arc = MathUtils.PI * 2 } = {}) {
		let circle = {};
		circle.position = [];
		circle.normal = [];
		circle.uv = [];
		circle.index = [];

		circle.position.push(0, 0, 0);
		circle.normal.push(0, 0, 1);
		circle.uv.push(0.5, 0.5);

		for (let s = 0, i = 3; s <= segments; s++, i += 3) {
			const segment = startRad + s / segments * arc;
			circle.position.push(radius * MathUtils.cos(segment), radius * MathUtils.sin(segment), 0);
			circle.normal.push(0, 0, 1);
			circle.uv.push((circle.position[i] / radius + 1) / 2, (circle.position[i + 1] / radius + 1) / 2);
		}
		for (let i = 1; i <= segments; i++) {
			circle.index.push(i, i + 1, 0);
		}

		return circle;
	}

	/**
	 * @public
	 * @typedef {object} CylinderParam 构造圆柱需要的参数
	 * @property {number} [radiusTop=1] 顶部半径
	 * @property {number} [radiusBottom=1] 底部半径
	 * @property {number} [height=2] 高度
	 * @property {number} [radialSegments=64] 圆柱体截面是几边形 默认64代表圆柱体截面是64边形
	 * @property {number} [heightSegments=64] 高度方向分段数
	 * @property {boolean} [openEnded=false] 顶面和地面是否封闭
	 * @property {number} [thetaStart=0] 开始的角度(弧度)
	 * @property {number} [thetaLength=Math.PI*2] 结束角度(弧度)
	 */

	static createCylinder({ radiusTop = 1, radiusBottom = 1, height = 2, radialSegments = 64, heightSegments = 1, openEnded = false, thetaStart = 0, thetaLength = MathUtils.PI * 2, poslength = 0 } = {}) {
		let cylinder = {};
		cylinder.position = [];
		cylinder.normal = [];
		cylinder.uv = [];
		cylinder.index = [];

		radialSegments = MathUtils.floor(radialSegments);
		heightSegments = MathUtils.floor(heightSegments);

		let indexTemp = 0;
		const indexArray = [];
		const halfHeight = height / 2;

		generateTorso();

		if (openEnded === false) {
			if (radiusTop > 0) generateCap(true);
			if (radiusBottom > 0) generateCap(false);
		}

		function generateTorso() {
			const slope = (radiusBottom - radiusTop) / height;
			for (let y = 0; y <= heightSegments; y++) {
				const indexRow = [];
				const v = y / heightSegments;
				const radius = v * (radiusBottom - radiusTop) + radiusTop;

				for (let x = 0; x <= radialSegments; x++) {
					const u = x / radialSegments;
					const theta = u * thetaLength + thetaStart;
					const sinTheta = MathUtils.sin(theta);
					const cosTheta = MathUtils.cos(theta);
					const normalNor = MathUtils.normalizeVector([sinTheta, slope, cosTheta]);

					cylinder.position.push(radius * sinTheta, -v * height + halfHeight, radius * cosTheta);
					cylinder.normal.push(normalNor[0], normalNor[1], normalNor[2]);
					cylinder.uv.push(u, 1 - v);
					indexRow.push(indexTemp++);
				}

				indexArray.push(indexRow);
			}

			for (let x = 0; x < radialSegments; x++) {
				for (let y = 0; y < heightSegments; y++) {
					const a = indexArray[y][x];
					const b = indexArray[y + 1][x];
					const c = indexArray[y + 1][x + 1];
					const d = indexArray[y][x + 1];

					cylinder.index.push(a + poslength, b + poslength, d + poslength);
					cylinder.index.push(b + poslength, c + poslength, d + poslength);
				}
			}
		}

		function generateCap(top) {
			const centerIndexStart = indexTemp;
			const radius = (top === true) ? radiusTop : radiusBottom;
			const sign = (top === true) ? 1 : -1;

			for (let x = 1; x <= radialSegments; x++) {
				cylinder.position.push(0, halfHeight * sign, 0);
				cylinder.normal.push(0, sign, 0);
				cylinder.uv.push(0.5, 0.5);
				indexTemp++;
			}
			const centerIndexEnd = indexTemp;

			for (let x = 0; x <= radialSegments; x++) {
				const u = x / radialSegments;
				const theta = u * thetaLength + thetaStart;
				const cosTheta = MathUtils.cos(theta);
				const sinTheta = MathUtils.sin(theta);

				cylinder.position.push(radius * sinTheta, halfHeight * sign, radius * cosTheta);
				cylinder.normal.push(0, sign, 0);
				cylinder.uv.push((sinTheta * 0.5) + 0.5, -(cosTheta * 0.5 * sign) + 0.5);
				indexTemp++;
			}

			for (let x = 0; x < radialSegments; x++) {
				const c = centerIndexStart + x;
				const i = centerIndexEnd + x;

				if (top === true) {
					cylinder.index.push(i + poslength, i + 1 + poslength, c + poslength);
				}
				else {
					cylinder.index.push(i + 1 + poslength, i + poslength, c + poslength);
				}
			}
		}

		return cylinder;
	}

	/**
	 * @public
	 * @typedef {object} TorusParam 构造圆环需要的参数
	 * @property {number} [radius=0.8] 圆环内径
	 * @property {number} [tube=0.2] 圆环截面(管)的半径
	 * @property {number} [radialSegments=64] 圆环横切面使用多少边形
	 * @property {number} [tubularSegments=64] 圆环纵切面(管)使用多少边形
	 * @property {number} [arc=Math.PI*2] 圆环范围(默认是一圈,即Math.PI*2,如果是Math.PI,代表圆环是一个半圆)
	 */


	static createTorus({ radius = 0.8, tube = 0.2, radialSegments = 64, tubularSegments = 64, arc = MathUtils.PI * 2, startRad = 0 } = {}) {
		let torus = {};
		torus.position = [];
		torus.normal = [];
		torus.uv = [];
		torus.index = [];

		radialSegments = MathUtils.floor(radialSegments);
		tubularSegments = MathUtils.floor(tubularSegments);

		let centerTemp = [];
		let vertexTemp = [];
		let normalTemp = [];

		for (let j = 0; j <= radialSegments; j++) {
			for (let i = 0; i <= tubularSegments; i++) {
				const u = i / tubularSegments * arc + startRad;
				const v = j / radialSegments * MathUtils.PI * 2;

				vertexTemp = [
					(radius + tube * MathUtils.cos(v)) * MathUtils.cos(u),
					(radius + tube * MathUtils.cos(v)) * MathUtils.sin(u),
					tube * MathUtils.sin(v)
				];
				torus.position.push(
					(radius + tube * MathUtils.cos(v)) * MathUtils.cos(u),
					(radius + tube * MathUtils.cos(v)) * MathUtils.sin(u),
					tube * MathUtils.sin(v)
				);
				centerTemp = [radius * MathUtils.cos(u), radius * MathUtils.sin(u), 0];
				normalTemp = MathUtils.normalizeVector(MathUtils.subVector(vertexTemp, centerTemp));
				torus.normal.push(normalTemp[0], normalTemp[1], normalTemp[2]);
				torus.uv.push(i / tubularSegments);
				torus.uv.push(j / radialSegments);
			}
		}

		for (let j = 1; j <= radialSegments; j++) {
			for (let i = 1; i <= tubularSegments; i++) {
				const a = (tubularSegments + 1) * j + i - 1;
				const b = (tubularSegments + 1) * (j - 1) + i - 1;
				const c = (tubularSegments + 1) * (j - 1) + i;
				const d = (tubularSegments + 1) * j + i;

				torus.index.push(a, b, d);
				torus.index.push(b, c, d);
			}
		}

		return torus;
	}


	/**
	 * @typedef {object} CapsuleParam 构造胶囊需要的参数
	 * @property {number} [radius=0.5] 中间界面的半径
	 * @property {number} [cylinderHeight=1.0] 胶囊的高度
	 * @property {number} [widthSegments=64] 横截面是多少边形,值越大越精细,三角面越密集
	 * @property {number} [heightSegments=64] 高度方向片段个数,值越大越精细,三角面越密集
	 * @public
	 */

	static createCapsule({ radius = 0.5, cylinderHeight = 1, widthSegments = 64, heightSegments = 64 } = {}) {
		let capsule = {};
		capsule.position = [];
		capsule.normal = [];
		capsule.uv = [];
		capsule.index = [];

		let upSphere = mathSphere(radius, widthSegments, heightSegments, 0, -MathUtils.PI * 2, -MathUtils.PI / 2, MathUtils.PI, cylinderHeight / 2);
		let downSphere = mathSphere(radius, widthSegments, heightSegments, 0, -MathUtils.PI * 2, MathUtils.PI / 2, MathUtils.PI, -cylinderHeight / 2, upSphere.position.length / 3);
		let cylinder = this.createCylinder({
			radiusTop: radius, radiusBottom: radius, height: cylinderHeight, radialSegments: heightSegments, heightSegments: 1,
			openEnded: true, thetaStart: -MathUtils.PI / 4 * 2, thetaLength: MathUtils.PI * 2, poslength: upSphere.position.length / 3 + downSphere.position.length / 3
		})

		capsule.position = [
			...upSphere.position,
			...downSphere.position,
			...cylinder.position
		];
		capsule.normal = [
			...upSphere.normal,
			...downSphere.normal,
			...cylinder.normal
		];
		capsule.uv = [
			...upSphere.uv,
			...downSphere.uv,
			...cylinder.uv
		];
		capsule.index = [
			...upSphere.index,
			...downSphere.index,
			...cylinder.index
		];

		function mathSphere(radius = 10, widthSegments = 8, heightSegments = 8, phiStart = 0, phiLength = MathUtils.PI * 2,
			thetaStart = MathUtils.PI, thetaLength = MathUtils.PI, heightOffset = 0, poslength = 0) {
			let sphere = {};
			sphere.position = [];
			sphere.normal = [];
			sphere.uv = [];
			sphere.index = [];

			widthSegments = MathUtils.max(3, MathUtils.floor(widthSegments));
			heightSegments = MathUtils.max(2, MathUtils.floor(heightSegments));

			const thetaEnd = MathUtils.min(thetaStart + thetaLength, MathUtils.PI);
			let indexTemp = 0;
			const grid = [];

			for (let iy = 0; iy <= heightSegments; iy++) {
				const verticesRow = [];
				const v = iy / heightSegments;
				let uOffset = 0;
				if (iy == 0 && thetaStart == 0) {
					uOffset = 0.5 / widthSegments;
				}
				else if (iy == heightSegments && thetaEnd == MathUtils.PI) {
					uOffset = -0.5 / widthSegments;
				}
				for (let ix = 0; ix <= widthSegments; ix++) {
					const u = ix / widthSegments;
					sphere.position.push(
						-radius * MathUtils.cos(phiStart + u * phiLength) * MathUtils.sin(thetaStart + v * thetaLength),
						radius * MathUtils.cos(thetaStart + v * thetaLength) + heightOffset,
						radius * MathUtils.sin(phiStart + u * phiLength) * MathUtils.sin(thetaStart + v * thetaLength)
					);
					let normali = [
						-radius * MathUtils.cos(phiStart + u * phiLength) * MathUtils.sin(thetaStart + v * thetaLength),
						radius * MathUtils.cos(thetaStart + v * thetaLength),
						radius * MathUtils.sin(phiStart + u * phiLength) * MathUtils.sin(thetaStart + v * thetaLength)
					]
					normali = MathUtils.normalizeVector(normali);
					sphere.normal.push(normali[0], normali[1], normali[2]);
					if (thetaStart > 0) { sphere.uv.push(-u, -v); }
					else { sphere.uv.push(-u, v); }
					verticesRow.push(indexTemp++);
				}
				grid.push(verticesRow);
			}

			for (let iy = 0; iy < heightSegments / 2; iy++) {
				for (let ix = 0; ix < widthSegments; ix++) {
					const a = grid[iy][ix + 1];
					const b = grid[iy][ix];
					const c = grid[iy + 1][ix];
					const d = grid[iy + 1][ix + 1];
					if (thetaStart > 0) {
						sphere.index.push(a + poslength, d + poslength, b + poslength);
						sphere.index.push(b + poslength, d + poslength, c + poslength);
					}
					else {
						sphere.index.push(b + poslength, c + poslength, d + poslength);
						sphere.index.push(b + poslength, d + poslength, a + poslength);
					}
				}
			}

			return sphere;
		}

		return capsule;
	}

	static createSphere({ radius = 1, widthSegments = 32, heightSegments = 16, phiStart = 0, phiLength = MathUtils.PI * 2,
		thetaStart = 0, thetaLength = MathUtils.PI } = {}) {
		let sphere = {};
		sphere.position = [];
		sphere.normal = [];
		sphere.uv = [];
		sphere.index = [];

		widthSegments = MathUtils.max(3, MathUtils.floor(widthSegments));
		heightSegments = MathUtils.max(2, MathUtils.floor(heightSegments));

		const thetaEnd = MathUtils.min(thetaStart + thetaLength, MathUtils.PI);
		let indexTemp = 0;
		const grid = [];

		for (let iy = 0; iy <= heightSegments; iy++) {
			const verticesRow = [];
			const v = iy / heightSegments;
			let uOffset = 0;
			if (iy == 0 && thetaStart == 0) {
				uOffset = 0.5 / widthSegments;
			}
			else if (iy == heightSegments && thetaEnd == MathUtils.PI) {
				uOffset = -0.5 / widthSegments;
			}
			for (let ix = 0; ix <= widthSegments; ix++) {
				const u = ix / widthSegments;

				let vertex = [
					-radius * MathUtils.cos(phiStart + u * phiLength) * MathUtils.sin(thetaStart + v * thetaLength),
					radius * MathUtils.cos(thetaStart + v * thetaLength),
					radius * MathUtils.sin(phiStart + u * phiLength) * MathUtils.sin(thetaStart + v * thetaLength)
				];

				sphere.position.push(...vertex);

				let normali = MathUtils.normalizeVector(vertex);
				sphere.normal.push(...normali);

				sphere.uv.push(u + uOffset, 1 - v);

				verticesRow.push(indexTemp++);
			}
			grid.push(verticesRow);
		}

		for (let iy = 0; iy < heightSegments; iy++) {
			for (let ix = 0; ix < widthSegments; ix++) {
				const a = grid[iy][ix + 1];
				const b = grid[iy][ix];
				const c = grid[iy + 1][ix];
				const d = grid[iy + 1][ix + 1];

				if (iy !== 0 || thetaStart > 0) sphere.index.push(a, b, d);
				if (iy !== heightSegments - 1 || thetaEnd < Math.PI) sphere.index.push(b, c, d);
			}
		}

		return sphere;
	}

	static createRing({ innerRadius = 0.5, outerRadius = 1, thetaSegments = 32, phiSegments = 1, thetaStart = 0, thetaLength = Math.PI * 2 } = {}) {
		let ring = {};
		ring.position = [];
		ring.normal = [];
		ring.uv = [];
		ring.index = [];

		thetaSegments = MathUtils.max(3, MathUtils.floor(thetaSegments));
		phiSegments = MathUtils.max(2, MathUtils.floor(phiSegments));

		let radius = innerRadius;
		const radiusStep = ((outerRadius - innerRadius) / phiSegments);

		for (let j = 0; j <= phiSegments; j++) {
			for (let i = 0; i <= thetaSegments; i++) {
				// values are generate from the inside of the ring to the outside

				const segment = thetaStart + i / thetaSegments * thetaLength;

				// vertex

				const vertex = [
					radius * MathUtils.cos(segment),
					radius * MathUtils.sin(segment),
					0
				];

				ring.position.push(...vertex);

				// normal

				ring.normal.push(0, 0, 1);

				// uv

				const uv = [
					(vertex[0] / outerRadius + 1) / 2,
					(vertex[1] / outerRadius + 1) / 2
				];

				ring.uv.push(...uv);
			}

			// increase the radius for next row of vertices

			radius += radiusStep;
		}

		// indices

		for (let j = 0; j < phiSegments; j++) {
			const thetaSegmentLevel = j * (thetaSegments + 1);

			for (let i = 0; i < thetaSegments; i++) {
				const segment = i + thetaSegmentLevel;

				const a = segment;
				const b = segment + thetaSegments + 1;
				const c = segment + thetaSegments + 2;
				const d = segment + 1;

				// faces

				ring.index.push(a, b, d);
				ring.index.push(b, c, d);
			}
		}

		return ring;
	}

	static createBox({ width = 1, height = 1, depth = 1, widthSegments = 1, heightSegments = 1, depthSegments = 1 } = {}) {
		// segments

		widthSegments = Math.floor(widthSegments);
		heightSegments = Math.floor(heightSegments);
		depthSegments = Math.floor(depthSegments);

		let box = {};
		box.position = [];
		box.normal = [];
		box.uv = [];
		box.index = [];
		// helper variables

		let numberOfVertices = 0;
		let groupStart = 0;

		buildPlane('z', 'y', 'x', -1, -1, depth, height, width, depthSegments, heightSegments, 0); // px
		buildPlane('z', 'y', 'x', 1, -1, depth, height, -width, depthSegments, heightSegments, 1); // nx
		buildPlane('x', 'z', 'y', 1, 1, width, depth, height, widthSegments, depthSegments, 2); // py
		buildPlane('x', 'z', 'y', 1, -1, width, depth, -height, widthSegments, depthSegments, 3); // ny
		buildPlane('x', 'y', 'z', 1, -1, width, height, depth, widthSegments, heightSegments, 4); // pz
		buildPlane('x', 'y', 'z', -1, -1, width, height, -depth, widthSegments, heightSegments, 5); // nz

		function buildPlane(u, v, w, udir, vdir, width, height, depth, gridX, gridY, materialIndex) {
			const segmentWidth = width / gridX;
			const segmentHeight = height / gridY;

			const widthHalf = width / 2;
			const heightHalf = height / 2;
			const depthHalf = depth / 2;

			const gridX1 = gridX + 1;
			const gridY1 = gridY + 1;

			let vertexCounter = 0;
			let groupCount = 0;

			const vector = { x: 0, y: 0, z: 0 };

			// generate vertices, normals and uvs

			for (let iy = 0; iy < gridY1; iy++) {
				const y = iy * segmentHeight - heightHalf;

				for (let ix = 0; ix < gridX1; ix++) {
					const x = ix * segmentWidth - widthHalf;

					// set values to correct vector component

					vector[u] = x * udir;
					vector[v] = y * vdir;
					vector[w] = depthHalf;

					// now apply vector to vertex buffer

					box.position.push(vector.x, vector.y, vector.z);

					// set values to correct vector component

					vector[u] = 0;
					vector[v] = 0;
					vector[w] = depth > 0 ? 1 : -1;

					// now apply vector to normal buffer

					box.normal.push(vector.x, vector.y, vector.z);

					// uvs

					box.uv.push(ix / gridX);
					box.uv.push(1 - (iy / gridY));

					// counters

					vertexCounter += 1;
				}
			}

			// indices

			// 1. you need three indices to draw a single face
			// 2. a single segment consists of two faces
			// 3. so we need to generate six (2*3) indices per segment

			for (let iy = 0; iy < gridY; iy++) {
				for (let ix = 0; ix < gridX; ix++) {
					const a = numberOfVertices + ix + gridX1 * iy;
					const b = numberOfVertices + ix + gridX1 * (iy + 1);
					const c = numberOfVertices + (ix + 1) + gridX1 * (iy + 1);
					const d = numberOfVertices + (ix + 1) + gridX1 * iy;

					// faces

					box.index.push(a, b, d);
					box.index.push(b, c, d);

					// increase counter

					groupCount += 6;
				}
			}



			// calculate new start value for groups

			groupStart += groupCount;

			// update total number of vertices

			numberOfVertices += vertexCounter;
		}

		return box;
	}

}

export { MeshBuilder };