<script setup lang="ts">
import {
	CheckIcon,
	EditIcon,
	EyeIcon,
	InfoIcon,
	RotateCounterClockwiseIcon,
	ShirtIcon,
	SpinnerIcon,
	WindowsIcon,
} from '@modrinth/assets'
import {
	Button,
	commonMessages,
	ConfirmModal,
	defineMessages,
	injectAuth,
	injectModrinthClient,
	injectNotificationManager,
	SkinPreviewRenderer,
	Toggle,
	useVIntl,
} from '@modrinth/ui'
import { arrayBufferToBase64 } from '@modrinth/utils'
import { useQuery } from '@tanstack/vue-query'
import { type DragDropEvent, getCurrentWebview } from '@tauri-apps/api/webview'
import { computedAsync } from '@vueuse/core'
import type { Ref } from 'vue'
import { computed, inject, onMounted, onUnmounted, ref, useTemplateRef, watch } from 'vue'

import EarsModIcon from '@/assets/skins/ears-mod.png'
import type AccountsCard from '@/components/ui/AccountsCard.vue'
import EditSkinModal from '@/components/ui/skin/EditSkinModal.vue'
import VirtualSkinSectionList from '@/components/ui/skin/VirtualSkinSectionList.vue'
import { useAppSettings } from '@/composables/use-app-settings.ts'
import { handleSevereError } from '@/composables/use-error.js'
import { trackEvent } from '@/helpers/analytics'
import { check_reachable, get_default_user, login as login_flow, users } from '@/helpers/auth'
import type { RenderResult } from '@/helpers/rendering/batch-skin-renderer.ts'
import {
	generateSkinPreviews,
	getSkinPreviewKey,
	skinBlobUrlMap,
} from '@/helpers/rendering/batch-skin-renderer.ts'
import type { Cape, Skin, SkinTextureUrl } from '@/helpers/skins.ts'
import {
	equip_skin,
	filterDefaultSkins,
	filterSavedSkins,
	flush_pending_skin_change,
	flush_pending_skin_change_for_profile,
	get_available_capes,
	get_available_skins,
	get_dragged_skin_data,
	get_normalized_skin_texture,
	normalize_skin_texture,
	remove_custom_skin,
	save_custom_skin,
	set_custom_skin_order,
} from '@/helpers/skins.ts'
import { hasPride26Badge } from '@/helpers/user-campaigns.ts'
import { useRootBreadcrumb } from '@/providers/breadcrumbs'
import { appMessages } from '@/utils/app-messages'

useRootBreadcrumb({
	slot: 'root',
	id: 'skins',
	label: 'Skin selector',
	to: '/skins',
	visual: { type: 'icon', component: ShirtIcon },
})

type UnlistenFn = () => void
type VirtualSkinSectionListExpose = {
	getAddSkinButtonElement: () => HTMLElement | null | undefined
}

const PENDING_SKIN_REFRESH_DELAY_MS = 11_000
const DEFAULT_SKIN_SECTION_SORT_ORDER = ['Default skins', 'Modrinth Pride']
const EARS_NOTICE_PLACEHOLDER = '__EARS_MOD_NAME__'
const messages = defineMessages({
	modrinthPrideSection: {
		id: 'app.skins.section.modrinth-pride',
		defaultMessage: 'Modrinth Pride',
	},
	modrinthPrideTooltip: {
		id: 'app.skins.section.modrinth-pride.tooltip',
		defaultMessage:
			'You received these skins for donating to a Modrinth Pride fundraiser during Pride Month.',
	},
	modrinthSection: {
		id: 'app.skins.section.modrinth',
		defaultMessage: 'Modrinth',
	},
	defaultSkinsSection: {
		id: 'app.skins.section.default-skins',
		defaultMessage: 'Default skins',
	},
	mineconEarth2017Section: {
		id: 'app.skins.section.minecon-earth-2017',
		defaultMessage: 'MINECON Earth 2017',
	},
	buildersAndBiomesSection: {
		id: 'app.skins.section.builders-and-biomes',
		defaultMessage: 'Builders & Biomes',
	},
	stridingHeroSection: {
		id: 'app.skins.section.striding-hero',
		defaultMessage: 'Striding Hero',
	},
	theGardenAwakensSection: {
		id: 'app.skins.section.the-garden-awakens',
		defaultMessage: 'The Garden Awakens',
	},
	chaseTheSkiesSection: {
		id: 'app.skins.section.chase-the-skies',
		defaultMessage: 'Chase the Skies',
	},
	theCopperAgeSection: {
		id: 'app.skins.section.the-copper-age',
		defaultMessage: 'The Copper Age',
	},
	mountsOfMayhemSection: {
		id: 'app.skins.section.mounts-of-mayhem',
		defaultMessage: 'Mounts of Mayhem',
	},
	tinyTakeoverSection: {
		id: 'app.skins.section.tiny-takeover',
		defaultMessage: 'Tiny Takeover',
	},
	chaosCubedSection: {
		id: 'app.skins.section.chaos-cubed',
		defaultMessage: 'Chaos Cubed',
	},
	rateLimitTitle: {
		id: 'app.skins.rate-limit.title',
		defaultMessage: 'Slow down!',
	},
	rateLimitText: {
		id: 'app.skins.rate-limit.text',
		defaultMessage:
			"You're changing your skin too frequently. Mojang's servers have temporarily blocked further requests. Please wait a moment before trying again.",
	},
	droppedFileErrorTitle: {
		id: 'app.skins.dropped-file-error.title',
		defaultMessage: 'Error processing file',
	},
	droppedFileErrorText: {
		id: 'app.skins.dropped-file-error.text',
		defaultMessage: 'Failed to read the dropped file.',
	},
	reorderSkinErrorTitle: {
		id: 'app.skins.reorder-error.title',
		defaultMessage: 'Failed to reorder skins',
	},
	reorderSkinErrorText: {
		id: 'app.skins.reorder-error.text',
		defaultMessage: 'Your skin order could not be saved.',
	},
	deleteSkinTitle: {
		id: 'app.skins.delete-modal.title',
		defaultMessage: 'Are you sure you want to delete this skin?',
	},
	deleteSkinDescription: {
		id: 'app.skins.delete-modal.description',
		defaultMessage: 'This will permanently delete the selected skin. This action cannot be undone.',
	},
	previewingBadge: {
		id: 'app.skins.previewing-badge',
		defaultMessage: 'Previewing',
	},
	applyButton: {
		id: 'app.skins.apply-button',
		defaultMessage: 'Apply',
	},
	demoApplyTooltip: {
		id: 'app.skins.demo.apply-tooltip',
		defaultMessage: 'Sign in to apply skins.',
	},
	editSkinButton: {
		id: 'app.skins.preview.edit-button',
		defaultMessage: 'Edit skin',
	},
	earsFeatureNotice: {
		id: 'app.skins.ears-feature-notice',
		defaultMessage: 'This skin uses features from the {ears} mod',
	},
	toggleEarsFeaturesOff: {
		id: 'app.skins.toggle-ears-features-off',
		defaultMessage: 'Toggle off',
	},
	toggleEarsFeaturesOn: {
		id: 'app.skins.toggle-ears-features-on',
		defaultMessage: 'Toggle on',
	},
	demoTitle: {
		id: 'app.skins.demo.title',
		defaultMessage: 'Editing with a demo account',
	},
	demoDescription: {
		id: 'app.skins.demo.description',
		defaultMessage: 'Sign in to your Minecraft account to save and apply skins!',
	},
	signInButton: {
		id: 'app.skins.sign-in.button',
		defaultMessage: 'Sign in to Microsoft',
	},
})

