<template>
	<Transition
		:name="transitionName"
		:enter-active-class="
			$style['transition_' + transitionName + '_enterActive']
		"
		:leave-active-class="
			$style['transition_' + transitionName + '_leaveActive']
		"
		:enter-from-class="
			$style['transition_' + transitionName + '_enterFrom']
		"
		:leave-to-class="$style['transition_' + transitionName + '_leaveTo']"
		:duration="transitionDuration"
		appear
		@after-leave="emit('closed')"
		@keyup.esc="emit('click')"
		@enter="emit('opening')"
		@after-enter="onOpened"
	>
		<FocusTrap
			v-model:active="isActive"
			:return-focus-on-deactivate="!noReturnFocus"
		>
			<div
				v-show="manualShowing != null ? manualShowing : showing"
				v-hotkey.global="keymap"
				:class="[
					$style.root,
					{
						[$style.drawer]: type === 'drawer',
						[$style.dialog]:
							type === 'dialog' || type === 'dialog:top',
						[$style.popup]: type === 'popup',
					},
				]"
				:style="{
					zIndex,
					pointerEvents: (
						manualShowing != null ? manualShowing : showing
					)
						? 'auto'
						: 'none',
					'--transformOrigin': transformOrigin,
				}"
				tabindex="-1"
				v-focus
			>
				<div
					class="_modalBg data-cy-bg"
					:class="[
						$style.bg,
						{
							[$style.bgTransparent]: isEnableBgTransparent,
							'data-cy-transparent': isEnableBgTransparent,
						},
					]"
					:style="{ zIndex }"
					@click="onBgClick"
					@mousedown="onBgClick"
					@contextmenu.prevent.stop="() => {}"
				></div>
				<div
					ref="content"
					:class="[
						$style.content,
						{ [$style.fixed]: fixed, top: type === 'dialog:top' },
					]"
					:style="{ zIndex }"
					@click.self="onBgClick"
				>
					<slot :max-height="maxHeight" :type="type"></slot>
				</div>
			</div>
		</FocusTrap>
	</Transition>
</template>

<script lang="ts" setup>
import { nextTick, onMounted, watch, provide, onUnmounted, ref, shallowRef, computed } from 'vue';
import * as os from "@/os";
import { isTouchUsing } from "@/scripts/touch";
import { defaultStore } from "@/store";
import { deviceKind } from "@/scripts/device-kind";
import { FocusTrap } from "focus-trap-vue";

function getFixedContainer(el: Element | null): Element | null {
	if (el == null || el.tagName === "BODY") return null;
	const position = window.getComputedStyle(el).getPropertyValue("position");
	if (position === "fixed") {
		return el;
	} else {
		return getFixedContainer(el.parentElement);
	}
}

type ModalTypes = "popup" | "dialog" | "dialog:top" | "drawer";

const props = withDefaults(
	defineProps<{
		manualShowing?: boolean | null;
		anchor?: { x: string; y: string };
		src?: HTMLElement;
		preferType?: ModalTypes | "auto";
		zPriority?: "low" | "middle" | "high";
		noOverlap?: boolean;
		transparentBg?: boolean;
		noReturnFocus?: boolean;
	}>(),
	{
		manualShowing: null,
		src: null,
		anchor: () => ({ x: "center", y: "bottom" }),
		preferType: "auto",
		zPriority: "low",
		noOverlap: true,
		transparentBg: false,
		noReturnFocus: false,
	},
);

const emit = defineEmits<{
	(ev: "opening"): void;
	(ev: "opened"): void;
	(ev: "click"): void;
	(ev: "esc"): void;
	(ev: "close"): void;
	(ev: "closed"): void;
}>();

provide("modal", true);

