
import { Point, MathConsts,  Matrix } from '@awayjs/core';

import { ImageSampler, AttributesBuffer, AttributesView, Float2Attributes } from '@awayjs/stage';

import { IMaterial, Style, TriangleElements } from '@awayjs/renderer';

import { Shape } from '../renderables/Shape';
import { GraphicsPath } from '../draw/GraphicsPath';
import { CapsStyle } from '../draw/CapsStyle';
import { MaterialManager } from '../managers/MaterialManager';
import { Settings } from '../Settings';

export class GraphicsFactoryHelper {
	public static _tess_obj: any;

	public static drawRectangles (inputRectangles: number[], color: number, alpha: number): Shape {

		if (inputRectangles.length % 4 > 0) {
			console.log(
				'GraphicsFactoryHelper.drawRectangles: inputRectangles.length is not a multiple of 4', inputRectangles);
			return;
		}

		const final_vert_list: number[] = [];
		final_vert_list.length = (inputRectangles.length / 4) * 12;
		let i: number = 0;
		let outCnt: number = 0;
		const len: number = inputRectangles.length;
		let x: number = 0;
		let y: number = 0;
		let w: number = 0;
		let h: number = 0;

		for (i = 0; i < len; i += 4) {
			x = inputRectangles[i];
			y = inputRectangles[i + 1];
			w = inputRectangles[i + 2];
			h = inputRectangles[i + 3];
			final_vert_list[outCnt++] = x;
			final_vert_list[outCnt++] = y;
			final_vert_list[outCnt++] = x + w;
			final_vert_list[outCnt++] = y;
			final_vert_list[outCnt++] = x + w;
			final_vert_list[outCnt++] = y + h;
			final_vert_list[outCnt++] = x;
			final_vert_list[outCnt++] = y;
			final_vert_list[outCnt++] = x;
			final_vert_list[outCnt++] = y + h;
			final_vert_list[outCnt++] = x + w;
			final_vert_list[outCnt++] = y + h;
		}
		const obj: any = MaterialManager.getMaterialForColor(color, alpha);
		const material: IMaterial = obj.material;
		const attributesView: AttributesView = new AttributesView(Float32Array, material.curves ? 3 : 2);
		attributesView.set(final_vert_list);
		const attributesBuffer: AttributesBuffer = attributesView.attributesBuffer.cloneBufferView();
		attributesView.dispose();
		const elements: TriangleElements = new TriangleElements(attributesBuffer);
		elements.setPositions(new Float2Attributes(attributesBuffer));

		const shape: Shape = Shape.getShape(elements, material);
		if (obj.colorPos) {
			shape.style = new Style();
			const sampler: ImageSampler = new ImageSampler();
			material.animateUVs = true;
			shape.style.color = color;
			shape.style.addSamplerAt(sampler, material.getTextureAt(0));
			shape.style.uvMatrix = new Matrix(0, 0, 0, 0, obj.colorPos.x, obj.colorPos.y);
		}
		return shape;
	}

	public static updateRectanglesShape(shape: Shape, inputRectangles: number[]) {

		const final_vert_list: number[] = [];
		final_vert_list.length = (inputRectangles.length / 4) * 12;
		let i: number = 0;
		let outCnt: number = 0;
		const len: number = inputRectangles.length;
		let x: number = 0;
		let y: number = 0;
		let w: number = 0;
		let h: number = 0;

		for (i = 0; i < len; i += 4) {
			x = inputRectangles[i];
			y = inputRectangles[i + 1];
			w = inputRectangles[i + 2];
			h = inputRectangles[i + 3];
			final_vert_list[outCnt++] = x;
			final_vert_list[outCnt++] = y;
			final_vert_list[outCnt++] = x + w;
			final_vert_list[outCnt++] = y;
			final_vert_list[outCnt++] = x + w;
			final_vert_list[outCnt++] = y + h;
			final_vert_list[outCnt++] = x;
			final_vert_list[outCnt++] = y;
			final_vert_list[outCnt++] = x;
			final_vert_list[outCnt++] = y + h;
			final_vert_list[outCnt++] = x + w;
			final_vert_list[outCnt++] = y + h;
		}
		const elements: TriangleElements = <TriangleElements> shape.elements;
		elements.concatenatedBuffer.count = final_vert_list.length / 2;
		elements.setPositions(final_vert_list);
		elements.invalidate();
	}