const editSkinModal = useTemplateRef('editSkinModal')
const addSkinFileInput = useTemplateRef<HTMLInputElement>('addSkinFileInput')
const skinSectionList = useTemplateRef<VirtualSkinSectionListExpose>('skinSectionList')

const { formatMessage } = useVIntl()
const notifications = injectNotificationManager()
const { addNotification, handleError } = notifications
const auth = injectAuth()
const client = injectModrinthClient()

const appSettings = useAppSettings()
const skins = ref<Skin[]>([])
const capes = ref<Cape[]>([])
const offline = ref(!navigator.onLine)

const accountsCard = inject('accountsCard') as Ref<typeof AccountsCard>
const currentUser = ref(undefined)
const currentUserId = ref<string | undefined>(undefined)

const username = computed(() => currentUser.value?.profile?.name ?? undefined)
const selectedSkin = ref<Skin | null>(null)
const isApplyingSkin = ref(false)
const earsFeaturesEnabled = ref(true)
const selectedSkinHasEarsFeatures = ref(false)

const originalSelectedSkin = ref<Skin | null>(null)
const earsFeatureNoticeParts = computed(() => {
	const notice = formatMessage(messages.earsFeatureNotice, {
		ears: EARS_NOTICE_PLACEHOLDER,
	})
	const placeholderIndex = notice.indexOf(EARS_NOTICE_PLACEHOLDER)

	if (placeholderIndex === -1) {
		return {
			before: notice,
			after: '',
			hasEarsLink: false,
		}
	}

	return {
		before: notice.slice(0, placeholderIndex),
		after: notice.slice(placeholderIndex + EARS_NOTICE_PLACEHOLDER.length),
		hasEarsLink: true,
	}
})

const savedSkins = computed(() => {
	try {
		return filterSavedSkins(skins.value)
	} catch (error) {
		handleError(error as Error)
		return []
	}
})
const authServerQuery = useQuery({
	queryKey: ['authServerReachability'],
	queryFn: async () => {
		await check_reachable()
		return true
	},
	refetchInterval: 5 * 60 * 1000,
	retry: false,
	refetchOnWindowFocus: false,
})
const { data: modrinthUser } = useQuery({
	queryKey: computed(() => ['authenticated-user', 'campaigns', auth.user.value?.id]),
	queryFn: () => client.labrinth.users_v3.getAuthenticated(),
	enabled: () => !!auth.session_token.value,
	retry: false,
})
const hasModrinthPrideCampaign = computed(
	() => !!auth.session_token.value && hasPride26Badge(modrinthUser.value?.campaigns?.pride_26),
)
const defaultSkins = computed(() =>
	filterDefaultSkins(skins.value).filter(
		(skin) => skin.section !== 'Modrinth Pride' || hasModrinthPrideCampaign.value,
	),
)
const defaultSkinSections = computed(() => {
	const sections = new Map<string, Skin[]>()

	for (const skin of defaultSkins.value) {
		const section = skin.section ?? 'Default skins'
		const sectionSkins = sections.get(section)

		if (sectionSkins) {
			sectionSkins.push(skin)
		} else {
			sections.set(section, [skin])
		}
	}

	return Array.from(sections, ([section, skins]) => ({
		section,
		title: getDefaultSkinSectionTitle(section),
		infoTooltip: getDefaultSkinSectionInfoTooltip(section),
		skins,
	})).sort(
		(a, b) => getDefaultSkinSectionSortIndex(a.section) - getDefaultSkinSectionSortIndex(b.section),
	)
})

