<template>
	<ListWrapper class="list-kanban" :list-id="listId" viewName="kanban">
		<template #header>
			<div class="filter-container" v-if="!isSavedFilter">
				<div class="items">
					<filter-popup v-model="params" />
				</div>
			</div>
		</template>

		<template #default>
			<div
				:class="{ 'is-loading': loading && !oneTaskUpdating}"
				class="kanban kanban-bucket-container loader-container"
			>
				<draggable
					v-bind="DRAG_OPTIONS"
					:modelValue="buckets"
					@update:modelValue="updateBuckets"
					@start="() => isDraggingBucket = true"
					@end="updateBucketPosition"
					group="buckets"
					:disabled="!canWrite"
					tag="transition-group"
					:item-key="(id: number) => `bucket${id}`"
					:component-data="bucketDraggableComponentData"
				>
					<template #item="{element: bucket, index: bucketIndex }">
						<Bucket
							class="bucket"
							:bucket-index="bucketIndex"
							:is-collapsed="collapsedBuckets[bucket.id]"
							:can-write="canWrite"
							:bucket="bucket"
							:isOnlyBucketLeft="buckets.length <= 1"
							:drag-options="DRAG_OPTIONS"
							:params="params"
							:should-accept-drop="shouldAcceptDrop(bucket)"
							:isDraggingTask="isDraggingTask"
							:taskUpdating="tasksUpdating"
							@dragstart="dragstart"
							@dragend="updateTaskPosition"
							@openDeleteBucketModal="openDeleteBucketModal"
						/>
					</template>
				</draggable>

				<BucketNew
					v-if="canWrite && !loading && buckets.length > 0"
					class="bucket"
					:listId="listId"
				/>
			</div>

			<transition name="modal">
				<modal
					v-if="hasBucketDeleteModal"
					@close="hasBucketDeleteModal = false"
					@submit="deleteBucket()"
				>
					<template #header><span>{{ $t('list.kanban.deleteHeaderBucket') }}</span></template>

					<template #text>
						<p>{{ $t('list.kanban.deleteBucketText1') }}<br/>
							{{ $t('list.kanban.deleteBucketText2') }}</p>
					</template>
				</modal>
			</transition>
		</template>
	</ListWrapper>
</template>

<script setup lang="ts">
import {ref, computed, watch} from 'vue'
import draggable from 'zhyswan-vuedraggable'
import cloneDeep from 'lodash.clonedeep'
import {useI18n} from 'vue-i18n'
import {SortableEvent} from 'sortablejs'

import {success} from '@/message'
import {calculateItemPosition} from '@/helpers/calculateItemPosition'
import {getCollapsedBucketState, saveCollapsedBucketState} from '@/helpers/saveCollapsedBucketState'

import ListModel from '@/models/list'
import BucketModel from '@/models/bucket'
import TaskModel from '@/models/task'
import Rights from '@/models/constants/rights.json'

import ListWrapper from './ListWrapper.vue'
import FilterPopup from '@/components/list/partials/filter-popup.vue'
import Bucket from '@/features/kanban/Bucket.vue'
import BucketNew from '@/features/kanban/BucketNew.vue'

const DRAG_OPTIONS = {
	// sortable options
	animation: 150,
	ghostClass: 'ghost',
	dragClass: 'task-dragging',
	delayOnTouchOnly: true,
	delay: 150,
}

const props = defineProps<{
	listId: number
}>()

const {t} = useI18n()

/**
 * Load collapsed Buckets
 */

const collapsedBuckets = ref<{ [listId: number]: any}>({})
watch(collapsedBuckets, (collapsedBuckets) => saveCollapsedBucketState(props.listId, collapsedBuckets))

/**
 * Load Bucket Data
 */

const params = ref({
	filter_by: [],
	filter_value: [],
	filter_comparator: [],
	filter_concat: 'and',
})

watch(() => ({
	listId: props.listId,
	params: params.value,
}), ({listId, params}) => {
	store.dispatch('kanban/loadBucketsForList', {listId, params})
	collapsedBuckets.value = getCollapsedBucketState(listId)
}, {immediate: true, deep: true})

const list = computed(() => store.state.currentList as ListModel)

const buckets = computed({
	get: () => store.state.kanban.buckets,
	set(value: BucketModel[]) {
		// (1) buckets get updated in store and tasks positions get invalidated
		store.commit('kanban/setBuckets', value)
	},
})


/**
 * Template helpers
 */

const isSavedFilter = computed(() => list.value.isSavedFilter && list.value.isSavedFilter())
const canWrite = computed(() => store.state.currentList.maxRight > Rights.READ)
const loading = computed(() => store.state.loading && store.state.loadingModule === 'kanban')

// FIXME: seems unused ?
// const taskLoading = computed(() => store.state.loading && store.state.loadingModule === 'tasks')

/**
 * Manage list of updateing tasks
 */

// FIXME: save globally if a specific task is updated.
// We're using this to show the loading animation only at the task when updating it
const tasksUpdating = ref<{ [taskId: TaskModel['id']]: boolean }>({})

function setTaskUpdating(id: TaskModel['id'], isUpdating: boolean) {
	tasksUpdating.value[id] = isUpdating
}

const oneTaskUpdating = computed(() => Object.values(tasksUpdating.value).some((isUpdating) => isUpdating))

/**
 * Delete Bucket
 */

const bucketToDeleteId = ref<BucketModel['id']>(0)
const hasBucketDeleteModal = ref(false)

