/**
 * 自定义动画指令
 * v-custom-animation.scroll.once.parent="{GetRoot, activeClass}"
 *  修饰符
 *   scroll: 是否监听滚动事件
 *   once: 是否只执行一次
 *   parent: 是否监听父元素滚动事件-优先级（GetRoot > parent > document）
 *	参数
 *   GetRoot: 获取根元素函数-优先级（GetRoot > parent > document）
 *   activeClass: 激活类名-默认值（active）
 *   duration: 动画时间-默认值（0.5s）
 *   delay: 延迟时间-默认值（0s）
 *   easing: 缓动函数-默认值（ease-out）
 *   transformDuration: 变换时间-默认值 duration
 *   transformDelay: 变换延迟时间-默认值 delay
 *   transformEasing: 变换缓动函数-默认值 easing
 *   opacityDuration: 透明度时间-默认值 duration
 *   opacityDelay: 透明度延迟时间-默认值 delay
 *   opacityEasing: 透明度缓动函数-默认值 easing
 * 
 * 子元素动画
 * <div translate-x-s="-100" translate-x="100"></div>
 *  添加动画属性
 *   translate-x-s: 水平方向移动开始位置
 *   translate-x: 水平方向移动结束位置
 * 	 ......(属性支持查看 T 对象)
 * 
*/
const roots = new Map()
const els = new Map()
const T = {
	translateX_s: 'translate-x-s',
	translateX: 'translate-x',
	translateY_s: 'translate-y-s',
	translateY: 'translate-y',
	scale_s: 'scale-s',
	scale: 'scale',
	scaleX_s: 'scale-x-s',
	scaleX: 'scale-x',
	scaleY_s: 'scale-y-s',
	scaleY: 'scale-y',
	rotate_s: 'rotate-s',
	rotate: 'rotate',
	rotateX_s: 'rotate-x-s',
	rotateX: 'rotate-x',
	rotateY_s: 'rotate-y-s',
	rotateY: 'rotate-y',
	rotateZ_s: 'rotate-z-s',
	rotateZ: 'rotate-z',
	opacity_s: 'opacity-s',
	opacity: 'opacity',
	// 动画属性配置
	duration: 'duration',
	delay: 'delay',
	easing: 'easing',
	transformDuration: 'transform-duration',
	transformDelay: 'transform-delay',
	transformEasing: 'transform-easing',
	opacityDuration: 'opacity-duration',
	opacityDelay: 'opacity-delay',
	opacityEasing: 'opacity-easing',
}
const types = Object.values(T)
const typesStr = types.map(v => `[${v}]`).join(',')
const resize = new ResizeObserver((e) => {
	e.forEach(({ target }) => {
		requestAnimationFrame(() => handleScroll({ target }))
	})
})

export default {
	name: 'custom-animation',
	mounted(el, binding) {
		const { value, modifiers } = binding
		const {
			GetRoot,// 获取根元素函数
			activeClass = 'active',// 激活类名
			duration = '0.5s',// 动画时间
			delay = '0s',// 延迟时间
			easing = 'ease-out',// 缓动函数
		} = value || {}
		const transition = {
			duration,
			delay,
			easing,
			transformDuration: value?.transformDuration || value?.[T.transformDuration],
			transformDelay: value?.transformDelay || value?.[T.transformDelay],
			transformEasing: value?.transformEasing || value?.[T.transformEasing],
			opacityDuration: value?.opacityDuration || value?.[T.opacityDuration],
			opacityDelay: value?.opacityDelay || value?.[T.opacityDelay],
			opacityEasing: value?.opacityEasing || value?.[T.opacityEasing],
		}
		const {
			scroll = false,// 是否监听滚动事件
			once = false,// 是否只执行一次
			parent = false,// 是否监听父元素滚动事件
		} = modifiers
		const root = GetRoot ? GetRoot() : parent ? el.parentElement : document;
		if (el === root) return;
		const config = {
			root,// 根元素
			scroll,
			once,
			activeClass,
			isActive: false,
			transition,
		}
		console.log(config)
		els.set(el, config)
		add(el, root, config)
	},
	beforeUnmount(el, binding) {
		remove(el)
		els.delete(el)
	}
};
function add(el, root = document, config) {
	if (config.scroll) {
		requestAnimationFrame(() => handleScroll({ target: root }))
	} else {
		getChildren(el).forEach((child) => setDocumentStyles(el, child, 0))
	}
	resize.observe(el)
	if (roots.has(root)) {
		let obj = roots.get(root)
		obj.els.push(el)
		obj.observer.observe(el)
		return
	}
	resize.observe(isDocumentRoot(root))
	root.addEventListener('scroll', handleScroll)
	const observer = new IntersectionObserver((entries) => {
		entries.forEach((entry) => {
			const { target } = entry
			const obj = els.get(target)
			if (!obj) return
			if (obj.once && obj.isActive) return;// 只执行一次，且已可见，不执行
			obj.isActive = entry.isIntersecting;
			target.classList.toggle(obj.activeClass, obj.isActive)
			getChildren(target).forEach((el) => {
				setDocumentStyles(target, el, obj.isActive ? 1 : 0)
			})
		})
	}, { root })
	observer.observe(el)
	roots.set(root, { els: [el], observer, resize })
}
function remove(el, root = document) {
	if (!roots.has(root)) return
	const obj = roots.get(root)
	if (obj.els.includes(el)) {
		obj.observer.unobserve(el)
		obj.resize.unobserve(el)
		obj.els.splice(obj.els.indexOf(el), 1)
	}
	if (obj.els.length === 0) {
		obj.observer.disconnect()
		resize.unobserve(isDocumentRoot(root))
		root.removeEventListener('scroll', handleScroll)
		roots.delete(root)
	}
}

