import dayjs from 'dayjs';
import { Position, Range, Selection, TextDocument, TextEditorRevealType, TextLine, Uri, window, workspace, WorkspaceEdit } from 'vscode';
import { DueDate } from './dueDate';
import { $config } from './extension';
import { parseDocument } from './parse';
import { Count, TheTask } from './TheTask';
import { dateWithoutTime, DATE_FORMAT, durationTo, getDateInISOFormat } from './time/timeUtils';
import { updateArchivedTasks } from './treeViewProviders/treeViews';
import { DueState } from './types';
import { applyEdit, checkArchiveFileAndNotify, getActiveOrDefaultDocument, helpCreateSpecialTag, SpecialTagName, taskToString } from './utils/extensionUtils';
import { forEachTask, getNestedTasksLineNumbers, getTaskAtLineExtension } from './utils/taskUtils';
import { unique } from './utils/utils';

// This file contains 2 types of functions
// 1) Performs an action on the document and applies an edit (saves the document)
// 2) Has a `WorkspaceEdit` suffix that accepts an edit and performs actions(insert/replace/delete) without applying

// ────────────────────────────────────────────────────────────
// ──── Apply Edit ────────────────────────────────────────────
// ────────────────────────────────────────────────────────────
/**
 * Replace entire line range with new text. (text is take from task transformed to string).
 */
export async function editTask(document: TextDocument, task: TheTask) {
	const edit = new WorkspaceEdit();
	const newTaskAsText = taskToString(task);
	const line = document.lineAt(task.lineNumber);
	edit.replace(document.uri, line.range, newTaskAsText);
	return applyEdit(edit, document);
}
/**
 * Add `{h}` special tag
 */
export async function hideTaskAtLine(document: TextDocument, lineNumber: number) {
	const edit = new WorkspaceEdit();
	const line = document.lineAt(lineNumber);
	const task = getTaskAtLineExtension(lineNumber);
	if (!task) {
		return undefined;
	}
	if (!task.isHidden) {
		edit.insert(document.uri, line.range.end, ' {h}');
	}
	return applyEdit(edit, document);
}
/**
 * Replace entire line range with new text.
 */
export async function editTaskRawText(document: TextDocument, lineNumber: number, newRawText: string) {
	const edit = new WorkspaceEdit();
	const line = document.lineAt(lineNumber);
	edit.replace(document.uri, line.range, newRawText);
	return applyEdit(edit, document);
}
/**
 * Toggle `{c}` special tag
 */
export async function toggleTaskCollapse(document: TextDocument, lineNumber: number) {
	const edit = new WorkspaceEdit();
	toggleTaskCollapseWorkspaceEdit(edit, document, lineNumber);
	return applyEdit(edit, document);
}
/**
 * Recursively expand/collapse all nested tasks
 */
export async function toggleTaskCollapseRecursive(document: TextDocument, lineNumber: number) {
	const parentTask = getTaskAtLineExtension(lineNumber);
	if (!parentTask) {
		return undefined;
	}
	const edit = new WorkspaceEdit();

	if (parentTask.isCollapsed) {
		forEachTask(task => {
			if (task.isCollapsed && task.subtasks.length) {
				toggleTaskCollapseWorkspaceEdit(edit, document, task.lineNumber);
			}
		}, parentTask.subtasks);
	} else {
		forEachTask(task => {
			if (!task.isCollapsed && task.subtasks.length) {
				toggleTaskCollapseWorkspaceEdit(edit, document, task.lineNumber);
			}
		}, parentTask.subtasks);
	}
	toggleTaskCollapseWorkspaceEdit(edit, document, lineNumber);
	return await applyEdit(edit, document);
}
/**
 * Insert/Replace due date
 */
export async function setDueDateAtLine(document: TextDocument, lineNumber: number, newDueDate: string) {
	const edit = new WorkspaceEdit();
	setDueDateWorkspaceEdit(edit, document, lineNumber, newDueDate);
	return await applyEdit(edit, document);
}
/**
 * Delete the task. Show confirmation dialog if necessary. Modal dialog shows all the tasks that will be deleted.
 */
