import { Matrix3D, Vector3D, Box, Sphere, Rectangle } from '@awayjs/core';

import { AttributesView, Short2Attributes } from '@awayjs/stage';

import { HitTestCache } from './HitTestCache';
import { LineElements } from '../elements/LineElements';

const MAX_INT =  268435456 - 1; // 2^28 - 1
const MIN_INT = -268435456; // -2^28
export class LineElementsUtils {
	//TODO - generate this dyanamically based on num tris

	public static hitTest(x: number, y: number, z: number, thickness: number, box: Box, lineElements: LineElements, count: number, offset: number = 0): boolean {
		const positionAttributes: AttributesView = lineElements.positions;

		const posStride: number = positionAttributes.stride;

		let positions: ArrayBufferView = positionAttributes.get(count, offset);

		let indices: Uint16Array;

		let len: number;
		if (lineElements.indices) {
			indices = lineElements.indices.get(count, offset);
			positions = positionAttributes.get(positionAttributes.count);
			len = count * lineElements.indices.dimensions;
		} else {
			positions = positionAttributes.get(count, offset);
			len = count;
		}

		let id0: number;
		let id1: number;

		let ax: number;
		let ay: number;
		let bx: number;
		let by: number;

		const hitTestCache: HitTestCache = lineElements.hitTestCache[offset] || (lineElements.hitTestCache[offset] = new HitTestCache());
		const index: number = hitTestCache.lastCollisionIndex;

		if (index != -1 && index < len) {
			precheck: {
				if (indices) {
					id0 = indices[index] * posStride;
					id1 = indices[index + 1] * posStride;
				} else {
					id0 = index * posStride;
					id1 = (index + 1) * posStride;
				}

				ax = positions[id0];
				ay = positions[id0 + 1];
				bx = positions[id1];
				by = positions[id1 + 1];

				//from a to p
				const dx: number = ax - x;
				const dy: number = ay - y;

				//edge normal (a-b)
				const nx: number = by - ay;
				const ny: number = -(bx - ax);
				const D: number = Math.sqrt(nx * nx + ny * ny);

				//TODO: should strictly speaking be an elliptical calculation, use circle to approx temp
				if (Math.abs((dx * nx) + (dy * ny)) > thickness * D)
					break precheck;

				//edge vector
				const dot: number = (dx * ny) - (dy * nx);

				if (dot > D * D || dot < 0)
					break precheck;

				return true;
			}
		}

		//hard coded min vertex count to bother using a grid for
		if (len > 150) {
			const cells: Array<Array<number>> = hitTestCache.cells;
			const divisions: number = cells.length ? hitTestCache.divisions : (hitTestCache.divisions = Math.min(Math.ceil(Math.sqrt(len)), 32));
			const conversionX: number = divisions / box.width;
			const conversionY: number = divisions / box.height;
			const minx: number = box.x;
			const miny: number = box.y;

			if (!cells.length) { //build grid

				//now we have bounds start creating grid cells and filling
				cells.length = divisions * divisions;

				for (let k: number = 0; k < len; k += 3) {
					if (indices) {
						id0 = indices[k] * posStride;
						id1 = indices[k + 1] * posStride;
					} else {
						id0 = k * posStride;
						id1 = (k + 1) * posStride;
					}

					ax = positions[id0];
					ay = positions[id0 + 1];
					bx = positions[id1];
					by = positions[id1 + 1];

					//subtractions to push into positive space
					const min_index_x: number = Math.floor((Math.min(ax, bx) - minx) * conversionX);
					const min_index_y: number = Math.floor((Math.min(ay, by) - miny) * conversionY);

					const max_index_x: number = Math.floor((Math.max(ax, bx) - minx) * conversionX);
					const max_index_y: number = Math.floor((Math.max(ay, by) - miny) * conversionY);

					for (let i: number = min_index_x; i <= max_index_x; i++) {
						for (let j: number = min_index_y; j <= max_index_y; j++) {
							const c: number = i + j * divisions;
							const nodes: Array<number> = cells[c] || (cells[c] = new Array<number>());

							//push in the triangle ids
							nodes.push(k);
						}
					}
				}
			}

			const index_x: number = Math.floor((x - minx) * conversionX);
			const index_y: number = Math.floor((y - miny) * conversionY);
			const nodes: Array<number> = cells[index_x + index_y * divisions];

			if (nodes == null) {
				hitTestCache.lastCollisionIndex = -1;
				return false;
			}

			const nodeCount: number = nodes.length;
			for (let n: number = 0; n < nodeCount; n++) {
				const k: number = nodes[n];

				if (indices) {
					id0 = indices[k] * posStride;
					id1 = indices[k + 1] * posStride;
				} else {
					id0 = k * posStride;
					id1 = (k + 1) * posStride;
				}

				ax = positions[id0];
				ay = positions[id0 + 1];
				bx = positions[id1];
				by = positions[id1 + 1];

				//from a to p
				const dx: number = ax - x;
				const dy: number = ay - y;

				//edge normal (a-b)
				const nx: number = by - ay;
				const ny: number = -(bx - ax);
				const D: number = Math.sqrt(nx * nx + ny * ny);

				//TODO: should strictly speaking be an elliptical calculation, use circle to approx temp
				if (Math.abs((dx * nx) + (dy * ny)) > thickness * D)
					continue;

				//edge vector
				const dot: number = (dx * ny) - (dy * nx);

				if (dot > D * D || dot < 0)
					continue;

				hitTestCache.lastCollisionIndex = k;
				return true;
			}
			hitTestCache.lastCollisionIndex = -1;
			return false;
		}

		//brute force
		for (let k: number = 0; k < len; k += 6) {
			if (indices) {
				id0 = indices[k] * posStride;
				id1 = indices[k + 1] * posStride;
			} else {
				id0 = k * posStride;
				id1 = (k + 1) * posStride;
			}

			ax = positions[id0];
			ay = positions[id0 + 1];
			bx = positions[id1];
			by = positions[id1 + 1];

			//from a to p
			const dx: number = ax - x;
			const dy: number = ay - y;

			//edge normal (a-b)
			const nx: number = by - ay;
			const ny: number = -(bx - ax);
			const D: number = Math.sqrt(nx * nx + ny * ny);

			//TODO: should strictly speaking be an elliptical calculation, use circle to approx temp
			if (Math.abs((dx * nx) + (dy * ny)) > thickness * D)
				continue;

			//edge vector
			const dot: number = (dx * ny) - (dy * nx);

			if (dot > D * D || dot < 0)
				continue;

			hitTestCache.lastCollisionIndex = k;
			return true;
		}
		hitTestCache.lastCollisionIndex = -1;
		return false;
	}

