import Atk from "gi://Atk";
import Clutter from "gi://Clutter";
import Meta from "gi://Meta";
import St from "gi://St";

import Gio from 'gi://Gio';
import GObject from 'gi://GObject';
import GLib from 'gi://GLib';
import Shell from 'gi://Shell';

import * as Main from 'resource:///org/gnome/shell/ui/main.js';
import * as AltTab from 'resource:///org/gnome/shell/ui/altTab.js';
import * as SwitcherPopup from 'resource:///org/gnome/shell/ui/switcherPopup.js';

import { Extension } from 'resource:///org/gnome/shell/extensions/extension.js';

import * as Utils from './utils.js';


const baseIconSizes = [96, 64, 48, 32, 22];


let injections = {};
let extension = null;

function getWindows(workspace) {
	// We ignore skip-taskbar windows in switchers, but if they are attached
	// to their parent, their position in the MRU list may be more appropriate
	// than the parent; so start with the complete list ...
	let windows = global.display.get_tab_list(Meta.TabList.NORMAL_ALL, workspace);
	// ... map windows to their parent where appropriate ...
	return windows.map(w => {
		return w.is_attached_dialog() ? w.get_transient_for() : w;
	// ... and filter out skip-taskbar windows and duplicates
	}).filter((w, i, a) => !w.skip_taskbar && a.indexOf(w) === i);
}

// https://gitlab.gnome.org/GNOME/gnome-shell/-/blob/master/js/ui/altTab.js#L272
function _finish(timestamp) {
	this._currentWindow = this._currentWindow < 0 ? 0 : this._currentWindow;
	return injections._finish.call(this, timestamp);
}

// https://gitlab.gnome.org/GNOME/gnome-shell/commit/092e1a691d57a3be205100f7d3910534d3c59f84
function _initialSelection(backward, binding) {
	if (backward || binding != 'switch-applications'
			|| this._items.length == 0  || this._items[0].cachedWindows.length < 2) {
		injections._initialSelection.call(this, backward, binding);
		return;
	}

	let ws = global.workspace_manager.get_active_workspace();
	let wt = Shell.WindowTracker.get_default();
	let tab_list = global.display.get_tab_list(Meta.TabList.NORMAL, ws);

	let currentApp = wt.get_window_app(tab_list[0]);
	let secondApp = wt.get_window_app(tab_list[1]);

	if (currentApp == secondApp) {
		this._select(0, 1);
	} else {
		injections._initialSelection.call(this, backward, binding);
	}
}

// from SwitcherPopup.SwitcherList
function highlight2(index, justOutline) {
	if (this._items[this._highlighted]) {
		this._items[this._highlighted].remove_style_pseudo_class('outlined');
		this._items[this._highlighted].remove_style_pseudo_class('selected');
	}

	if (this._items[index]) {
		if (justOutline)
			this._items[index].add_style_pseudo_class('outlined');
		else
			this._items[index].add_style_pseudo_class('selected');
	}

	this._highlighted = index;

	let adjustment = this._scrollView.hscroll.adjustment;
	let [value] = adjustment.get_values();
	let [absItemX] = this._items[index].get_transformed_position();
	let [result_, posX, posY_] = this.transform_stage_point(absItemX, 0);
	let [containerWidth] = this.get_transformed_size();
	this._scroll(index);
}

function _scroll(index) {
	let adjustment = this._scrollView.hscroll.adjustment;
	let [value, lower_, upper, stepIncrement_, pageIncrement_, pageSize] = adjustment.get_values();

	let n = this._items.length;
	let fakeSize = 2;

	this._scrollableRight = index !== n - 1;
	this._scrollableLeft = index !== 0;
	if (upper === pageSize)
		return;

	let item = this._items[index];
	let sizeItem = (item.allocation.x2 - item.allocation.x1);
	value = (upper - pageSize + sizeItem) * (index / n);
	let maxScrollingAmount = (upper - pageSize);
	let percentaje = (index-fakeSize) / (n - 1 - 2*fakeSize);
	value = percentaje * maxScrollingAmount;

	// special cases
	if (index < fakeSize || percentaje <= 0) {
		this._scrollableLeft = false;
		value = 0;
	} else if (index >= n - fakeSize || percentaje >= 1) {
		this._scrollableRight = false;
		value = maxScrollingAmount;
	}

	adjustment.ease(value, {
		progress_mode: Clutter.AnimationMode.EASE_OUT_EXPO,
		duration: 250, // POPUP_SCROLL_TIME,
		onComplete: () => {
			this.queue_relayout();
		},
	});
}