var timer = null
async function handleScroll({ target: root }) {
	clearTimeout(timer)
	timer = await new Promise(resolve => setTimeout(resolve, 10))
	const obj = roots.get(root)
	if (!obj) return
	obj.els.forEach((el) => {
		const item = els.get(el)
		if (!item) return
		if (!item.scroll) return
		const children = getChildren(el)
		if (children.length === 0) return
		const rootEl = isDocumentRoot(root)
		const offsetHeight = root === document ? window.innerHeight : rootEl.offsetHeight
		const offsetTop = rootEl.offsetTop
		const scrollTop = rootEl.scrollTop
		const elTop_bottom = offsetHeight - (getDocumentTop(el) - offsetTop - rootEl.scrollTop)
		const maxHeight = offsetHeight + el.offsetHeight
		const p = Math.min(1, Math.max(0, elTop_bottom / maxHeight))
		children.forEach((item) => {
			setDocumentStyles(el, item, p)
		})
	})
}
function getDocumentTop(el, root = document, offset = 0) {
	const parent = el.parentElement
	offset = offset + el.offsetTop
	if (parent && parent !== document.documentElement) return getDocumentTop(parent, root, offset)
	return offset
}
function getChildren(el, oneself = true) {
	const children = Array.from(el.querySelectorAll(typesStr))
	if (oneself && Object.values(T).some(v => hasAttr(el, v))) children.push(el)
	return children
}
function setDocumentStyles(parent, el, p = 0) {
	const item = els.get(parent)
	if (!item) return
	const t = getAttrs(el, item.transition)
	const tDuration = t.duration || t.transformDuration
	const tDelay = t.delay || t.transformDelay
	const tEasing = t.easing || t.transformEasing
	const oDuration = t.duration || t.opacityDuration
	const oDelay = t.delay || t.opacityDelay
	const oEasing = t.easing || t.opacityEasing
	const transitionArr = [
		`transform ${tDuration} ${tDelay} ${tEasing}`,
		`opacity ${oDuration} ${oDelay} ${oEasing}`,
	]
	el.style.transition = transitionArr.join(', ')
	const { num: tX, unit: tXUnit } = getCurrentValue(el, T.translateX_s, T.translateX, p)
	const { num: tY, unit: tYUnit } = getCurrentValue(el, T.translateY_s, T.translateY, p)
	const { num: sx } = getCurrentValue(el, T.scaleX_s, T.scaleX, p, T.scale_s, T.scale, 1)
	const { num: sy } = getCurrentValue(el, T.scaleY_s, T.scaleY, p, T.scale_s, T.scale, 1)
	const { num: r } = getCurrentValue(el, T.rotate_s, T.rotate, p)
	const { num: rx } = getCurrentValue(el, T.rotateX_s, T.rotateX, p)
	const { num: ry } = getCurrentValue(el, T.rotateY_s, T.rotateY, p)
	const { num: rz } = getCurrentValue(el, T.rotateZ_s, T.rotateZ, p)
	const transform = `translate(${tX}${tXUnit || 'px'}, ${tY}${tYUnit || 'px'}) scale(${sx}, ${sy}) rotate(${r}deg) rotateX(${rx}deg) rotateY(${ry}deg) rotateZ(${rz}deg)`
	el.style.transform = transform
	if (hasAttr(el, [T.opacity_s, T.opacity])) {
		el.style.opacity = getCurrentValue(el, T.opacity_s, T.opacity, p, T.opacity_s, T.opacity, 1).num
	}
}
function getAttrs(el, attrs = {}) {
	const arrs = Object.keys(attrs)
	const obj = {}
	arrs.forEach((item) => {
		obj[item] = el.getAttribute(T[item]) || attrs[item]
	})
	return obj
}
function getCurrentValue(el, start, end, progress, bStart, bEnd, defaultValue = 0) {
	const startStr = hasAttr(el, start) ? el.getAttribute(start) : hasAttr(el, bStart) ? el.getAttribute(bStart) : String(defaultValue)
	const endStr = hasAttr(el, end) ? el.getAttribute(end) : hasAttr(el, bEnd) ? el.getAttribute(bEnd) : String(defaultValue)
	const startNum = parseInt(startStr)
	const endNum = parseInt(endStr)
	const starUnit = startStr.match(/(px|deg|%|rem|em|vh|vw|pt|pc|mm|cm|in)$/i)?.[1] || ''
	const endUnit = endStr.match(/(px|deg|%|rem|em|vh|vw|pt|pc|mm|cm|in)$/i)?.[1] || ''
	return {
		num: startNum + (endNum - startNum) * progress,
		unit: starUnit || endUnit
	}
}
function hasAttr(el, attr) {
	if (Array.isArray(attr)) {
		return attr.some(v => el.hasAttribute(v))
	} else {
		return el.hasAttribute(attr)
	}
}
// 检查document是否为根元素
function isDocumentRoot(root) {
	return root === document ? document.documentElement : root
}
