// dragAndDrop.js
// GPLv3

const DND = imports.ui.dnd;
const AppDisplay = imports.ui.appDisplay;
const Clutter = imports.gi.Clutter;
const St = imports.gi.St;
const Main = imports.ui.main;
const Mainloop = imports.mainloop;

const ExtensionUtils = imports.misc.extensionUtils;
const Me = ExtensionUtils.getCurrentExtension();
const Convenience = Me.imports.convenience;
const Extension = Me.imports.extension;

const CHANGE_PAGE_TIMEOUT = 400;

const Gettext = imports.gettext.domain('appfolders-manager');
const _ = Gettext.gettext;

//-------------------------------------------------

var OVERLAY_MANAGER;

/* This method is called by extension.js' enable function. It does code injections
 * to AppDisplay.AppIcon, connecting it to DND-related signals.
 */
function initDND () {
	OVERLAY_MANAGER = new OverlayManager();
}

//--------------------------------------------------------------

/* Amazing! A singleton! It allows easy (and safer?) access to general methods,
 * managing other objects: it creates/updates/deletes all overlays (for folders,
 * pages, creation, removing).
 */
class OverlayManager {
	constructor () {
		this.addActions = [];
		this.removeAction = new FolderActionArea('remove');
		this.createAction = new FolderActionArea('create');
		this.upAction = new NavigationArea('up');
		this.downAction = new NavigationArea('down');
		
		this.next_drag_should_recompute = true;
		this.current_width = 0;
	}

	on_drag_begin () {
		this.ensurePopdowned();
		this.ensureFolderOverlayActors();
		this.updateFoldersVisibility();
		this.updateState(true);
	}

	on_drag_end () {
		// force to compute new positions if a drop occurs
		this.next_drag_should_recompute = true;
		this.updateState(false);
	}

	on_drag_cancelled () {
		this.updateState(false);
	}

	updateArrowVisibility () {
		let grid = Main.overview.viewSelector.appDisplay._views[1].view._grid;
		if (grid.currentPage == 0) {
			this.upAction.setActive(false);
		} else {
			this.upAction.setActive(true);
		}
		if (grid.currentPage == grid._nPages -1) {
			this.downAction.setActive(false);
		} else {
			this.downAction.setActive(true);
		}
		this.upAction.show();
		this.downAction.show();
	}

	updateState (isDragging) {
		if (isDragging) {
			this.removeAction.show();
			if (this.openedFolder == null) {
				this.removeAction.setActive(false);
			} else {
				this.removeAction.setActive(true);
			}
			this.createAction.show();
			this.updateArrowVisibility();
		} else {
			this.hideAll();
		}
	}

	hideAll () {
		this.removeAction.hide();
		this.createAction.hide();
		this.upAction.hide();
		this.downAction.hide();
		this.hideAllFolders();
	}

	hideAllFolders () {
		for (var i = 0; i < this.addActions.length; i++) {
			this.addActions[i].hide();
		}
	}

	updateActorsPositions () {
		let monitor = Main.layoutManager.primaryMonitor;
		this.topOfTheGrid = Main.overview.viewSelector.actor.get_parent().get_parent().get_allocation_box().y1;
		let temp = Main.overview.viewSelector.appDisplay._views[1].view.actor.get_parent();
		let bottomOfTheGrid = this.topOfTheGrid + temp.get_allocation_box().y2;
		
		let _availHeight = bottomOfTheGrid - this.topOfTheGrid;
		let _availWidth = Main.overview.viewSelector.appDisplay._views[1].view._grid.actor.width;
		let sideMargin = (monitor.width - _availWidth) / 2;

		let xMiddle = ( monitor.x + monitor.width ) / 2;
		let yMiddle = ( monitor.y + monitor.height ) / 2;

		// Positions of areas
		this.removeAction.setPosition( xMiddle , bottomOfTheGrid );
		this.createAction.setPosition( xMiddle, Main.overview._panelGhost.height );
		this.upAction.setPosition( 0, Main.overview._panelGhost.height );
		this.downAction.setPosition( 0, bottomOfTheGrid );

		// Sizes of areas
		this.removeAction.setSize(xMiddle, monitor.height - bottomOfTheGrid);
		this.createAction.setSize(xMiddle, this.topOfTheGrid - Main.overview._panelGhost.height);
		this.upAction.setSize(xMiddle, this.topOfTheGrid - Main.overview._panelGhost.height);
		this.downAction.setSize(xMiddle, monitor.height - bottomOfTheGrid);

		this.updateArrowVisibility();
	}

