#!/usr/bin/env node
"use strict";

const path = require("path");
const open = require("opn");
const fs = require("fs");
const net = require("net");
const portfinder = require("portfinder");
const addDevServerEntrypoints = require("../lib/util/addDevServerEntrypoints");
const createDomain = require("../lib/util/createDomain");

// Local version replaces global one
try {
	const localWebpackDevServer = require.resolve(path.join(process.cwd(), "node_modules", "webpack-dev-server", "bin", "webpack-dev-server.js"));
	if(__filename !== localWebpackDevServer) {
		return require(localWebpackDevServer);
	}
} catch(e) {}

const Server = require("../lib/Server");
const webpack = require("webpack");

function versionInfo() {
	return `webpack-dev-server ${require("../package.json").version}\n` +
		`webpack ${require("webpack/package.json").version}`;
}

function colorInfo(useColor, msg) {
	if(useColor)
		// Make text blue and bold, so it *pops*
		return `\u001b[1m\u001b[34m${msg}\u001b[39m\u001b[22m`;
	return msg;
}

function colorError(useColor, msg) {
	if(useColor)
		// Make text red and bold, so it *pops*
		return `\u001b[1m\u001b[31m${msg}\u001b[39m\u001b[22m`;
	return msg;
}

const yargs = require("yargs")
	.usage(`${versionInfo()
		}\nUsage: https://webpack.js.org/configuration/dev-server/`);

require("webpack/bin/config-yargs")(yargs);

// It is important that this is done after the webpack yargs config,
// so it overrides webpack's version info.
yargs.version(versionInfo);

const ADVANCED_GROUP = "Advanced options:";
const DISPLAY_GROUP = "Stats options:";
const SSL_GROUP = "SSL options:";
const CONNECTION_GROUP = "Connection options:";
const RESPONSE_GROUP = "Response options:";
const BASIC_GROUP = "Basic options:";

// Taken out of yargs because we must know if
// it wasn't given by the user, in which case
// we should use portfinder.
const DEFAULT_PORT = 8080;

yargs.options({
	"lazy": {
		type: "boolean",
		describe: "Lazy"
	},
	"inline": {
		type: "boolean",
		default: true,
		describe: "Inline mode (set to false to disable including client scripts like livereload)"
	},
	"progress": {
		type: "boolean",
		describe: "Print compilation progress in percentage",
		group: BASIC_GROUP
	},
	"hot-only": {
		type: "boolean",
		describe: "Do not refresh page if HMR fails",
		group: ADVANCED_GROUP
	},
	"stdin": {
		type: "boolean",
		describe: "close when stdin ends"
	},
	"open": {
		type: "boolean",
		describe: "Open default browser"
	},
	"color": {
		type: "boolean",
		alias: "colors",
		default: function supportsColor() {
			return require("supports-color");
		},
		group: DISPLAY_GROUP,
		describe: "Enables/Disables colors on the console"
	},
	"info": {
		type: "boolean",
		group: DISPLAY_GROUP,
		default: true,
		describe: "Info"
	},
	"quiet": {
		type: "boolean",
		group: DISPLAY_GROUP,
		describe: "Quiet"
	},
	"client-log-level": {
		type: "string",
		group: DISPLAY_GROUP,
		default: "info",
		describe: "Log level in the browser (info, warning, error or none)"
	},
	"https": {
		type: "boolean",
		group: SSL_GROUP,
		describe: "HTTPS"
	},
	"key": {
		type: "string",
		describe: "Path to a SSL key.",
		group: SSL_GROUP
	},
	"cert": {
		type: "string",
		describe: "Path to a SSL certificate.",
		group: SSL_GROUP
	},
	"cacert": {
		type: "string",
		describe: "Path to a SSL CA certificate.",
		group: SSL_GROUP
	},
	"pfx": {
		type: "string",
		describe: "Path to a SSL pfx file.",
		group: SSL_GROUP
	},
	"pfx-passphrase": {
		type: "string",
		describe: "Passphrase for pfx file.",
		group: SSL_GROUP
	},
	"content-base": {
		type: "string",
		describe: "A directory or URL to serve HTML content from.",
		group: RESPONSE_GROUP
	},
	"watch-content-base": {
		type: "boolean",
		describe: "Enable live-reloading of the content-base.",
		group: RESPONSE_GROUP
	},
	"history-api-fallback": {
		type: "boolean",
		describe: "Fallback to /index.html for Single Page Applications.",
		group: RESPONSE_GROUP
	},
	"compress": {
		type: "boolean",
		describe: "Enable gzip compression",
		group: RESPONSE_GROUP
	},
	"port": {
		describe: "The port",
		group: CONNECTION_GROUP
	},
	"socket": {
		type: "String",
		describe: "Socket to listen",
		group: CONNECTION_GROUP
	},
	"public": {
		type: "string",
		describe: "The public hostname/ip address of the server",
		group: CONNECTION_GROUP
	},
	"host": {
		type: "string",
		default: "localhost",
		describe: "The hostname/ip address the server will bind to",
		group: CONNECTION_GROUP
	}
});