	public static isClockWiseXY(
		point1x: number, point1y: number, point2x: number,
		point2y: number, point3x: number, point3y: number): boolean {

		const num: number = (point1x - point2x) * (point3y - point2y) - (point1y - point2y) * (point3x - point2x);
		if (num < 0)
			return false;
		return true;
	}

	public static getSign(ax: number, ay: number, cx: number, cy: number, bx: number, by: number): number {
		return (ax - bx) * (cy - by) - (ay - by) * (cx - bx);
	}

	public static pointInTri(
		ax: number, ay: number,
		bx: number, by: number,
		cx: number, cy: number,
		xx: number, xy: number): boolean {

		const b1: boolean = this.getSign(ax, ay, xx, xy, bx, by) > 0;
		const b2: boolean = this.getSign(bx, by, xx, xy, cx, cy) > 0;
		const b3: boolean = this.getSign(cx, cy, xx, xy, ax, ay) > 0;
		return ((b1 == b2) && (b2 == b3));
	}

	public static getControlXForCurveX(_a: number, c: number, _b: number): number {
		return c;
	}

	public static getControlYForCurveY(_a: number, c: number, _b: number): number {
		return c;
	}

	public static drawPoint(startX: number,startY: number, vertices: Array<number>, curves: boolean): void {
		this.addTriangle(startX - 2, startY - 2, startX + 2, startY - 2, startX + 2, startY + 2, 0, vertices, curves);
		this.addTriangle(startX - 2, startY - 2, startX - 2, startY + 2, startX + 2, startY + 2, 0, vertices, curves);
	}

	public static drawElipse(
		x: number,y: number,
		width: number, height: number,
		vertices: Array<number>,
		startAngle: number, endAngle: number, stepAngle: number,
		curves: boolean): void {

		// todo: validate input / check edge cases
		const degreeTotal: number = endAngle - startAngle;
		const steps: number = degreeTotal / stepAngle;
		let x_last = x + width * Math.cos(startAngle * (Math.PI / 180));
		let y_last = y + height * Math.sin(startAngle * (Math.PI / 180));
		for (let i = 1; i <= steps;i++) {
			const x_tmp = x + width * Math.cos((startAngle + i * stepAngle) * (Math.PI / 180));
			const y_tmp = y + height * Math.sin((startAngle + i * stepAngle) * (Math.PI / 180));
			this.addTriangle(x,y,x_tmp,y_tmp, x_last, y_last, 0, vertices, curves);
			x_last = x_tmp;
			y_last = y_tmp;
		}
	}

	public static drawElipseStrokes(x: number,y: number,width: number, height: number,
		strokePath: GraphicsPath, startAngle: number,
		endAngle: number, stepAngle: number): void {

		// todo: validate input / check edge cases
		const degreeTotal: number = endAngle - startAngle;
		const steps: number = degreeTotal / stepAngle;
		const x_last = x + (width) * Math.cos(startAngle * (Math.PI / 180));
		const y_last = y + (height) * Math.sin(startAngle * (Math.PI / 180));
		strokePath.moveTo(x_last, y_last);
		for (let i = 1; i <= steps;i++) {
			const x_tmp = x + (width) * Math.cos((startAngle + i * stepAngle) * (Math.PI / 180));
			const y_tmp = y + (height) * Math.sin((startAngle + i * stepAngle) * (Math.PI / 180));
			strokePath.lineTo(x_tmp, y_tmp);
		}
	}