const currentCape = computed(() => {
	if (selectedSkin.value?.cape_id) {
		const overrideCape = capes.value.find((c) => c.id === selectedSkin.value?.cape_id)
		if (overrideCape) {
			return overrideCape
		}
	}
	return undefined
})

const skinTexture = computedAsync(async () => {
	const skin = selectedSkin.value
	if (skin?.texture) {
		try {
			return await get_normalized_skin_texture(skin)
		} catch (error) {
			if (skin.texture.startsWith('data:image/')) {
				return skin.texture
			}

			handleError(error as Error)
			return ''
		}
	} else {
		return ''
	}
})
const capeTexture = computed(() => currentCape.value?.texture)
const skinVariant = computed(() => selectedSkin.value?.variant)
const skinNametag = computed(() => (appSettings.hideNametagSkinsPage ? undefined : username.value))
const isSkinManagementReadOnly = computed(
	() =>
		!!currentUser.value &&
		(offline.value || (authServerQuery.isError.value && !authServerQuery.isLoading.value)),
)
const hasPendingSkinChange = computed(
	() => !skinsMatch(selectedSkin.value, originalSelectedSkin.value),
)

let userCheckInterval: number | null = null
let pendingSkinRefreshTimeout: number | null = null
let unlistenAddSkinDragDrop: UnlistenFn | null = null
let isUnmounted = false

const isDraggingSkinFile = ref(false)
const isAddSkinButtonDragActive = ref(false)

const deleteSkinModal = ref()
const skinToDelete = ref<Skin | null>(null)

function confirmDeleteSkin(skin: Skin) {
	if (isSkinManagementReadOnly.value) return

	skinToDelete.value = skin
	deleteSkinModal.value?.show()
}

async function deleteSkin() {
	if (isSkinManagementReadOnly.value) return

	const deletedSkin = skinToDelete.value
	if (!deletedSkin) return

	try {
		await remove_custom_skin(deletedSkin)
		removeLocalSkin(deletedSkin)
	} catch (error) {
		handleError(error as Error)
	} finally {
		skinToDelete.value = null
	}
}

async function loadCapes() {
	try {
		capes.value = (await get_available_capes()) ?? []
	} catch (error) {
		if (currentUser.value && error instanceof Error) {
			handleError(error)
		}
	}
}

async function loadSkins() {
	try {
		const loadedSkins = (await get_available_skins()) ?? []
		const loadedEquippedSkin = loadedSkins.find((s) => s.is_equipped)
		const locallyKnownEquippedSkin =
			originalSelectedSkin.value &&
			(loadedSkins.find((skin) => skinsMatch(skin, originalSelectedSkin.value)) ??
				(originalSelectedSkin.value.texture.startsWith('data:image/')
					? originalSelectedSkin.value
					: undefined))
		const shouldPreserveKnownEquippedSkin =
			isSkinManagementReadOnly.value &&
			locallyKnownEquippedSkin &&
			!skinsMatch(loadedEquippedSkin, locallyKnownEquippedSkin)

		skins.value =
			shouldPreserveKnownEquippedSkin && locallyKnownEquippedSkin
				? mergeEquippedSkin(loadedSkins, locallyKnownEquippedSkin)
				: loadedSkins
		generateSkinPreviews(skins.value, capes.value)
		selectedSkin.value = skins.value.find((s) => s.is_equipped) ?? null
		originalSelectedSkin.value = selectedSkin.value
	} catch (error) {
		if (currentUser.value && error instanceof Error) {
			handleError(error)
		}
	}
}

function mergeEquippedSkin(list: Skin[], equippedSkin: Skin) {
	let foundEquippedSkin = false
	const mergedSkins = list.map((skin) => {
		const isEquipped = skinsMatch(skin, equippedSkin)
		foundEquippedSkin ||= isEquipped

		return {
			...skin,
			is_equipped: isEquipped,
		}
	})

	if (!foundEquippedSkin) {
		mergedSkins.unshift({
			...equippedSkin,
			is_equipped: true,
		})
	}

	return mergedSkins
}

function skinsMatch(a?: Skin | null, b?: Skin | null) {
	return (
		a?.source === b?.source &&
		a?.texture_key === b?.texture_key &&
		a?.variant === b?.variant &&
		(a?.cape_id ?? null) === (b?.cape_id ?? null)
	)
}

function skinsMatchIgnoringSource(a?: Skin | null, b?: Skin | null) {
	return (
		a?.texture_key === b?.texture_key &&
		a?.variant === b?.variant &&
		(a?.cape_id ?? null) === (b?.cape_id ?? null)
	)
}

function isSkinSelected(skin: Skin) {
	return skinsMatch(selectedSkin.value, skin)
}

function isSkinActive(skin: Skin) {
	return hasPendingSkinChange.value && skinsMatch(originalSelectedSkin.value, skin)
}

function getErrorMessage(error: unknown) {
	return error instanceof Error ? error.message : String(error)
}

function isMinecraftSkinRateLimitError(error: unknown) {
	const message = getErrorMessage(error)
	return message.includes('429 Too Many Requests') || message.includes('client error (429')
}

