import { child, classAdd, classDel, classHas, listen, listenDel, svgEl } from '../infrastructure/util.js';
import { moveEvtProc, movementApplay } from '../infrastructure/move-evt-proc.js';
import { placeToCell, pointInCanvas } from '../infrastructure/move-scale-applay.js';
import { priorityElemFromPoint } from '../infrastructure/evt-route-applay.js';
import { ShapeSmbl } from './shape-smbl.js';
import { PathSettings } from './path-settings.js';
import { PathSmbl } from './path-smbl.js';
import { CanvasSmbl } from '../infrastructure/canvas-smbl.js';
import { modalCreate } from './modal-create.js';
import { canvasSelectionClearSet } from '../diagram/canvas-clear.js';
import { listenCopy } from '../diagram/group-select-applay.js';

/**
 * @param {CanvasElement} canvas
 * @param {PathData} pathData
 */
export function path(canvas, pathData) {

	/** @type {PathElement} */
	const svgGrp = svgEl('g', `
		<path data-key="outer" d="M0 0" stroke="transparent" stroke-width="20" fill="none" />
		<path data-key="path" class="path" d="M0 0" stroke="#495057" stroke-width="1.8" fill="none" style="pointer-events: none;" />
		<path data-key="selected" d="M0 0" stroke="transparent" stroke-width="10" fill="none" style="pointer-events: none;" />
		<g data-key="start">
			<circle data-evt-index="1" class="path-end" r="10" stroke-width="0" fill="transparent" />
			<path class="path" d="M-7 7 l 7 -7 l -7 -7" stroke="#495057" stroke-width="1.8" fill="none" style="pointer-events: none;"></path>
		</g>
		<g data-key="end">
			<circle data-evt-index="1" class="path-end" r="10" stroke-width="0" fill="transparent" />
			<path class="path" d="M-7 7 l 7 -7 l -7 -7" stroke="#495057" stroke-width="1.8" fill="none" style="pointer-events: none;"></path>
		</g>`);
	classAdd(svgGrp, 'shpath');

	pathData.s.el = child(svgGrp, 'start');
	pathData.e.el = child(svgGrp, 'end');
	pathData.styles = pathData.styles ?? ['arw-e'];
	const paths = childs(svgGrp, 'path', 'outer', 'selected');

function draw() {
    const endDir = dirByAngle(pathData.s.data.position, pathData.e.data.position);
    pathData.e.data.dir = endDir;
    pathData.s.data.dir = dirReverse(endDir);

    const dAttr = pathCalc(pathData);
    paths.forEach(pp => pp.setAttribute('d', dAttr));

    endDraw(pathData.s);
    endDraw(pathData.e);
}


	/** @param {PathEnd} pathEnd */
	function pathDelFromShape(pathEnd) { shapeObj(pathEnd.shape)?.pathDel(svgGrp); }

	/** @param {PathEnd} pathEnd */
	function pathAddToShape(pathEnd) {
		if (pathEnd.shape) {
			pathEnd.data = shapeObj(pathEnd.shape).pathAdd(pathEnd.shape.connectorKey, svgGrp);
		}
	};

	/** @type { {position:(bottomX:number, bottomY:number)=>void, del:()=>void} } */
	let settingsPnl;
	function del() {
		unSelect();
		reset();
		pathDelFromShape(pathData.s);
		pathDelFromShape(pathData.e);
		svgGrp.remove();
	}

	/**
	 * @type {0|1|2}
	 * 0 - init, 1 - selected, 2 - edit
	*/
	let state = 0;

	/** @type {()=>void} */
	let listenCopyDispose;

	/** @param {PointerEvent} evt */
	function select(evt) {
		// in edit mode
		if (state === 2) { return; }

		// to edit mode
		if (state === 1) {
			state = 2;
			settingsPnl = modalCreate(evt.clientX - 10, evt.clientY - 10, new PathSettings(canvas, svgGrp));
			return;
		}

		// to select mode
		state = 1;
		classAdd(svgGrp, 'select');
		endSetEvtIndex(pathData.s, 2);
		endSetEvtIndex(pathData.e, 2);

		canvasSelectionClearSet(canvas, unSelect);
		listenCopyDispose = listenCopy(() => [svgGrp]);
	};

	/** @type { {():void} } */
	let hoverEmulateDispose;
	function unSelect() {
		state = 0;
		classDel(svgGrp, 'select');
		endSetEvtIndex(pathData.s, 1);
		endSetEvtIndex(pathData.e, 1);

		settingsPnl?.del();	settingsPnl = null;

		if (hoverEmulateDispose) {
			hoverEmulateDispose();
			hoverEmulateDispose = null;
			svgGrp.style.pointerEvents = 'unset';
		}

		canvasSelectionClearSet(canvas, null);
		if (listenCopyDispose) { listenCopyDispose(); listenCopyDispose = null;	}
	};

	/** @type {'s'|'e'} */
	let movedEnd;

	const reset = moveEvtProc(
		canvas.ownerSVGElement,
		svgGrp,
		canvas[CanvasSmbl].data,
		// data.end.position,
		{
			get x() { return pathData[movedEnd]?.data.position.x; },
			set x(val) { if (movedEnd) { pathData[movedEnd].data.position.x = val; } },

			get y() { return pathData[movedEnd]?.data.position.y; },
			set y(val) { if (movedEnd) { pathData[movedEnd].data.position.y = val; } }
		},
		// onMoveStart
		/** @param {PointerEvent & { target: Element} } evt */ evt => {
			unSelect();

			movedEnd = pathData.e.el.contains(evt.target) ? 'e' : pathData.s.el.contains(evt.target) ? 's' : null;

			//
			// move whole path
			if (!movedEnd) {
				return;
			}

			//
			// move path end

			// disconnect from shape
			if (pathData[movedEnd].shape) {
				if (pathData[movedEnd].shape.shapeEl !== pathData[movedEnd === 's' ? 'e' : 's'].shape?.shapeEl) {
					pathDelFromShape(pathData[movedEnd]);
				}
				pathData[movedEnd].shape = null;
				pathData[movedEnd].data = {
					dir: pathData[movedEnd].data.dir,
					position: pointInCanvas(canvas[CanvasSmbl].data, evt.clientX, evt.clientY)
				};
			}

			// hover emulation - start
			svgGrp.style.pointerEvents = 'none';
			hoverEmulateDispose = hoverEmulate(svgGrp.parentElement);
		},
		// onMove
/** @param {PointerEventFixMovement} evt */
evt => {
    if (!movedEnd) {
        moveWholePath(canvas[CanvasSmbl].data, pathData, draw, evt);
    } else {
        const diagram = document.getElementById('diagram');
        const rect = diagram.getBoundingClientRect();
        pathData[movedEnd].data.position = {
            x: evt.clientX - rect.left,
            y: evt.clientY - rect.top
        };
        draw();
    }
},
		// onMoveEnd
		evt => {
			if (!movedEnd) {
				moveWholePathFinish(canvas[CanvasSmbl].data, pathData, draw);
			} else {
				// connect to shape
				const elemFromPoint = priorityElemFromPoint(evt);
				const connectorKey = elemFromPoint?.getAttribute('data-connect');
				if (connectorKey) {
					// @ts-ignore
					pathData[movedEnd].shape = { shapeEl: elemFromPoint.parentElement, connectorKey };
					pathAddToShape(pathData[movedEnd]);
				} else {
					placeToCell(pathData[movedEnd].data.position, canvas[CanvasSmbl].data.cell);
				}
				draw();
			}

			// hover emulation - end
			unSelect();
		},
		// onClick
		select,
		// onOutdown
		unSelect
	);

	svgGrp[PathSmbl] = {
		draw,
		/** @param {PointerEventInit} evt */
		pointerCapture: evt => pathData.e.el.dispatchEvent(new PointerEvent('pointerdown', evt)),
		del,
		data: pathData
	};

	if (pathData.styles) { classAdd(svgGrp, ...pathData.styles); }
	pathAddToShape(pathData.s);
	pathAddToShape(pathData.e);
	draw();

	return svgGrp;
}