	public static addTriangle(
		startX: number, startY: number,
		controlX: number, controlY: number,
		endX: number, endY: number,
		tri_type: number,
		vertices: Array<number>, curves: boolean): void {

		const x1 = startX;
		const y1 = startY;
		const x2 = controlX;
		const y2 = controlY;
		const x3 = endX;
		const y3 = endY;
		if (GraphicsFactoryHelper.isClockWiseXY(x1, y1, x2, y2, x3, y3)) {
			startX = x3;
			startY = y3;
			controlX = x2;
			controlY = y2;
			endX = x1;
			endY = y1;
		}
		let final_vert_cnt: number = vertices.length;
		if (tri_type == 0) {
			vertices[final_vert_cnt++] = startX;
			vertices[final_vert_cnt++] = startY;
			if (curves)
				vertices[final_vert_cnt++] = 4.5736980577097704e-41;// ((127<<24)+(127<<16)+0+0)
			vertices[final_vert_cnt++] = controlX;
			vertices[final_vert_cnt++] = controlY;
			if (curves)
				vertices[final_vert_cnt++] = 4.5736980577097704e-41;// ((127<<24)+(127<<16)+0+0)
			vertices[final_vert_cnt++] = endX;
			vertices[final_vert_cnt++] = endY;
			if (curves)
				vertices[final_vert_cnt++] = 4.5736980577097704e-41;// ((127<<24)+(127<<16)+0+0)
		} else if (tri_type < 0) {
			vertices[final_vert_cnt++] = startX;
			vertices[final_vert_cnt++] = startY;
			if (curves)
				vertices[final_vert_cnt++] = 1.1708844992641982e-38;// ((127<<24)+(127<<16)+0+0)
			vertices[final_vert_cnt++] = controlX;
			vertices[final_vert_cnt++] = controlY;
			if (curves)
				vertices[final_vert_cnt++] = 2.2778106537599901e-41;// ((127<<24)+(63<<16)+0+0)
			vertices[final_vert_cnt++] = endX;
			vertices[final_vert_cnt++] = endY;
			if (curves)
				vertices[final_vert_cnt++] = 1.7796490496925177e-43;// ((127<<24)+0+0+0)
		} else if (tri_type > 0) {
			vertices[final_vert_cnt++] = startX;
			vertices[final_vert_cnt++] = startY;
			if (curves)
				vertices[final_vert_cnt++] = 1.1708846393940446e-38;// ((-128<<24)+(127<<16)+0+0)
			vertices[final_vert_cnt++] = controlX;
			vertices[final_vert_cnt++] = controlY;
			if (curves)
				vertices[final_vert_cnt++] = 2.2779507836064226e-41;// ((-128<<24)+(63<<16)+0+0)
			vertices[final_vert_cnt++] = endX;
			vertices[final_vert_cnt++] = endY;
			if (curves)
				vertices[final_vert_cnt++] = 1.793662034335766e-43;// ((-128<<24)+0+0+0)
		}
	}

