// TODO(chris): Comments

import GridItem from './Grid/Item.js';
import GridLogic from '../../composables/GridLogic.js';

const MODE_IDLE = 0;
const MODE_MOVE = 1;
const MODE_RESIZE = 2;
const MODE_MOUSE_DOWN = 3;

export default {
	name: 'Grid',
	components: {
		GridItem,
	},
	props: {
		cols: Number,
		items: Array,
		itemsSetup: Object,
		resizeLimit: Function,
		active: {
			type: Boolean,
			default: true
		},
		marginForExtraRow: {
			type: Number,
			default: 0
		},
		additionalRow:{
			type: Boolean,
			default: false,
		}
	},
	emits: [
		"rearrangeItems",
		"newItem",
		"gridHeight",
		"draggedItem",
		"update:additionalRow"
	],
	data() {
		return {
			x: -1,
			y: -1,
			clientX:0,
			clientY: 0,
			mode: MODE_IDLE,
			grid: null,
			dragGrid: null,
			permUpdates: [],
			positionUpdates: null,
			fixedPositionUpdates: null,
			draggedOffset: [0,0],
			draggedItem: null,
			draggedNode: null,
			reorderedItems:[],
			clonedWidget:null,
		}
	},
	inject:{
		sectionName: {
			type: String,
			default: '',
		},
	},
	computed: {
		additionalRowComputed: {
			get() {
				return this.additionalRow;
			},
			set(value) {
				this.$emit('update:additionalRow', value);
			}
		},
		items_hashmap() {
			let items = {};
			this.items.forEach(item => {
				if (this.reorderedItems.length > 0 && this.needsReordering(item)){
						let rearrangedPosition = this.reorderedItems.filter(widget => widget.data.widgetid == item.widgetid)?.pop();
						if (rearrangedPosition) {			
							item.x = rearrangedPosition.x;
							item.y = rearrangedPosition.y;
						}
				}
				items[`x${item.x}y${item.y}`] = item;
			});	
			return items
		},
		items_placeholders(){
			let placeholders = [];
			let col_max = this.cols;
			let rows_max = this.rows;

			// occupied hashmap to keep track of the occupied cells
			let occupied = {};

			for (let y = 0; y < rows_max; y++) {
				for (let x = 0; x < col_max; x++) {
					// skip current position if it was registered as occupied
					if (Object.keys(occupied).length && occupied[`x${x}y${y}`]) {
						continue;
					}
					let current_item = this.items_hashmap[`x${x}y${y}`];
					if (current_item) {
						//calculate the occupied cells from the width and the height from the items 
						let width = current_item.w;
						let height = current_item.h;
						let max_x = x + width - 1;
						let max_y = y + height - 1;
						if(x != max_x || y != max_y){
							for (let occupied_y = y; occupied_y <= max_y; occupied_y++) {
								for (let occupied_x = x; occupied_x <= max_x; occupied_x++) {
									if (occupied_x != x || occupied_y != y) {
										occupied[`x${occupied_x}y${occupied_y}`]=true;
									}
								}
							}
						}
					}
					else {
						placeholders.push({ x: x, y: y, w: 1, h: 1, placeholder: true, 
							data: { id: 'placeholder_' + String(placeholders.length).padStart(4, "0") } });
					}
				}
			}
			return placeholders;
		},
		placedItems_withPlaceholders() {
			return [...this.placedItems, ...this.items_placeholders];
		},
		rows() {
			if (this.additionalRowComputed) {
					return this.grid ? (this.grid.h+1) : 1;
			}
			return this.grid ? this.grid.h : 1;
			
		},
		gridStyle() {
			const addH = this.active ? this.marginForExtraRow : 0;
			return {
				'--fhc-dg-row-height': 100/(this.rows + addH) + '%',
				'--fhc-dg-col-width': 100/this.cols + '%',
				'--fhc-dg-item-padding-horizontal': '0.25%',
				'--fhc-dg-item-padding-top': '0.5%',
				'padding-bottom': 100 * (this.rows + addH)/this.cols + '%'
			}
		},
		indexedItems() {
			return this.items.map(
				(item, index) => {
					return {
						index: index,
						x: item.x,
						y: item.y,
						w: item.w,
						h: item.h,
						weight: item.weight || 0,
						data: item
					}
				}
			);
		},
		prePlacedItems() {
			if (!this.fixedPositionUpdates)
				return this.indexedItems;
			return this.indexedItems.map(item => {
				if (!this.fixedPositionUpdates[item.index])
					return item;
				return {
					index: item.index,
					weight: item.weight,
					data: item.data,
					x: this.fixedPositionUpdates[item.index].x === undefined ? item.x : this.fixedPositionUpdates[item.index].x,
					y: this.fixedPositionUpdates[item.index].y === undefined ? item.y : this.fixedPositionUpdates[item.index].y,
					w: this.fixedPositionUpdates[item.index].w === undefined ? item.w : this.fixedPositionUpdates[item.index].w,
					h: this.fixedPositionUpdates[item.index].h === undefined ? item.h : this.fixedPositionUpdates[item.index].h
				};
			});
		},
		placedItems() {
			if (!this.positionUpdates)
				return this.prePlacedItems;
			let mappedPlacedItems= this.prePlacedItems.map(item => {
				if (!this.positionUpdates[item.index] )
					return item;
				let height_diff = this.positionUpdates[item.index]?.h - item.h;
				let width_diff = this.positionUpdates[item.index]?.w - item.w;
				return {
					resize: this.positionUpdates[item.index]?.resize,
					index: item.index,
					weight: item.weight,
					data: item.data,
					x: this.positionUpdates[item.index].x === undefined ? item.x : this.positionUpdates[item.index].x,
					y: this.positionUpdates[item.index].y === undefined ? item.y : this.positionUpdates[item.index].y,
					w: width_diff>0?item.w:this.positionUpdates[item.index].w === undefined ? item.w : this.positionUpdates[item.index].w,
					h: height_diff > 0 ?item.h:this.positionUpdates[item.index].h === undefined ? item.h : this.positionUpdates[item.index].h
					
				};
			});

			let temporaryResizeItems = [];
			mappedPlacedItems.forEach(item=>{
				if(item.resize){
					let newItem = {
						...item,
						w:this.positionUpdates[item.index].w === undefined ? item.w : this.positionUpdates[item.index].w,
						h:this.positionUpdates[item.index].h === undefined ? item.h : this.positionUpdates[item.index].h,
						resizeOverlay:true,
						blank:true,
					};
					temporaryResizeItems.push(newItem)
				}
			})
			return [...mappedPlacedItems, ...temporaryResizeItems];
		},
		showEmptyTileHover() {
			if (!this.active || !this.grid || this.mode != MODE_IDLE || this.x < 0 || this.y < 0 || this.x >= this.cols || this.y >= this.rows)
				return false;
			return this.grid.isFreeSlot(this.x, this.y);
		},
		widgetSetup(){
			if (!this.widgetsSetup)
				return;
			return this.widgetsSetup.reduce((acc, ele) => { 
				acc[ele.widget_id] =ele;
				return acc;
			} ,{});
		},
	},
	watch: {
		active(active) {
			if (!active)
				this.dragCancel();
		},
		cols() {
			this.dragCancel();
		},
	    rows: {
			handler(value) {
				this.$emit('gridHeight', value);
			},
			immediate: true
		},
		indexedItems: {
			handler(value) {
				this.dragCancel();

				const updated = this.createNewGrid(value);

				this.fixedPositionUpdates = updated;
				if (updated.length)
					this.$emit('rearrangeItems', updated.filter(v => v));
			},
			immediate: true,
			deep: true
		}
	},
	methods: {
		needsReordering(item){
			if (!item?.data?.place[this.cols]){
				return true;
			}
			return false;
		},
		toggleDraggedItemOverlay(condition){
			if(!this.draggedNode)
				return;
			if(condition){
				this.draggedNode.firstElementChild.classList.add("dashboard-item-overlay");
			}else{
				this.draggedNode.firstElementChild.classList.remove("dashboard-item-overlay");
			}
		},
		dragging(event){
			if(this.mode == MODE_MOVE){
				this.toggleDraggedItemOverlay(true);
				
				const containerRect = this.$refs.container.getBoundingClientRect();
				const clonedWidgetRect = this.clonedWidget.getBoundingClientRect();
				
				let desiredTop = this.clientY - 20;
				let desiredLeft = this.clientX - 15;
				
				const minTop = 0;
				const maxTop = containerRect.height - clonedWidgetRect.height;
				const minLeft = 0;
				const maxLeft = containerRect.width - clonedWidgetRect.width;
				
				const constrainedTop = Math.max(minTop, Math.min(maxTop, desiredTop));
				const constrainedLeft = Math.max(minLeft, Math.min(maxLeft, desiredLeft));
				
				this.clonedWidget.style.top = `${constrainedTop}px`;
				this.clonedWidget.style.left = `${constrainedLeft}px`;
			}
		},
		createNewGrid(items) {
			this.grid = new GridLogic(this.cols);
			const result = [];
			let sortedItems = [...items].sort((a, b) => {
				if(this.needsReordering(a) && this.needsReordering(b)){
					return 0;
				}
				else if(this.needsReordering(a)){
					return 999;
				}
				else if(this.needsReordering(b)){
					return -999;
				}
				
				return a.weight > b.weight;
			}); 
			let reorderedItems = [];
			sortedItems.forEach(item => {
				let freeSlots = this.grid.getFreeSlots();
				
				if(this.needsReordering(item)){
					let firstFreeSlot = freeSlots.shift();
					if (!firstFreeSlot) {
						item.x = 0;
						item.y = this.grid.h;
					}else{
						item.x = firstFreeSlot.x;
						item.y = firstFreeSlot.y;
					}
					reorderedItems.push(item);
					
				}
				if (item.x + item.w > this.cols) {
					let targetW = this.cols-item.x,
						targetX = undefined;
					if (this.resizeLimit) {
						[targetW] = this.resizeLimit(item.data, targetW, item.h);
					}
					if (targetW < 1)
						targetW = 1;
					if (targetW > this.cols)
						targetW = this.cols;
					if (item.x + targetW > this.cols) {
						targetX = this.cols - targetW;
					}
					if (targetW == item.w)
						targetW = undefined;
					result[item.index] = {
						item: item.data,
						x: targetX,
						w: targetW
					};
				}
				item.frame = this.grid.getItemFrame(item);
				this.convertGridResultToUpdate(this.grid.add(item), result, items);
			});
			this.reorderedItems = reorderedItems;
			this.grid.clearWeights();
			return result;
		},
		convertGridResultToUpdate(input, output, baseArray) {
			
			if (!input)
				return;
			if (!baseArray)
				baseArray = this.indexedItems;
			input.forEach(item => {
				let result = {
					item: baseArray[item.index].data
				};
				if (item.x !== undefined)
					result.x = item.x;
				if (item.y !== undefined)
					result.y = item.y;
				if (item.w !== undefined)
					result.w = item.w;
				if (item.h !== undefined)
					result.h = item.h;
				output[item.index] = result;
			});
		},
		mouseLeave() {
			/* if (this.mode == MODE_IDLE) {
				this.x = -1;
				this.y = -1;
				
			}  */
		},
		updateCursor(evt) {
			if (!this.active) {
				this.x = this.y = -1;
				return false;
			}
			const addH = this.active ? this.marginForExtraRow : 0;
			const rect = this.$refs.container.getBoundingClientRect();
			
			if (!evt.clientX && !evt.clientY && evt.touches){
				evt.clientX = evt.touches[0].clientX;
				evt.clientY = evt.touches[0].clientY;
			}

			this.clientX = (evt.clientX - rect.left);
			this.clientY = (evt.clientY - rect.top);
			const gridX = Math.floor(this.cols * (evt.clientX - rect.left) / this.$refs.container.clientWidth);
			const gridY = Math.floor((this.rows + addH) * (evt.clientY - rect.top) / this.$refs.container.clientHeight);
			
			if (this.x == gridX && this.y == gridY)
				return false;
			
			this.x = gridX;
			this.y = gridY;

			return true;
		},
		_dragStart(evt, item) {
			if (evt.dataTransfer) {
				evt.dataTransfer.setDragImage(evt.target, -99999, -99999);
				evt.dataTransfer.dropEffect = 'move';
				evt.dataTransfer.effectAllowed = 'move';
			}
		},
		startMove(evt, item) {
			
			if (!this.active)
				return;
			
			this.mode = MODE_MOVE;
			
			this.draggedItem = item;
			
			this.$emit('draggedItem', item);
			// workaround for chrome fireing event dragend when styles are manipulated during dragging
			setTimeout(() => {
				this.draggedNode = evt.target.closest(".drop-grid-item");
				//clones the widget for the drag Image
				
				let clone = evt.target.closest(".drop-grid-item")?.cloneNode(true);

				clone.style.zIndex = 5;
				clone.classList.add("widgetClone");
				this.$refs.container.appendChild(clone);
				const hiddenWidget = clone.querySelector("[style='display: none;']");
				hiddenWidget.style.removeProperty("display");
				this.clonedWidget = clone;
			}, 0);

			this.draggedOffset = [item.x - this.x, item.y - this.y];
			this._dragStart(evt, item);
		},
		startResize(evt, item) {
			if (!this.active)
				return;
			this.mode = MODE_RESIZE;
			this.draggedItem = item;
			this.$emit('draggedItem', item);
			this._dragStart(evt);
		},
		dragOver(evt) {
			if ((this.y + 1) > this.rows && (this.mode == MODE_MOVE || this.mode == MODE_RESIZE)) {
				this.dragCancel();
				
			}
			if (!this.active)
				return this.dragCancel();
			this.checkPinnedWidgetAnimation();
			if(this.mode == MODE_RESIZE){
				this.checkWidgetSizeLimitAnimation();
			}
			if (this.updateCursor(evt)) {
				switch(this.mode) {
					case MODE_MOVE: {
						evt.preventDefault();
						this.dragGrid = new GridLogic(this.grid);
						let x = this.x + this.draggedOffset[0];
						let y = this.y + this.draggedOffset[1];
						if (x < 0) {
							this.draggedOffset[0] += x;
							x = 0;
						} else if (x + this.draggedItem.w > this.cols) {
							this.draggedOffset[0] += this.cols - this.draggedItem.w - x;
							x = this.cols - this.draggedItem.w;
						}
						if (y < 0) {
							this.draggedOffset[1] += y;
							y = 0;
						}
						this.positionUpdates= this.dragGrid.move(this.draggedItem, x, y);
						break;
					}
					case MODE_RESIZE: {
						evt.preventDefault();
						this.dragGrid = new GridLogic(this.grid);
						let w = Math.min(this.cols - this.draggedItem.x, Math.max(1, this.x - this.draggedItem.x + 1));
						let h = Math.max(1, this.y - this.draggedItem.y + 1);
						if (this.resizeLimit)
							[w, h] = this.resizeLimit(this.draggedItem.data, w, h);
						this.positionUpdates = this.dragGrid.resize(this.draggedItem, w, h);
						break;
					}
				}
			}
		},
		dragCancel() {
			this.removeWidgetClones();
			this.additionalRowComputed = false;
			this.toggleDraggedItemOverlay(false);
			this.mode = MODE_IDLE;
			this.positionUpdates = null;
			this.draggedOffset = [0,0],
			this.draggedItem = null;
			this.$emit('draggedItem',null);
			this.draggedNode = null;
			
		},
		dragEnd() {
			this.removeWidgetClones();
			this.toggleDraggedItemOverlay(false);
			
			if (this.mode == MODE_IDLE){
				return;
			}
			// clean up unused classes
			let draggedItemNode = document.getElementById(this.draggedItem.data.widgetid);
			draggedItemNode.classList.remove("border-danger");
			Array.from(document.getElementsByClassName("denied-dragging-animation"))?.forEach(ele => {
				ele.classList.remove("denied-dragging-animation");
			})
			
			//if (!this.active || this.x < 0 || this.y < 0 || this.x >= this.cols)
				//return this.dragCancel();

			this.mode = MODE_IDLE;
			let updated = [];
			this.convertGridResultToUpdate(this.positionUpdates, updated);
			updated = this._updateFixedPositions(updated);
			if (updated.length)
				this.$emit('rearrangeItems', updated.filter(v => v));

			this.draggedItem = null;
			this.draggedNode = null;
			this.$emit('draggedItem', null);
		},
		_updateFixedPositions(updated) {
			updated.forEach((item, index) => {
				if (!this.fixedPositionUpdates[index])
					this.fixedPositionUpdates[index] = item;
				else
					this.fixedPositionUpdates[index] = {...this.fixedPositionUpdates[index], ...item};
			});
			let additionalUpdates = this.createNewGrid(this.prePlacedItems);
			if (additionalUpdates.length) {
				// NOTE(chris): this should never happen but it's here for safety
				additionalUpdates.forEach((item, index) => updated[index] = item);
				return this._updateFixedPositions(updated);
			}
			return updated;
		},
		emptyTileClicked() {
			this.additionalRowComputed = false;
			this.$emit('newItem', this.x, this.y);
		},
		updateCursorOnMouseMove(evt){
			if(this.mode == MODE_IDLE){
				this.updateCursor(evt);
			}
		},
		checkPinnedWidgetAnimation(){
			let itemAtPosition=[];
			switch(this.mode){
				case MODE_RESIZE:
					for (let x = this.draggedItem.x; x <= this.x; x++) {
						for (let y = this.draggedItem.y; y <= this.y; y++) {
							this.items.forEach(item => {
								if (item.x == x && item.y == y) {
									itemAtPosition.push(item);
								}
							});
						}
					}
					break;
				case MODE_MOVE:
					itemAtPosition = this.items.filter(item=>item.x == this.x && item.y == this.y);
					break;
			}
			
			Array.from(document.getElementsByClassName("denied-dragging-animation"))?.forEach(ele => {
				ele.classList.remove("denied-dragging-animation");
			})

			itemAtPosition.forEach(item=>{
				if (item.place[this.cols] && item.place[this.cols].pinned) {
					let pinnedWidget = document.getElementById(item.widgetid);
					let pinNode = pinnedWidget.querySelector("[pinned='true']");
					if (!pinNode.classList.contains("denied-dragging-animation")) {
						pinNode.classList.add("denied-dragging-animation");
					}
				}	
			})
		},
		checkWidgetSizeLimitAnimation() {

			let draggedItemSetup = this.itemsSetup[this.draggedItem.data.widget];
			let draggedItemMaxWidth = draggedItemSetup.width.max ?? draggedItemSetup.width;
			let draggedItemMinWidth = draggedItemSetup.width.min ?? draggedItemSetup.width;
			let draggedItemMaxHeight = draggedItemSetup.height.max ?? draggedItemSetup.height;
			let draggedItemMinHeight = draggedItemSetup.height.min ?? draggedItemSetup.height;
			let draggedItemNode = document.getElementById(this.draggedItem.data.widgetid);

			let width_after_resize = this.x - this.draggedItem.x + 1; 
			let height_after_resize = this.y - this.draggedItem.y + 1; 
			if(  
				(width_after_resize > 0 && (width_after_resize > draggedItemMaxWidth
				|| width_after_resize < draggedItemMinWidth)
				)
				||
				(height_after_resize > 0 && (height_after_resize > draggedItemMaxHeight
				|| height_after_resize < draggedItemMinHeight)
				)
			){
				draggedItemNode.classList.add("border-danger");
			}else{
				draggedItemNode.classList.remove("border-danger");
			}
		},
		removeWidgetClones(){
			let widgetClones = Array.from(document.getElementsByClassName("widgetClone"));
			for (let i = 0; i < widgetClones.length; i++) {
				this.$refs.container.removeChild(widgetClones[i]);
			}
		},
		mouseDown(){
			this.mode = MODE_MOUSE_DOWN;
		},
		mouseUp() {
			this.mode = MODE_IDLE;
		},
	},
	template: `
	<div
		ref="container"
		class="drop-grid position-relative h-0"
		:style="gridStyle"
		@touchmove="dragOver"
		@touchend="dragCancel"
		@dragover.prevent="dragOver"
		@drop="dragEnd($event)"
		@mousemove="updateCursorOnMouseMove"
		@mouseleave="mouseLeave">
		<TransitionGroup tag="div">
			<grid-item
				ref="gridItems"
				v-for="(item,index) in ((mode != 1 && mode != 2) && active ? placedItems_withPlaceholders : placedItems)"
				:key="item.data.id"
				:item="item"
				@start-move="startMove"
				@mouse-down="mouseDown"
				@mouse-up="mouseUp"
				@start-resize="startResize"
				@dragging="dragging"
				@end-drag="dragEnd"
				@touch-end="dragEnd();mouseUp();"
				@touch-start="updateCursorOnMouseMove($event); mouseDown();"
				class="position-absolute"
				:active="active"
				:style="{
					zIndex: item.resizeOverlay ? 1 : 'auto',
					top: 'calc(' + item.y + ' * var(--fhc-dg-row-height))',
					left: 'calc(' + item.x + ' * var(--fhc-dg-col-width))',
					width: 'calc(' + item.w + ' * var(--fhc-dg-col-width))',
					height: 'calc(' + item.h + ' * var(--fhc-dg-row-height))',
					paddingTop: 'var(--fhc-dg-item-padding-top)',
					paddingLeft: 'var(--fhc-dg-item-padding-horizontal)',
					paddingRight: 'var(--fhc-dg-item-padding-horizontal)'
				}">
				<template v-slot="item">
					<slot v-bind="{...item, ...item.data, index:index}" :x="item.x" :y="item.y" ></slot>
				</template>
			</grid-item>
		</TransitionGroup>
		
	</div>`
}

/*
OLD VERSION - ON HOVER
<div
	v-if="showEmptyTileHover"
	class="position-absolute d-flex justify-content-center align-items-center"
	:style="{
		cursor: 'pointer',
		top: 'calc(' + y + ' * var(--fhc-dg-row-height))',
		left: 'calc(' + x + ' * var(--fhc-dg-col-width))',
		width: 'var(--fhc-dg-col-width)',
		height: 'var(--fhc-dg-row-height)'
	}"
	@click="emptyTileClicked">
	<slot :x="x" :y="y" name="empty-tile-hover"></slot>
</div>
*/