/**
 * @param {{scale:number}} canvasData
 * @param {PathData} pathData
 * @param {{():void}} draw
 * @param {PointerEventFixMovement} evt
 */
function moveWholePath(canvasData, pathData, draw, evt) {
	/** @param {Point} point */
	const move = point => movementApplay(point, canvasData.scale, evt);
	moveShapeOrEnd(pathData.s, move);
	moveShapeOrEnd(pathData.e, move);

	// if any shape connected - shape will draw connected path
	if (!pathData.s.shape && !pathData.e.shape) { draw(); }
}

/**
 * @param {{cell:number}} canvasData
 * @param {PathData} pathData
 * @param {{():void}} draw
 */
function moveWholePathFinish(canvasData, pathData, draw) {
	/** @param {Point} point */
	const toCell = point => placeToCell(point, canvasData.cell);
	moveShapeOrEnd(pathData.s, toCell);
	moveShapeOrEnd(pathData.e, toCell);

	if (!pathData.s.shape || !pathData.e.shape) { draw(); }
}

/**
 * applay moveFn to connected shape or to path end point
 * @param {PathEnd} pathEnd, @param {{(point:Point):void}} moveFn */
function moveShapeOrEnd(pathEnd, moveFn) {
	if (pathEnd.shape) {
		moveFn(shapeObj(pathEnd.shape).data.position);
		shapeObj(pathEnd.shape).drawPosition();
	} else {
		moveFn(pathEnd.data.position);
	}
}

/** @param {PathConnectedShape} pathConnectedShape */
const shapeObj = pathConnectedShape => pathConnectedShape?.shapeEl[ShapeSmbl];

/** @param {PathEnd} pathEnd */
function endDraw(pathEnd) {
	pathEnd.el.style.transform = `translate(${pathEnd.data.position.x}px, ${pathEnd.data.position.y}px) rotate(${arrowAngle(pathEnd.data.dir)}deg)`;
}

