import { Edge, LayoutDirection, Tile, Window, Workspace } from "kwin-api";

declare const workspace: Workspace;
declare const registerShortcut: any;
declare const print: any;

enum Axis {
  Horiz = 1,
  Vert = 2,
}

let lastFocused = new Map<Tile, number>();
let selectedTile: Tile | null = null;
let resizeSense = 20;

function dir(axis: Axis): LayoutDirection {
  switch (axis) {
    case Axis.Horiz:
      return LayoutDirection.Horizontal;
    case Axis.Vert:
      return LayoutDirection.Vertical;
  }
}

function mod(a: number, b: number): number {
	while (a < 0) {
		a += b;
	}
	a %= b;
	return a;
}

function get_tile_index(tile: Tile): number {
	let p = tile.parent!;
	return p.tiles.findIndex((t: Tile) => t == tile);
}

/// Gets the closest ancestor of `tile` which is inside an `axis` layout.
function get_least_in_axis(axis: Axis, tile: Tile): Tile | null {
	while (true) {
		if (tile.parent == null) return null;
		if (tile.parent?.layoutDirection == dir(axis)) return tile;
		tile = tile.parent;
	}
}

function move(dir: Edge) {
	const axis = dir == Edge.LeftEdge || dir == Edge.RightEdge ? Axis.Horiz : Axis.Vert;
	const sign = dir == Edge.RightEdge || dir == Edge.BottomEdge ? 1 : -1;
	let t = workspace.activeWindow?.tile;
	if (t == null) return;
	t = get_least_in_axis(axis, t);
	if (t == null) return;
	// t.parent is not null because otherwise get_least_in_axis would have returned null
	const i = mod(get_tile_index(t) + sign, t.parent!.tiles.length);
	t = t.parent!.tiles[i]!;
	while (t.tiles.length != 0) {
		t = t.tiles[lastFocused.get(t) ?? 0];
	}
	workspace.activeWindow = t.windows[0];
}

function resize(axis: Axis, sign: number) {
	if (workspace.activeWindow?.tile == null) return;
	let child = get_least_in_axis(axis, workspace.activeWindow!.tile);
	if (child == null) return;
	let n = get_tile_index(child);
	let l = child.parent!.tiles.length;
	if (n == 0) {
		child.resizeByPixels(resizeSense * sign, axis == Axis.Horiz ? Edge.RightEdge : Edge.BottomEdge);
	} else if (n == l-1) {
		child.resizeByPixels(-resizeSense * sign, axis == Axis.Horiz ? Edge.LeftEdge : Edge.TopEdge);
	} else {
		child.resizeByPixels(resizeSense * sign * 0.5, axis == Axis.Horiz ? Edge.RightEdge : Edge.BottomEdge);
		child.resizeByPixels(-resizeSense * sign * 0.5, axis == Axis.Horiz ? Edge.LeftEdge : Edge.TopEdge);
	}
}

function relayout(dir: LayoutDirection) {
	if (selectedTile == null) return;
	selectedTile.layoutDirection = dir;
}

workspace.windowActivated.connect((w: Window) => {
	if (w?.tile == null) return;
	const t = w.tile;
	selectedTile = t;
	const p = t.parent;
	if (p == null) return;
	lastFocused.set(p, get_tile_index(t));
});

workspace.windowAdded.connect((w: Window) => {
	const root = workspace.tilingForScreen(workspace.activeScreen).rootTile;
	if (["krunner", "yakuake", "kded", "polkit", "plasmashell", "ksmserver-logout-greeter", "kwin_wayland", "spectacle"]
	    .includes(w.resourceName))
		return;
	if (!w.normalWindow || w.transient || w.resourceClass == "") return;
	if (root.tiles.length == 0 && root.windows.length == 0) {
		w.tile = root;
		root.layoutDirection = LayoutDirection.Horizontal;
		selectedTile = root;
		return;
	}
	let t = selectedTile;
	if (t == null) return;
	const dir = t.layoutDirection;
	t.split(dir);
	let p = t.parent;
	let i: number;
	if (p?.layoutDirection == dir) {
		// we actually split p, not t
		i = mod(get_tile_index(t) + 1, p.tiles.length);
	} else if (p == null) {
		// t is the root
		p = t;
		i = 1;
	} else {
		// we split t for real
		p = t;
		i = 1;
	}
	t = p.tiles[i];
	w.tile = t;
	selectedTile = t;
});

workspace.windowRemoved.connect((w: Window) => {
	if (w.tile == null) return;
	const t = w.tile;
	let sibling_windows: Window[] = [];
	if (t.parent?.tiles?.length == 2) {
		// if this tile has only one sibling, we need to move that sibling's windows into the parent
		// when this tile gets destroyed
		// kwin kind of tries to do this but fails - it resizes the windows properly but the windows are
		// left untiled somehow
		const sibling_index = 1 - get_tile_index(t);
		const sibling_tile = t.parent!.tiles[sibling_index];
		// the other window has just been activated but we are going to destroy its tile,
		// so we preemptively select the parent, which will soon contain the window
		selectedTile = t.parent!;
		if (sibling_tile.tiles.length == 0) {
			// it contains only windows and no tiles
			// hopefully kwin takes the wheel in the other case
			for (const w of sibling_tile.windows) {
				sibling_windows.push(w);
			}
		}
	}
	const p = t.parent;
	t.remove();
	if (p != null) {
		for (const w of sibling_windows) {
			w.tile = p;
		}
	}
});

registerShortcut(
  "kwi3:moveleft", "kwi3: Move one container to the left", "Meta+N",
  () => move(Edge.LeftEdge));
registerShortcut(
  "kwi3:movedown", "kwi3: Move one container down", "Meta+E",
  () => move(Edge.BottomEdge));
registerShortcut(
  "kwi3:moveup", "kwi3: Move one container up", "Meta+I",
  () => move(Edge.TopEdge));
registerShortcut(
  "kwi3:moveright", "kwi3: Move one container to the right", "Meta+O",
  () => move(Edge.RightEdge));

registerShortcut(
  "kwi3:shrinkhorizontal", "kwi3: Shrink the current window horizontally", "Meta+Ctrl+N",
  () => resize(Axis.Horiz, -1));
registerShortcut(
  "kwi3:growvertical", "kwi3: Grow the current window vertically", "Meta+Ctrl+E",
  () => resize(Axis.Vert, 1));
registerShortcut(
  "kwi3:shrinkvertical", "kwi3: Shrink the current window vertically", "Meta+Ctrl+I",
  () => resize(Axis.Vert, -1));
registerShortcut(
  "kwi3:growhorizontal", "kwi3: Grow the current window horizontally", "Meta+Ctrl+O",
  () => resize(Axis.Horiz, 1));

registerShortcut(
  "kwi3:splith", "kwi3: Horizontal split", "Meta+V",
  () => relayout(LayoutDirection.Horizontal));
registerShortcut(
  "kwi3:splitv", "kwi3: Vertical split", "Meta+B",
  () => relayout(LayoutDirection.Vertical));