	ensureFolderOverlayActors () {
		// A folder was opened, and just closed.
		if (this.openedFolder != null) {
			this.updateActorsPositions();
			this.computeFolderOverlayActors();
			this.next_drag_should_recompute = true;
			return;
		}

		// The grid "moved" or the whole shit needs forced updating
		let allAppsGrid = Main.overview.viewSelector.appDisplay._views[1].view._grid;
		let new_width = allAppsGrid.actor.allocation.get_width();
		if (new_width != this.current_width || this.next_drag_should_recompute) {
			this.next_drag_should_recompute = false;
			this.updateActorsPositions();
			this.computeFolderOverlayActors();
		}
	}

	computeFolderOverlayActors () {
		let monitor = Main.layoutManager.primaryMonitor;
		let xMiddle = ( monitor.x + monitor.width ) / 2;
		let yMiddle = ( monitor.y + monitor.height ) / 2;
		let allAppsGrid = Main.overview.viewSelector.appDisplay._views[1].view._grid;
		
		let nItems = 0;
		let indexes = [];
		let folders = [];
		let x, y;

		Main.overview.viewSelector.appDisplay._views[1].view._allItems.forEach(function(icon) {
			if (icon.actor.visible) {
				if (icon instanceof AppDisplay.FolderIcon) {
					indexes.push(nItems);
					folders.push(icon);
				}
				nItems++;
			}
		});

		this.current_width = allAppsGrid.actor.allocation.get_width();
		let x_correction = (monitor.width - this.current_width)/2;
		let availHeightPerPage = (allAppsGrid.actor.height)/(allAppsGrid._nPages);
		
		for (var i = 0; i < this.addActions.length; i++) {
			this.addActions[i].actor.destroy();
		}

		for (var i = 0; i < indexes.length; i++) {
			let inPageIndex = indexes[i] % allAppsGrid._childrenPerPage;
			let page = Math.floor(indexes[i] / allAppsGrid._childrenPerPage);
			x = folders[i].actor.get_allocation_box().x1;
			y = folders[i].actor.get_allocation_box().y1;

			// Invalid coords (example: when dragging out of the folder) should
			// not produce a visible overlay, a negative page number is an easy
			// way to be sure it stays hidden.
			if (x == 0) {
				page = -1;
			}
			x = Math.floor(x + x_correction);
			y = y + this.topOfTheGrid;
			y = y - (page * availHeightPerPage);

			this.addActions[i] = new FolderArea(folders[i].id, x, y, page);
		}
	}

	updateFoldersVisibility () {
		let appView = Main.overview.viewSelector.appDisplay._views[1].view;
		for (var i = 0; i < this.addActions.length; i++) {
			if ((this.addActions[i].page == appView._grid.currentPage) && (!appView._currentPopup)) {
				this.addActions[i].show();
			} else {
				this.addActions[i].hide();
			}
		}
	}

	ensurePopdowned () {
		let appView = Main.overview.viewSelector.appDisplay._views[1].view;
		if (appView._currentPopup) {
			this.openedFolder = appView._currentPopup._source.id;
			appView._currentPopup.popdown();
		} else {
			this.openedFolder = null;
		}
	}

	goToPage (nb) {
		Main.overview.viewSelector.appDisplay._views[1].view.goToPage( nb );
		this.updateArrowVisibility();
		this.hideAllFolders();
		this.updateFoldersVisibility(); //load folders of the new page
	}

	destroy () {
		for (let i = 0; i > this.addActions.length; i++) {
			this.addActions[i].destroy();
		}
		this.removeAction.destroy();
		this.createAction.destroy();
		this.upAction.destroy();
		this.downAction.destroy();
		//log('OverlayManager destroyed');
	}
};

//-------------------------------------------------------

// Abstract overlay with very generic methods
class DroppableArea {