/** @param {PathEnd} pathEnd, @param {number} index */
function endSetEvtIndex(pathEnd, index) { pathEnd.el.firstElementChild.setAttribute('data-evt-index', index.toString()); }

/** @param {Dir} dir */
const arrowAngle = dir => dir === 'right'
	? 180
	: dir === 'left'
		? 0
		: dir === 'bottom'
			? 270
			: 90;

/** @param {Dir} dir, @return {Dir} */
export const dirReverse = dir =>	dir === 'left'
	? 'right'
	: dir === 'right'
		? 'left'
		: dir === 'top' ? 'bottom' : 'top';

/** @param {Point} s, @param {Point} e, @return {Dir} */
function dirByAngle(s, e) {
	const rad = Math.atan2(e.y - s.y, e.x - s.x);
	return numInRangeIncludeEnds(rad, -0.8, 0.8)
		? 'left'
		: numInRangeIncludeEnds(rad, 0.8, 2.4)
			? 'top'
			: numInRangeIncludeEnds(rad, 2.4, 3.2) || numInRangeIncludeEnds(rad, -3.2, -2.4) ? 'right' : 'bottom';
}

/** @param {PathData} data */
function pathCalc(data) {
	let coef = Math.hypot(
		data.s.data.position.x - data.e.data.position.x,
		data.s.data.position.y - data.e.data.position.y) * 0.5;
	coef = coef > 70
		? 70
		: coef < 15 ? 15 : coef;

	/** @param {PathEndData} pathEnd */
	function cx(pathEnd) {
		return (pathEnd.dir === 'right' || pathEnd.dir === 'left')
			? pathEnd.dir === 'right' ? pathEnd.position.x + coef : pathEnd.position.x - coef
			: pathEnd.position.x;
	}

	/** @param {PathEndData} pathEnd */
	function cy(pathEnd) {
		return (pathEnd.dir === 'right' || pathEnd.dir === 'left')
			? pathEnd.position.y
			: pathEnd.dir === 'bottom' ? pathEnd.position.y + coef : pathEnd.position.y - coef;
	}

	return `M ${data.s.data.position.x} ${data.s.data.position.y} C ${cx(data.s.data)} ${cy(data.s.data)}, ` +
		`${cx(data.e.data)} ${cy(data.e.data)}, ${data.e.data.position.x} ${data.e.data.position.y}`;
}

/** @param {Element} element */
function hoverEmulate(element) {
	/** @type {Element} */
	let elemFromPoint = null;

	/** @param {PointerEvent} evt */
	function move(evt) {
		const elemFromPointNew = priorityElemFromPoint(evt);
		if (elemFromPoint !== elemFromPointNew) {
			if (classHas(elemFromPointNew, 'hovertrack')) {
				classAdd(elemFromPointNew, 'hover');
			}
			let parentHover = false;
			if (classHas(elemFromPointNew?.parentElement, 'hovertrack')) {
				classAdd(elemFromPointNew.parentElement, 'hover');
				parentHover = true;
			}

			classDel(elemFromPoint, 'hover');
			if (elemFromPoint?.parentElement !== elemFromPointNew?.parentElement || !parentHover) {
				classDel(elemFromPoint?.parentElement, 'hover');
			}

			elemFromPoint = elemFromPointNew;
		}
	}

	listen(element, 'pointermove', move);
	// dispose fn
	return function() {
		listenDel(element, 'pointermove', move);
		classDel(elemFromPoint, 'hover');
		classDel(elemFromPoint?.parentElement, 'hover');
		elemFromPoint = null;
	};
}

/** @param {Element} el, @param  {...string} keys */
const childs = (el, ...keys) => keys.map(kk => child(el, kk));

/** @param {number} num, @param {number} a, @param {number} b */
const numInRangeIncludeEnds = (num, a, b) => a <= num && num <= b;

/** @typedef { {x:number, y:number} } Point */
/** @typedef { 'left' | 'right' | 'top' | 'bottom' } Dir */
/** @typedef { {shapeEl: ShapeElement, connectorKey: string} } PathConnectedShape */
/** @typedef { {position: Point, dir: Dir }} PathEndData */
/** @typedef { {shape?:PathConnectedShape, data?:PathEndData, el?:SVGElement} } PathEnd */
/**
@typedef {{
	s: PathEnd,
	e: PathEnd,
	styles?: string[],
}} PathData
*/
/** @typedef { {shape?:PathConnectedShape, data?:PathEndData, oppositeShape?:PathConnectedShape, type:number} } MovedEnd */
/**
@typedef {{
	draw(): void
	pointerCapture: (evt:PointerEventInit)=>void
	del(): void
	data: PathData
}} Path
*/

/** @typedef { import('./path-smbl.js').PathElement } PathElement */
/** @typedef { import('../infrastructure/canvas-smbl.js').CanvasElement } CanvasElement */
/** @typedef { import('./shape-smbl').ShapeElement } ShapeElement */
/** @typedef { import('./shape-evt-proc').Shape } Shape */
/** @typedef { import('../infrastructure/move-evt-mobile-fix.js').PointerEventFixMovement } PointerEventFixMovement */
