/* eslint-disable eqeqeq */
import {
	CodeAction,
	CodeActionParams,
	DiagnosticSeverity,
	CodeActionKind,
	Diagnostic,
	MessageType,
	ShowMessageRequestParams
} from 'vscode-languageserver';
import { URI } from 'vscode-uri';
import { isNullOrUndefined } from "util";
import * as fse from "fs-extra";

import { DocumentsManager } from './DocumentsManager';
import { applyTextDocumentEditOnWorkspace, getUpdatedSource, notifyFixFailures } from './clientUtils';
import { parseLinterResults } from './linterParser';
import { StatusNotification, OpenNotification } from './types';
import {
	COMMAND_LINT_QUICKFIX,
	COMMAND_LINT_QUICKFIX_FILE,
	COMMAND_DISABLE_ERROR_FOR_LINE,
	COMMAND_DISABLE_ERROR_FOR_FILE,
	COMMAND_DISABLE_ERROR_FOR_PROJECT,
	COMMAND_SHOW_RULE_DOCUMENTATION
} from './commands';
import path = require('path');
import { TextDocument } from 'vscode-languageserver-textdocument';
const debug = require("debug")("vscode-groovy-lint");

/**
 * Provide quick-fixes for a piece of code  *
 * @export
 * @param {TextDocument} textDocument
 * @param {CodeActionParams} parms
 * @returns {CodeAction[]}
 */
export function provideQuickFixCodeActions(textDocument: TextDocument, codeActionParams: CodeActionParams, docQuickFixes: any): CodeAction[] {
	const diagnostics = codeActionParams.context.diagnostics;
	const quickFixCodeActions: CodeAction[] = [];
	if (isNullOrUndefined(diagnostics) || diagnostics.length === 0) {
		return quickFixCodeActions;
	}
	// Browse diagnostics to get related CodeActions
	for (const diagnostic of codeActionParams.context.diagnostics) {
		// Skip Diagnostics not from VsCodeGroovyLint
		if (diagnostic.source !== 'GroovyLint') {
			continue;
		}
		// Get corresponding QuickFix if existing and convert it as QuickAction
		const diagCode: string = diagnostic.code + '';
		if (docQuickFixes && docQuickFixes[diagCode]) {
			for (const quickFix of docQuickFixes[diagCode]) {
				const codeActions = createQuickFixCodeActions(diagnostic, quickFix, textDocument.uri);
				quickFixCodeActions.push(...codeActions);
			}
		}
		// Add Ignores for this error
		const disableActions = createDisableActions(diagnostic, textDocument.uri);
		quickFixCodeActions.push(...disableActions);
		const viewDocAction = createViewDocAction(diagnostic, textDocument.uri);
		if (viewDocAction) {
			quickFixCodeActions.push(viewDocAction);
		}
	}
	debug(`Provided ${quickFixCodeActions.length} codeActions for ${textDocument.uri}`);
	return quickFixCodeActions;
}

// Create QuickFix codeActions for diagnostic
function createQuickFixCodeActions(diagnostic: Diagnostic, quickFix: any, textDocumentUri: string): CodeAction[] {
	const codeActions: CodeAction[] = [];

	// Quick fix only this error
	const quickFixAction: CodeAction = {
		title: quickFix.label,
		kind: CodeActionKind.RefactorRewrite,
		command: {
			command: COMMAND_LINT_QUICKFIX.command,
			title: quickFix.label,
			arguments: [textDocumentUri, diagnostic]
		},
		diagnostics: [diagnostic],
		isPreferred: true
	};
	codeActions.push(quickFixAction);
	codeActions.push(Object.assign(Object.assign({}, quickFixAction), { kind: CodeActionKind.QuickFix }));

	// Quick fix error in file
	const quickFixActionAllFile: CodeAction = {
		title: `${quickFix.label} for this entire file`,
		kind: CodeActionKind.Source,
		command: {
			command: COMMAND_LINT_QUICKFIX_FILE.command,
			title: `${quickFix.label} for this entire file`,
			arguments: [textDocumentUri, diagnostic]
		},
		diagnostics: [diagnostic],
		isPreferred: true
	};
	codeActions.push(quickFixActionAllFile);
	codeActions.push(Object.assign(Object.assign({}, quickFixActionAllFile), { kind: CodeActionKind.QuickFix }));
	return codeActions;
}