function getDefaultSkinSectionTitle(section?: string) {
	switch (section) {
		case 'Modrinth Pride':
			return formatMessage(messages.modrinthPrideSection)
		case 'Modrinth':
			return formatMessage(messages.modrinthSection)
		case 'MINECON Earth 2017':
			return formatMessage(messages.mineconEarth2017Section)
		case 'Builders & Biomes':
			return formatMessage(messages.buildersAndBiomesSection)
		case 'Striding Hero':
			return formatMessage(messages.stridingHeroSection)
		case 'The Garden Awakens':
			return formatMessage(messages.theGardenAwakensSection)
		case 'Chase the Skies':
			return formatMessage(messages.chaseTheSkiesSection)
		case 'The Copper Age':
			return formatMessage(messages.theCopperAgeSection)
		case 'Mounts of Mayhem':
			return formatMessage(messages.mountsOfMayhemSection)
		case 'Tiny Takeover':
			return formatMessage(messages.tinyTakeoverSection)
		case 'Chaos Cubed':
			return formatMessage(messages.chaosCubedSection)
		case 'Default skins':
			return formatMessage(messages.defaultSkinsSection)
		default:
			return section ?? formatMessage(messages.defaultSkinsSection)
	}
}

function getDefaultSkinSectionInfoTooltip(section: string) {
	switch (section) {
		case 'Modrinth Pride':
			return formatMessage(messages.modrinthPrideTooltip)
		default:
			return undefined
	}
}

function getDefaultSkinSectionSortIndex(section: string) {
	const index = DEFAULT_SKIN_SECTION_SORT_ORDER.indexOf(section)
	return index === -1 ? DEFAULT_SKIN_SECTION_SORT_ORDER.length : index
}

function changeSkin(newSkin: Skin) {
	if (isSkinManagementReadOnly.value) return

	selectedSkin.value = newSkin
}

function resetSelectedSkin() {
	selectedSkin.value =
		skins.value.find((skin) => skinsMatch(skin, originalSelectedSkin.value)) ??
		originalSelectedSkin.value
}

function removeLocalSkin(deletedSkin: Skin) {
	const nextSkins = skins.value.filter((skin) => !skinsMatch(skin, deletedSkin))
	skins.value = nextSkins

	if (selectedSkin.value && skinsMatch(selectedSkin.value, deletedSkin)) {
		selectedSkin.value =
			nextSkins.find((skin) => skinsMatch(skin, originalSelectedSkin.value)) ??
			nextSkins.find((skin) => skin.is_equipped) ??
			null
	}

	if (originalSelectedSkin.value && skinsMatch(originalSelectedSkin.value, deletedSkin)) {
		originalSelectedSkin.value = nextSkins.find((skin) => skin.is_equipped) ?? null
	}

	generateSkinPreviews(skins.value, capes.value)
}

function setLocallyEquippedSkin(skinToApply: Skin) {
	skins.value = skins.value.map((skin) => ({
		...skin,
		is_equipped: skinsMatch(skin, skinToApply),
	}))
	originalSelectedSkin.value =
		skins.value.find((skin) => skinsMatch(skin, skinToApply)) ?? skinToApply
	selectedSkin.value = originalSelectedSkin.value
	void accountsCard.value?.setEquippedSkin(originalSelectedSkin.value)
}

function insertLocalSkin(savedSkin: Skin) {
	const firstNonCustomSkinIndex = skins.value.findIndex((skin) => skin.source !== 'custom')

	if (firstNonCustomSkinIndex === -1) {
		skins.value = [...skins.value, savedSkin]
		return
	}

	const nextSkins = [...skins.value]
	nextSkins.splice(firstNonCustomSkinIndex, 0, savedSkin)
	skins.value = nextSkins
}

function updateLocalSkin(savedSkin: Skin, applied: boolean, previousSkin?: Skin) {
	let foundSkin = false
	const replacesSelectedSkin =
		selectedSkin.value?.texture_key === savedSkin.texture_key ||
		(previousSkin ? skinsMatch(selectedSkin.value, previousSkin) : false)
	const replacesOriginalSkin =
		originalSelectedSkin.value?.texture_key === savedSkin.texture_key ||
		(previousSkin ? skinsMatch(originalSelectedSkin.value, previousSkin) : false)

	skins.value = skins.value.map((skin) => {
		const isUpdatedSkin = skin.texture_key === savedSkin.texture_key
		const isPreviousSkin = previousSkin && skinsMatch(skin, previousSkin)

		if (isUpdatedSkin || isPreviousSkin) {
			foundSkin = true
			return {
				...savedSkin,
				is_equipped: applied || savedSkin.is_equipped,
			}
		}

		return {
			...skin,
			is_equipped: applied ? false : skin.is_equipped,
		}
	})

	if (!foundSkin) {
		insertLocalSkin({
			...savedSkin,
			is_equipped: applied || savedSkin.is_equipped,
		})
	}

	if (applied) {
		const locallyEquippedSkin =
			skins.value.find((skin) => skin.texture_key === savedSkin.texture_key) ?? savedSkin

		originalSelectedSkin.value = locallyEquippedSkin
		selectedSkin.value = locallyEquippedSkin
		void accountsCard.value?.setEquippedSkin(locallyEquippedSkin)
	} else {
		const locallySavedSkin =
			skins.value.find((skin) => skin.texture_key === savedSkin.texture_key) ?? savedSkin

		if (replacesSelectedSkin) {
			selectedSkin.value = locallySavedSkin
		}

		if (replacesOriginalSkin) {
			originalSelectedSkin.value = locallySavedSkin
		}
	}

	generateSkinPreviews(skins.value, capes.value)
}

