"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
const tslib_1 = require("tslib");
const fs = require("fs");
const path = require("path");
const chalk = require("chalk");
const errors_1 = require("../errors");
exports.WATCH_PATTERNS = [
    'www/**/*',
    '!www/lib/**/*',
    '!www/**/*.map'
];
exports.LOGGER_DIR = '__ion-dev-server';
exports.IONIC_LAB_URL = '/ionic-lab';
exports.DEFAULT_ADDRESS = '0.0.0.0';
exports.DEFAULT_LIVERELOAD_PORT = 35729;
exports.DEFAULT_SERVER_PORT = 8100;
exports.IOS_PLATFORM_PATH = path.join('platforms', 'ios', 'www');
exports.ANDROID_PLATFORM_PATH = path.join('platforms', 'android', 'assets', 'www');
function serve(args) {
    return tslib_1.__awaiter(this, void 0, void 0, function* () {
        const { str2num } = yield Promise.resolve().then(function () { return require('../utils/string'); });
        const { minimistOptionsToArray } = yield Promise.resolve().then(function () { return require('../utils/command'); });
        const { ERROR_NETWORK_ADDRESS_NOT_AVAIL, findClosestOpenPort, getAvailableIPAddress } = yield Promise.resolve().then(function () { return require('../utils/network'); });
        const address = args.options['address'] || exports.DEFAULT_ADDRESS;
        const locallyAccessible = ['0.0.0.0', 'localhost', '127.0.0.1'].includes(address);
        let externalIP = address;
        if (address === '0.0.0.0') {
            // Find appropriate IP to use for cordova to reference
            const availableIPs = getAvailableIPAddress();
            if (availableIPs.length === 0) {
                throw new Error(`It appears that you do not have any external network interfaces. ` +
                    `In order to use livereload with emulate you will need one.`);
            }
            externalIP = availableIPs[0].address;
            if (availableIPs.length > 1) {
                args.env.log.warn(`${chalk.bold('Multiple network interfaces detected!')}\n` +
                    'You will be prompted to select an external-facing IP for the livereload server that your device or emulator has access to.\n' +
                    `You may also use the ${chalk.green('--address')} option to skip this prompt.\n`);
                const promptedIp = yield args.env.prompt({
                    type: 'list',
                    name: 'promptedIp',
                    message: 'Please select which IP to use:',
                    choices: availableIPs.map(ip => ip.address)
                });
                externalIP = promptedIp;
            }
        }
        const serverArgs = minimistOptionsToArray(args.options);
        args.env.log.info(`Starting server: ${chalk.bold(serverArgs.join(' '))} - Ctrl+C to cancel`);
        const projectConfig = yield args.env.project.load();
        // Setup Options and defaults
        const serverOptions = {
            projectRoot: args.env.project.directory,
            wwwDir: path.join(args.env.project.directory, projectConfig.documentRoot || 'www'),
            address,
            protocol: 'http',
            externalAddress: externalIP,
            port: str2num(args.options['port'], exports.DEFAULT_SERVER_PORT),
            httpPort: str2num(args.options['port'], exports.DEFAULT_SERVER_PORT),
            livereloadPort: str2num(args.options['livereload-port'], exports.DEFAULT_LIVERELOAD_PORT),
            browser: args.options['browser'],
            browseroption: args.options['browseroption'],
            platform: args.options['platform'],
            consolelogs: args.options['consolelogs'] || false,
            serverlogs: args.options['serverlogs'] || false,
            nobrowser: args.options['nobrowser'] || false,
            nolivereload: args.options['nolivereload'] || false,
            noproxy: args.options['noproxy'] || false,
            lab: args.options['lab'] || false,
            iscordovaserve: args.options['iscordovaserve'] || false,
        };
        // Clean up args based on environment state
        try {
            const portResults = yield Promise.all([
                findClosestOpenPort(serverOptions.address, serverOptions.port),
                findClosestOpenPort(serverOptions.address, serverOptions.livereloadPort),
            ]);
            serverOptions.port = serverOptions.httpPort = portResults[0];
            serverOptions.livereloadPort = portResults[1];
        }
        catch (e) {
            if (e !== ERROR_NETWORK_ADDRESS_NOT_AVAIL) {
                throw e;
            }
            throw new errors_1.FatalException(`${chalk.green(serverOptions.address)} is not available--cannot bind.`);
        }
        // Start up server
        const settings = yield setupServer(args.env, serverOptions);
        const localAddress = 'http://localhost:' + serverOptions.port;
        const externalAddress = 'http://' + serverOptions.externalAddress + ':' + serverOptions.port;
        const externallyAccessible = localAddress !== externalAddress;
        args.env.log.info(`Development server running\n` +
            (locallyAccessible ? `Local: ${chalk.bold(localAddress)}\n` : '') +
            (externallyAccessible ? `External: ${chalk.bold(externalAddress)}\n` : ''));
        if (locallyAccessible && !serverOptions.nobrowser) {
            const openOptions = [localAddress]
                .concat(serverOptions.lab ? [exports.IONIC_LAB_URL] : [])
                .concat(serverOptions.browseroption ? [serverOptions.browseroption] : [])
                .concat(serverOptions.platform ? ['?ionicplatform=', serverOptions.platform] : []);
            const opn = yield Promise.resolve().then(function () { return require('opn'); });
            opn(openOptions.join(''));
        }
        return Object.assign({ publicIp: serverOptions.externalAddress, localAddress: 'localhost', locallyAccessible,
            externallyAccessible }, settings);
    });
}
exports.serve = serve;
function setupServer(env, options) {
    return tslib_1.__awaiter(this, void 0, void 0, function* () {
        const liveReloadBrowser = yield createLiveReloadServer(options);
        yield createHttpServer(env, options);
        const chokidar = yield Promise.resolve().then(function () { return require('chokidar'); });
        const projectConfig = yield env.project.load();
        if (!projectConfig.watchPatterns) {
            projectConfig.watchPatterns = [];
        }
        const watchPatterns = [...new Set([...projectConfig.watchPatterns, ...exports.WATCH_PATTERNS])];
        env.log.debug(() => `Watch patterns: ${watchPatterns.map(v => chalk.bold(v)).join(', ')}`);
        const watcher = chokidar.watch(watchPatterns, { cwd: env.project.directory });
        env.events.emit('watch:init');
        watcher.on('change', (filePath) => {
            env.log.info(`[${new Date().toTimeString().slice(0, 8)}] ${chalk.bold(filePath)} changed`);
            liveReloadBrowser([filePath]);
            env.events.emit('watch:change', filePath);
        });
        watcher.on('error', (err) => {
            env.log.error(err.toString());
        });
        return options;
    });
}
function createLiveReloadServer(options) {
    return tslib_1.__awaiter(this, void 0, void 0, function* () {
        const tinylr = yield Promise.resolve().then(function () { return require('tiny-lr'); });
        const liveReloadServer = tinylr();
        liveReloadServer.listen(options.livereloadPort);
        return (changedFiles) => {
            liveReloadServer.changed({
                body: {
                    files: changedFiles.map(changedFile => ('/' + path.relative(options.wwwDir, changedFile)))
                }
            });
        };
    });
}
exports.createLiveReloadServer = createLiveReloadServer;
function injectLiveReloadScript(content, host, port) {
    let contentStr = content.toString();
    const liveReloadScript = getLiveReloadScript(host, port);
    if (contentStr.indexOf('/livereload.js') > -1) {
        // already added script
        return content;
    }
    let match = contentStr.match(/<\/body>(?![\s\S]*<\/body>)/i);
    if (!match) {
        match = contentStr.match(/<\/html>(?![\s\S]*<\/html>)/i);
    }
    if (match) {
        contentStr = contentStr.replace(match[0], `${liveReloadScript}\n${match[0]}`);
    }
    else {
        contentStr += liveReloadScript;
    }
    return contentStr;
}
exports.injectLiveReloadScript = injectLiveReloadScript;
function getLiveReloadScript(host, port) {
    if (host === '0.0.0.0') {
        host = 'localhost';
    }
    const src = `//${host}:${port}/livereload.js?snipver=1`;
    return `  <!-- Ionic Dev Server: Injected LiveReload Script -->\n` +
        `  <script src="${src}" async="" defer=""></script>`;
}
/**
 * Create HTTP server
 */