function _setIconSize() {
	this._iconSize = 96 * 1.5;

	for (let i = 0; i < this.icons.length; i++) {
		if (this.icons[i].icon != null)
			break;
		this.icons[i].set_size(this._iconSize);
	}
}

function addColours() {
	injections.WINDOW_PREVIEW_SIZE = AltTab.WINDOW_PREVIEW_SIZE;
	// AltTab.WINDOW_PREVIEW_SIZE = 256;

	// injections._setIconSize = AltTab.AppSwitcher.prototype._setIconSize;
	// AltTab.AppSwitcher.prototype._setIconSize = _setIconSize;

	// injections.highlight = AltTab.AppSwitcher.prototype.highlight;
	// AltTab.AppSwitcher.prototype.highlight = highlight;

	// injections._addIcon = AltTab.AppSwitcher.prototype._addIcon;
	// AltTab.AppSwitcher.prototype._addIcon = _addIcon;
	injections._init = AltTab.AppSwitcherPopup.prototype._init;
	AltTab.AppSwitcherPopup.prototype._init = _init;

	// injections.POPUP_SCROLL_TIME = SwitcherPopup.POPUP_SCROLL_TIME;
	// SwitcherPopup.POPUP_SCROLL_TIME = 250;

	injections.highlight2 = SwitcherPopup.SwitcherList.prototype.highlight;
	SwitcherPopup.SwitcherList.prototype.highlight = highlight2;

	injections._scroll = SwitcherPopup.SwitcherList.prototype._scroll;
	SwitcherPopup.SwitcherList.prototype._scroll = _scroll;
}

function removeColours() {
	// AltTab.WINDOW_PREVIEW_SIZE = injections.WINDOW_PREVIEW_SIZE;

	// AltTab.AppSwitcher.prototype._setIconSize = injections._setIconSize;

	// AltTab.AppSwitcher.prototype.highlight = injections.highlight;

	// AltTab.AppSwitcher.prototype._addIcon = injections._addIcon;
	AltTab.AppSwitcherPopup.prototype._init = injections._init;


	// SwitcherPopup.POPUP_SCROLL_TIME = injections.POPUP_SCROLL_TIME;
	// injections.POPUP_SCROLL_TIME = undefined;

	SwitcherPopup.SwitcherList.prototype.highlight = injections.highlight2;
	injections.highlight2 = undefined;

	SwitcherPopup.SwitcherList.prototype._scroll = injections._scroll; // undefined
	injections._scroll = undefined;
}

function setInitialSelection(argument) {
	if (!injections._finish) {		
		injections._finish = AltTab.AppSwitcherPopup.prototype._finish;
		AltTab.AppSwitcherPopup.prototype._finish = _finish;
	}

	if (!injections._initialSelection) {		
		injections._initialSelection = AltTab.AppSwitcherPopup.prototype._initialSelection;
		AltTab.AppSwitcherPopup.prototype._initialSelection = _initialSelection;
	}
}

function resetInitialSelection(argument) {
	if (injections._finish) {
		AltTab.AppSwitcherPopup.prototype._finish = injections._finish;
		injections._finish = undefined;
	}

	if (injections._initialSelection) {
		AltTab.AppSwitcherPopup.prototype._initialSelection = injections._initialSelection;
		injections._initialSelection = undefined;
	}
}

class MyExtension {
	constructor(settings) {
		this._settings = settings;

		this._connectSettings();

		this._firstChangeWindowChanged();
	}

	_connectSettings() {
		this._settingsHandlerFirstSwitch = this._settings.connect(
			'changed::first-change-window',
			this._firstChangeWindowChanged.bind(this)
		);
	}

	_firstChangeWindowChanged() {
		this._firstChangeWindow = this._settings.get_boolean('first-change-window');
		if (this._firstChangeWindow) {
			setInitialSelection();
		} else {
			resetInitialSelection();
		}
	}

	destroy() {
		this._disconnectSettings();
		resetInitialSelection();
	}

	_disconnectSettings() {
		this._settings.disconnect(this._settingsHandlerFirstSwitch);
	}
}

export default class UnityLikeAppSwitcherExtension extends Extension {
	enable() {
		const settings = this.getSettings();
		extension = new MyExtension(settings);

		addColours();
	}

	disable() {
		removeColours();

		extension.destroy();
		extension = null;
	}
}