function createDisableActions(diagnostic: Diagnostic, textDocumentUri: string): CodeAction[] {
	// Sometimes it comes there whereas it shouldn't ... let's avoid a crash
	if (diagnostic == null) {
		console.warn('Warning: we should not be in createDisableActions as there is no diagnostic set');
		return [];
	}
	const disableActions: CodeAction[] = [];
	let errorLabel = (diagnostic.code as string).split('-')[0].replace(/([A-Z])/g, ' $1').trim();

	if (diagnostic.severity === DiagnosticSeverity.Warning ||
		diagnostic.severity === DiagnosticSeverity.Error ||
		diagnostic.severity === DiagnosticSeverity.Information) {

		// Ignore only this error
		const disableErrorAction: CodeAction = {
			title: `Disable ${errorLabel} for this line`,
			kind: CodeActionKind.QuickFix,
			command: {
				command: COMMAND_DISABLE_ERROR_FOR_LINE.command,
				title: `Disable ${errorLabel} for this line`,
				arguments: [textDocumentUri, diagnostic]
			},
			diagnostics: [diagnostic],
			isPreferred: false
		};
		disableActions.push(disableErrorAction);

		// disable this error type in all file
		const disableErrorInFileAction: CodeAction = {
			title: `Disable ${errorLabel} for this entire file`,
			kind: CodeActionKind.QuickFix,
			command: {
				command: COMMAND_DISABLE_ERROR_FOR_FILE.command,
				title: `Disable ${errorLabel} for this entire file`,
				arguments: [textDocumentUri, diagnostic]
			},
			diagnostics: [diagnostic],
			isPreferred: false
		};
		disableActions.push(disableErrorInFileAction);

		// disable this error type in all project (will update .groovylintrc.json)
		const disableInProjectAction: CodeAction = {
			title: `Disable ${errorLabel} for the entire project`,
			kind: CodeActionKind.QuickFix,
			command: {
				command: COMMAND_DISABLE_ERROR_FOR_PROJECT.command,
				title: `Disable ${errorLabel} for the entire project`,
				arguments: [textDocumentUri, diagnostic]
			},
			diagnostics: [diagnostic],
			isPreferred: false
		};
		disableActions.push(disableInProjectAction);
	}
	return disableActions;
}

// Create action to view documentation
function createViewDocAction(diagnostic: Diagnostic, textDocumentUri: string): CodeAction | null {
	// Sometimes it comes there whereas it shouldn't ... let's avoid a crash
	if (diagnostic == null) {
		console.warn('Warning: we should not be in createViewDocAction as there is no diagnostic set');
		return null;
	}
	const ruleCode = (diagnostic.code as string).split('-')[0];
	let errorLabel = ruleCode.replace(/([A-Z])/g, ' $1').trim();
	const viewCodeAction: CodeAction = {
		title: `Show documentation for ${errorLabel}`,
		kind: CodeActionKind.QuickFix,
		command: {
			command: COMMAND_SHOW_RULE_DOCUMENTATION.command,
			title: `Show documentation for ${errorLabel}`,
			arguments: [ruleCode]
		},
		diagnostics: [diagnostic],
		isPreferred: false
	};
	return viewCodeAction;
}

// Apply quick fixes
export async function applyQuickFixes(diagnostics: Diagnostic[], textDocumentUri: string, docManager: DocumentsManager) {
	// Sometimes it comes there whereas it shouldn't ... let's avoid a crash
	if (diagnostics == null || diagnostics.length === 0) {
		console.warn('Warning: we should not be in applyQuickFixes as there is no diagnostics set');
		return;
	}

	const textDocument: TextDocument = docManager.getDocumentFromUri(textDocumentUri);
	const errorIds: number[] = [];
	for (const diagnostic of diagnostics) {
		errorIds.push(parseInt((diagnostic.code as string).split('-')[1], 10));
	}
	debug(`Request apply QuickFixes for ${textDocumentUri}: ${errorIds.join(',')}`);

	// Call NpmGroovyLint instance fixer
	const docLinter = docManager.getDocLinter(textDocument.uri);
	debug(`Start fixing ${textDocument.uri}`);
	docManager.connection.sendNotification(StatusNotification.type, {
		state: 'lint.start.fix',
		documents: [{ documentUri: textDocument.uri }],
		lastFileName: textDocument.uri
	});
	await docLinter.fixErrors(errorIds, { nolintafter: true });
	// Parse fix results
	const { fixFailures } = parseLinterResults(docLinter.lintResult, textDocument.getText(), textDocument, docManager);
	// Notify user of failures if existing
	await notifyFixFailures(fixFailures, docManager);
	// Just Notify client of end of fixing 
	await docManager.connection.sendNotification(StatusNotification.type, {
		state: 'lint.end',
		documents: [{
			documentUri: textDocument.uri
		}],
		lastFileName: textDocument.uri
	});
	// Apply updates to textDocument
	if (docLinter.status === 0) {
		await applyTextDocumentEditOnWorkspace(docManager, textDocument, getUpdatedSource(docLinter, textDocument.getText()));
		setTimeout(() => { // Wait 500ms so we are more sure that the textDocument is already updated
			const newDoc = docManager.getUpToDateTextDocument(textDocument);
			docManager.validateTextDocument(newDoc, { force: true });
		}, 500);
	}
	debug(`End fixing ${textDocument.uri}`);
}