function createHttpServer(env, options) {
    return tslib_1.__awaiter(this, void 0, void 0, function* () {
        const express = yield Promise.resolve().then(function () { return require('express'); });
        const app = express();
        app.set('serveOptions', options);
        app.listen(options.port, options.address);
        app.get('/', serveIndex);
        app.use('/', express.static(options.wwwDir));
        // Lab routes
        app.use(exports.IONIC_LAB_URL + '/static', express.static(path.join(__dirname, '..', '..', 'assets', 'ionic1', 'lab', 'static')));
        app.get(exports.IONIC_LAB_URL, (req, res) => res.sendFile('index.html', { root: path.join(__dirname, '..', '..', 'assets', 'ionic1', 'lab') }));
        app.get(exports.IONIC_LAB_URL + '/api/v1/cordova', (req, res) => tslib_1.__awaiter(this, void 0, void 0, function* () {
            const [info] = yield env.hooks.fire('cordova:project:info', { env });
            if (info) {
                res.json(info);
            }
            else {
                res.status(400).json({ status: 'error', message: 'Unable to load config.xml' });
            }
        }));
        app.get('/cordova.js', servePlatformResource, serveMockCordovaJS);
        app.get('/cordova_plugins.js', servePlatformResource);
        app.get('/plugins/*', servePlatformResource);
        if (!options.noproxy) {
            yield setupProxies(env.project, app);
        }
        return app;
    });
}
exports.createHttpServer = createHttpServer;
function setupProxies(project, app) {
    return tslib_1.__awaiter(this, void 0, void 0, function* () {
        const url = yield Promise.resolve().then(function () { return require('url'); });
        const projectConfig = yield project.load();
        for (const proxy of projectConfig.proxies || []) {
            let opts = url.parse(proxy.proxyUrl);
            if (proxy.proxyNoAgent) {
                opts.agent = false;
            }
            opts.rejectUnauthorized = !(proxy.rejectUnauthorized === false);
            const proxyMiddleware = yield Promise.resolve().then(function () { return require('proxy-middleware'); });
            app.use(proxy.path, proxyMiddleware(opts));
            console.log('Proxy added:' + proxy.path + ' => ' + url.format(opts));
        }
    });
}
/**
 * http responder for /index.html base entrypoint
 */