async function reorderSavedSkins(orderedSkins: Skin[]) {
	const previousSkins = skins.value
	const previousSelectedSkin = selectedSkin.value
	const previousOriginalSelectedSkin = originalSelectedSkin.value
	const orderedTextureKeys = orderedSkins.map((skin) => skin.texture_key)
	const orderedTextureKeySet = new Set(orderedTextureKeys)
	const remainingSavedSkins = previousSkins.filter(
		(skin) => skin.source !== 'default' && !orderedTextureKeySet.has(skin.texture_key),
	)
	const defaultSkins = previousSkins.filter((skin) => skin.source === 'default')
	const nextSavedSkins = [...orderedSkins, ...remainingSavedSkins]

	skins.value = [...nextSavedSkins, ...defaultSkins]
	generateSkinPreviews(skins.value, capes.value)

	try {
		const persistedSavedSkins = await preserveExternalSkins(nextSavedSkins)

		if (persistedSavedSkins.some((skin, index) => skin !== nextSavedSkins[index])) {
			skins.value = [...persistedSavedSkins, ...defaultSkins]
			generateSkinPreviews(skins.value, capes.value)
		}

		await set_custom_skin_order(
			persistedSavedSkins
				.filter((skin) => skin.source === 'custom')
				.map((skin) => skin.texture_key),
		)
	} catch (error) {
		skins.value = previousSkins
		selectedSkin.value = previousSelectedSkin
		originalSelectedSkin.value = previousOriginalSelectedSkin
		generateSkinPreviews(skins.value, capes.value)
		addNotification({
			type: 'error',
			title: formatMessage(messages.reorderSkinErrorTitle),
			text: error instanceof Error ? error.message : formatMessage(messages.reorderSkinErrorText),
		})
		await loadSkins()
	}
}

async function preserveExternalSkins(skinsToPersist: Skin[]) {
	const preservedSkins: Skin[] = []

	for (const skin of skinsToPersist) {
		if (skin.source !== 'custom_external') {
			preservedSkins.push(skin)
			continue
		}

		const textureBlob = await normalize_skin_texture(skin.texture)
		const capeId = skin.cape_id ? capes.value.find((cape) => cape.id === skin.cape_id) : undefined
		const savedSkin = await save_custom_skin(skin, textureBlob, skin.variant, capeId, false)
		const preservedSkin: Skin = {
			...savedSkin,
			source: 'custom',
			is_equipped: skin.is_equipped,
		}

		if (skinsMatchIgnoringSource(selectedSkin.value, skin)) {
			selectedSkin.value = preservedSkin
		}

		if (skinsMatchIgnoringSource(originalSelectedSkin.value, skin)) {
			originalSelectedSkin.value = preservedSkin
			void accountsCard.value?.setEquippedSkin(preservedSkin)
		}

		preservedSkins.push(preservedSkin)
	}

	return preservedSkins
}

function schedulePendingSkinRefresh() {
	if (pendingSkinRefreshTimeout !== null) {
		window.clearTimeout(pendingSkinRefreshTimeout)
	}

	const pendingProfileId = currentUserId.value

	pendingSkinRefreshTimeout = window.setTimeout(async () => {
		pendingSkinRefreshTimeout = null

		if (isUnmounted) {
			return
		}

		try {
			if (pendingProfileId) {
				await flush_pending_skin_change_for_profile(pendingProfileId)
			} else {
				await flush_pending_skin_change()
			}
		} catch (error) {
			handleError(error as Error)
			schedulePendingSkinRefresh()
			return
		}

		if (accountsCard.value) {
			await accountsCard.value.refreshValues()
		}

		await loadCapes()
		await loadSkins()
	}, PENDING_SKIN_REFRESH_DELAY_MS)
}

async function applySelectedSkin() {
	const skinToApply = selectedSkin.value
	if (
		!currentUser.value ||
		!skinToApply ||
		!hasPendingSkinChange.value ||
		isApplyingSkin.value ||
		isSkinManagementReadOnly.value
	)
		return

	isApplyingSkin.value = true
	try {
		await equip_skin(skinToApply)
		setLocallyEquippedSkin(skinToApply)
		schedulePendingSkinRefresh()
	} catch (error) {
		if (isMinecraftSkinRateLimitError(error)) {
			notifications.addNotification({
				type: 'error',
				title: formatMessage(messages.rateLimitTitle),
				text: formatMessage(messages.rateLimitText),
			})
		} else {
			handleError(error as Error)
		}
	} finally {
		isApplyingSkin.value = false
	}
}

async function onSkinSaved(options: { applied: boolean; skin?: Skin; previousSkin?: Skin }) {
	if (options.skin) {
		updateLocalSkin(options.skin, options.applied, options.previousSkin)
	}

	if (!options.skin) {
		await loadCapes()
		await loadSkins()
	}

	if (options.applied) {
		schedulePendingSkinRefresh()
	}
}

async function loadCurrentUser() {
	try {
		const defaultId = await get_default_user()
		currentUserId.value = defaultId

		const allAccounts = await users()
		currentUser.value = allAccounts.find((acc) => acc.profile.id === defaultId)
	} catch (e) {
		handleError(e as Error)
		currentUser.value = undefined
		currentUserId.value = undefined
	}
}