// Quick fix in the whole file
export async function applyQuickFixesInFile(diagnostics: Diagnostic[], textDocumentUri: string, docManager: DocumentsManager) {
	// Sometimes it comes there whereas it shouldn't ... let's avoid a crash
	if (diagnostics == null || diagnostics.length === 0) {
		console.warn('Warning: we should not be in applyQuickFixesInFile as there is no diagnostics set');
		return;
	}

	const textDocument: TextDocument = docManager.getDocumentFromUri(textDocumentUri);
	const fixRule = (diagnostics[0].code as string).split('-')[0];
	debug(`Request apply QuickFixes in file for ${fixRule} error in ${textDocumentUri}`);
	// Fix call
	await docManager.validateTextDocument(textDocument, { fix: true, fixrules: [fixRule] });
	// Lint after call
	debug(`Request new lint of ${textDocumentUri} after fix action`);
	setTimeout(() => { // Wait 500ms so we are more sure that the textDocument is already updated
		const newDoc = docManager.getUpToDateTextDocument(textDocument);
		docManager.validateTextDocument(newDoc, { force: true });
	}, 500);
}

// Disable error with comment groovylint-disable
export async function disableErrorWithComment(diagnostic: Diagnostic, textDocumentUri: string, scope: string, docManager: DocumentsManager) {
	// Sometimes it comes there whereas it shouldn't ... let's avoid a crash
	if (diagnostic == null) {
		console.warn('Warning: we should not be in disableErrorWithComment as there is no diagnostic set');
		return;
	}

	const textDocument: TextDocument = docManager.getDocumentFromUri(textDocumentUri);
	const allLines = docManager.getTextDocumentLines(textDocument);
	// Get line to check or create
	let linePos: number = 0;
	let disableKey: string = '';
	switch (scope) {
		// Get single error line position
		case 'line':
			linePos = getDiagnosticRangeInfo(diagnostic.range, 'start').line || 0;
			disableKey = 'groovylint-disable-next-line';
			break;
		// Manage shebang case ( https://en.wikipedia.org/wiki/Shebang_(Unix) ): use first or second line if shebang
		case 'file':
			linePos = (allLines[0] && allLines[0].startsWith('#!')) ? 1 : 0;
			disableKey = 'groovylint-disable';
			break;
	}
	const line: string = allLines[linePos];
	const prevLinePos = (linePos === 0) ? 0 : (linePos === 1) ? 1 : linePos - 1;
	const prevLine: string = allLines[prevLinePos] || '';
	const indent = " ".repeat(line.search(/\S/));
	const errorCode = (diagnostic.code as string).split('-')[0];
	// Avoid new lint to be triggered, as diagnostics will be up to date thanks to removeDiagnostics()
	docManager.recordSkipNextOnDidChangeContent(textDocument.uri);
	// Update existing /* groovylint-disable */ or /* groovylint-disable-next-line */
	const commentRules = parseGroovyLintComment(disableKey, prevLine);
	if (commentRules) {
		commentRules.push(errorCode);
		commentRules.sort();
		const disableLine = indent + `/* ${disableKey} ${[...new Set(commentRules)].join(", ")} */`;
		await applyTextDocumentEditOnWorkspace(docManager, textDocument, disableLine, { replaceLinePos: prevLinePos });
		// Removed as validateTextDocument is called after. Worse performances but safer. 
		// docManager.removeDiagnostics([diagnostic], textDocument.uri, disableKey === 'groovylint-disable');
	}
	else {
		// Add new /* groovylint-disable */ or /* groovylint-disable-next-line */
		const disableLine = indent + `/* ${disableKey} ${errorCode} */`;
		await applyTextDocumentEditOnWorkspace(docManager, textDocument, disableLine, { insertLinePos: linePos });
		// Removed as validateTextDocument is called after. Worse performances but safer. 
		// docManager.removeDiagnostics([diagnostic], textDocument.uri, disableKey === 'groovylint-disable', linePos);
	}
	docManager.validateTextDocument(textDocument, { force: true });
}