	public static createCap(
		startX: number, startY: number,
		start_le_x: number, start_le_y: number,
		start_ri_x: number, start_ri_y: number,
		direction_x: number, direction_y: number,
		capstyle: number,
		cap_position: number,
		thicknessX: number, thicknessY: number,
		vertices: Array<number>, curves: boolean): void {

		direction_x *= cap_position;
		direction_y *= cap_position;
		if (capstyle == CapsStyle.ROUND) {
			//console.log("add round cap");
			const end_x: number = startX + ((direction_x * thicknessX));
			const end_y: number = startY + ((direction_y * thicknessY));
			//end_x = end_x * 2 - start_le.x/2 - start_ri.x/2;
			//end_y = end_y * 2 - start_le.y/2 - start_ri.y/2;
			const tmp1_x: number = start_le_x + ((direction_x * thicknessX));
			const tmp1_y: number = start_le_y + ((direction_y * thicknessY));
			const tmp2_x: number = start_ri_x + ((direction_x * thicknessX));
			const tmp2_y: number = start_ri_y + ((direction_y * thicknessY));

			this.tesselateCurve(start_le_x, start_le_y, tmp1_x, tmp1_y, end_x, end_y, vertices, true);
			this.tesselateCurve(end_x, end_y, tmp2_x, tmp2_y, start_ri_x, start_ri_y, vertices, true);
			this.addTriangle(start_le_x, start_le_y, end_x, end_y, start_ri_x, start_ri_y, -1, vertices, curves);
		} else if (capstyle == CapsStyle.SQUARE) {
			//console.log("add square cap");
			const tmp1_x: number = start_le_x + ((direction_x * thicknessX));
			const tmp1_y: number = start_le_y + ((direction_y * thicknessY));
			const tmp2_x: number = start_ri_x + ((direction_x * thicknessX));
			const tmp2_y: number = start_ri_y + ((direction_y * thicknessY));

			this.addTriangle(tmp2_x,tmp2_y, tmp1_x, tmp1_y, start_le_x, start_le_y, 0, vertices, curves);
			this.addTriangle(tmp2_x,tmp2_y, start_le_x, start_le_y, start_ri_x, start_ri_y, 0, vertices, curves);
		}
	}

	public static getLineFormularData(a: Point, b: Point): Point {
		const tmp_x = b.x - a.x;
		const tmp_y = b.y - a.y;
		const return_point: Point = new Point();
		if ((tmp_x != 0) && (tmp_y != 0))
			return_point.x = tmp_y / tmp_x;
		return_point.y = -(return_point.x * a.x - a.y);
		return return_point;
	}

	public static getQuadricBezierPosition(t: number, start: number, control: number, end: number): number {
		const xt = 1 - t;
		return xt * xt * start + 2 * xt * t * control + t * t * end;
	}

	public static subdivideCurve(
		startx: number, starty: number,
		cx: number, cy: number,
		endx: number, endy: number,
		startx2: number, starty2: number,
		cx2: number, cy2: number,
		endx2: number, endy2: number,
		array_out: Array<number>, array2_out: Array<number>): void {

		const angle_1: number = Math.atan2(cy - starty, cx - startx) * MathConsts.RADIANS_TO_DEGREES;
		const angle_2: number = Math.atan2(endy - cy, endx - cx) * MathConsts.RADIANS_TO_DEGREES;
		let angle_delta: number = angle_2 - angle_1;
		//console.log("angle_delta "+angle_delta);

		if (angle_delta > 180) {
			angle_delta -= 360;
		}
		if (angle_delta < -180) {
			angle_delta += 360;
		}
		if (Math.abs(angle_delta) >= 175) {
			array_out.push(startx, starty, cx, cy,  endx, endy);
			array2_out.push(startx2, starty2, cx2, cy2, endx2, endy2);
			return;
		}

		let b1: boolean = false;
		//let b2: boolean = false;
		if (angle_delta < 0) {
			// curve is curved to right side. right side is convex
			b1 = GraphicsFactoryHelper.getSign(startx, starty, cx2, cy2, endx, endy) > 0;
			//b2 = GraphicsFactoryHelper.getSign(startx, starty, cx, cy, endx, endy) > 0;

			// eslint-disable-next-line max-len
			b1 = (((starty - endy) * (cx - startx) + (endx - startx) * (cy - starty)) * ((starty - endy) * (cx2 - startx) + (endx - startx) * (cy2 - starty))) < 0;

		} else {
			// curve is curved to left side. left side is convex
			b1 = GraphicsFactoryHelper.getSign(startx2, starty2, cx2, cy2, endx2, endy2) > 0;
			//b2 = GraphicsFactoryHelper.getSign(startx2, starty2, cx, cy, endx2, endy2) > 0;
			// eslint-disable-next-line max-len
			b1 = (((starty2 - endy) * (cx - startx2) + (endx2 - startx2) * (cy - starty2)) * ((starty2 - endy2) * (cx2 - startx2) + (endx2 - startx2) * (cy2 - starty2))) < 0;

		}
		if (b1) {
			array_out.push(startx, starty, cx, cy,  endx, endy);
			array2_out.push(startx2, starty2, cx2, cy2, endx2, endy2);
			return;
		}
		// triangles overlap. we must subdivide:

		const c1x = startx + (cx - startx) * 0.5;// new controlpoint 1.1
		const c1y = starty + (cy - starty) * 0.5;
		const c2x = cx + (endx - cx) * 0.5;// new controlpoint 1.2
		const c2y = cy + (endy - cy) * 0.5;
		const ax = c1x + (c2x - c1x) * 0.5;// new middlepoint 1
		const ay = c1y + (c2y - c1y) * 0.5;

		const c1x2 = startx2 + (cx2 - startx2) * 0.5;// new controlpoint 2.1
		const c1y2 = starty2 + (cy2 - starty2) * 0.5;
		const c2x2 = cx2 + (endx2 - cx2) * 0.5;// new controlpoint 2.2
		const c2y2 = cy2 + (endy2 - cy2) * 0.5;
		const ax2 = c1x2 + (c2x2 - c1x2) * 0.5;// new middlepoint 2
		const ay2 = c1y2 + (c2y2 - c1y2) * 0.5;

		this.subdivideCurve(
			startx, starty, c1x, c1y, ax, ay, startx2, starty2, c1x2, c1y2, ax2, ay2, array_out, array2_out);
		this.subdivideCurve(
			ax, ay, c2x, c2y, endx, endy, ax2, ay2, c2x2, c2y2, endx2, endy2, array_out, array2_out);

	}