	constructor (id) {
		this.id = id;
		this.styleClass = 'folderArea';

		this.actor = new St.BoxLayout ({
			width: 10,
			height: 10,
			visible: false,
		});
		this.actor._delegate = this;

		this.lock = true;
		this.use_frame = Convenience.getSettings('org.gnome.shell.extensions.appfolders-manager').get_boolean('debug');
	}

	setPosition  (x, y) {
		let monitor = Main.layoutManager.primaryMonitor;
		this.actor.set_position(monitor.x + x, monitor.y + y);
	}

	setSize (w, h) {
		this.actor.width = w;
		this.actor.height = h;
	}

	hide () {
		this.actor.visible = false;
		this.lock = true;
	}

	show () {
		this.actor.visible = true;
	}

	setActive (active) {
		this._active = active;
		if (this._active) {
			this.actor.style_class = this.styleClass;
		} else {
			this.actor.style_class = 'insensitiveArea';
		}
	}

	destroy () {
		this.actor.destroy();
	}
}

/* Overlay representing an "action". Actions can be creating a folder, or
 * removing an app from a folder. These areas accept drop, and display a label.
 */
class FolderActionArea extends DroppableArea {
	constructor (id) {
		super(id);

		let x, y, label;

		switch (this.id) {
			case 'create':
				label = _("Create a new folder");
				this.styleClass = 'shadowedAreaTop';
			break;
			case 'remove':
				label = '';
				this.styleClass = 'shadowedAreaBottom';
			break;
			default:
				label = 'invalid id';
			break;
		}
		if (this.use_frame) {
			this.styleClass = 'framedArea';
		}
		this.actor.style_class = this.styleClass;

		this.label = new St.Label({
			text: label,
			style_class: 'dropAreaLabel',
			x_expand: true,
			y_expand: true,
			x_align: Clutter.ActorAlign.CENTER,
			y_align: Clutter.ActorAlign.CENTER,
		});
		this.actor.add(this.label);

		this.setPosition(10, 10);
		Main.layoutManager.overviewGroup.add_actor(this.actor);
	}

	getRemoveLabel () {
		let label;
		if (OVERLAY_MANAGER.openedFolder == null) {
			label = '…';
		} else {
			let folder_schema = Extension.folderSchema (OVERLAY_MANAGER.openedFolder);
			label = folder_schema.get_string('name');
		}
		return (_("Remove from %s")).replace('%s', label);
	}

	setActive (active) {
		super.setActive(active);
		if (this.id == 'remove') {
			this.label.text = this.getRemoveLabel();
		}
	}

	handleDragOver (source, actor, x, y, time) {
		if (source instanceof AppDisplay.AppIcon && this._active) {
			return DND.DragMotionResult.MOVE_DROP;
		}
		Main.overview.endItemDrag(this);
		return DND.DragMotionResult.NO_DROP;
	}

	acceptDrop (source, actor, x, y, time) {
		if ((source instanceof AppDisplay.AppIcon) && (this.id == 'create')) {
			Extension.createNewFolder(source);
			Main.overview.endItemDrag(this);
			return true;
		}
		if ((source instanceof AppDisplay.AppIcon) && (this.id == 'remove')) {
			this.removeApp(source);
			Main.overview.endItemDrag(this);
			return true;
		}
		Main.overview.endItemDrag(this);
		return false;
	}

	removeApp (source) {
		let id = source.app.get_id();
		Extension.removeFromFolder(id, OVERLAY_MANAGER.openedFolder);
		OVERLAY_MANAGER.updateState(false);
		Main.overview.viewSelector.appDisplay._views[1].view._redisplay();
	}

	destroy () {
		this.label.destroy();
		super.destroy();
	}
};

/* Overlay reacting to hover, but isn't droppable. The goal is to go to an other
 * page of the grid while dragging an app.
 */
class NavigationArea extends DroppableArea {
	constructor (id) {
		super(id);

		let x, y, i;
		switch (this.id) {
			case 'up':
				i = 'pan-up-symbolic';
				this.styleClass = 'shadowedAreaTop';
			break;
			case 'down':
				i = 'pan-down-symbolic';
				this.styleClass = 'shadowedAreaBottom';
			break;
			default:
				i = 'dialog-error-symbolic';
			break;
		}
		if (this.use_frame) {
			this.styleClass = 'framedArea';
		}
		this.actor.style_class = this.styleClass;

		this.actor.add(new St.Icon({
			icon_name: i,
			icon_size: 24,
			style_class: 'system-status-icon',
			x_expand: true,
			y_expand: true,
			x_align: Clutter.ActorAlign.CENTER,
			y_align: Clutter.ActorAlign.CENTER,
		}));

		this.setPosition(x, y);
		Main.layoutManager.overviewGroup.add_actor(this.actor);
	}