export async function tryToDeleteTask(document: TextDocument, lineNumber: number) {
	const task = getTaskAtLineExtension(lineNumber);
	if (!task) {
		return undefined;
	}
	const edit = new WorkspaceEdit();

	let numberOfTasksToBeDeleted = '';
	let deletedTasksAsText = '';
	let showConfirmationDialog = false;

	const taskLineNumbersToDelete = [lineNumber];
	if (task.subtasks.length) {
		taskLineNumbersToDelete.push(...getNestedTasksLineNumbers(task.subtasks));
	}

	for (const ln of taskLineNumbersToDelete) {
		const taskAtLine = getTaskAtLineExtension(ln);
		if (!taskAtLine) {
			continue;
		}
		deletedTasksAsText += `${taskAtLine.rawText.replace(/\s\s/g, '┄')}\n`;
	}
	numberOfTasksToBeDeleted = `❗ [ ${taskLineNumbersToDelete.length} ] task${taskLineNumbersToDelete.length > 1 ? 's' : ''} will be deleted.`;

	if ($config.confirmTaskDelete === 'always') {
		showConfirmationDialog = true;
	} else if ($config.confirmTaskDelete === 'hasNestedTasks') {
		if (task.subtasks.length) {
			showConfirmationDialog = true;
		}
	}

	if (showConfirmationDialog) {
		const confirmBtnName = 'Delete';
		const button = await window.showWarningMessage(`${numberOfTasksToBeDeleted}\n${deletedTasksAsText}`, {
			modal: true,
		}, confirmBtnName);
		if (button !== confirmBtnName) {
			return undefined;
		}
	}

	for (const ln of taskLineNumbersToDelete) {
		deleteTaskWorkspaceEdit(edit, document, ln);
	}

	return applyEdit(edit, document);
}
/**
 * Either toggle done or increment count
 */
export async function toggleDoneOrIncrementCount(document: TextDocument, lineNumber: number) {
	const task = getTaskAtLineExtension(lineNumber);
	if (!task) {
		return undefined;
	}
	if (task.count) {
		return await incrementCountForTask(document, lineNumber, task);
	} else {
		await toggleDoneAtLine(document, lineNumber);
		return undefined;
	}
}
/**
 * Increment count special tag. If already max `3/3` then set it to `0/3`
 */
export async function incrementCountForTask(document: TextDocument, lineNumber: number, task: TheTask) {
	const line = document.lineAt(lineNumber);
	const edit = new WorkspaceEdit();
	const count = task.count;
	if (!count) {
		return Promise.resolve(undefined);
	}
	let newValue = 0;
	// TODO: this function must call toggleDoneAtLine() !!!
	if (count.current !== count.needed) {
		newValue = count.current + 1;
		if (newValue === count.needed) {
			insertCompletionDateWorkspaceEdit(edit, document, line, task);
			removeOverdueWorkspaceEdit(edit, document.uri, task);
		}
		setCountCurrentValueWorkspaceEdit(edit, document.uri, count, String(newValue));
	} else {
		setCountCurrentValueWorkspaceEdit(edit, document.uri, count, '0');
		removeCompletionDateWorkspaceEdit(edit, document.uri, task);
	}
	return applyEdit(edit, document);
}
/**
 * Decrement count special tag. If alredy min `0/3` then do nothing.
 */
export async function decrementCountForTask(document: TextDocument, lineNumber: number, task: TheTask) {
	const edit = new WorkspaceEdit();
	const count = task.count;
	if (!count) {
		return undefined;
	}
	if (count.current === 0) {
		return undefined;
	} else if (count.current === count.needed) {
		removeCompletionDateWorkspaceEdit(edit, document.uri, task);
	}
	setCountCurrentValueWorkspaceEdit(edit, document.uri, count, String(count.current - 1));
	return applyEdit(edit, document);
}
/**
 * Remove overdue special tag
 */
async function removeOverdueFromLine(document: TextDocument, task: TheTask) {
	const edit = new WorkspaceEdit();
	removeOverdueWorkspaceEdit(edit, document.uri, task);
	return applyEdit(edit, document);
}
/**
 * Toggle task completion. Handle what to insert/delete.
 */