let maxHeight = ref<number>();
let fixed = ref(false);
let transformOrigin = ref("center");
let showing = ref(true);
let content = shallowRef<HTMLElement>();
const zIndex = os.claimZIndex(props.zPriority);
let useSendAnime = ref(false);
const type = computed<ModalTypes>(() => {
	if (props.preferType === "auto") {
		if (
			!defaultStore.state.disableDrawer &&
			isTouchUsing &&
			deviceKind === "smartphone"
		) {
			return "drawer";
		} else {
			return props.src != null ? "popup" : "dialog";
		}
	} else {
		return props.preferType!;
	}
});
const isEnableBgTransparent = computed(
	() => props.transparentBg && type.value === "popup",
);
let transitionName = computed(() =>
	defaultStore.state.animation
		? useSendAnime.value
			? "send"
			: type.value === "drawer"
			? "modal-drawer"
			: type.value === "popup"
			? "modal-popup"
			: "modal"
		: "",
);
let transitionDuration = computed(() =>
	transitionName.value === "send"
		? 400
		: transitionName.value === "modal-popup"
		? 100
		: transitionName.value === "modal"
		? 200
		: transitionName.value === "modal-drawer"
		? 200
		: 0,
);

let contentClicking = false;

const focusedElement = document.activeElement;
function close(ev, opts: { useSendAnimation?: boolean } = {}) {
	// removeEventListener("popstate", close);
	// if (props.preferType == "dialog") {
	// 	history.forward();
	// }
	if (opts.useSendAnimation) {
		useSendAnime.value = true;
	}

	// eslint-disable-next-line vue/no-mutating-props
	if (props.src) props.src.style.pointerEvents = "auto";
	showing.value = false;
	emit("close");
	if (!props.noReturnFocus) {
		focusedElement.focus();
	}
}

function onBgClick() {
	if (contentClicking) return;
	emit("click");
}

if (type.value === "drawer") {
	maxHeight.value = window.innerHeight / 1.5;
}

const keymap = {
	esc: () => emit("esc"),
};

const MARGIN = 16;

const align = () => {
	if (props.src == null) return;
	if (type.value === "drawer") return;
	if (type.value === "dialog") return;

	if (content.value == null) return;

	const srcRect = props.src.getBoundingClientRect();

	const width = content.value!.offsetWidth;
	const height = content.value!.offsetHeight;

	let left;
	let top;

	const x = srcRect.left + (fixed.value ? 0 : window.pageXOffset);
	const y = srcRect.top + (fixed.value ? 0 : window.pageYOffset);

	if (props.anchor.x === "center") {
		left = x + props.src.offsetWidth / 2 - width / 2;
	} else if (props.anchor.x === "left") {
		// TODO
	} else if (props.anchor.x === "right") {
		left = x + props.src.offsetWidth;
	}

	if (props.anchor.y === "center") {
		top = y - height / 2;
	} else if (props.anchor.y === "top") {
		// TODO
	} else if (props.anchor.y === "bottom") {
		top = y + props.src.offsetHeight;
	}

	if (fixed.value) {
		// 画面から横にはみ出る場合
		if (left + width > window.innerWidth) {
			left = window.innerWidth - width;
		}

		const underSpace = window.innerHeight - MARGIN - top;
		const upperSpace = srcRect.top - MARGIN;

		// 画面から縦にはみ出る場合
		if (top + height > window.innerHeight - MARGIN) {
			if (props.noOverlap && props.anchor.x === "center") {
				if (underSpace >= upperSpace / 3) {
					maxHeight.value = underSpace;
				} else {
					maxHeight.value = upperSpace;
					top = upperSpace + MARGIN - height;
				}
			} else {
				top = window.innerHeight - MARGIN - height;
			}
		} else {
			maxHeight.value = underSpace;
		}
	} else {
		// 画面から横にはみ出る場合
		if (left + width - window.scrollX > window.innerWidth) {
			left = window.innerWidth - width + window.scrollX - 1;
		}

		const underSpace =
			window.innerHeight - MARGIN - (top - window.pageYOffset);
		const upperSpace = srcRect.top - MARGIN;

		// 画面から縦にはみ出る場合
		if (top + height - window.scrollY > window.innerHeight - MARGIN) {
			if (props.noOverlap && props.anchor.x === "center") {
				if (underSpace >= upperSpace / 3) {
					maxHeight.value = underSpace;
				} else {
					maxHeight.value = upperSpace;
					top = window.scrollY + (upperSpace + MARGIN - height);
				}
			} else {
				top =
					window.innerHeight -
					MARGIN -
					height +
					window.pageYOffset -
					1;
			}
		} else {
			maxHeight.value = underSpace;
		}
	}

	if (top < 0) {
		top = MARGIN;
	}

	if (left < 0) {
		left = 0;
	}

	let transformOriginX = "center";
	let transformOriginY = "center";

	if (
		top >=
		srcRect.top + props.src.offsetHeight + (fixed.value ? 0 : window.pageYOffset)
	) {
		transformOriginY = "top";
	} else if (top + height <= srcRect.top + (fixed.value ? 0 : window.pageYOffset)) {
		transformOriginY = "bottom";
	}

	if (
		left >=
		srcRect.left + props.src.offsetWidth + (fixed.value ? 0 : window.pageXOffset)
	) {
		transformOriginX = "left";
	} else if (
		left + width <=
		srcRect.left + (fixed.value ? 0 : window.pageXOffset)
	) {
		transformOriginX = "right";
	}

	transformOrigin.value = `${transformOriginX} ${transformOriginY}`;

	content.value.style.left = left + "px";
	content.value.style.top = top + "px";
};

