type TapCallback = (event: { clientX: number, clientY: number }) => void
type CallbackType = 'single' | 'double'

export default class TapDetector {
	private singleTapCallbacks: TapCallback[] = []
	private doubleTapCallbacks: TapCallback[] = []
	private isTouchMode = false
	private lastTapTimestamp = 0
	private tappedCount = 0
	private touchMovedLength = 0
	private lastPointerX = 0
	private lastPointerY = 0

	attach(dom: HTMLElement): void {
		if (!(dom instanceof Element)) {
			console.error('TapDetector.attach: arg must be an Element')
			return
		}
		dom.addEventListener('touchstart', this.onTouchStart)
		dom.addEventListener('touchmove', this.onTouchMove)
		dom.addEventListener('touchend', this.onTouchEnd)
		dom.addEventListener('mousedown', this.onMouseDown)
		dom.addEventListener('mouseup', this.onMouseUp)
		dom.addEventListener('mousemove', this.onMouseMove)
	}

	detach(dom: HTMLElement): void {
		dom.removeEventListener('touchstart', this.onTouchStart)
		dom.removeEventListener('touchmove', this.onTouchMove)
		dom.removeEventListener('touchend', this.onTouchEnd)
		dom.removeEventListener('mousedown', this.onMouseDown)
		dom.removeEventListener('mouseup', this.onMouseUp)
		dom.removeEventListener('mousemove', this.onMouseMove)
	}

	onSingleTap(callback: TapCallback): void {
		if (typeof callback === 'function' && !this.singleTapCallbacks.includes(callback)) {
			this.singleTapCallbacks.push(callback)
		}
	}

	onDoubleTap(callback: TapCallback): void {
		if (typeof callback === 'function' && !this.doubleTapCallbacks.includes(callback)) {
			this.doubleTapCallbacks.push(callback)
		}
	}

	private triggerCallbacks(callbackType: CallbackType, event: { clientX: number, clientY: number }): void {
		const callbacks = callbackType === 'single' ? this.singleTapCallbacks : this.doubleTapCallbacks
		callbacks.forEach((callback) => { callback(event) })
	}

	private onTouchStart = (event: TouchEvent): void => {
		this.isTouchMode = true
		if (event.touches.length === 1) {
			this.onPointerDown(event.touches[0].clientX, event.touches[0].clientY)
		}
	}

	private onTouchMove = (event: TouchEvent): void => {
		if (event.touches.length === 1) {
			this.onPointerMove(event.touches[0].clientX, event.touches[0].clientY)
		}
	}

	private onTouchEnd = (): void => {
		if (this.isTouchMode) this.onPointerUp()
	}

	private onMouseDown = (event: MouseEvent): void => {
		if (!this.isTouchMode) {
			this.onPointerDown(event.clientX, event.clientY)
		}
	}

	private onMouseMove = (event: MouseEvent): void => {
		if (!this.isTouchMode && event.button === 0) {
			this.onPointerMove(event.clientX, event.clientY)
		}
	}

	private onMouseUp = (): void => {
		if (!this.isTouchMode) this.onPointerUp()
	}

	private onPointerDown(x: number, y: number): void {
		this.lastPointerX = x
		this.lastPointerY = y
		this.touchMovedLength = 0
	}

	private onPointerUp(): void {
		const currTimeStamp = Date.now()
		if (this.touchMovedLength < 10) {
			if (currTimeStamp - this.lastTapTimestamp < 300) {
				this.tappedCount++
			} else {
				this.tappedCount = 1
			}

			this.lastTapTimestamp = currTimeStamp
			this.triggerCallbacks('single', { clientX: this.lastPointerX, clientY: this.lastPointerY })

			if (this.tappedCount === 2) {
				this.triggerCallbacks('double', { clientX: this.lastPointerX, clientY: this.lastPointerY })
				this.tappedCount = 0
			}
		}
		this.touchMovedLength = 0
	}

	private onPointerMove(x: number, y: number): void {
		const deltaX = this.lastPointerX - x
		const deltaY = this.lastPointerY - y
		this.touchMovedLength += Math.sqrt(deltaX * deltaX + deltaY * deltaY)
		this.lastPointerX = x
		this.lastPointerY = y
	}
}