	public static getBoxBounds(positionAttributes: AttributesView, indexAttributes: Short2Attributes, matrix3D: Matrix3D, thicknessScale: Vector3D, cache: Box, target: Box, count: number, offset: number = 0): Box {
		let positions: ArrayBufferView;
		const posDim: number = positionAttributes.dimensions;
		const posStride: number = positionAttributes.stride;

		let minX: number = 0, minY: number = 0, minZ: number = 0;
		let maxX: number = 0, maxY: number = 0, maxZ: number = 0;

		let indices: Uint16Array;
		let len: number;
		if (indexAttributes) {
			len = count * indexAttributes.dimensions;
			indices = indexAttributes.get(count, offset);
			positions = positionAttributes.get(positionAttributes.count);
		} else {
			len = count;
			positions = positionAttributes.get(count, offset);
		}

		if (len == 0)
			return target;

		let index: number;
		let pos1: number, pos2: number, pos3: number, rawData: Float32Array;

		if (matrix3D)
			rawData = matrix3D._rawData;

		for (let i: number = 0; i < len; i += 3) {
			index = (indices) ? indices[i] * posStride : i * posStride;

			if (matrix3D) {
				if (posDim == 6) {
					pos1 = positions[index] * rawData[0] + positions[index + 1] * rawData[4] + positions[index + 2] * rawData[8] + rawData[12];
					pos2 = positions[index] * rawData[1] + positions[index + 1] * rawData[5] + positions[index + 2] * rawData[9] + rawData[13];
					pos3 = positions[index] * rawData[2] + positions[index + 1] * rawData[6] + positions[index + 2] * rawData[10] + rawData[14];
				} else {
					pos1 = positions[index] * rawData[0] + positions[index + 1] * rawData[4] + rawData[12];
					pos2 = positions[index] * rawData[1] + positions[index + 1] * rawData[5] + rawData[13];
				}
			} else {
				pos1 = positions[index];
				pos2 = positions[index + 1];
				pos3 = (posDim == 6) ? positions[index + 2] : 0;
			}

			if (i == 0) {
				maxX = minX = pos1;
				maxY = minY = pos2;
				maxZ = minZ = (posDim == 6) ? pos3 : 0;
			} else {
				if (pos1 < minX)
					minX = pos1;
				else if (pos1 > maxX)
					maxX = pos1;

				if (pos2 < minY)
					minY = pos2;
				else if (pos2 > maxY)
					maxY = pos2;

				if (posDim == 6) {
					if (pos3 < minZ)
						minZ = pos3;
					else if (pos3 > maxZ)
						maxZ = pos3;
				}
			}
		}

		const box: Box = new Box(minX, minY);
		box.right = maxX;
		box.bottom = maxY;

		this.mergeThinkness(box, thicknessScale, matrix3D);

		return box.union(target, target || cache);
	}