function _init() {
	SwitcherPopup.SwitcherPopup.prototype._init.call(this);

	this._thumbnails = null;
	this._thumbnailTimeoutId = 0;
	this._currentWindow = -1;

	this.thumbnailsVisible = false;

	let apps = Shell.AppSystem.get_default().get_running();

	this._switcherList = new AppSwitcher(apps, this);
	this._items = this._switcherList.icons;
}

const AppSwitcher = GObject.registerClass(
class AppSwitcher extends SwitcherPopup.SwitcherList {
	_init(apps, altTabPopup) {
		super._init(true);

		this.icons = [];
		this._arrows = [];

		let windowTracker = Shell.WindowTracker.get_default();
		let settings = new Gio.Settings({schema_id: 'org.gnome.shell.app-switcher'});

		let workspace = null;
		if (settings.get_boolean('current-workspace-only')) {
			let workspaceManager = global.workspace_manager;

			workspace = workspaceManager.get_active_workspace();
		}

		let allWindows = getWindows(workspace);

		// Construct the AppIcons, add to the popup
		for (let i = 0; i < apps.length; i++) {
			let appIcon = new AltTab.AppIcon(apps[i]);
			// Cache the window list now; we don't handle dynamic changes here,
			// and we don't want to be continually retrieving it
			appIcon.cachedWindows = allWindows.filter(
				w => windowTracker.get_window_app(w) === appIcon.app);
			if (appIcon.cachedWindows.length > 0)
				this._addIcon(appIcon);
		}

		this._altTabPopup = altTabPopup;
		this._delayedHighlighted = -1;
		this._mouseTimeOutId = 0;

		this.connect('destroy', this._onDestroy.bind(this));
	}

	_onDestroy() {
		if (this._mouseTimeOutId !== 0)
			GLib.source_remove(this._mouseTimeOutId);

		this.icons.forEach(
			icon => icon.app.disconnectObject(this));
	}

	_setIconSize() {
		let j = 0;
		while (this._items.length > 1 && this._items[j].style_class !== 'item-box')
			j++;

		let themeNode = this._items[j].get_theme_node();
		this._list.ensure_style();

		let iconPadding = themeNode.get_horizontal_padding();
		let iconBorder = themeNode.get_border_width(St.Side.LEFT) + themeNode.get_border_width(St.Side.RIGHT);
		let [, labelNaturalHeight] = this.icons[j].label.get_preferred_height(-1);
		let iconSpacing = labelNaturalHeight + iconPadding + iconBorder;
		let totalSpacing = this._list.spacing * (this._items.length - 1);

		// We just assume the whole screen here due to weirdness happening with the passed width
		let primary = Main.layoutManager.primaryMonitor;
		let parentPadding = this.get_parent().get_theme_node().get_horizontal_padding();
		let availWidth = primary.width - parentPadding - this.get_theme_node().get_horizontal_padding();

		let scaleFactor = St.ThemeContext.get_for_stage(global.stage).scale_factor;
		let iconSizes = baseIconSizes.map(s => s * scaleFactor);
		let iconSize = baseIconSizes[0];

		if (this._items.length > 1) {
			for (let i =  0; i < baseIconSizes.length; i++) {
				iconSize = baseIconSizes[i];
				let height = iconSizes[i] + iconSpacing;
				let w = height * this._items.length + totalSpacing;
				if (w <= availWidth)
					break;
			}
		}

		this._iconSize = iconSize;

		for (let i = 0; i < this.icons.length; i++) {
			if (this.icons[i].icon != null)
				break;
			this.icons[i].set_size(iconSize);
		}
	}

	vfunc_get_preferred_height(forWidth) {
		if (!this._iconSize)
			this._setIconSize();

		return super.vfunc_get_preferred_height(forWidth);
	}

	vfunc_allocate(box) {
		// Allocate the main list items
		super.vfunc_allocate(box);

		let contentBox = this.get_theme_node().get_content_box(box);

		let arrowHeight = Math.floor(this.get_theme_node().get_padding(St.Side.BOTTOM) / 3);
		let arrowWidth = arrowHeight * 2;

		// Now allocate each arrow underneath its item
		let childBox = new Clutter.ActorBox();
		for (let i = 0; i < this._items.length; i++) {
			let itemBox = this._items[i].allocation;
			childBox.x1 = contentBox.x1 + Math.floor(itemBox.x1 + (itemBox.x2 - itemBox.x1 - arrowWidth) / 2);
			childBox.x2 = childBox.x1 + arrowWidth;
			childBox.y1 = contentBox.y1 + itemBox.y2 + arrowHeight;
			childBox.y2 = childBox.y1 + arrowHeight;
			this._arrows[i].allocate(childBox);
		}
	}

	// We override SwitcherList's _onItemMotion method to delay
	// activation when the thumbnail list is open
	_onItemMotion(item) {
		if (item === this._items[this._highlighted] ||
			item === this._items[this._delayedHighlighted])
			return Clutter.EVENT_PROPAGATE;

		const index = this._items.indexOf(item);

		if (this._mouseTimeOutId !== 0) {
			GLib.source_remove(this._mouseTimeOutId);
			this._delayedHighlighted = -1;
			this._mouseTimeOutId = 0;
		}

		if (this._altTabPopup.thumbnailsVisible) {
			this._delayedHighlighted = index;
			this._mouseTimeOutId = GLib.timeout_add(
				GLib.PRIORITY_DEFAULT,
				APP_ICON_HOVER_TIMEOUT,
				() => {
					this._enterItem(index);
					this._delayedHighlighted = -1;
					this._mouseTimeOutId = 0;
					return GLib.SOURCE_REMOVE;
				});
			GLib.Source.set_name_by_id(this._mouseTimeOutId, '[gnome-shell] this._enterItem');
		} else {
			this._itemEntered(index);
		}

		return Clutter.EVENT_PROPAGATE;
	}

	_enterItem(index) {
		let [x, y] = global.get_pointer();
		let pickedActor = global.stage.get_actor_at_pos(Clutter.PickMode.ALL, x, y);
		if (this._items[index].contains(pickedActor))
			this._itemEntered(index);
	}

	// We override SwitcherList's highlight() method to also deal with
	// the AppSwitcher->ThumbnailSwitcher arrows. Apps with only 1 window
	// will hide their arrows by default, but show them when their
	// thumbnails are visible (ie, when the app icon is supposed to be
	// in justOutline mode). Apps with multiple windows will normally
	// show a dim arrow, but show a bright arrow when they are
	// highlighted.
	highlight(n, justOutline) {
		if (this.icons[this._highlighted]) {
			if (this.icons[this._highlighted].cachedWindows.length === 1)
				this._arrows[this._highlighted].hide();
			else
				this._arrows[this._highlighted].remove_style_pseudo_class('highlighted');
		}

		// mein
		let previous = this._items[this._highlighted];
		if (previous) {
			let st = previous.get_style();
			previous.set_style(st.substr(0, st.indexOf(';')+1));
		}

		let item = this._items[n];
		if (item) {
			item.set_style(item.get_style() + 'box-shadow: inset 0 0 10px '+ item.colorPalette.lighter + '; border: 2px solid '+ item.colorPalette.lighter + ';');
		}
		// end mein
		// super.highlight(n, justOutline);
		highlight2.call(this, n, justOutline);
		this._curApp = n;

		if (this._highlighted !== -1) {
			if (justOutline && this.icons[this._highlighted].cachedWindows.length === 1)
				this._arrows[this._highlighted].show();
			else
				this._arrows[this._highlighted].add_style_pseudo_class('highlighted');
		}
	}

	_addIcon(appIcon) {
		this.icons.push(appIcon);
		let item = this.addItem(appIcon, appIcon.label);

		item.colorPalette = new Utils.DominantColorExtractor(appIcon.app)._getColorPalette();
		if (item.colorPalette == null) {
			item.colorPalette = {
				original: '#888888',
				lighter: '#ffffff',
				darker: '#000000'
			}
		}
		let hex = item.colorPalette.original;
		let rgb = Utils.ColorUtils._hexToRgb(hex);
		item.set_style('background: rgba('+ rgb.r + ',' + rgb.g + ',' + rgb.b + ', 0.3);');
		// item.set_style('background: '+ item.colorPalette.darker);

		appIcon.app.connectObject('notify::state', app => {
			if (app.state !== Shell.AppState.RUNNING)
				this._removeIcon(app);
		}, this);

		let arrow = new St.DrawingArea({style_class: 'switcher-arrow'});
		arrow.connect('repaint', () => SwitcherPopup.drawArrow(arrow, St.Side.BOTTOM));
		this.add_child(arrow);
		this._arrows.push(arrow);

		if (appIcon.cachedWindows.length === 1)
			arrow.hide();
		else
			item.add_accessible_state(Atk.StateType.EXPANDABLE);
	}

	_removeIcon(app) {
		let index = this.icons.findIndex(icon => {
			return icon.app === app;
		});
		if (index === -1)
			return;

		this._arrows[index].destroy();
		this._arrows.splice(index, 1);

		this.icons.splice(index, 1);
		this.removeItem(index);
	}
});