<template>
	<input
		type="text"
    data-input
    :disabled="disabled"
		v-bind="attrs"
    ref="root"
	/>
</template>

<script lang="ts">
import Flatpickr from 'flatpickr'
import 'flatpickr/dist/flatpickr.css'

// FIXME: Not sure how to alias these correctly
// import Options = Flatpickr.Options doesn't work
type Hook = Flatpickr.Options.Hook
type HookKey = Flatpickr.Options.HookKey
type Options = Flatpickr.Options.Options
type DateOption = Flatpickr.Options.DateOption

function camelToKebab(string: string) {
  return string.replace(/([a-z])([A-Z])/g, '$1-$2').toLowerCase()
}

function arrayify<T = unknown>(obj: T) {
  return obj instanceof Array
		? obj
		: [obj]
}

function nullify<T = unknown>(value: T) {
  return (value && (value as unknown[]).length)
		? value
		: null
}

function cloneObject<T = Record<string, unknown>>(obj: T): T {
  return Object.assign({}, obj)
}

// Events to emit, copied from flatpickr source
const includedEvents = [
  'onChange',
  'onClose',
  'onDestroy',
  'onMonthChange',
  'onOpen',
  'onYearChange',
] as HookKey[]

// Let's not emit these events by default
const excludedEvents = [
  'onValueUpdate',
  'onDayCreate',
  'onParseConfig',
  'onReady',
  'onPreCalendarPosition',
  'onKeyDown',
] as HookKey[]

// Keep a copy of all events for later use
const allEvents = includedEvents.concat(excludedEvents)

export default {inheritAttrs: false}
</script>

<script setup lang="ts">
import {computed, onBeforeUnmount, onMounted, ref, toRefs, useAttrs, watch, watchEffect, type PropType} from 'vue'

const props = defineProps({
	modelValue: {
		type: [String, Number, Date, Array] as PropType<DateOption | DateOption[]>,
		default: null,
		required: true,
		// validator(value) {
		// 	return (
		// 		value === null ||
		// 		value instanceof Date ||
		// 		typeof value === 'string' ||
		// 		value instanceof String ||
		// 		value instanceof Array ||
		// 		typeof value === 'number'
		// 	);
		// }
	},
	// https://chmln.github.io/flatpickr/options/
	config: {
		type: Object as PropType<Options>,
		default: () => ({
			wrap: false,
			defaultDate: null,
		}),
	},
	events: {
		type: Array as PropType<HookKey[]>,
		default: () => includedEvents,
	},
	disabled: {
		type: Boolean,
		default: false,
	},
})

const emit = defineEmits([
	'blur',
	'update:modelValue',
	...allEvents.map(camelToKebab),
])

const {modelValue, config, disabled} = toRefs(props)

// bind listener like onBlur
const attrs = useAttrs()

const root = ref<HTMLInputElement | null>(null)
const fp = ref<Flatpickr.Instance | null>(null)
const safeConfig = ref<Options>(cloneObject(props.config))

onMounted(() => {
	// Don't mutate original object on parent component
	const newConfig = cloneObject(props.config)

	if (
		fp.value || // Return early if flatpickr is already loaded
		!root.value // our input needs to be mounted
	) {
		return
	}

	props.events.forEach((hook) => {
		// Respect global callbacks registered via setDefault() method
		const globalCallbacks = Flatpickr.defaultConfig[hook] || []
	
		// Inject our own method along with user callback
		const localCallback: Hook = (...args) => emit(camelToKebab(hook), ...args)
	
		// Overwrite with merged array
		newConfig[hook] = arrayify(newConfig[hook] || []).concat(
			globalCallbacks,
			localCallback,
		)
	})

	// Watch for value changed by date-picker itself and notify parent component
	const onChange: Hook = (dates) => emit('update:modelValue', dates)
	newConfig['onChange'] = arrayify(newConfig['onChange'] || []).concat(onChange)

	// Flatpickr does not emit input event in some cases
	// const onClose: Hook = (_selectedDates, dateStr) => emit('update:modelValue', dateStr)
	// newConfig['onClose'] = arrayify(newConfig['onClose'] || []).concat(onClose)

	// Set initial date without emitting any event
	newConfig.defaultDate = props.modelValue || newConfig.defaultDate

	safeConfig.value = newConfig

	/**
	* Get the HTML node where flatpickr to be attached
	* Bind on parent element if wrap is true
	*/
	const element = props.config.wrap
		? root.value.parentNode
		: root.value

	// Init flatpickr
	fp.value = Flatpickr(element, safeConfig.value)
})
onBeforeUnmount(() => fp.value?.destroy())

watch(config, () => {
	if (!fp.value) return
	// Workaround: Don't pass hooks to configs again otherwise
	// previously registered hooks will stop working
	// Notice: we are looping through all events
	// This also means that new callbacks can not be passed once component has been initialized
	allEvents.forEach((hook) => {
		delete safeConfig.value?.[hook]
	})
	fp.value.set(safeConfig.value)

	// Passing these properties in `set()` method will cause flatpickr to trigger some callbacks
	const configCallbacks = ['locale', 'showMonths'] as (keyof Options)[]

	// Workaround: Allow to change locale dynamically
	configCallbacks.forEach(name => {
		if (typeof safeConfig.value?.[name] !== 'undefined' && fp.value) {
			fp.value.set(name, safeConfig.value[name])
		}
	})
}, {deep:true})

const fpInput = computed(() => {
	if (!fp.value) return
	return fp.value.altInput || fp.value.input
})

/**
 * init blur event
 * (is required by many validation libraries)
 */
function onBlur(event: Event) {
	emit('blur', nullify((event.target as HTMLInputElement).value))
}

watchEffect(() => fpInput.value?.addEventListener('blur', onBlur))
onBeforeUnmount(() => fpInput.value?.removeEventListener('blur', onBlur))

/**
* Watch for the disabled property and sets the value to the real input.
*/
watchEffect(() => {
	if (disabled.value) {
		fpInput.value?.setAttribute('disabled', '')
	} else {
		fpInput.value?.removeAttribute('disabled')
	}
})

/**
 * Watch for changes from parent component and update DOM
 */
watch(
	modelValue,
	newValue => {
		// Prevent updates if v-model value is same as input's current value
		if (!root.value || newValue === nullify(root.value.value)) return
		// Make sure we have a flatpickr instanceand
		// Notify flatpickr instance that there is a change in value
		fp.value?.setDate(newValue, true)
	},
	{deep: true},
)
</script>