	public static mergeThinkness(target: Box, thicknessScale: Vector3D, matrix3D: Matrix3D): Box {
		const rawData = matrix3D?._rawData;

		const thicknessX = matrix3D
			? thicknessScale.x * rawData[0] + thicknessScale.y * rawData[4]
			: thicknessScale.x;
		const thicknessY = matrix3D
			? thicknessScale.x * rawData[1] + thicknessScale.y * rawData[5]
			: thicknessScale.y;

		target.x -= thicknessX;
		target.y -= thicknessY;
		target.width += thicknessX * 2;
		target.height += thicknessY * 2;

		return target;
	}

	public static getSphereBounds(positionAttributes: AttributesView, center: Vector3D, matrix3D: Matrix3D, cache: Sphere, output: Sphere, count: number, offset: number = 0): Sphere {
		const positions: ArrayBufferView = positionAttributes.get(count, offset);
		const posDim: number = positionAttributes.dimensions;
		const posStride: number = positionAttributes.stride;

		let maxRadiusSquared: number = 0;
		let radiusSquared: number;
		const len = count * posStride;
		let distanceX: number;
		let distanceY: number;
		let distanceZ: number;

		for (let i: number = 0; i < len; i += posStride) {
			distanceX = positions[i] - center.x;
			distanceY = positions[i + 1] - center.y;
			distanceZ = (posDim == 6) ? positions[i + 2] - center.z : -center.z;
			radiusSquared = distanceX * distanceX + distanceY * distanceY + distanceZ * distanceZ;

			if (maxRadiusSquared < radiusSquared)
				maxRadiusSquared = radiusSquared;
		}

		if (output == null)
			output = new Sphere();

		output.x = center.x;
		output.y = center.y;
		output.z = center.z;
		output.radius = Math.sqrt(maxRadiusSquared);

		return output;
	}