const onOpened = () => {
	emit("opened");

	// モーダルコンテンツにマウスボタンが押され、コンテンツ外でマウスボタンが離されたときにモーダルバックグラウンドクリックと判定させないためにマウスイベントを監視しフラグ管理する
	const el = content.value!.children[0];
	el.addEventListener(
		"mousedown",
		(ev) => {
			contentClicking = true;
			window.addEventListener(
				"mouseup",
				(ev) => {
					// click イベントより先に mouseup イベントが発生するかもしれないのでちょっと待つ
					window.setTimeout(() => {
						contentClicking = false;
					}, 100);
				},
				{ passive: true, once: true },
			);
		},
		{ passive: true },
	);
	// if (props.preferType == "dialog") {
	// 	history.pushState(null, "", location.href);
	// }
	// addEventListener("popstate", close);
};

onMounted(() => {
	watch(
		() => props.src,
		async () => {
			if (props.src) {
				// eslint-disable-next-line vue/no-mutating-props
				props.src.style.pointerEvents = "none";
			}
			fixed.value = type.value === "drawer" || getFixedContainer(props.src) != null;

			await nextTick();

			align();
		},
		{ immediate: true },
	);

	nextTick(() => {
		new ResizeObserver((entries, observer) => {
			align();
		}).observe(content.value!);
	});
});
onUnmounted(() => {
	// removeEventListener("popstate", close);
	// if (props.preferType == "dialog") {
	// 	history.back();
	// }
});

defineExpose({
	close,
});
</script>

<style lang="scss" module>
.transition_send_enterActive,
.transition_send_leaveActive {
	> .bg {
		transition: opacity 0.3s !important;
	}

	> .content {
		transform: translateY(0px);
		transition:
			opacity 0.3s ease-in,
			transform 0.3s cubic-bezier(0.5, -0.5, 1, 0.5) !important;
	}
}
.transition_send_enterFrom,
.transition_send_leaveTo {
	> .bg {
		opacity: 0;
	}

	> .content {
		pointer-events: none;
		opacity: 0;
		transform: translateY(-300px);
	}
}

.transition_modal_enterActive,
.transition_modal_leaveActive {
	> .bg {
		transition: opacity 0.2s !important;
	}

	> .content {
		transform-origin: var(--transformOrigin);
		transition:
			opacity 0.2s,
			transform 0.2s !important;
	}
}
.transition_modal_enterFrom,
.transition_modal_leaveTo {
	> .bg {
		opacity: 0;
	}

	> .content {
		pointer-events: none;
		opacity: 0;
		transform-origin: var(--transformOrigin);
		transform: scale(0.9);
	}
}