	public static tesselateCurve(
		startx: number,
		starty: number,
		cx: number,
		cy: number,
		endx: number,
		endy: number,
		array_out: Array<number>,
		filled: boolean = false,
		iterationCnt: number = 0,
		qualityScale: number = 1
	): void {

		const maxIterations: number = Settings.CURVE_TESSELATION_COUNT;
		const minAngle = 1 / Math.sqrt(qualityScale);
		const minLengthSqr = 1 / qualityScale;

		// subdivide the curve
		const c1x = (startx + cx) * 0.5;// new controlpoint 1
		const c1y = (starty + cy) * 0.5;
		const c2x = (cx + endx) * 0.5;// new controlpoint 2
		const c2y = (cy + endy) * 0.5;
		const ax = (c1x + c2x) * 0.5;// new middlepoint 1
		const ay = (c1y + c2y) * 0.5;

		// if "filled" is true, we are collecting final vert positions in the array,
		// ready to use for rendering. (6-position values for each tri)
		// if "filled" is false, we are collecting vert positions for a path (we do not need the start-position).

		// stop tesselation on maxIteration level. Set it to 0 for no tesselation at all.
		if (iterationCnt >= maxIterations) {
			if (filled) {
				array_out.push(startx, starty, ax, ay, endx, endy);
				return;
			}
			array_out.push(ax, ay, endx, endy);
			return;
		}

		// calculate length of segment
		// this does not include the crtl-point position
		const diff_x = endx - startx;
		const diff_y = endy - starty;
		const lenSq = diff_x * diff_x + diff_y * diff_y;

		// stop subdividing if the angle or the length is to small
		if (lenSq < minLengthSqr) {
			if (filled) {
				array_out.push(startx, starty, ax, ay, endx, endy);
			} else {
				array_out.push(endx, endy);
			}
			return;
		}

		// calculate angle between segments
		const angle_1 = Math.atan2(cy - starty, cx - startx) * MathConsts.RADIANS_TO_DEGREES;
		const angle_2 = Math.atan2(endy - cy, endx - cx) * MathConsts.RADIANS_TO_DEGREES;
		let angle_delta = angle_2 - angle_1;

		// make sure angle is in range -180 - 180
		while (angle_delta > 180) {
			angle_delta -= 360;
		}
		while (angle_delta < -180) {
			angle_delta += 360;
		}

		angle_delta = angle_delta < 0 ? -angle_delta : angle_delta;

		// stop subdividing if the angle or the length is to small
		if (angle_delta <= minAngle) {
			if (filled) {
				array_out.push(startx, starty, ax, ay, endx, endy);
			} else {
				array_out.push(endx, endy);
			}
			return;
		}

		// if the output should be directly in valid tris, we always must create a tri,
		// even when we will keep on subdividing.
		if (filled) {
			array_out.push(startx, starty, ax, ay, endx, endy);
		}

		iterationCnt++;

		GraphicsFactoryHelper.tesselateCurve(
			startx, starty, c1x, c1y, ax, ay, array_out, filled, iterationCnt, qualityScale);
		GraphicsFactoryHelper.tesselateCurve(
			ax, ay, c2x, c2y, endx, endy, array_out, filled, iterationCnt, qualityScale);
	}