	public static prepareScale9 (
		elem: LineElements,
		bounds: Rectangle,
		grid: Rectangle,
		clone: boolean
	): LineElements {

		const target = clone ? elem.clone() : elem;

		const shapeBounds = LineElementsUtils.getBoxBounds (
			elem.positions,
			elem.indices,
			null,
			new Vector3D(),
			null,
			null,
			elem._numElements || elem._numVertices
		);

		const sliceX = [
			MIN_INT,
			grid.x,
			grid.right,
			MAX_INT
		];

		const sliceY = [
			MIN_INT,
			grid.y,
			grid.bottom,
			MAX_INT
		];

		const chunkX = {
			from: 0, to: 0
		};

		const chunkY = {
			from: 0, to: 0
		};

		for (let i = 1; i < 3; i++) {
			if (shapeBounds.x > sliceX[i]) {
				chunkX.from = chunkX.to = i;
			}

			if (shapeBounds.y > sliceY[i]) {
				chunkY.from = chunkY.to = i;
			}
		}

		for (let i = 0; i < 3; i++) {
			if (shapeBounds.right >= sliceX[i] && i >= chunkX.from) {
				chunkX.to = i;
			}

			if (shapeBounds.bottom >= sliceY[i] && i >= chunkY.from) {
				chunkY.to = i;
			}
		}

		target.scale9Grid = grid;
		target.originalScale9Bounds = bounds;
		const indices = target.scale9Indices = Array.from({ length: 9 }, (_) => 0);

		const stack = this.restoreLineSegments (target);

		// shape already in valid region
		// not require run slicer for this case
		if (chunkX.from === chunkX.to && chunkY.from === chunkY.to) {

			const buff = new Float32Array(stack.length * 3);

			for (let i = 0; i < stack.length; i++) {
				buff[i * 3 + 0] = stack[i].x;
				buff[i * 3 + 1] = stack[i].y;
				buff[i * 3 + 2] = 0;
			}

			target.initialScale9Positions = <any>buff;
			target.scale9Indices[chunkY.from * 3 + chunkX.from] = target._numElements || target._numVertices;

			return target;
		}

		const posByChunks: number[][] = [];
		let count = 0;

		while (stack.length && count < 10000) {
			const b = stack.pop();
			const a = stack.pop();

			let cX = 0;
			let cY = 0;

			let breakAll = false;

			for (let y = chunkY.from; y <= chunkY.to; y++) {
				const day = ~~((sliceY[y + 1] - a.y) * 10000) / 10000;
				const dby = ~~((sliceY[y + 1] - b.y) * 10000) / 10000;

				// slicer is crossed, emit point
				if (day * dby < 0) {
					const alpha = day / (day - dby);
					const c = new Vector3D(a.x + alpha * (b.x - a.x), a.y + alpha * (b.y - a.y), 0, 0);

					// push new segs;
					stack.push(c, b, a, c);
					// drop process
					breakAll = true;
					break;
				}

				if ((a.y + b.y) * 0.5 > sliceY[y] && (a.y + b.y) * 0.5 <= sliceY[y + 1]) {
					cY = y;
				}

				for (let x = chunkX.from; x <= chunkX.to; x++) {
					const dax = ~~((sliceX[x + 1] - a.x) * 10000) / 10000;
					const dbx = ~~((sliceX[x + 1] - b.x) * 10000) / 10000;

					// slicer is crossed, emit point
					if (dax * dbx < 0) {
						const alpha = dax / (dax - dbx);
						const c = new Vector3D(a.x + alpha * (b.x - a.x), a.y + alpha * (b.y - a.y), 0, 0);

						// push new segs;
						stack.push(c, b, a, c);
						// drop process
						breakAll = true;
						break;
					}

					if ((a.x + b.x) * 0.5 > sliceX[x] && (a.x + b.x) * 0.5 <= sliceX[x + 1]) {
						cX = x;
					}
				}

				if (breakAll) break;
			}

			if (!breakAll) {
				if (!posByChunks[cX + cY * 3]) {
					posByChunks[cX + cY * 3] = [];
				}

				posByChunks[cX + cY * 3].push(a.x, a.y, 0, b.x, b.y, 0);
				count += 2;
			}
		}

		const posBuff = new Float32Array(count * 3);
		const thinBuff = new Float32Array(count);

		// fill by same thinkness
		thinBuff.fill(target.thickness.get(1)[0]);

		let nextIndices = 0;

		for (let i = 0; i < 9; i++) {
			if (!posByChunks[i]) {
				continue;
			}

			posBuff.set(posByChunks[i], nextIndices * 3);
			nextIndices += posByChunks[i].length / 3;
			indices[i] = nextIndices;
		}

		target.initialScale9Positions = posBuff;

		target.setPositions(posBuff);
		target.setThickness(thinBuff);

		target.invalidate();

		return target;
	}

	private static restoreLineSegments(elem: LineElements): Vector3D[] {
		const positionAttributes = elem.positions;
		const indexAttributes = elem.indices;
		const count = elem._numElements || elem._numVertices;

		const posDim = positionAttributes.dimensions;
		const posStride = positionAttributes.stride;

		let indices: Uint16Array;
		let positions: Float32Array;
		let len: number;

		if (indexAttributes) {
			len = count * indexAttributes.dimensions;
			indices = indexAttributes.get(count, 0);
			positions = <any> positionAttributes.get(positionAttributes.count);
		} else {
			len = count;
			positions = <any> positionAttributes.get(count, 0);
		}

		const out: Vector3D[] = [];

		for (let i = 0; i < len; i += 6) {
			let index = indices ? indices[i] * posStride : i * posStride;

			out.push(new Vector3D(
				positions[index],
				positions[index + 1],
				posDim === 6 ?  positions[index + 2] : 0,
				0,
			));

			index += (posDim == 6) ? 3 : 2;
			out.push(new Vector3D(
				positions[index],
				positions[index + 1],
				posDim === 6 ?  positions[index + 2] : 0,
				0,
			));
		}

		return out;
	}