function openDeleteBucketModal(bucketId: BucketModel['id']) {
	if (buckets.value.length <= 1) {
		return
	}

	bucketToDeleteId.value = bucketId
	hasBucketDeleteModal.value = true
}

async function deleteBucket() {
	try {
		await store.dispatch('kanban/deleteBucket', {
			bucket: new BucketModel({
				id: bucketToDeleteId.value,
				listId: props.listId,
			}),
			params: params.value,
		})
		success({message: t('list.kanban.deleteBucketSuccess')})
	} finally {
		hasBucketDeleteModal.value = false
	}
}

/**
 * Move / Drag Bucket
 */

const isDraggingBucket = ref(false)
const bucketDraggableComponentData = computed(() => ({
	type: 'transition',
	tag: 'div',
	name: !isDraggingBucket.value ? 'move-bucket' : null,
	class: [
		'kanban-bucket-container',
		{'dragging-disabled': !canWrite.value},
	],
}))

function updateBucketPosition({newDraggableIndex}: SortableEvent) {
	// (2) bucket positon is changed
	isDraggingBucket.value = false

	const bucket = buckets.value[newDraggableIndex]
	const bucketBefore = buckets.value[newDraggableIndex - 1] ?? null
	const bucketAfter = buckets.value[newDraggableIndex + 1] ?? null

	store.dispatch('kanban/updateBucket', {
		id: bucket.id,
		position: calculateItemPosition(
			bucketBefore !== null ? bucketBefore.position : null,
			bucketAfter !== null ? bucketAfter.position : null,
		),
	})
}

/**
 * Move / Drag Task
 */

const sourceBucketId = ref<number>()

function shouldAcceptDrop(bucket: BucketModel) {
	return !isDraggingBucket.value && (
		// It's always possible to drag a task inside the same a bucket
		bucket.id === sourceBucketId.value ||
		// If there is no limit set, dragging & dropping should always work
		bucket.limit === 0 ||
		// Disallow dropping to buckets which have their limit reached
		bucket.tasks.length < bucket.limit
	)
}

const isDraggingTask = ref(false)

function dragstart(bucketId: BucketModel['id']) {
	isDraggingTask.value = true
	sourceBucketId.value = bucketId
}


async function updateTaskPosition({newDraggableIndex, to}: SortableEvent) {
	isDraggingTask.value = false

	// FIXME: Cases
	// e.to.dataset.bucketIndex === undefined
	// e.newIndex === undefined

	// While we could just pass the bucket index in through the function call, this would not give us the 
	// new bucket id when a task has been moved between buckets, only the new bucket. Using the data-bucket-id
	// of the drop target works all the time.
	const bucketIndex = parseInt(to.dataset.bucketIndex)

	const newBucket = buckets.value[bucketIndex]

	// HACK:
	// this is a hacky workaround for a known problem of vue.draggable.next when using the footer slot
	// the problem: https://github.com/SortableJS/vue.draggable.next/issues/108
	// This hack doesn't remove the problem that the ghost item is still displayed below the footer
	// It just makes releasing the item possible.

	// The newIndex of the event doesn't count in the elements of the footer slot.
	// This is why in case the length of the tasks is identical with the newIndex
	// we have to remove 1 to get the correct index.
	// const newTaskIndex = newBucket.tasks.length === e.newIndex
	// 	? e.newIndex - 1
	// 	: e.newIndex

	const taskBefore = newBucket.tasks[newDraggableIndex - 1] ?? null
	const taskAfter = newBucket.tasks[newDraggableIndex + 1] ?? null

	const task = {
		// cloning the task to avoid vuex store mutations
		...cloneDeep(newBucket.tasks[newDraggableIndex]),
		bucketId: newBucket.id,
		kanbanPosition: calculateItemPosition(
			taskBefore !== null ? taskBefore.kanbanPosition : null,
			taskAfter !== null ? taskAfter.kanbanPosition : null,
		),
	} as TaskModel
	
	setTaskUpdating(task.id, false)

	try {
		await store.dispatch('tasks/update', task)
	} finally {
		setTaskUpdating(task.id, false)
	}
}
</script>



<style lang="scss">
// FIXME:
.app-content.list\.kanban {
	padding-bottom: 0 !important;
}

$ease-out: all .3s cubic-bezier(0.23, 1, 0.32, 1);

// $crazy-height-calculation: '100vh - 4.5rem - 1.5rem - 1rem - 1.5rem - 11px';
// $filter-container-height: '1rem - #{$switch-view-height}';

.kanban {
	--bucket-width: 300px;
	--bucket-right-margin: 1rem;
	overflow-x: auto;
	overflow-y: hidden;
	// height: calc(#{$crazy-height-calculation});
	margin: 0 -1.5rem;
	padding: 0 1.5rem;
	scroll-snap-type: x mandatory;

	// @media screen and (max-width: $tablet) {
	// 	height: calc(#{$crazy-height-calculation} - #{$filter-container-height});
	// }
}

.kanban-bucket-container {
	display: flex;
}

.bucket {
	margin-right: var(--bucket-right-margin);
	max-height: 100%;
	min-height: 20px;
	width: var(--bucket-width);
}

.ghost {
	position: relative;

	* {
		opacity: 0;
	}

	&::after {
		content: '';
		position: absolute;
		display: block;
		top: 0.25rem;
		right: 0.5rem;
		bottom: 0.25rem;
		left: 0.5rem;
		border: 3px dashed var(--grey-300);
		border-radius: $radius;
	}
}

.task-dragging {
	transform: rotateZ(3deg);
	transition: transform 0.18s ease;
}

@include modal-transition();
</style>