.transition_modal-popup_enterActive,
.transition_modal-popup_leaveActive {
	> .bg {
		transition: opacity 0.2s !important;
	}

	> .content {
		transform-origin: var(--transformOrigin);
		transition:
			opacity 0.2s cubic-bezier(0, 0, 0.2, 1),
			transform 0.2s cubic-bezier(0, 0, 0.2, 1) !important;
	}
}
.transition_modal-popup_enterFrom,
.transition_modal-popup_leaveTo {
	> .bg {
		opacity: 0;
	}

	> .content {
		pointer-events: none;
		opacity: 0;
		transform-origin: var(--transformOrigin);
		transform: scale(0.9);
	}
}

.transition_modal-drawer_enterActive {
	> .bg {
		transition: opacity 0.2s !important;
	}

	> .content {
		transition: transform 0.2s cubic-bezier(0, 0.5, 0, 1) !important;
	}
}
.transition_modal-drawer_leaveActive {
	> .bg {
		transition: opacity 0.2s !important;
	}

	> .content {
		transition: transform 0.2s cubic-bezier(0, 0.5, 0, 1) !important;
	}
}
.transition_modal-drawer_enterFrom,
.transition_modal-drawer_leaveTo {
	> .bg {
		opacity: 0;
	}

	> .content {
		pointer-events: none;
		transform: translateY(100%);
	}
}

.root {
	outline: none;
	&.dialog {
		> .content {
			position: fixed;
			top: 0;
			bottom: 0;
			left: 0;
			right: 0;
			margin: auto;
			padding: 32px;
			// TODO: mask-imageはiOSだとやたら重い。なんとかしたい
			-webkit-mask-image: linear-gradient(
				0deg,
				rgba(0, 0, 0, 0) 0%,
				rgba(0, 0, 0, 1) 32px,
				rgba(0, 0, 0, 1) calc(100% - 32px),
				rgba(0, 0, 0, 0) 100%
			);
			mask-image: linear-gradient(
				0deg,
				rgba(0, 0, 0, 0) 0%,
				rgba(0, 0, 0, 1) 32px,
				rgba(0, 0, 0, 1) calc(100% - 32px),
				rgba(0, 0, 0, 0) 100%
			);
			overflow: auto;
			display: flex;

			@media (max-width: 500px) {
				padding: 16px;
				-webkit-mask-image: linear-gradient(
					0deg,
					rgba(0, 0, 0, 0) 0%,
					rgba(0, 0, 0, 1) 16px,
					rgba(0, 0, 0, 1) calc(100% - 16px),
					rgba(0, 0, 0, 0) 100%
				);
				mask-image: linear-gradient(
					0deg,
					rgba(0, 0, 0, 0) 0%,
					rgba(0, 0, 0, 1) 16px,
					rgba(0, 0, 0, 1) calc(100% - 16px),
					rgba(0, 0, 0, 0) 100%
				);
			}

			> ::v-deep(*) {
				margin: auto;
			}

			&.top {
				> ::v-deep(*) {
					margin-top: 0;
				}
			}
		}
	}

	&.popup {
		> .content {
			position: absolute;

			&.fixed {
				position: fixed;
			}
		}
	}

	&.drawer {
		position: fixed;
		top: 0;
		left: 0;
		width: 100%;
		height: 100%;
		overflow: clip;

		> .content {
			position: fixed;
			bottom: 0;
			left: 0;
			right: 0;
			margin: auto;

			> ::v-deep(*) {
				margin: auto;
			}
		}
	}
}

.bg {
	&.bgTransparent {
		background: transparent;
		-webkit-backdrop-filter: none;
		backdrop-filter: none;
	}
}
</style>