function getBakedSkinTextures(skin: Skin): RenderResult | undefined {
	return skinBlobUrlMap.get(getSkinPreviewKey(skin))
}

async function login() {
	accountsCard.value.setLoginDisabled(true)
	const loggedIn = await login_flow().catch(handleSevereError)

	if (loggedIn && accountsCard) {
		await accountsCard.value.refreshValues()
	}

	trackEvent('AccountLogIn')
	accountsCard.value.setLoginDisabled(false)
}

function openAddSkinFileBrowser() {
	if (isSkinManagementReadOnly.value) return

	addSkinFileInput.value?.click()
}

async function onAddSkinFileInputChange(e: Event) {
	if (isSkinManagementReadOnly.value) return

	const files = (e.target as HTMLInputElement).files
	const file = files?.[0]

	if (!file) {
		return
	}

	await processSkinFileBuffer(await file.arrayBuffer())

	if (addSkinFileInput.value) {
		addSkinFileInput.value.value = ''
	}
}

function isSkinImagePath(path: string) {
	return path.toLowerCase().endsWith('.png')
}

function isSkinFileDrag(event: DragEvent) {
	const items = Array.from(event.dataTransfer?.items ?? [])
	const files = Array.from(event.dataTransfer?.files ?? [])

	return (
		items.some((item) => item.kind === 'file' && item.type === 'image/png') ||
		files.some((file) => file.type === 'image/png' || isSkinImagePath(file.name))
	)
}

function isPositionOverAddSkinButton(position: { x: number; y: number }) {
	const element = skinSectionList.value?.getAddSkinButtonElement()

	if (!element) {
		return false
	}

	const { x, y } = position
	const rect = element.getBoundingClientRect()

	return x >= rect.left && x <= rect.right && y >= rect.top && y <= rect.bottom
}

async function handleAddSkinNativeDragDrop(event: { payload: DragDropEvent }) {
	if (isSkinManagementReadOnly.value) return

	const payload = event.payload

	if (payload.type === 'leave') {
		isDraggingSkinFile.value = false
		isAddSkinButtonDragActive.value = false
		return
	}

	if (payload.type === 'enter') {
		isDraggingSkinFile.value = payload.paths.some(isSkinImagePath)
	}

	if (payload.type === 'enter' || payload.type === 'over') {
		isAddSkinButtonDragActive.value =
			isDraggingSkinFile.value && isPositionOverAddSkinButton(payload.position)
		return
	}

	const hasSkinPath = payload.paths.some(isSkinImagePath)
	const shouldUpload =
		(isDraggingSkinFile.value || hasSkinPath) && isPositionOverAddSkinButton(payload.position)

	isDraggingSkinFile.value = false
	isAddSkinButtonDragActive.value = false

	if (!shouldUpload) {
		return
	}

	const skinPath = payload.paths.find(isSkinImagePath)

	if (!skinPath) {
		return
	}

	try {
		const data = await get_dragged_skin_data(skinPath)
		await processSkinFileBuffer(data)
	} catch (error) {
		addNotification({
			title: formatMessage(messages.droppedFileErrorTitle),
			text: error instanceof Error ? error.message : formatMessage(messages.droppedFileErrorText),
			type: 'error',
		})
	}
}

function onAddSkinDragOver(event: DragEvent) {
	if (isSkinManagementReadOnly.value) return

	if (!isSkinFileDrag(event)) {
		return
	}

	isAddSkinButtonDragActive.value = true
}

function onAddSkinDragLeave() {
	if (isSkinManagementReadOnly.value) return

	isAddSkinButtonDragActive.value = false
}

async function onAddSkinDrop(event: DragEvent) {
	if (isSkinManagementReadOnly.value) return

	isAddSkinButtonDragActive.value = false

	const file = Array.from(event.dataTransfer?.files ?? []).find(
		(file) => file.type === 'image/png' || isSkinImagePath(file.name),
	)

	if (!file) {
		return
	}

	await processSkinFileBuffer(await file.arrayBuffer())
}

async function setupAddSkinDragDropListener() {
	try {
		const unlisten = await getCurrentWebview().onDragDropEvent(handleAddSkinNativeDragDrop)

		if (isUnmounted) {
			unlisten()
			return
		}

		unlistenAddSkinDragDrop = unlisten
	} catch (error) {
		handleError(error as Error)
	}
}

async function processSkinFileBuffer(buffer: Uint8Array | ArrayBuffer) {
	if (isSkinManagementReadOnly.value) return

	const fakeEvent = new MouseEvent('click')
	const originalSkinTexUrl = `data:image/png;base64,` + arrayBufferToBase64(buffer)
	try {
		const skinTextureNormalized = await normalize_skin_texture(originalSkinTexUrl)
		const skinTexUrl: SkinTextureUrl = {
			original: originalSkinTexUrl,
			normalized: `data:image/png;base64,` + arrayBufferToBase64(skinTextureNormalized),
		}
		editSkinModal.value?.showNew(fakeEvent, skinTexUrl)
	} catch (error) {
		handleError(error as Error)
	}
}

watch(
	() => selectedSkin.value?.cape_id,
	() => {},
)

watch(selectedSkin, () => {
	earsFeaturesEnabled.value = true
})

watch(isSkinManagementReadOnly, (readOnly) => {
	if (readOnly) {
		isDraggingSkinFile.value = false
		isAddSkinButtonDragActive.value = false
	}
})

