<script setup lang="ts">
import type { Options as SignaturePadOptions } from 'signature_pad'
import { Btn } from '@bagelink/vue'
import { useEventListener, useResizeObserver } from '@vueuse/core'
import SignaturePad from 'signature_pad'
import { onMounted, watch } from 'vue'

export interface WaterMark {
	text: string
	x: number
	y: number
	sx: number
	sy: number
	font: string
	fillStyle: string
	strokeStyle: string
	style: string
}

export type FormatType = 'image/png' | 'image/jpg' | 'image/jpeg' | 'image/svg+xml'

const props = withDefaults(defineProps<{
	sigOption?: SignaturePadOptions
	width?: string
	height?: string
	ratio?: number
	clearOnResize?: boolean
	waterMark?: WaterMark
	disabled?: boolean
	defaultUrl?: string
	format?: FormatType
	clearable?: boolean
	required?: boolean
}>(), {
	clearable: true,
})

const sigData = defineModel()
const fileData = defineModel<File>('file')

const vCanvas = $ref<HTMLCanvasElement>()

let sig = $ref<SignaturePad>()

let _isEmpty = $ref<boolean>(true)

function clear() {
	sig?.clear()
	_isEmpty = true
	sigData.value = undefined
}

function save(format?: FormatType) {
	format = format || props.format
	return format ? sig?.toDataURL(format) : sig?.toDataURL()
}

function saveFile() {
	const data = save()
	if (!data) return

	if (props.format === 'image/svg+xml') {
		// For SVG, create file directly from the SVG string
		fileData.value = new File([data], 'signature.svg', { type: 'image/svg+xml' })
	} else {
		try {
			// For PNG/JPG, convert base64 to blob first
			const base64Data = data.split(',')[1]
			if (!base64Data) throw new Error('Invalid data URL format')

			const byteCharacters = atob(base64Data)
			// Create byte array directly with mapping function
			const byteArray = new Uint8Array(
				Array.from(byteCharacters, char => char.charCodeAt(0))
			)

			const mimeType = props.format || 'image/png'
			const blob = new Blob([byteArray], { type: mimeType })
			const extension = mimeType.split('/')[1]

			fileData.value = new File([blob], `signature.${extension}`, { type: mimeType })
		} catch (error) {
			console.error('Error saving signature file:', error)
		}
	}
}

function fromDataURL(url: string) {
	return sig?.fromDataURL(url, {
		height: vCanvas?.height,
		width: vCanvas?.width,
		ratio: props.ratio,
		xOffset: 0,
		yOffset: 0,
	})
}

const isEmpty = () => sig?.isEmpty()
function undo() {
	const data = sig?.toData()
	if (data) {
		data.pop()
		sig?.fromData(data)
	}
}

const defaultOptions = $ref<SignaturePadOptions>({
	penColor: 'rgb(0, 0, 0)',
	backgroundColor: 'rgb(255, 255, 255)',
})

const signatureOptions = $computed(() => ({ ...defaultOptions, ...props.sigOption }))

watch(
	() => props.disabled,
	(val) => {
		if (val) sig?.off()
		else sig?.on()
	},
)

function addWaterMark(data: WaterMark) {
	if (!(Object.prototype.toString.call(data) === '[object Object]')) {
		throw new Error(`Expected Object, got ${typeof data}.`)
	} else {
		const textData = {
			text: data.text || '',
			x: data.x || 20,
			y: data.y || 20,
			sx: data.sx || 40,
			sy: data.sy || 40,
		}

		const ctx = vCanvas?.getContext('2d')
		if (!ctx) return
		ctx.font = data.font || '20px sans-serif'
		ctx.fillStyle = data.fillStyle || '#333'
		ctx.strokeStyle = data.strokeStyle || '#333'
		if (data.style === 'all') {
			ctx.fillText(textData.text, textData.x, textData.y)
			ctx.strokeText(textData.text, textData.sx, textData.sy)
		} else if (data.style === 'stroke') {
			ctx.strokeText(textData.text, textData.sx, textData.sy)
		} else {
			ctx.fillText(textData.text, textData.x, textData.y)
		}
	}
}

function onEndStroke() {
	_isEmpty = sig?.isEmpty() ?? false
	const data = save()
	sigData.value = data
	saveFile()
}

function loadFile(file: File | undefined) {
	if (!file) return
	const reader = new FileReader()
	return new Promise((resolve) => {
		reader.onload = (e) => {
			const dataUrl = String(e.target?.result)
			void fromDataURL(dataUrl)
			_isEmpty = false
			sigData.value = dataUrl
			resolve(true)
		}
		reader.readAsDataURL(file)
	})
}

watch(fileData, (newFile) => {
	void loadFile(newFile)
})

function resizeCanvas() {
	let url
	if (!isEmpty()) url = save()
	const ratio = props.ratio ?? 1 / 2
	const { width } = vCanvas!.getBoundingClientRect()
	vCanvas!.width = props.width ? Number(props.width.replaceAll('px', '')) : width
	vCanvas!.height = props.height ? Number(props.height.replaceAll('px', '')) : width * ratio
	clear()
	if (!props.clearOnResize && url !== undefined) void fromDataURL(url)
	if (props.waterMark) addWaterMark(props.waterMark)
}

function draw() {
	sig = new SignaturePad(vCanvas!, signatureOptions)
	resizeCanvas()
	if (props.disabled) sig.off()
	else sig.on()
	_isEmpty = sig.isEmpty()

	// Load initial file if provided
	if (fileData.value) {
		void loadFile(fileData.value)
	}
}

onMounted(draw)
useResizeObserver(() => vCanvas, resizeCanvas)
useEventListener(() => sig, 'endStroke', onEndStroke)

defineExpose({
	save,
	clear,
	isEmpty,
	undo,
	addWaterMark,
	fromDataURL,
})
</script>

<template>
	<div
		class="bgl_input signature-pad relative"
		:class="{ 'bg-transparent': disabled }"
		@touchmove.prevent
	>
		<Btn
			v-if="clearable && !disabled"
			flat
			thin
			icon="refresh"
			class="position-bottom-end"
			@click="clear"
		/>
		<canvas
			ref="vCanvas"
			class="canvas"
			:disabled
		/>
		<input
			placeholder="required"
			type="text"
			:required="required && _isEmpty"
			class="pixel opacity-0"
			inputMode="none"
		>
		<!--
			we add a hidden input to make the signature pad required
					we need to set inputMode="none"
			! this prevents the keyboard from opening on mobile
		-->
	</div>
</template>

<style>
.bgl_input .canvas {
	width: 100%;
	height: 100%;
}

.bgl_input.signature-pad {
	background: var(--input-bg);
	border: none;
	border-radius: var(--input-border-radius);
}

.bgl_input.signature-pad .canvas[disabled] {
	background: var(--input-disabled-bg);
	border: 1px solid var(--bgl-gray);
	border-radius: var(--input-border-radius);
}
</style>