	handleDragOver (source, actor, x, y, time) {
		if (this.id == 'up' && this._active) {
			this.pageUp();
			return DND.DragMotionResult.CONTINUE;
		}

		if (this.id == 'down' && this._active) {
			this.pageDown();
			return DND.DragMotionResult.CONTINUE;
		}

		Main.overview.endItemDrag(this);
		return DND.DragMotionResult.NO_DROP;
	}

	pageUp () {
		if(this.lock && !this.timeoutSet) {
			this._timeoutId = Mainloop.timeout_add(CHANGE_PAGE_TIMEOUT, this.unlock.bind(this));
			this.timeoutSet = true;
		}
		if(!this.lock) {
			let currentPage = Main.overview.viewSelector.appDisplay._views[1].view._grid.currentPage;
			this.lock = true;
			OVERLAY_MANAGER.goToPage(currentPage - 1);
		}
	}

	pageDown () {
		if(this.lock && !this.timeoutSet) {
			this._timeoutId = Mainloop.timeout_add(CHANGE_PAGE_TIMEOUT, this.unlock.bind(this));
			this.timeoutSet = true;
		}
		if(!this.lock) {
			let currentPage = Main.overview.viewSelector.appDisplay._views[1].view._grid.currentPage;
			this.lock = true;
			OVERLAY_MANAGER.goToPage(currentPage + 1);
		}
	}

	acceptDrop (source, actor, x, y, time) {
		Main.overview.endItemDrag(this);
		return false;
	}

	unlock () {
		this.lock = false;
		this.timeoutSet = false;
		Mainloop.source_remove(this._timeoutId);
	}
};

/* This overlay is the area upon a folder. Position and visibility of the actor
 * is handled by exterior functions.
 * "this.id" is the folder's id, a string, as written in the gsettings key.
 * Dropping an app on this folder will add it to the folder
 */
class FolderArea extends DroppableArea {
	constructor (id, asked_x, asked_y, page) {
		super(id);
		this.page = page;

		let grid = Main.overview.viewSelector.appDisplay._views[1].view._grid;
		this.actor.width = grid._getHItemSize();
		this.actor.height = grid._getVItemSize();

		if (this.use_frame) {
			this.styleClass = 'framedArea';
			this.actor.add(new St.Label({
				text: this.id,
				x_expand: true,
				y_expand: true,
				x_align: Clutter.ActorAlign.CENTER,
				y_align: Clutter.ActorAlign.CENTER,
			}));
		} else {
			this.styleClass = 'folderArea';
			this.actor.add(new St.Icon({
				icon_name: 'list-add-symbolic',
				icon_size: 24,
				style_class: 'system-status-icon',
				x_expand: true,
				y_expand: true,
				x_align: Clutter.ActorAlign.CENTER,
				y_align: Clutter.ActorAlign.CENTER,
			}));
		}
		if (this.use_frame) {
			this.styleClass = 'framedArea';
		}
		this.actor.style_class = this.styleClass;

		this.setPosition(asked_x, asked_y);
		Main.layoutManager.overviewGroup.add_actor(this.actor);
	}

	handleDragOver (source, actor, x, y, time) {
		if (source instanceof AppDisplay.AppIcon) {
			return DND.DragMotionResult.MOVE_DROP;
		}
		Main.overview.endItemDrag(this);
		return DND.DragMotionResult.NO_DROP;
	}

	acceptDrop (source, actor, x, y, time) { //FIXME recharger la vue ou au minimum les miniatures des dossiers
		if ((source instanceof AppDisplay.AppIcon) &&
		                            !Extension.isInFolder(source.id, this.id)) {
			Extension.addToFolder(source, this.id);
			Main.overview.endItemDrag(this);
			return true;
		}
		Main.overview.endItemDrag(this);
		return false;
	}
};