export async function toggleDoneAtLine(document: TextDocument, lineNumber: number) {
	const task = getTaskAtLineExtension(lineNumber);
	if (!task) {
		return;
	}
	if (task.overdue) {
		await removeOverdueFromLine(document, task);
	}
	const line = document.lineAt(lineNumber);
	const edit = new WorkspaceEdit();
	if (task.done) {
		removeCompletionDateWorkspaceEdit(edit, document.uri, task);
		removeDurationWorkspaceEdit(edit, document.uri, task);
		removeStartWorkspaceEdit(edit, document.uri, task);
	} else {
		insertCompletionDateWorkspaceEdit(edit, document, line, task);
	}
	await applyEdit(edit, document);

	if ($config.autoArchiveTasks) {
		await archiveTasks([task], document);
	}
}
/**
 * - Warning and noop when default archive file path is not specified
 * - Archive only works for completed tasks
 * - When the task is non-root (has parent task) - noop
 * - When the task has subtasks -> archive them too
 */
export async function archiveTasks(tasks: TheTask[], document: TextDocument) {
	const isDefaultArchiveFileSpecified = await checkArchiveFileAndNotify();
	if (!isDefaultArchiveFileSpecified) {
		return undefined;
	}

	const fileEdit = new WorkspaceEdit();
	const archiveFileEdit = new WorkspaceEdit();
	const archiveFileUri = Uri.file($config.defaultArchiveFile);
	const archiveDocument = await workspace.openTextDocument(archiveFileUri);
	let taskLineNumbersToArchive = [];

	for (const task of tasks) {
		// Only root tasks provided will be archived
		if (task.parentTaskLineNumber !== undefined) {
			continue;
		}
		// Recurring tasks cannot be archived
		if (task.due?.isRecurring) {
			continue;
		}
		taskLineNumbersToArchive.push(task.lineNumber);
		if (task.subtasks.length) {
			taskLineNumbersToArchive.push(...getNestedTasksLineNumbers(task.subtasks));
		}
	}

	taskLineNumbersToArchive = unique(taskLineNumbersToArchive);
	for (const lineNumber of taskLineNumbersToArchive) {
		const task = getTaskAtLineExtension(lineNumber);
		if (!task) {
			continue;
		}
		const line = document.lineAt(lineNumber);
		archiveTaskWorkspaceEdit(fileEdit, archiveFileEdit, archiveDocument, document.uri, line, true);
	}

	await applyEdit(fileEdit, document);
	await applyEdit(archiveFileEdit, archiveDocument);
	updateArchivedTasks();
	return undefined;
}
/**
 * Reveal the line/task in the file.
 *
 * Move cursor, reveal range, highlight the line for a moment
 */
export async function revealTask(lineNumber: number, document?: TextDocument) {
	const documentToReveal = document ?? await getActiveOrDefaultDocument();
	const editor = await window.showTextDocument(documentToReveal);
	const range = new Range(lineNumber, 0, lineNumber, 0);
	editor.selection = new Selection(range.start, range.end);
	editor.revealRange(range, TextEditorRevealType.Default);
	// Highlight for a short time revealed range
	const lineHighlightDecorationType = window.createTextEditorDecorationType({
		backgroundColor: '#ffa30468',
		isWholeLine: true,
	});
	editor.setDecorations(lineHighlightDecorationType, [range]);
	setTimeout(() => {
		editor.setDecorations(lineHighlightDecorationType, []);
	}, 700);
}
/**
 * Recurring tasks completion state should reset every day.
 * This function goes through all tasks in a document and resets their completion/count, adds `{overdue}` tag when needed
 */
export async function resetAllRecurringTasks(document: TextDocument, lastVisit: Date | string = new Date()) {
	if (typeof lastVisit === 'string') {
		lastVisit = new Date(lastVisit);
	}
	const edit = new WorkspaceEdit();
	const tasks = (await parseDocument(document)).tasks;
	const now = new Date();
	const nowWithoutTime = dateWithoutTime(now);

	for (const task of tasks) {
		if (task.due?.isRecurring) {
			const line = document.lineAt(task.lineNumber);
			if (task.done) {
				removeCompletionDateWorkspaceEdit(edit, document.uri, task);
				removeStartWorkspaceEdit(edit, document.uri, task);
				removeDurationWorkspaceEdit(edit, document.uri, task);
			} else {
				if (!task.overdue && !dayjs().isSame(lastVisit, 'day')) {
					const lastVisitWithoutTime = dateWithoutTime(lastVisit);
					const daysSinceLastVisit = dayjs(nowWithoutTime).diff(lastVisitWithoutTime, 'day');
					for (let i = daysSinceLastVisit; i > 0; i--) {
						const date = dayjs().subtract(i, 'day');
						const res = new DueDate(task.due.raw, {
							targetDate: date.toDate(),
						});
						if (res.isDue === DueState.Due || res.isDue === DueState.Overdue) {
							addOverdueSpecialTagWorkspaceEdit(edit, document.uri, line, date.format(DATE_FORMAT));
							break;
						}
					}
				}
			}

			const count = task.count;
			if (count) {
				setCountCurrentValueWorkspaceEdit(edit, document.uri, count, '0');
			}
		}
	}
	return applyEdit(edit, document);
}
/**
 * Insert line break `\n` and some text to the file
 */