onMounted(() => {
	window.addEventListener('offline', onOffline)
	window.addEventListener('online', onOnline)
	userCheckInterval = window.setInterval(checkUserChanges, 250)
	void setupAddSkinDragDropListener()
})

onUnmounted(() => {
	isUnmounted = true
	window.removeEventListener('offline', onOffline)
	window.removeEventListener('online', onOnline)

	if (userCheckInterval !== null) {
		window.clearInterval(userCheckInterval)
	}

	if (pendingSkinRefreshTimeout !== null) {
		window.clearTimeout(pendingSkinRefreshTimeout)
		pendingSkinRefreshTimeout = null
	}

	if (unlistenAddSkinDragDrop) {
		unlistenAddSkinDragDrop()
		unlistenAddSkinDragDrop = null
	}
})

function onOffline() {
	offline.value = true
}

function onOnline() {
	offline.value = false
	void authServerQuery.refetch()
}

async function checkUserChanges() {
	try {
		const defaultId = await get_default_user()
		if (defaultId !== currentUserId.value) {
			await accountsCard.value?.refreshValues()
			await loadCurrentUser()
			await loadCapes()
			await loadSkins()
		}
	} catch (error) {
		if (currentUser.value && error instanceof Error) {
			handleError(error)
		}
	}
}

await Promise.all([loadCapes(), loadCurrentUser()])
await loadSkins()
</script>

