import * as jsdom from 'jsdom';
import * as assert from 'assert';
import * as glob from 'tiny-glob/sync.js';
import * as fs from 'fs';
import * as colors from 'kleur';

// for coverage purposes, we need to test source files,
// but for sanity purposes, we need to test dist files
export function loadSvelte(test) {
	process.env.TEST = test ? 'true' : '';

	const resolved = require.resolve('../compiler.js');

	delete require.cache[resolved];
	return require(resolved);
}

export const svelte = loadSvelte();

export function exists(path) {
	try {
		fs.statSync(path);
		return true;
	} catch (err) {
		return false;
	}
}

export function tryToLoadJson(file) {
	try {
		return JSON.parse(fs.readFileSync(file, 'utf-8'));
	} catch (err) {
		if (err.code !== 'ENOENT') throw err;
		return null;
	}
}

export function tryToReadFile(file) {
	try {
		return fs.readFileSync(file, 'utf-8');
	} catch (err) {
		if (err.code !== 'ENOENT') throw err;
		return null;
	}
}

const virtualConsole = new jsdom.VirtualConsole();
virtualConsole.sendTo(console);

global.window = new jsdom.JSDOM('<main></main>', {virtualConsole}).window;
global.document = window.document;
global.navigator = window.navigator;
global.getComputedStyle = window.getComputedStyle;
global.requestAnimationFrame = null; // placeholder, filled in using set_raf

// add missing ecmascript globals to window
for (const key of Object.getOwnPropertyNames(global)) {
	window[key] = window[key] || global[key];
}

export function env() {
	window.document.title = '';
	window.document.body.innerHTML = '<main></main>';

	return window;
}

function cleanChildren(node) {
	let previous = null;

	// sort attributes
	const attributes = Array.from(node.attributes).sort((a, b) => {
		return a.name < b.name ? -1 : 1;
	});

	attributes.forEach(attr => {
		node.removeAttribute(attr.name);
	});

	attributes.forEach(attr => {
		node.setAttribute(attr.name, attr.value);
	});

	// recurse
	[...node.childNodes].forEach(child => {
		if (child.nodeType === 3) {
			// text
			if (
				node.namespaceURI === 'http://www.w3.org/2000/svg' &&
				node.tagName !== 'text' &&
				node.tagName !== 'tspan'
			) {
				node.removeChild(child);
			}

			child.data = child.data.replace(/\s+/g, '\n');

			if (previous && previous.nodeType === 3) {
				previous.data += child.data;
				previous.data = previous.data.replace(/\s+/g, '\n');

				node.removeChild(child);
				child = previous;
			}
		} else {
			cleanChildren(child);
		}

		previous = child;
	});

	// collapse whitespace
	if (node.firstChild && node.firstChild.nodeType === 3) {
		node.firstChild.data = node.firstChild.data.replace(/^\s+/, '');
		if (!node.firstChild.data) node.removeChild(node.firstChild);
	}

	if (node.lastChild && node.lastChild.nodeType === 3) {
		node.lastChild.data = node.lastChild.data.replace(/\s+$/, '');
		if (!node.lastChild.data) node.removeChild(node.lastChild);
	}
}

export function normalizeHtml(window, html) {
	try {
		const node = window.document.createElement('div');
		node.innerHTML = html
			.replace(/<!--.*?-->/g, '')
			.replace(/>[\s\r\n]+</g, '><')
			.trim();
		cleanChildren(node);
		return node.innerHTML.replace(/<\/?noscript\/?>/g, '');
	} catch (err) {
		throw new Error(`Failed to normalize HTML:\n${html}`);
	}
}

export function setupHtmlEqual() {
	const window = env();

	assert.htmlEqual = (actual, expected, message) => {
		assert.deepEqual(
			normalizeHtml(window, actual),
			normalizeHtml(window, expected),
			message
		);
	};
}

export function loadConfig(file) {
	try {
		const resolved = require.resolve(file);
		delete require.cache[resolved];

		const config = require(resolved);
		return config.default || config;
	} catch (err) {
		if (err.code === 'MODULE_NOT_FOUND') {
			return {};
		}

		throw err;
	}
}

export function addLineNumbers(code) {
	return code
		.split('\n')
		.map((line, i) => {
			i = String(i + 1);
			while (i.length < 3) i = ` ${i}`;

			return (
				colors.gray(`  ${i}: `) +
				line.replace(/^\t+/, match => match.split('\t').join('    '))
			);
		})
		.join('\n');
}

export function showOutput(cwd, options = {}, compile = svelte.compile) {
	glob('**/*.svelte', { cwd }).forEach(file => {
		if (file[0] === '_') return;

		const { js } = compile(
			fs.readFileSync(`${cwd}/${file}`, 'utf-8'),
			Object.assign(options, {
				filename: file
			})
		);

		console.log( // eslint-disable-line no-console
			`\n>> ${colors.cyan().bold(file)}\n${addLineNumbers(js.code)}\n<< ${colors.cyan().bold(file)}`
		);
	});
}

const start = /\n(\t+)/;
export function deindent(strings, ...values) {
	const indentation = start.exec(strings[0])[1];
	const pattern = new RegExp(`^${indentation}`, 'gm');

	let result = strings[0].replace(start, '').replace(pattern, '');

	let trailingIndentation = getTrailingIndentation(result);

	for (let i = 1; i < strings.length; i += 1) {
		let expression = values[i - 1];
		const string = strings[i].replace(pattern, '');

		if (Array.isArray(expression)) {
			expression = expression.length ? expression.join('\n') : null;
		}

		if (expression || expression === '') {
			const value = String(expression).replace(
				/\n/g,
				`\n${trailingIndentation}`
			);
			result += value + string;
		} else {
			let c = result.length;
			while (/\s/.test(result[c - 1])) c -= 1;
			result = result.slice(0, c) + string;
		}

		trailingIndentation = getTrailingIndentation(result);
	}

	return result.trim().replace(/\t+$/gm, '');
}

function getTrailingIndentation(str) {
	let i = str.length;
	while (str[i - 1] === ' ' || str[i - 1] === '\t') i -= 1;
	return str.slice(i, str.length);
}

export function spaces(i) {
	let result = '';
	while (i--) result += ' ';
	return result;
}