export async function appendTaskToFile(text: string, filePath: string) {
	const uri = Uri.file(filePath);
	const document = await workspace.openTextDocument(uri);
	const edit = new WorkspaceEdit();
	const eofPosition = document.lineAt(document.lineCount - 1).rangeIncludingLineBreak.end;
	edit.insert(uri, eofPosition, `\n${text}`);
	return applyEdit(edit, document);
}
export async function startTaskAtLine(lineNumber: number, document: TextDocument) {
	const edit = new WorkspaceEdit();
	startTaskAtLineWorkspaceEdit(edit, document, lineNumber);
	return await applyEdit(edit, document);
}
// ────────────────────────────────────────────────────────────
// ──── Do not apply edit ─────────────────────────────────────
// ────────────────────────────────────────────────────────────
export function toggleTaskCollapseWorkspaceEdit(edit: WorkspaceEdit, document: TextDocument, lineNumber: number) {
	const line = document.lineAt(lineNumber);
	const task = getTaskAtLineExtension(lineNumber);
	if (task?.collapseRange) {
		edit.delete(document.uri, task.collapseRange);
	} else {
		edit.insert(document.uri, line.range.end, ' {c}');
	}
}
export function deleteTaskWorkspaceEdit(edit: WorkspaceEdit, document: TextDocument, lineNumber: number) {
	edit.delete(document.uri, document.lineAt(lineNumber).rangeIncludingLineBreak);
}
export function removeOverdueWorkspaceEdit(edit: WorkspaceEdit, uri: Uri, task: TheTask) {
	if (task.overdueRange) {
		edit.delete(uri, task.overdueRange);
	}
}
export function insertCompletionDateWorkspaceEdit(edit: WorkspaceEdit, document: TextDocument, line: TextLine, task: TheTask, forceIncludeTime = false) {
	const dateInIso = getDateInISOFormat(new Date(), forceIncludeTime || $config.completionDateIncludeTime);
	const newCompletionDate = helpCreateSpecialTag(SpecialTagName.CompletionDate, $config.completionDateIncludeDate ? dateInIso : undefined);
	if (task.completionDateRange) {
		edit.replace(document.uri, task.completionDateRange, newCompletionDate);
	} else {
		edit.insert(document.uri, new Position(line.lineNumber, line.range.end.character), ` ${newCompletionDate}`);
	}
	if (task.start) {
		insertDurationWorkspaceEdit(edit, document, line, task);
	}
}
export function insertDurationWorkspaceEdit(edit: WorkspaceEdit, document: TextDocument, line: TextLine, task: TheTask) {
	if (!task.start) {
		return;
	}

	const newDurationDate = helpCreateSpecialTag(SpecialTagName.Duration, durationTo(task, true, $config.durationIncludeSeconds));
	if (task.durationRange) {
		edit.replace(document.uri, task.durationRange, newDurationDate);
	} else {
		edit.insert(document.uri, line.range.end, ` ${newDurationDate}`);
	}
}
export function removeCompletionDateWorkspaceEdit(edit: WorkspaceEdit, uri: Uri, task: TheTask) {
	if (task.completionDateRange) {
		edit.delete(uri, task.completionDateRange);
	}
}
export function removeDurationWorkspaceEdit(edit: WorkspaceEdit, uri: Uri, task: TheTask) {
	if (task.durationRange) {
		edit.delete(uri, task.durationRange);
	}
}
export function removeStartWorkspaceEdit(edit: WorkspaceEdit, uri: Uri, task: TheTask) {
	if (task.startRange) {
		edit.delete(uri, task.startRange);
	}
}
export function archiveTaskWorkspaceEdit(edit: WorkspaceEdit, archiveFileEdit: WorkspaceEdit, archiveDocument: TextDocument, uri: Uri, line: TextLine, shouldDelete: boolean) {
	appendTaskToFileWorkspaceEdit(archiveFileEdit, archiveDocument, line.text);// Add task to archive file
	if (shouldDelete) {
		edit.delete(uri, line.rangeIncludingLineBreak);// Delete task from active file
	}
}
function addOverdueSpecialTagWorkspaceEdit(edit: WorkspaceEdit, uri: Uri, line: TextLine, overdueDateString: string) {
	edit.insert(uri, new Position(line.lineNumber, line.range.end.character), ` {overdue:${overdueDateString}}`);
}
export function setCountCurrentValueWorkspaceEdit(edit: WorkspaceEdit, uri: Uri, count: Count, value: string) {
	const charIndexWithOffset = count.range.start.character + 'count:'.length + 1;
	const currentRange = new Range(count.range.start.line, charIndexWithOffset, count.range.start.line, charIndexWithOffset + String(count.current).length);
	edit.replace(uri, currentRange, String(value));
}
function appendTaskToFileWorkspaceEdit(edit: WorkspaceEdit, document: TextDocument, text: string) {
	const eofPosition = document.lineAt(document.lineCount - 1).rangeIncludingLineBreak.end;
	edit.insert(document.uri, eofPosition, `\n${text}`);
}
export function toggleCommentAtLineWorkspaceEdit(edit: WorkspaceEdit, document: TextDocument, lineNumber: number) {
	const line = document.lineAt(lineNumber);
	if (line.text.startsWith('# ')) {
		edit.delete(document.uri, new Range(lineNumber, 0, lineNumber, 2));
	} else {
		edit.insert(document.uri, new Position(lineNumber, 0), '# ');
	}
}
export function editTaskWorkspaceEdit(edit: WorkspaceEdit, document: TextDocument, task: TheTask) {
	const newTaskAsText = taskToString(task);
	const line = document.lineAt(task.lineNumber);
	edit.replace(document.uri, line.range, newTaskAsText);
}
/**
 * Increment/Decrement a priority. Create it if the task doesn't have one.
 */
