/*eslint-disable no-console */
const path = require('path');
const fs = require('fs');
const babel = require('babel-core');
const less = require('less');
const chokidar = require('chokidar');
const path_join = path.resolve;

// for file watcher
const app = require('express')();
const http = require('http').Server(app);
const io = require('socket.io')(http);
const touch = require("touch");

// basic setup
const sites_path = path_join(__dirname, '..', '..', '..', 'sites');
const apps_path = path_join(__dirname, '..', '..', '..', 'apps'); // the apps folder
const apps_contents = fs.readFileSync(path_join(sites_path, 'apps.txt'), 'utf8');
const apps = apps_contents.split('\n');
const app_paths = apps.map(app => path_join(apps_path, app, app)) // base_path of each app
const assets_path = path_join(sites_path, 'assets');
let build_map = make_build_map();
let compiled_js_cache = {}; // cache each js file after it is compiled
const file_watcher_port = get_conf().file_watcher_port;

// command line args
const action = process.argv[2] || '--build';

if (['--build', '--watch'].indexOf(action) === -1) {
	console.log('Invalid argument: ', action);
	process.exit();
}

if (action === '--build') {
	const minify = process.argv[3] === '--minify' ? true : false;
	build(minify);
}

if (action === '--watch') {
	watch();
}

function build(minify) {
	for (const output_path in build_map) {
		pack(output_path, build_map[output_path], minify);
	}
	touch(path_join(sites_path, '.build'), {force:true});
}

let socket_connection = false;

function watch() {
	http.listen(file_watcher_port, function () {
		console.log('file watching on *:', file_watcher_port);
	});

	if (process.env.CI) {
		// don't watch inside CI
		return;
	}

	compile_less().then(() => {
		build();
		watch_less(function (filename) {
			if(socket_connection) {
				io.emit('reload_css', filename);
			}
		});
		watch_js(//function (filename) {
			// if(socket_connection) {
			// 	io.emit('reload_js', filename);
			// }
		//}
		);
		watch_build_json();
	});

	io.on('connection', function (socket) {
		socket_connection = true;

		socket.on('disconnect', function() {
			socket_connection = false;
		})
	});
}