function serveIndex(req, res) {
    const options = req.app.get('serveOptions');
    // respond with the index.html file
    const indexFileName = path.join(options.wwwDir, 'index.html');
    fs.readFile(indexFileName, (err, indexHtml) => {
        if (!options.nolivereload) {
            indexHtml = injectLiveReloadScript(indexHtml, options.externalAddress, options.livereloadPort);
        }
        res.set('Content-Type', 'text/html');
        res.send(indexHtml);
    });
}
/**
 * http responder for cordova.js file
 */
function serveMockCordovaJS(req, res) {
    res.set('Content-Type', 'application/javascript');
    res.send('// mock cordova file during development');
}
/**
 * Middleware to serve platform resources
 */
function servePlatformResource(req, res, next) {
    const options = req.app.get('serveOptions');
    const userAgent = req.header('user-agent') || '';
    let resourcePath = options.wwwDir;
    if (!options.iscordovaserve) {
        return next();
    }
    if (isUserAgentIOS(userAgent)) {
        resourcePath = path.join(options.projectRoot, exports.IOS_PLATFORM_PATH);
    }
    else if (isUserAgentAndroid(userAgent)) {
        resourcePath = path.join(options.projectRoot, exports.ANDROID_PLATFORM_PATH);
    }
    fs.stat(path.join(resourcePath, req.url), (err, stats) => {
        if (err) {
            return next();
        }
        res.sendFile(req.url, { root: resourcePath });
    });
}
function isUserAgentIOS(ua) {
    ua = ua.toLowerCase();
    return (ua.indexOf('iphone') > -1 || ua.indexOf('ipad') > -1 || ua.indexOf('ipod') > -1);
}
function isUserAgentAndroid(ua) {
    ua = ua.toLowerCase();
    return ua.indexOf('android') > -1;
}
