/*
 * Copyright 2016-2019 (C) Raster Software Vigo (Sergio Costas)
 *
 * This file is part of Terminus
 *
 * Terminus is free software; you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation; either version 3 of the License.
 *
 * Terminus is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with this program.  If not, see <http://www.gnu.org/licenses/>.
 */

import GLib from 'gi://GLib';
import Meta from 'gi://Meta';
import Gio from 'gi://Gio';
import Shell from 'gi://Shell';
import * as Main from 'resource:///org/gnome/shell/ui/main.js';

export default class TerminusClass {

	constructor() {
		this._enabled = false;
		this._settings = new Gio.Settings({ schema: 'org.rastersoft.terminus.keybindings' });
		this._settings2 = new Gio.Settings({ schema: 'org.rastersoft.terminus' });
		this._settingsChanged("guake-mode"); // copy the guake-mode key to guake-mode-gnome-shell key
		this.terminusInstance = null;
		this._shown_error = false;
		this._currentProcess = null;
	}

	_launch_process() {
		if (this._currentProcess !== null) {
			return;
		}
		let argv = [];
		argv.push("terminus");
		argv.push("--no-window");
		argv.push("--nobindkey");

		this._currentProcess = new LaunchSubprocess(0, "TERMINUS");
		this._currentProcess.spawnv(argv);
		this._currentProcess.subprocess.wait_async(null, () => {
			this._reloadTime = 1000;
			if (this._currentProcess.subprocess.get_if_exited()) {
				let retVal = this._currentProcess.subprocess.get_exit_status();
				if (retVal == 1) {
					if (!this._shown_error) {
						// show it only once
						Main.notify("Can't launch Terminus", "There is already an instance of Terminus running. You must kill all of them to allow Terminus guake mode to work.");
					}
					this._shown_error = true;
					this._reloadTime = 1000;
				} else {
					this._shown_error = false;
				}
			} else {
				this._shown_error = false;
			}
			this._desktopWindow = null;
			this._currentProcess = null;
			if (this._launchDesktopId) {
				GLib.source_remove(this._launchDesktopId);
			}
			if (this._enabled === false) {
				return;
			}
			this._launchProcessId = GLib.timeout_add(GLib.PRIORITY_DEFAULT, this._reloadTime, () => {
				this._launchProcessId = 0;
				this._launch_process();
			});
		});
	}

	/**
	 * Enables the extension
	 */
	enable() {
		if (this._enabled) {
			return;
		}
		// If the desktop is still starting up, we wait until it is ready
		if (Main.layoutManager._startingUp) {
			this._startupPreparedId = Main.layoutManager.connect('startup-complete', () => {
				this._innerEnable();
			});
		} else {
			this._innerEnable();
		}
	}

	_innerEnable() {
		this._enabled = true;
		this._launch_process();
		if (this._startupPreparedId) {
			Main.layoutManager.disconnect(this._startupPreparedId);
			this._startupPreparedId = null;
		}
		this._settingsChangedConnect = this._settings.connect('changed', (st, name) => {
			this._settingsChanged(name);
		});
		let mode = Shell.ActionMode ? Shell.ActionMode.NORMAL : Shell.KeyBindingMode.ALL;
		let flags = Meta.KeyBindingFlags.NONE;
		Main.wm.addKeybinding(
			"guake-mode-gnome-shell",
			this._settings,
			flags,
			mode,
			() => {
				if (this.terminusInstance === null) {
					this.terminusInstance = Gio.DBusActionGroup.get(
						Gio.DBus.session,
						'com.rastersoft.terminus',
						'/com/rastersoft/terminus'
					);
				}
				this.terminusInstance.activate_action('swap-guake', null);
			}
		);
		this._idMap = global.window_manager.connect_after('map', (obj, windowActor) => {
			if (!this._currentProcess) {
				return false;
			}
			let window = windowActor.get_meta_window();
			if (window.get_title() !== 'TerminusGuake') {
				return;
			}
			let belongs;
			try {
				belongs = this._currentProcess.query_window_belongs_to(window);
			} catch (err) {
				belongs = false;
			}
			if (belongs) {
				// This is the Guake Terminal window, so ensure that it is kept above and shown in all workspaces
				window.make_above();
				window.stick();
				this._set_window_position(window);
				this._set_window_size(window);
				window.focus(0);
				window.connect('position-changed', () => {
					this._set_window_position(window);
				});
				window.connect('size-changed', () => {
					this._set_window_size(window);
				});
			}
		});
	}

	_set_window_position(window) {
		let area = window.get_work_area_current_monitor();
		window.move_frame(false, area.x, area.y);
	}