	public static updateScale9(
		elem: LineElements,
		originalRect: Rectangle,
		scaleX: number,
		scaleY: number,
		init: boolean = false,
		copy: boolean = false,
	): LineElements {
		// todo: for now this only works for Float2Attributes.

		if (elem.scale9Indices.length !== 9) {
			throw 'ElementUtils: Error - triangleElement does not provide valid slice9Indices!';
		}

		const offsets = elem.scale9Grid;
		const left = offsets.x - originalRect.x;
		const right = originalRect.right - offsets.right;
		const top = offsets.y - originalRect.y;
		const bottom = originalRect.bottom - offsets.bottom;

		const s_len = elem.scale9Indices.length;

		let innerWidth = originalRect.width * scaleX - (left + right);
		let innerHeight = originalRect.height * scaleY - (top + bottom);

		let cornerScaleX = 1;
		let cornerScaleY = 1;

		// reduce a overflow, when scale to small
		if (innerWidth < 0) {
			innerWidth = 0;

			cornerScaleX = originalRect.width * scaleX / (left + right);
		}

		if (innerHeight < 0) {
			innerHeight = 0;

			cornerScaleY = originalRect.height * scaleY / (top + bottom);
		}

		const innerScaleX = innerWidth / offsets.width;
		const innerScaleY = innerHeight / offsets.height;

		const stride = elem.positions.stride;
		const attrOffset = elem.positions.offset;
		const dim = elem.positions.dimensions;

		const positions = elem.positions.get(elem._numVertices);

		// todo: i had trouble when just cloning the positions
		//	for now i just create the initialSlice9Positions by iterating the positions

		const initPos = elem.initialScale9Positions;

		const slice9Indices: number[] = elem.scale9Indices;

		const slice9Offsets_x = [
			0,
			left * cornerScaleX - left * innerScaleX,
			innerWidth - offsets.width * cornerScaleX,
		];

		const slice9Offsets_y = [
			0,
			top * cornerScaleY - top * innerScaleY,
			innerHeight - offsets.height * cornerScaleY,
		];

		// internal buffer iterator
		let attrindex = attrOffset;
		let vindex = 0;

		// iterating over the 9 chunks - keep in mind that we are constructing a 3x3 grid:
		for (let s = 0; s < s_len; s++) {

			const row = s / 3 | 0;
			const col = s % 3;

			// only need to x-scale if this is the middle column
			// if the innerWidth<=0 we can skip this complete column
			const scalex = col === 1 ? innerScaleX : cornerScaleX;

			// only need to y-scale if this is the middle row
			// if the innerHeight<=0 we can skip this complete row
			const scaley = row === 1 ? innerScaleY : cornerScaleY;

			// offsetx is different for each column
			const offsetx = slice9Offsets_x[col];

			// offsety is different for each row
			const offsety = slice9Offsets_y[row];

			// iterate the verts and apply the translation / scale
			// slice9Indices is vertices indeces, is not attribute indices
			while (vindex < slice9Indices[s]) {
				// start point
				const vxs = originalRect.x + (offsetx + (initPos[vindex * 3 + 0] - originalRect.x) * scalex) / scaleX;
				const vys = originalRect.y + (offsety + (initPos[vindex * 3 + 1] - originalRect.y) * scaley) / scaleY;
				// end point
				const vxe = originalRect.x + (offsetx + (initPos[vindex * 3 + 3] - originalRect.x) * scalex) / scaleX;
				const vye = originalRect.y + (offsety + (initPos[vindex * 3 + 4] - originalRect.y) * scaley) / scaleY;

				for (let i = 0; i < 2; i++) {
					let index = attrindex;
					// super complex, line has a lot of doubled vertices
					positions[index++] = vxs;
					positions[index++] = vys;
					dim === 6 && (positions[index++] = 0);

					positions[index++] = vxe;
					positions[index++] = vye;
					dim === 6 && (positions[index++] = 0);

					attrindex += stride;
					index = attrindex;

					positions[index++] = vxe;
					positions[index++] = vye;
					dim === 6 && (positions[index++] = 0);

					positions[index++] = vxs;
					positions[index++] = vys;
					dim === 6 && (positions[index++] = 0);

					attrindex += stride;
				}
				// we should include a stride, because buffer maybe be contecated
				// or XYZ instead of XY

				vindex += 2;
			}
		}

		elem.positions.invalidate();
		elem.invalidate();

		return elem;
	}
}