/* Depending of context, diagnostic.range can be 
{ start : {line: 1, character:1}, end : {line: 2, character:2} }
or 
[ {line: 1, character:1}, {line: 2, character:2] ]
*/
function getDiagnosticRangeInfo(range: any, startOrEnd: string): any {
	if (Array.isArray(range)) {
		return (startOrEnd === 'start') ? range[0] : range[1];
	}
	else {
		return range[startOrEnd];
	}
}

// Parse groovylint comment 
function parseGroovyLintComment(type: string, line: string) {
	if (line.includes(type) &&
		!(type === 'groovylint-disable' && line.includes('groovylint-disable-next-line'))) {
		const typeDetail = line
			.replace("/*", "")
			.replace("//", "")
			.replace("*/", "")
			.replace(type, "")
			.trim();
		if (typeDetail) {
			const errors = typeDetail.split(",").map((errType: string) => errType.trim());
			return errors;
		}
		return [];
	}
	return false;
}


// Create/ Update .groovylintrc.json file
export async function disableErrorForProject(diagnostic: Diagnostic, textDocumentUri: string, docManager: DocumentsManager) {
	debug(`Request disable error in all project from ${textDocumentUri}`);
	// Sometimes it comes there whereas it shouldn't ... let's avoid a crash
	if (diagnostic == null) {
		console.warn('Warning: we should not be in alwaysIgnoreError as there is no diagnostic set');
		return [];
	}
	const textDocument: TextDocument = docManager.getDocumentFromUri(textDocumentUri);
	// Get line to check or create
	const errorCode: string = (diagnostic.code as string).split('-')[0];
	debug(`Error code to be disabled is ${errorCode}`);
	// Get or create configuration file path using NpmGroovyLint instance associated to this document
	const docLinter = docManager.getDocLinter(textDocument.uri);
	const textDocumentFilePath: string = URI.parse(textDocument.uri).fsPath;
	const startPath = path.dirname(textDocumentFilePath);
	let configFilePath: string = await docLinter.getConfigFilePath(startPath);
	let configFileContent = JSON.parse(fse.readFileSync(configFilePath, "utf8").toString());
	if (configFilePath.endsWith(".groovylintrc-recommended.json")) {
		const projectFolder = docManager.getCurrentWorkspaceFolder();
		configFilePath = `${projectFolder}/.groovylintrc.json`;
		configFileContent = { extends: "recommended", rules: {} };
	}
	debug(`Config file to be created/updated is ${configFilePath}`);
	// Find / Create disabled rule
	const newRuleContent = { enabled: false };
	let existingRule: Array<any> = Object.entries(configFileContent.rules).filter(mapElt => {
		mapElt[0].includes(errorCode);
	});
	if (existingRule.length > 0) {
		if (typeof configFileContent.rules[existingRule[0]] === 'string') { // ex: "warning"
			Object.assign(newRuleContent, { severity: configFileContent.rules[existingRule[0]] });
		} else { // ex: 'indentationLevel: 4'
			delete configFileContent.rules[existingRule[0]].enabled;
			delete configFileContent.rules[existingRule[0]].disabled;
			Object.assign(newRuleContent, configFileContent.rules[existingRule[0]]);
		}
		delete configFileContent.rules[existingRule[0]];
	}
	configFileContent.rules[errorCode] = newRuleContent;

	// Reorder rules
	const rulesSorted: any = {};
	for (const ruleKey of Object.keys(configFileContent.rules).sort()) {
		rulesSorted[ruleKey] = configFileContent.rules[ruleKey];
	}
	configFileContent.rules = rulesSorted;

	// Write new JSON config
	await fse.writeFile(configFilePath, JSON.stringify(configFileContent, null, 4));
	debug(`Updated file ${configFilePath}`);

	// Remove Diagnostics corresponding to this error
	const removeAll = true;
	docManager.removeDiagnostics([diagnostic], textDocument.uri, removeAll);

	// Lint again all open documents
	docManager.lintAgainAllOpenDocuments();

	// Show message to user and propose to open the configuration file
	const msg: ShowMessageRequestParams = {
		type: MessageType.Info,
		message: `Disabled rule ${errorCode} in config file`,
		actions: [
			{ title: "Open" }
		]
	};
	try {
		const req = await docManager.connection.sendRequest('window/showMessageRequest', msg);
		if (req.title === "Open") {
			await docManager.connection.sendNotification(OpenNotification.type, { file: configFilePath });
		}
	} catch (e) {
		debug(`Error with window/showMessageRequest or Opening config file: ${e.message}`);
	}
}