export function incrementOrDecrementPriorityWorkspaceEdit(edit: WorkspaceEdit, document: TextDocument, lineNumber: number, type: 'decrement' | 'increment') {
	const task = getTaskAtLineExtension(lineNumber);
	if (!task ||
			type === 'increment' && task.priority === 'A' ||
			type === 'decrement' && task.priority === 'Z') {
		return;
	}
	const newPriority = type === 'increment' ? String.fromCharCode(task.priority.charCodeAt(0) - 1) : String.fromCharCode(task.priority.charCodeAt(0) + 1);
	if (task.priorityRange) {
		// Task has a priority
		edit.replace(document.uri, task.priorityRange, `(${newPriority})`);
	} else {
		// No priority, create one
		edit.insert(document.uri, new Position(lineNumber, 0), `(${newPriority}) `);
	}
}
/**
 * Start time tracking (task duration). Triggered manually by user.
 */
export function startTaskAtLineWorkspaceEdit(edit: WorkspaceEdit, document: TextDocument, lineNumber: number) {
	const line = document.lineAt(lineNumber);
	const task = getTaskAtLineExtension(lineNumber);
	if (!task) {
		return;
	}
	const newStartDate = helpCreateSpecialTag(SpecialTagName.Started, getDateInISOFormat(undefined, true));
	if (task.startRange) {
		edit.replace(document.uri, task.startRange, newStartDate);
	} else {
		edit.insert(document.uri, line.range.end, ` ${newStartDate}`);
	}
}
export function setDueDateWorkspaceEdit(edit: WorkspaceEdit, document: TextDocument, lineNumber: number, newDueDate: string) {
	const dueDate = `{due:${newDueDate}}`;
	const task = getTaskAtLineExtension(lineNumber);
	if (task?.overdueRange) {
		edit.delete(document.uri, task.overdueRange);
	}
	if (task?.dueRange) {
		edit.replace(document.uri, task.dueRange, dueDate);
	} else {
		const line = document.lineAt(lineNumber);
		const isLineEndsWithWhitespace = line.text.endsWith(' ');
		edit.insert(document.uri, line.range.end, `${isLineEndsWithWhitespace ? '' : ' '}${dueDate}`);
	}
}