// TODO(chris): Comments

const DIR_UP = 0;
const DIR_LEFT = 1;
const DIR_RIGHT = 2;
const DIR_DOWN = 3;

class GridLogic {
	constructor(w) {
		if (w.w) {
			this.w = w.w;
			this.h = w.h;
			this.data = [...w.data];
			this.grid = [...w.grid];
		} else {
			this.w = w;
			this.h = 1;
			this.data = [];
			this.grid = [];
		}
	}
	isFreeSlot(x, y) {
		const i = y*this.w + x;
		return !this.grid[i] && this.grid[i] !== 0;
	}
	getMaxY(){
		return this.data.reduce((acc, item) => { 
			if (item?.y > acc) {
				 acc = item.y; 
			} 
			return acc; 
		}, 0);
	}
	getFreeSlots() {
		const freeSlots = [];
		let biggestY = this.getMaxY();
		let totalSpaces = this.w * (biggestY+1);
		for(let i=0; i < totalSpaces; i++){
			if (!this.grid[i] && this.grid[i] !== 0){
				this.grid[i] = undefined;
			}
		}
		for(let i =0; i < this.grid.length; i++){
			if (!this.grid[i] && this.grid[i] !== 0){
				let x = i % this.w;
				let y = Math.floor(i / this.w);
				freeSlots.push({x, y});
			}
		}
		return freeSlots;
	}
	add(item, prefer) {
		let occupiers = this.getItemsInFrame(item.frame);
		if (!occupiers.length) {
			item.frame.forEach(f => this.grid[f] = item.index);
			this.data[item.index] = item;
			this.h = Math.max(this.h, item.y + item.h);
			return [];
		} else {
			const intermGrid = new GridLogic(this);
			
			item.frame.forEach(f => intermGrid.grid[f] = -1);

			const possiblities = intermGrid.tryMoving(occupiers, prefer);
			if (possiblities.length) {
				const bestOption = possiblities.sort((a,b) => {
					if (a.cost === undefined)
						a.cost = a.moves.reduce((a, v) => a * v.cost, 1);
					if (b.cost === undefined)
						b.cost = b.moves.reduce((a, v) => a * v.cost, 1);
					return a.cost > b.cost;
				});
				const result = [];
				bestOption[0].moves.forEach(move => {
					const currItem = {...this.data[move.index]};
					currItem.x += move.x;
					currItem.y += move.y;
					currItem.frame = this.getItemFrame(currItem);
					this.h = Math.max(this.h, currItem.y + currItem.h);
					this.remove(currItem);
					currItem.frame.forEach(f => this.grid[f] = currItem.index);
					this.data[move.index] = currItem;
					result[move.index] = {
						index: currItem.index,
						x: currItem.x,
						y: currItem.y
					};
				});
				item.frame.forEach(f => this.grid[f] = item.index);
				this.data[item.index] = item;

				return result;
			} else {
				console.error('FATAL', "can't arrange item on grid");
			}
		}
	}
	move(item, x, y) {
		if (item.data.place[this.w]?.pinned)
			return [];
		if (item.x == x && item.y == y)
			return [];
		
		this.remove(item);

		let prefer = undefined;
		if (item.x == x) {
			if (y-item.y > 0)
				prefer = DIR_UP;
			else
				prefer = DIR_DOWN;
		} else if (item.y == y) {
			if (x-item.x > 0)
				prefer = DIR_LEFT;
			else
				prefer = DIR_RIGHT;
		}

		const originalFrame = Array.isArray(item.frame) ? [...item.frame] : [item.frame];

		const currItem = {...item};
		currItem.x = x;
		currItem.y = y;
		currItem.frame = this.getItemFrame(currItem);
		let occupiers = this.getItemsInFrame(currItem.frame);
		
		// does not update if the target conatins pinned widgets
		if (occupiers.some(frame => this.data[frame]?.data.place[this.w]?.pinned)) {
			return [];
		}
		
		// checks if target contains widget with the same high and width
		let occupiersData = occupiers.map(occupier => this.data[occupier]);
		let occupiersFrame = occupiersData.map(occupier => occupier.frame).flat();
		if (!occupiersFrame.some(frame => !currItem.frame.includes(frame)) && !occupiersFrame.some(frame => originalFrame.includes(frame))){
			let replaceUpdate = [];
			let newOccupierFrames = [];
			for(let f of originalFrame){
				if(newOccupierFrames.includes(f)){
					continue;
				}
				let occ = occupiersData.shift();
				if(occ){
					newOccupierFrames = [...newOccupierFrames, ...this.getItemFrame({ ...occ, ...this.getSingleFramePosition(f) })];
					replaceUpdate[occ.index] = { index: occ.index, ...this.getSingleFramePosition(f)}
				}
			}
			replaceUpdate[item.index] = { index: item.index, x, y };
			
			return replaceUpdate;
		}
		
		const updates = this.add(currItem, prefer);
		updates[item.index] = {index: item.index, x, y};
		return updates;
	}
	resize(item, w, h) {
		if (item.w == w && item.h == h)
			return [];
		this.remove(item);

		const currItem = {...item};
		currItem.w = w;
		currItem.h = h;
		currItem.frame = this.getItemFrame(currItem);
		
		const updates = this.add(currItem);
		if(updates)
			updates[item.index] = {index: item.index, w, h, x:item.x, y:item.y, resize:true};

		return updates;
	}
	tryMoving(index, prefer) {
		if (Array.isArray(index)) {
			index.forEach(i => this.remove({index:i}));
			let possiblities = [{grid: this, moves: []}];
			index.forEach(i => {
				let newPoss = [];
				possiblities.forEach(possiblity => {
					possiblity.grid.tryMoving(i, prefer).forEach(p => {
						possiblity.moves
						p.moves = [...p.moves, ...possiblity.moves];
						newPoss.push(p)
					});
				});
				possiblities = newPoss;
			});
			return possiblities;
		}
		const directions = [DIR_UP, DIR_DOWN, DIR_LEFT, DIR_RIGHT];

		this.remove({index});

		const weight = 1 + .2*(this.data[index].weight || 0);

		return directions.reduce((result, dir) => {
			let res = this.tryMovingInDirection(dir, index, 1, (prefer === dir ? .5 : 1 + dir*.1) * weight);
			if (!res)
				return result;
			return [...result, ...res];
		}, []).filter(p => p);
	}
	tryMovingInDirection(dir, index, amount, cost) {
		const move = {index, x:0, y: 0, cost: cost};
		let targetframe;
		switch(dir) {
			case DIR_UP:
				if (this.data[index].data?.place[this.w]?.pinned || this.data[index].y - amount < 0)
					return false;
				targetframe = this.data[index].frame.map(i => i-this.w*amount);
				move.y = -amount;
				break;
			case DIR_DOWN:
				if (this.data[index].data?.place[this.w]?.pinned)
					return false;
				if (this.data[index].y + this.data[index].h + amount > this.h)
					cost += .4;
				targetframe = this.data[index].frame.map(i => i+this.w*amount);
				move.y = amount;
				break;
			case DIR_LEFT:
				if (this.data[index].data?.place[this.w]?.pinned || this.data[index].x - amount < 0)
					return false;
				targetframe = this.data[index].frame.map(i => i-amount);
				move.x = -amount;
				break;
			case DIR_RIGHT:
				if (this.data[index].data?.place[this.w]?.pinned || this.data[index].x + this.data[index].w + amount > this.w)
					return false;
				targetframe = this.data[index].frame.map(i => i+amount);
				move.x = amount;
				break;
		}

		const occupiers = this.getItemsInFrame(targetframe);
		if (occupiers.includes(-1)) {
			return this.tryMovingInDirection(dir, index, amount+1, cost);
		}
		
		const intermGrid = new GridLogic(this);
		targetframe.forEach(f => intermGrid.grid[f] = -1);
		
		if (!occupiers.length) {
			return [{grid: intermGrid, moves: [move]}];
		}
		const possiblities = intermGrid.tryMoving(occupiers).map(possiblity => possiblity.moves.unshift(move) && possiblity);
		return possiblities.length ? possiblities : false;
	}
	clearWeights() {
		this.data.forEach(item => item.weight = undefined);
	}
	getItemsInFrame(frame) {
		return frame.map(i => this.grid[i]).filter((v,i,a) => (v || v === 0) && a.indexOf(v) === i);
	}
	remove(item) {
		this.grid = this.grid.map(i => i != item.index ? i : undefined);
	}
	getItemFrame(item) {
		const frame = [];
		for (let i = 0; i < item.w; i++)
			for (let j = 0; j < item.h; j++)
				frame.push(i + item.x + (j + item.y) * this.w);
		return frame;
	}
	getSingleFramePosition(frame){
		return { x: frame % this.w, y: Math.floor(frame / this.w)};
	}
	debug() {
		return this.grid;
	}
}

export default GridLogic;