Experimental, rust-based pijul editor support
import {
	commands,
	ExtensionContext,
	Uri,
	window,
	workspace,
	scm,
	SourceControl,
	RelativePattern,
	SourceControlResourceGroup,
	FileDecorationProvider,
	CancellationToken,
	FileDecoration,
	ProviderResult,
	EventEmitter,
	DecorationRenderOptions,
	ThemeColor,
	TextEditorDecorationType,
	Selection,
	Range,
} from "vscode";
import { join } from "path";
import fs from "fs";

import {
	FileState,
	allFiles,
	findClosestRoot,
	ignored,
	resolveRepositories,
} from "../index.node";

const scm_id = "pijul";
const scm_label = "Pijul";
const file_states: Map<string, FileState> = new Map();

// Resolve all paths that could be displayed in the editor
// TODO: this currently doesn't handle 'orphan' files not belonging to a workspace folder
function paths_to_resolve(): string[] {
	const folders = workspace.workspaceFolders || [];
	const paths = [];

	for (const folder of folders) {
		paths.push(folder.uri.fsPath);
	}

	return paths;
}

function update_states(path: string) {
	console.log(`Updating states for ${path}`);
	const states = allFiles(path);
	const modified = [];
	const untracked = [];

	for (const state of states) {
		const path = state.path();
		file_states.set(path, state);

		console.log(`Emitting decoration event for path ${path}`);
		decoration_provider.emitDecorationEvent(Uri.file(path));

		const to_add = {
			resourceUri: Uri.file(path),
			decorations: {
				tooltip: state.tooltip(),
			},
		};
		if (state.untracked()) {
			untracked.push(to_add);
		} else {
			modified.push(to_add);
		}
	}

	const root = findClosestRoot(path);
	const resource_groups = source_control_views.get(root)?.resource_groups;
	if (resource_groups === undefined) {
		console.error("Resource group for repository is undefined");
		return;
	}

	resource_groups[0].resourceStates = modified;
	resource_groups[1].resourceStates = untracked;
}

interface LoadedRepository {
	source_control: SourceControl;
	resource_groups: SourceControlResourceGroup[];
}

class DecorationProvider implements FileDecorationProvider {
	private readonly onDidFileDecorationsEmitter = new EventEmitter<
		Uri | Uri[] | undefined
	>();

	get onDidChangeFileDecorations() {
		return this.onDidFileDecorationsEmitter.event;
	}

	emitDecorationEvent(uri: Uri | Uri[]) {
		this.onDidFileDecorationsEmitter.fire(uri);
	}

	provideFileDecoration(
		uri: Uri,
		_token: CancellationToken,
	): ProviderResult<FileDecoration> {
		if (ignored(uri.fsPath)) {
			console.log(`Ignoring decoration for file: ${uri.fsPath}`);
			return new FileDecoration();
		} else {
			console.log(`Decorating file: ${uri.fsPath}`);
			const state = new FileState(uri.fsPath);

			return {
				badge: state.badge(),
				tooltip: state.tooltip(),
				propagate: true, // TODO: no propogation for deletions
				color: "", // TODO: make this actually work,
			};
		}
	}
}

const source_control_views = new Map<string, LoadedRepository>();
const decoration_provider = new DecorationProvider();
let last_decorations: Map<number, TextEditorDecorationType> = new Map();

function credit(path: string, selections: readonly Selection[]) {
	const active_editor = window.activeTextEditor;
	if (active_editor === undefined) {
		console.error("Active editor is undefined");
		return;
	}

	const credits = [];
	const file_state = file_states.get(path);
	for (const selection of selections) {
		if (file_state === undefined) {
			console.error(`No cached state found for ${path}`);
			continue;
		}

		for (let line = selection.start.line; line <= selection.end.line; line++) {
			const hash = file_state.hashAt(line);
			if (credits.length > 0) {
				if (credits[credits.length - 1].hash !== hash) {
					credits.push({
						hash,
						line,
					});
				}
			} else {
				credits.push({
					hash,
					line,
				});
			}
		}
	}

	const new_decorations: Map<number, TextEditorDecorationType> = new Map();
	for (const change of credits) {
		const last_decoration = last_decorations.get(change.line);
		if (last_decoration !== undefined) {
			new_decorations.set(change.line, last_decoration);
		} else {
			const decoration_options: DecorationRenderOptions = {
				after: {
					// TODO: find a way to align this text
					contentText: ` ${file_state.authorsAt(
						change.line,
					)} • ${file_state.messageAt(change.line)}`,
					color: new ThemeColor("disabledForeground"),
				},
				isWholeLine: true,
			};
			const decoration_type =
				window.createTextEditorDecorationType(decoration_options);

			const decoration_range: Range = new Range(change.line, 0, change.line, 0);
			active_editor.setDecorations(decoration_type, [decoration_range]);
			new_decorations.set(change.line, decoration_type);
		}
	}

	for (const [line, decoration] of last_decorations.entries()) {
		if (!new_decorations.has(line)) {
			console.log(`Dispose line ${line + 1}`);
			decoration.dispose();
		}
	}
	last_decorations = new_decorations;
}

export async function activate(context: ExtensionContext) {
	window.registerFileDecorationProvider(decoration_provider);

	window.onDidChangeTextEditorSelection((event) => {
		credit(event.textEditor.document.uri.fsPath, event.selections);
	});

	for (const repo_path of resolveRepositories(paths_to_resolve())) {
		if (!source_control_views.has(repo_path)) {
			console.log(`Activating source control for repository: ${repo_path}`);
			const repo_scm = scm.createSourceControl(
				scm_id,
				scm_label,
				Uri.file(repo_path),
			);

			// Each 'resource group' appears as its own heading
			const groups = [
				repo_scm.createResourceGroup("modified", "Modified"),
				repo_scm.createResourceGroup("untracked", "Untracked files"),
			];

			// TODO: might need to fix https://github.com/microsoft/vscode/issues/162433 to make dynamic
			repo_scm.inputBox.placeholder = "Message (Ctrl+Enter to record)";

			if (repo_scm.rootUri === undefined) {
				console.error("Root URI is undefined");
				return;
			}

			const watcher = workspace.createFileSystemWatcher(
				new RelativePattern(repo_scm.rootUri, "**/*"),
			);

			// When a file is created, check to see if pijul is tracking it
			// watcher.onDidCreate((event) => {

			// })
			watcher.onDidChange((event) => {
				if (ignored(event.fsPath)) {
					console.log(`Ignored change event from ${event.fsPath}`);
				} else {
					console.log(`Responding to change event on file: ${event.fsPath}`);
					update_states(event.fsPath);
				}
			});

			const loaded_repository: LoadedRepository = {
				source_control: repo_scm,
				resource_groups: groups,
			};
			source_control_views.set(repo_path, loaded_repository);
		}

		console.log(`Updating source control view for repository: ${repo_path}`);
		update_states(repo_path);
	}

	commands.registerCommand("pijul.run", async () => {});
}

export function deactivate() {}