function pack(output_path, inputs, minify, file_changed) {
	let output_txt = '';
	for (const file of inputs) {

		if (!fs.existsSync(file)) {
			console.log('File not found: ', file);
			continue;
		}

		let force_compile = false;
		if (file_changed) {
			// if file_changed is passed and is equal to file, force_compile it
			force_compile = file_changed === file;
		}

		let file_content = get_compiled_file(file, output_path, minify, force_compile);

		if(!minify) {
			output_txt += `\n/*\n *\t${file}\n */\n`
		}
		output_txt += file_content;
		output_txt = output_txt.replace(/['"]use strict['"];/, '');
	}

	const target = path_join(assets_path, output_path);

	try {
		fs.writeFileSync(target, output_txt);
		console.log(`Wrote ${output_path} - ${get_file_size(target)}`);
		return target;
	} catch (e) {
		console.log('Error writing to file', output_path);
		console.log(e);
	}
}

function get_compiled_file(file, output_path, minify, force_compile) {
	const output_type = output_path.split('.').pop();

	let file_content;

	if (force_compile === false) {
		// force compile is false
		// attempt to get from cache
		file_content = compiled_js_cache[file];
		if (file_content) {
			return file_content;
		}
	}

	file_content = fs.readFileSync(file, 'utf-8');

	if (file.endsWith('.html') && output_type === 'js') {
		file_content = html_to_js_template(file, file_content);
	}

	if(file.endsWith('class.js')) {
		file_content = minify_js(file_content, file);
	}

	if (minify && file.endsWith('.js') && !file.includes('/lib/') && output_type === 'js' && !file.endsWith('class.js')) {
		file_content = babelify(file_content, file, minify);
	}

	compiled_js_cache[file] = file_content;
	return file_content;
}

function babelify(content, path, minify) {
	let presets = ['env'];
	const plugins = ['transform-object-rest-spread']
	// Minification doesn't work when loading Frappe Desk
	// Avoid for now, trace the error and come back.
	try {
		return babel.transform(content, {
			presets: presets,
			plugins: plugins,
			comments: false
		}).code;
	} catch (e) {
		console.log('Cannot babelify', path);
		console.log(e);
		return content;
	}
}

function minify_js(content, path) {
	try {
		return babel.transform(content, {
			comments: false
		}).code;
	} catch (e) {
		console.log('Cannot minify', path);
		console.log(e);
		return content;
	}
}

function make_build_map() {
	const build_map = {};
	for (const app_path of app_paths) {
		const build_json_path = path_join(app_path, 'public', 'build.json');
		if (!fs.existsSync(build_json_path)) continue;

		let build_json = fs.readFileSync(build_json_path);
		try {
			build_json = JSON.parse(build_json);
		} catch (e) {
			console.log(e);
			continue;
		}

		for (const target in build_json) {
			const sources = build_json[target];

			const new_sources = [];
			for (const source of sources) {
				const s = path_join(app_path, source);
				new_sources.push(s);
			}

			if (new_sources.length)
				build_json[target] = new_sources;
			else
				delete build_json[target];
		}

		Object.assign(build_map, build_json);
	}
	return build_map;
}

function compile_less() {
	return new Promise(function (resolve) {
		const promises = [];
		for (const app_path of app_paths) {
			const public_path = path_join(app_path, 'public');
			const less_path = path_join(public_path, 'less');
			if (!fs.existsSync(less_path)) continue;

			const files = fs.readdirSync(less_path);
			for (const file of files) {
				if(file.includes('variables.less')) continue;
				promises.push(compile_less_file(file, less_path, public_path))
			}
		}

		Promise.all(promises).then(() => {
			console.log('Less files compiled');
			resolve();
		});
	});
}

function compile_less_file(file, less_path, public_path) {
	const file_content = fs.readFileSync(path_join(less_path, file), 'utf8');
	const output_file = file.split('.')[0] + '.css';
	console.log('compiling', file);

	return less.render(file_content, {
		paths: [less_path],
		filename: file,
		sourceMap: false
	}).then(output => {
		const out_css = path_join(public_path, 'css', output_file);
		fs.writeFileSync(out_css, output.css);
		return out_css;
	}).catch(e => {
		console.log('Error compiling ', file);
		console.log(e);
	});
}

function watch_less(ondirty) {
	const less_paths = app_paths.map(path => path_join(path, 'public', 'less'));

	const to_watch = filter_valid_paths(less_paths);
	chokidar.watch(to_watch).on('change', (filename) => {
		console.log(filename, 'dirty');
		var last_index = filename.lastIndexOf('/');
		const less_path = filename.slice(0, last_index);
		const public_path = path_join(less_path, '..');
		filename = filename.split('/').pop();

		compile_less_file(filename, less_path, public_path)
			.then(css_file_path => {
				// build the target css file for which this css file is input
				for (const target in build_map) {
					const sources = build_map[target];
					if (sources.includes(css_file_path)) {
						pack(target, sources);
						ondirty && ondirty(target);
						break;
					}
				}
			});
		touch(path_join(sites_path, '.build'), {force:true});
	});
}

function watch_js(ondirty) {
	chokidar.watch([
		path_join(apps_path, '**', '*.js'),
		path_join(apps_path, '**', '*.html')
	]).on('change', (filename) => {
		// build the target js file for which this js/html file is input
		for (const target in build_map) {
			const sources = build_map[target];
			if (sources.includes(filename)) {
				console.log(filename, 'dirty');
				pack(target, sources, null, filename);
				ondirty && ondirty(target);
				// break;
			}
		}
		touch(path_join(sites_path, '.build'), {force:true});
	});
}

function watch_build_json() {
	const build_json_paths = app_paths.map(path => path_join(path, 'public', 'build.json'));
	const to_watch = filter_valid_paths(build_json_paths);
	chokidar.watch(to_watch).on('change', (filename) => {
		console.log(filename, 'updated');
		build_map = make_build_map();
	});
}

function filter_valid_paths(paths) {
	return paths.filter(path => fs.existsSync(path));
}

function html_to_js_template(path, content) {
	let key = path.split('/');
	key = key[key.length - 1];
	key = key.split('.')[0];

	content = scrub_html_template(content);
	return `frappe.templates['${key}'] = '${content}';\n`;
}

function scrub_html_template(content) {
	content = content.replace(/\s/g, ' ');
	content = content.replace(/(<!--.*?-->)/g, '');
	return content.replace("'", "\'");
}

function get_file_size(filepath) {
	const stats = fs.statSync(filepath);
	const size = stats.size;
	// convert it to humanly readable format.
	const i = Math.floor(Math.log(size) / Math.log(1024));
	return (size / Math.pow(1024, i)).toFixed(2) * 1 + ' ' + ['B', 'KB', 'MB', 'GB', 'TB'][i];
}

function get_conf() {
	// defaults
	var conf = {
		file_watcher_port: 6787
	};

	var read_config = function(path) {
		if (!fs.existsSync(path)) return;
		var bench_config = JSON.parse(fs.readFileSync(path));
		for (var key in bench_config) {
			if (bench_config[key]) {
				conf[key] = bench_config[key];
			}
		}
	}

	read_config(path_join(sites_path, 'common_site_config.json'));
	return conf;
}