const argv = yargs.argv;

const wpOpt = require("webpack/bin/convert-argv")(yargs, argv, {
	outputFilename: "/bundle.js"
});

function processOptions(wpOpt) {
	// process Promise
	if(typeof wpOpt.then === "function") {
		wpOpt.then(processOptions).catch(function(err) {
			console.error(err.stack || err);
			process.exit(); // eslint-disable-line
		});
		return;
	}

	const firstWpOpt = Array.isArray(wpOpt) ? wpOpt[0] : wpOpt;

	const options = wpOpt.devServer || firstWpOpt.devServer || {};

	if(argv.host !== "localhost" || !options.host)
		options.host = argv.host;

	if(argv.public)
		options.public = argv.public;

	if(argv.socket)
		options.socket = argv.socket;

	if(!options.publicPath) {
		options.publicPath = firstWpOpt.output && firstWpOpt.output.publicPath || "";
		if(!/^(https?:)?\/\//.test(options.publicPath) && options.publicPath[0] !== "/")
			options.publicPath = `/${options.publicPath}`;
	}

	if(!options.filename)
		options.filename = firstWpOpt.output && firstWpOpt.output.filename;

	if(!options.watchOptions)
		options.watchOptions = firstWpOpt.watchOptions;

	if(argv["stdin"]) {
		process.stdin.on("end", function() {
			process.exit(0); // eslint-disable-line no-process-exit
		});
		process.stdin.resume();
	}

	if(!options.hot)
		options.hot = argv["hot"];

	if(!options.hotOnly)
		options.hotOnly = argv["hot-only"];

	if(!options.clientLogLevel)
		options.clientLogLevel = argv["client-log-level"];

	if(options.contentBase === undefined) {
		if(argv["content-base"]) {
			options.contentBase = argv["content-base"];
			if(Array.isArray(options.contentBase)) {
				options.contentBase = options.contentBase.map(function(val) {
					return path.resolve(val);
				});
			} else if(/^[0-9]$/.test(options.contentBase))
				options.contentBase = +options.contentBase;
			else if(!/^(https?:)?\/\//.test(options.contentBase))
				options.contentBase = path.resolve(options.contentBase);
		// It is possible to disable the contentBase by using `--no-content-base`, which results in arg["content-base"] = false
		} else if(argv["content-base"] === false) {
			options.contentBase = false;
		}
	}

	if(argv["watch-content-base"])
		options.watchContentBase = true;

	if(!options.stats) {
		options.stats = {
			cached: false,
			cachedAssets: false
		};
	}

	if(typeof options.stats === "object" && typeof options.stats.colors === "undefined")
		options.stats.colors = argv.color;

	if(argv["lazy"])
		options.lazy = true;

	if(!argv["info"])
		options.noInfo = true;

	if(argv["quiet"])
		options.quiet = true;

	if(argv["https"])
		options.https = true;

	if(argv["cert"])
		options.cert = fs.readFileSync(path.resolve(argv["cert"]));

	if(argv["key"])
		options.key = fs.readFileSync(path.resolve(argv["key"]));

	if(argv["cacert"])
		options.ca = fs.readFileSync(path.resolve(argv["cacert"]));

	if(argv["pfx"])
		options.pfx = fs.readFileSync(path.resolve(argv["pfx"]));

	if(argv["pfx-passphrase"])
		options.pfxPassphrase = argv["pfx-passphrase"];

	if(argv["inline"] === false)
		options.inline = false;

	if(argv["history-api-fallback"])
		options.historyApiFallback = true;

	if(argv["compress"])
		options.compress = true;

	if(argv["open"])
		options.open = true;

	// Kind of weird, but ensures prior behavior isn't broken in cases
	// that wouldn't throw errors. E.g. both argv.port and options.port
	// were specified, but since argv.port is 8080, options.port will be
	// tried first instead.
	options.port = argv.port === DEFAULT_PORT ? (options.port || argv.port) : (argv.port || options.port);
	if(options.port) {
		startDevServer(wpOpt, options);
		return;
	}

	portfinder.basePort = DEFAULT_PORT;
	portfinder.getPort(function(err, port) {
		if(err) throw err;
		options.port = port;
		startDevServer(wpOpt, options);
	});
}

function startDevServer(wpOpt, options) {
	addDevServerEntrypoints(wpOpt, options);

	let compiler;
	try {
		compiler = webpack(wpOpt);
	} catch(e) {
		if(e instanceof webpack.WebpackOptionsValidationError) {
			console.error(colorError(options.stats.colors, e.message));
			process.exit(1); // eslint-disable-line
		}
		throw e;
	}

	if(argv["progress"]) {
		compiler.apply(new webpack.ProgressPlugin({
			profile: argv["profile"]
		}));
	}

	const uri = createDomain(options) + (options.inline !== false || options.lazy === true ? "/" : "/webpack-dev-server/");

	let server;
	try {
		server = new Server(compiler, options);
	} catch(e) {
		const OptionsValidationError = require("../lib/OptionsValidationError");
		if(e instanceof OptionsValidationError) {
			console.error(colorError(options.stats.colors, e.message));
			process.exit(1); // eslint-disable-line
		}
		throw e;
	}

	["SIGINT", "SIGTERM"].forEach(function(sig) {
		process.on(sig, function() {
			server.close();
			process.exit(); // eslint-disable-line no-process-exit
		});
	});

	if(options.socket) {
		server.listeningApp.on("error", function(e) {
			if(e.code === "EADDRINUSE") {
				const clientSocket = new net.Socket();
				clientSocket.on("error", function(e) {
					if(e.code === "ECONNREFUSED") {
						// No other server listening on this socket so it can be safely removed
						fs.unlinkSync(options.socket);
						server.listen(options.socket, options.host, function(err) {
							if(err) throw err;
						});
					}
				});
				clientSocket.connect({ path: options.socket }, function() {
					throw new Error("This socket is already used");
				});
			}
		});
		server.listen(options.socket, options.host, function(err) {
			if(err) throw err;
			const READ_WRITE = 438; // chmod 666 (rw rw rw)
			fs.chmod(options.socket, READ_WRITE, function(err) {
				if(err) throw err;
				reportReadiness(uri, options);
			});
		});
	} else {
		server.listen(options.port, options.host, function(err) {
			if(err) throw err;
			reportReadiness(uri, options);
		});
	}
}

function reportReadiness(uri, options) {
	const useColor = argv.color;
	let startSentence = `Project is running at ${colorInfo(useColor, uri)}`
	if(options.socket) {
		startSentence = `Listening to socket at ${colorInfo(useColor, options.socket)}`;
	}
	console.log((argv["progress"] ? "\n" : "") + startSentence);

	console.log(`webpack output is served from ${colorInfo(useColor, options.publicPath)}`);
	const contentBase = Array.isArray(options.contentBase) ? options.contentBase.join(", ") : options.contentBase;
	if(contentBase)
		console.log(`Content not from webpack is served from ${colorInfo(useColor, contentBase)}`);
	if(options.historyApiFallback)
		console.log(`404s will fallback to ${colorInfo(useColor, options.historyApiFallback.index || "/index.html")}`);
	if(options.open) {
		open(uri).catch(function() {
			console.log("Unable to open browser. If you are running in a headless environment, please do not use the open flag.");
		});
	}
}

processOptions(wpOpt);