<template>
	<EditSkinModal
		ref="editSkinModal"
		:capes="capes"
		:demo="!currentUser"
		@saved="onSkinSaved"
		@deleted="() => loadSkins()"
	/>
	<input
		ref="addSkinFileInput"
		type="file"
		accept="image/png"
		class="hidden"
		@change="onAddSkinFileInputChange"
	/>
	<ConfirmModal
		ref="deleteSkinModal"
		:title="formatMessage(messages.deleteSkinTitle)"
		:description="formatMessage(messages.deleteSkinDescription)"
		:proceed-label="formatMessage(commonMessages.deleteLabel)"
		@proceed="deleteSkin"
	/>

	<div class="skin-layout box-border grow p-4" :class="{ 'pb-40': !currentUser }">
		<div class="sticky top-6 self-start p-2 pt-0">
			<h1 class="m-0 text-2xl font-bold flex items-center gap-2">
				{{ formatMessage(appMessages.skinSelectorLabel) }}
			</h1>
			<div
				class="ml-5 mt-4 flex h-[calc(80vh-1rem)] items-center justify-center max-[700px]:h-[calc(50vh-1rem)]"
			>
				<SkinPreviewRenderer
					:cape-src="capeTexture"
					:texture-src="skinTexture || ''"
					:ears-texture-src="selectedSkin?.texture"
					:variant="skinVariant"
					:nametag="skinNametag"
					:initial-rotation="Math.PI / 8"
					:ears-enabled="earsFeaturesEnabled"
					@ears-features-detected="selectedSkinHasEarsFeatures = $event"
				>
					<template v-if="hasPendingSkinChange" #nametag-badge>
						<div
							class="flex items-center justify-center gap-1.5 rounded-full border border-solid border-brand-blue bg-bg-blue px-3 py-1 text-base font-semibold leading-6 text-brand-blue"
						>
							<EyeIcon class="size-5 shrink-0" />
							{{ formatMessage(messages.previewingBadge) }}
						</div>
					</template>
					<template #subtitle>
						<div
							class="skin-preview-subtitle flex w-full flex-col items-center gap-6"
							:class="{ 'has-ears-features': selectedSkinHasEarsFeatures }"
						>
							<div
								v-if="hasPendingSkinChange"
								class="skin-preview-actions flex w-full items-center justify-center gap-1.5"
								:class="selectedSkinHasEarsFeatures ? 'flex-nowrap' : 'flex-wrap'"
							>
								<Button
									v-tooltip="
										selectedSkinHasEarsFeatures
											? formatMessage(commonMessages.resetButton)
											: undefined
									"
									type="base"
									size="lg"
									class="skin-preview-action-button"
									:disabled="isApplyingSkin || isSkinManagementReadOnly"
									:aria-label="formatMessage(commonMessages.resetButton)"
									@click="resetSelectedSkin"
								>
									<RotateCounterClockwiseIcon />
									<span class="skin-preview-action-label">
										{{ formatMessage(commonMessages.resetButton) }}
									</span>
								</Button>
								<Button
									v-tooltip="
										!currentUser
											? formatMessage(messages.demoApplyTooltip)
											: selectedSkinHasEarsFeatures
												? formatMessage(messages.applyButton)
												: undefined
									"
									type="colored"
									color="brand"
									size="lg"
									class="skin-preview-action-button"
									:disabled="!currentUser || isApplyingSkin || isSkinManagementReadOnly"
									:aria-label="formatMessage(messages.applyButton)"
									@click="applySelectedSkin"
								>
									<SpinnerIcon v-if="isApplyingSkin" class="animate-spin" />
									<CheckIcon v-else />
									<span class="skin-preview-action-label">
										{{ formatMessage(messages.applyButton) }}
									</span>
								</Button>
							</div>
							<Button
								v-else
								type="base"
								size="lg"
								:disabled="!selectedSkin || isSkinManagementReadOnly"
								@click="(e: MouseEvent) => selectedSkin && editSkinModal?.show(e, selectedSkin)"
							>
								<EditIcon />
								{{ formatMessage(messages.editSkinButton) }}
							</Button>

							<div
								v-if="selectedSkinHasEarsFeatures"
								class="ears-feature-notice box-border flex w-full max-w-[340px] items-center justify-center gap-1.5 px-2"
							>
								<div class="ears-feature-copy flex min-w-0 flex-1 items-center gap-1.5">
									<img
										:src="EarsModIcon"
										alt=""
										class="size-10 shrink-0 rounded-[7px] border border-solid border-surface-5 object-cover"
									/>
									<p
										class="ears-feature-description m-0 min-w-0 flex-1 text-sm font-medium leading-5 text-primary"
									>
										{{ earsFeatureNoticeParts.before
										}}<router-link
											v-if="earsFeatureNoticeParts.hasEarsLink"
											to="/project/mfzaZK3Z"
											class="text-inherit underline"
											>Ears</router-link
										>{{ earsFeatureNoticeParts.after }}
									</p>
									<router-link
										to="/project/mfzaZK3Z"
										class="ears-feature-compact-label hidden min-w-0 flex-1 text-sm font-medium leading-5 text-primary underline"
										>Ears</router-link
									>
								</div>
								<Button
									type="outlined"
									size="lg"
									class="ears-feature-toggle-button shadow-md"
									:aria-pressed="earsFeaturesEnabled"
									:aria-label="
										formatMessage(
											earsFeaturesEnabled
												? messages.toggleEarsFeaturesOff
												: messages.toggleEarsFeaturesOn,
										)
									"
									@click="earsFeaturesEnabled = !earsFeaturesEnabled"
								>
									{{
										formatMessage(
											earsFeaturesEnabled
												? messages.toggleEarsFeaturesOff
												: messages.toggleEarsFeaturesOn,
										)
									}}
								</Button>
								<Toggle
									v-model="earsFeaturesEnabled"
									v-tooltip="
										formatMessage(
											earsFeaturesEnabled
												? messages.toggleEarsFeaturesOff
												: messages.toggleEarsFeaturesOn,
										)
									"
									small
									class="ears-feature-toggle-switch"
									:aria-label="
										formatMessage(
											earsFeaturesEnabled
												? messages.toggleEarsFeaturesOff
												: messages.toggleEarsFeaturesOn,
										)
									"
								/>
							</div>
						</div>
					</template>
				</SkinPreviewRenderer>
			</div>
		</div>

		<div class="pt-2">
			<VirtualSkinSectionList
				ref="skinSectionList"
				:saved-skins="savedSkins"
				:default-skin-sections="defaultSkinSections"
				:get-baked-skin-textures="getBakedSkinTextures"
				:is-skin-selected="isSkinSelected"
				:is-skin-active="isSkinActive"
				:is-add-skin-button-drag-active="isAddSkinButtonDragActive"
				:read-only="isSkinManagementReadOnly"
				@select="changeSkin"
				@edit="(skin, event) => editSkinModal?.show(event, skin)"
				@delete="confirmDeleteSkin"
				@reorder-saved-skins="reorderSavedSkins"
				@add-skin="openAddSkinFileBrowser"
				@add-skin-dragenter="onAddSkinDragOver"
				@add-skin-dragover="onAddSkinDragOver"
				@add-skin-dragleave="onAddSkinDragLeave"
				@add-skin-drop="onAddSkinDrop"
			/>
		</div>
	</div>

	<div v-if="!currentUser" class="sticky w-full bottom-0 z-20 p-4 pt-0">
		<div
			class="mx-auto flex w-full max-w-5xl items-center justify-between gap-3 rounded-[20px] border border-solid border-surface-5 bg-surface-3 p-4"
		>
			<div class="flex min-w-0 grow items-start gap-3">
				<InfoIcon class="size-6 shrink-0 text-blue" />
				<div class="flex min-w-0 flex-col gap-1">
					<p class="m-0 text-lg font-semibold leading-6 text-contrast">
						{{ formatMessage(messages.demoTitle) }}
					</p>
					<p class="m-0 text-base leading-6 text-primary">
						{{ formatMessage(messages.demoDescription) }}
					</p>
				</div>
			</div>
			<Button
				v-show="accountsCard"
				type="colored"
				color="brand"
				:disabled="accountsCard.loginDisabled"
				@click="login"
			>
				<SpinnerIcon v-if="accountsCard.loginDisabled" class="animate-spin" />
				<WindowsIcon v-else />
				{{ formatMessage(messages.signInButton) }}
			</Button>
		</div>
	</div>
</template>

<style lang="scss" scoped>
.skin-preview-subtitle {
	container-type: inline-size;
}

.ears-feature-toggle-switch {
	display: none;
}

@container (max-width: 300px) {
	.skin-preview-subtitle {
		gap: 0.75rem;
	}

	.has-ears-features .skin-preview-action-button {
		padding-left: 0 !important;
		padding-right: 0 !important;
		width: 2.5rem;
	}

	.has-ears-features .skin-preview-action-label {
		display: none;
	}

	.ears-feature-toggle-button {
		display: none !important;
	}

	.ears-feature-notice {
		padding-left: 0;
		padding-right: 0;
	}

	.ears-feature-description {
		display: none;
	}

	.ears-feature-compact-label {
		display: block;
	}

	.ears-feature-toggle-switch {
		display: inline-flex;
	}
}

.skin-layout {
	display: grid;
	grid-template-columns: minmax(0, 1fr) minmax(0, 2.5fr);
	gap: 2.5rem;

	@media (max-width: 700px) {
		grid-template-columns: 1fr;
	}
}
</style>