	_set_window_size(window) {
		let area = window.get_work_area_current_monitor();
		let rectangle = window.get_frame_rect();
		let margin = 48;
		if (rectangle.height >= (area.height - margin)) {
			window.move_resize_frame(true, area.x, area.y, area.width, area.height - margin);
		}
	}

	disable() {
		if (!this._enabled) {
			return;
		}
		this._enabled = false;
		if (this._settingsChangedConnect) {
			this._settings.disconnect(this._settingsChangedConnect);
		}
		if (this._idMap) {
			global.window_manager.disconnect(this._idMap);
		}
		Main.wm.removeKeybinding("guake-mode-gnome-shell");
	}

	_settingsChanged(name) {
		if (name == "guake-mode") {
			var new_key = this._settings.get_string("guake-mode");
			this._settings.set_strv("guake-mode-gnome-shell", new Array(new_key));
		}
	}
}


/**
 * This class encapsulates the code to launch a subprocess that can detect whether a window belongs to it
 * It only accepts to do it under Wayland, because under X11 there is no need to do these tricks
 *
 * It is compatible with https://gitlab.gnome.org/GNOME/mutter/merge_requests/754 to simplify the code
 *
 * @param {int} flags Flags for the SubprocessLauncher class
 * @param {string} process_id An string id for the debug output
 */
class LaunchSubprocess {
	is_wayland_compositor() {
		return Meta.is_wayland_compositor === undefined ||
			Meta.is_wayland_compositor();
	}

	constructor(flags, process_id) {
		this._process_id = process_id;
		this.cancellable = new Gio.Cancellable();
		this._launcher = new Gio.SubprocessLauncher({ flags: flags | Gio.SubprocessFlags.STDOUT_PIPE | Gio.SubprocessFlags.STDERR_MERGE });
		if (this.is_wayland_compositor()) {
			try {
				this._waylandClient = Meta.WaylandClient.new(this._launcher);
			} catch (e) {
				this._waylandClient = Meta.WaylandClient.new(global.context, this._launcher);
			}
		}
		this.subprocess = null;
		this.process_running = false;
	}

	spawnv(argv) {
		if (Meta.WaylandClient.new_subprocess) {
			// New API introduced in https://gitlab.gnome.org/GNOME/mutter/-/merge_requests/4491
			this._waylandClient = Meta.WaylandClient.new_subprocess (global.context, this._launcher, argv);
			this.subprocess = this._waylandClient.get_subprocess();
		} else {
			try {
				if (this.is_wayland_compositor()) {
					this.subprocess = this._waylandClient.spawnv(global.display, argv);
				} else {
					this.subprocess = this._launcher.spawnv(argv);
				}
			} catch (e) {
				this.subprocess = null;
				console.log(`Error while trying to launch TERMINUS process: ${e.message}\n${e.stack}`);
			}
		}
		// This is for GLib 2.68 or greater
		if (this._launcher.close) {
			this._launcher.close();
		}
		this._launcher = null;
		if (this.subprocess) {
			/*
			     * It reads STDOUT and STDERR and sends it to the journal using console.log(). This allows to
			     * have any error from the desktop app in the same journal than other extensions. Every line from
			     * the desktop program is prepended with the "process_id" parameter sent in the constructor.
			     */
			this._dataInputStream = Gio.DataInputStream.new(this.subprocess.get_stdout_pipe());
			this.read_output();
			this.subprocess.wait_async(this.cancellable, () => {
				this.process_running = false;
				this._dataInputStream = null;
				this.cancellable = null;
			});
			this.process_running = true;
		}
		return this.subprocess;
	}

	set_cwd(cwd) {
		this._launcher.set_cwd(cwd);
	}

	read_output() {
		if (!this._dataInputStream) {
			return;
		}
		this._dataInputStream.read_line_async(GLib.PRIORITY_DEFAULT, this.cancellable, (object, res) => {
			try {
				const [output, length] = object.read_line_finish_utf8(res);
				if (length) {
					print(`${this._process_id}: ${output}`);
				}
			} catch (e) {
				if (e.matches(Gio.IOErrorEnum, Gio.IOErrorEnum.CANCELLED)) {
					return;
				}
				console.error(e, `${this._process_id}_Error`);
			}

			this.read_output();
		});
	}

	/**
	 * Queries whether the passed window belongs to the launched subprocess or not.
	 *
	 * @param {MetaWindow} window The window to check.
	 */
	query_window_belongs_to(window) {
		if (!this.process_running) {
			return false;
		}

		if (this.is_wayland_compositor()) {
			return this._waylandClient.owns_window(window);
		}

		try {
			const pid = parseInt(this.subprocess.get_identifier());
			const appid = window.get_gtk_application_id();
			return ((pid === window.get_pid()) && (appid == 'com.rastersoft.terminus'));
		} catch(e) {
			return false;
		}
	}
};