	public static tesselateCubicCurve(
		startx: number,
		starty: number,
		cx: number,
		cy: number,
		cx2: number,
		cy2: number,
		endx: number,
		endy: number,
		array_out: Array<number>,
		iterationCnt: number = 0,
		qualityScale: number = 1
	): void {

		const maxIterations: number = Settings.CURVE_TESSELATION_COUNT;
		const minAngle = 1 / Math.sqrt(qualityScale);
		const minLengthSqr = 1 / qualityScale;

		// calculate length of segment
		// this does not include the crtl-point positions
		const diff_x = endx - startx;
		const diff_y = endy - starty;
		const lenSq = diff_x * diff_x + diff_y * diff_y;

		// stop subdividing if the angle or the length is to small
		if (lenSq < minLengthSqr) {
			array_out.push(endx, endy);
			return;
		}

		// subdivide the curve
		const c1x = (startx + cx) * 0.5;// new controlpoint 1
		const c1y = (starty + cy) * 0.5;
		const c2x = (cx + cx2) * 0.5;// new controlpoint 2
		const c2y = (cy + cy2) * 0.5;
		const c3x = (cx2 + endx) * 0.5;// new controlpoint 3
		const c3y = (cy2 + endy) * 0.5;

		const d1x = (c1x + c2x) * 0.5;// new controlpoint 1
		const d1y = (c1y + c2y) * 0.5;
		const d2x = (c2x + c3x) * 0.5;// new controlpoint 2
		const d2y = (c2y + c3y) * 0.5;

		const ax = (d1x + d2x) * 0.5;// new middlepoint 1
		const ay = (d1y + d2y) * 0.5;

		// stop tesselation on maxIteration level. Set it to 0 for no tesselation at all.
		if (iterationCnt >= maxIterations) {
			array_out.push(ax, ay, endx, endy);
			return;
		}

		// calculate angle between segments
		const angle_1 = Math.atan2(cy - starty, cx - startx) * MathConsts.RADIANS_TO_DEGREES;
		const angle_2 = Math.atan2(endy - cy, endx - cx) * MathConsts.RADIANS_TO_DEGREES;
		let angle_delta = angle_2 - angle_1;

		// make sure angle is in range -180 - 180
		while (angle_delta > 180) {
			angle_delta -= 360;
		}
		while (angle_delta < -180) {
			angle_delta += 360;
		}

		angle_delta = angle_delta < 0 ? -angle_delta : angle_delta;

		// stop subdividing if the angle or the length is to small
		if (angle_delta <= minAngle) {
			array_out.push(endx, endy);
			return;
		}

		iterationCnt++;

		GraphicsFactoryHelper.tesselateCubicCurve(
			startx, starty, c1x, c1y, d1x, d1y, ax, ay, array_out, iterationCnt, qualityScale);
		GraphicsFactoryHelper.tesselateCubicCurve(
			ax, ay, d2x, d2y, c3x, c3y, endx, endy, array_out, iterationCnt, qualityScale);

	}
}