"use strict";
var __decorate = (this && this.__decorate) || function (decorators, target, key, desc) {
    var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
    if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
    else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
    return c > 3 && r && Object.defineProperty(target, key, r), r;
};
var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
    return new (P || (P = Promise))(function (resolve, reject) {
        function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
        function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
        function step(result) { result.done ? resolve(result.value) : new P(function (resolve) { resolve(result.value); }).then(fulfilled, rejected); }
        step((generator = generator.apply(thisArg, _arguments || [])).next());
    });
};
Object.defineProperty(exports, "__esModule", { value: true });
const path = require("path");
const choki = require("chokidar");
const os_1 = require("os");
const events_1 = require("events");
const helpers_1 = require("../../common/helpers");
const constants_1 = require("../../constants");
const constants_2 = require("../../common/constants");
const decorators_1 = require("../../common/decorators");
const constants = require("../../constants");
const preview_app_constants_1 = require("./playground/preview-app-constants");
const decorators_2 = require("../../common/decorators");
const deviceDescriptorPrimaryKey = "identifier";
const LiveSyncEvents = {
    liveSyncStopped: "liveSyncStopped",
    liveSyncError: "liveSyncError",
    previewAppLiveSyncError: preview_app_constants_1.PreviewAppLiveSyncEvents.PREVIEW_APP_LIVE_SYNC_ERROR,
    liveSyncExecuted: "liveSyncExecuted",
    liveSyncStarted: "liveSyncStarted",
    liveSyncNotification: "notify"
};
class LiveSyncService extends events_1.EventEmitter {
    constructor($platformService, $projectDataService, $devicesService, $mobileHelper, $devicePlatformsConstants, $nodeModulesDependenciesBuilder, $logger, $processService, $hooksService, $pluginsService, $debugService, $errors, $debugDataService, $analyticsService, $usbLiveSyncService, $previewAppLiveSyncService, $previewQrCodeService, $previewSdkService, $hmrStatusService, $injector) {
        super();
        this.$platformService = $platformService;
        this.$projectDataService = $projectDataService;
        this.$devicesService = $devicesService;
        this.$mobileHelper = $mobileHelper;
        this.$devicePlatformsConstants = $devicePlatformsConstants;
        this.$nodeModulesDependenciesBuilder = $nodeModulesDependenciesBuilder;
        this.$logger = $logger;
        this.$processService = $processService;
        this.$hooksService = $hooksService;
        this.$pluginsService = $pluginsService;
        this.$debugService = $debugService;
        this.$errors = $errors;
        this.$debugDataService = $debugDataService;
        this.$analyticsService = $analyticsService;
        this.$usbLiveSyncService = $usbLiveSyncService;
        this.$previewAppLiveSyncService = $previewAppLiveSyncService;
        this.$previewQrCodeService = $previewQrCodeService;
        this.$previewSdkService = $previewSdkService;
        this.$hmrStatusService = $hmrStatusService;
        this.$injector = $injector;
        this.liveSyncProcessesInfo = {};
    }
    liveSync(deviceDescriptors, liveSyncData) {
        return __awaiter(this, void 0, void 0, function* () {
            const projectData = this.$projectDataService.getProjectData(liveSyncData.projectDir);
            this.handleWarnings(liveSyncData, projectData);
            yield this.$pluginsService.ensureAllDependenciesAreInstalled(projectData);
            yield this.liveSyncOperation(deviceDescriptors, liveSyncData, projectData);
        });
    }
    liveSyncToPreviewApp(data) {
        return __awaiter(this, void 0, void 0, function* () {
            this.attachToPreviewAppLiveSyncError();
            yield this.liveSync([], {
                syncToPreviewApp: true,
                projectDir: data.projectDir,
                bundle: data.bundle,
                useHotModuleReload: data.useHotModuleReload,
                release: false,
                env: data.env,
            });
            const url = this.$previewSdkService.getQrCodeUrl({ useHotModuleReload: data.useHotModuleReload });
            const result = yield this.$previewQrCodeService.getLiveSyncQrCode(url);
            return result;
        });
    }
    stopLiveSync(projectDir, deviceIdentifiers, stopOptions) {
        return __awaiter(this, void 0, void 0, function* () {
            const liveSyncProcessInfo = this.liveSyncProcessesInfo[projectDir];
            if (liveSyncProcessInfo && !liveSyncProcessInfo.isStopped) {
                const shouldAwaitPendingOperation = !stopOptions || stopOptions.shouldAwaitAllActions;
                const deviceIdentifiersToRemove = deviceIdentifiers || _.map(liveSyncProcessInfo.deviceDescriptors, d => d.identifier);
                const removedDeviceIdentifiers = _.remove(liveSyncProcessInfo.deviceDescriptors, descriptor => _.includes(deviceIdentifiersToRemove, descriptor.identifier))
                    .map(descriptor => descriptor.identifier);
                if (!deviceIdentifiers || !deviceIdentifiers.length || !liveSyncProcessInfo.deviceDescriptors || !liveSyncProcessInfo.deviceDescriptors.length) {
                    if (liveSyncProcessInfo.timer) {
                        clearTimeout(liveSyncProcessInfo.timer);
                    }
                    if (liveSyncProcessInfo.watcherInfo && liveSyncProcessInfo.watcherInfo.watcher) {
                        liveSyncProcessInfo.watcherInfo.watcher.close();
                    }
                    liveSyncProcessInfo.watcherInfo = null;
                    liveSyncProcessInfo.isStopped = true;
                    if (liveSyncProcessInfo.actionsChain && shouldAwaitPendingOperation) {
                        yield liveSyncProcessInfo.actionsChain;
                    }
                    liveSyncProcessInfo.deviceDescriptors = [];
                    if (liveSyncProcessInfo.syncToPreviewApp) {
                        yield this.$previewAppLiveSyncService.stopLiveSync();
                        this.$previewAppLiveSyncService.removeAllListeners();
                    }
                    const projectData = this.$projectDataService.getProjectData(projectDir);
                    yield this.$hooksService.executeAfterHooks('watch', {
                        hookArgs: {
                            projectData
                        }
                    });
                    this.$usbLiveSyncService.isInitialized = false;
                }
                else if (liveSyncProcessInfo.currentSyncAction && shouldAwaitPendingOperation) {
                    yield liveSyncProcessInfo.currentSyncAction;
                }
                _.each(removedDeviceIdentifiers, deviceIdentifier => {
                    this.emitLivesyncEvent(LiveSyncEvents.liveSyncStopped, { projectDir, deviceIdentifier });
                });
            }
        });
    }
    getLiveSyncDeviceDescriptors(projectDir) {
        const liveSyncProcessesInfo = this.liveSyncProcessesInfo[projectDir] || {};
        const currentDescriptors = liveSyncProcessesInfo.deviceDescriptors;
        return currentDescriptors || [];
    }
    attachToPreviewAppLiveSyncError() {
        if (!this.$usbLiveSyncService.isInitialized) {
            this.$previewAppLiveSyncService.on(LiveSyncEvents.previewAppLiveSyncError, liveSyncData => {
                this.$logger.error(liveSyncData.error);
                this.emit(LiveSyncEvents.previewAppLiveSyncError, liveSyncData);
            });
        }
    }
    handleWarnings(liveSyncData, projectData) {
        if (liveSyncData.useHotModuleReload) {
            this.$logger.warn("Hot Module Replacement (HMR) is currently in Beta. For more information about the current development state and any known issues, please check the relevant GitHub issue: https://github.com/NativeScript/NativeScript/issues/6398");
            if (projectData.projectType === constants.NgFlavorName) {
                this.$logger.warn("To enable Hot Module Replacement (HMR) in Angular projects, follow the steps outlined in this wiki: https://github.com/NativeScript/nativescript-angular/wiki/HMR");
            }
        }
        if (liveSyncData.syncToPreviewApp && liveSyncData.bundle) {
            this.$logger.warn("Webpack support for the `tns preview` command is currently in Beta. Please, do not hesitate to report any problems that you experience with the feature by opening a new issue in the NativeScript CLI repository: https://github.com/NativeScript/nativescript-cli/issues/new/choose");
        }
    }
    refreshApplication(projectData, liveSyncResultInfo, debugOpts, outputPath) {
        return __awaiter(this, void 0, void 0, function* () {
            const deviceDescriptor = this.getDeviceDescriptor(liveSyncResultInfo.deviceAppData.device.deviceInfo.identifier, projectData.projectDir);
            return deviceDescriptor && deviceDescriptor.debugggingEnabled ?
                this.refreshApplicationWithDebug(projectData, liveSyncResultInfo, debugOpts, outputPath) :
                this.refreshApplicationWithoutDebug(projectData, liveSyncResultInfo, debugOpts, outputPath);
        });
    }
    refreshApplicationWithoutDebug(projectData, liveSyncResultInfo, debugOpts, outputPath, settings) {
        return __awaiter(this, void 0, void 0, function* () {
            const result = { didRefresh: false };
            const platform = liveSyncResultInfo.deviceAppData.platform;
            const platformLiveSyncService = this.getLiveSyncService(platform);
            const applicationIdentifier = projectData.projectIdentifiers[platform.toLowerCase()];
            try {
                let shouldRestart = yield platformLiveSyncService.shouldRestart(projectData, liveSyncResultInfo);
                if (!shouldRestart) {
                    result.didRefresh = yield platformLiveSyncService.tryRefreshApplication(projectData, liveSyncResultInfo);
                    shouldRestart = !result.didRefresh;
                }
                if (shouldRestart) {
                    const deviceIdentifier = liveSyncResultInfo.deviceAppData.device.deviceInfo.identifier;
                    this.emit(constants_1.DEBUGGER_DETACHED_EVENT_NAME, { deviceIdentifier });
                    yield platformLiveSyncService.restartApplication(projectData, liveSyncResultInfo);
                }
            }
            catch (err) {
                this.$logger.info(`Error while trying to start application ${applicationIdentifier} on device ${liveSyncResultInfo.deviceAppData.device.deviceInfo.identifier}. Error is: ${err.message || err}`);
                const msg = `Unable to start application ${applicationIdentifier} on device ${liveSyncResultInfo.deviceAppData.device.deviceInfo.identifier}. Try starting it manually.`;
                this.$logger.warn(msg);
                if (!settings || !settings.shouldSkipEmitLiveSyncNotification) {
                    this.emitLivesyncEvent(LiveSyncEvents.liveSyncNotification, {
                        projectDir: projectData.projectDir,
                        applicationIdentifier,
                        deviceIdentifier: liveSyncResultInfo.deviceAppData.device.deviceInfo.identifier,
                        notification: msg
                    });
                }
                if (settings && settings.shouldCheckDeveloperDiscImage) {
                    this.handleDeveloperDiskImageError(err, liveSyncResultInfo, projectData, debugOpts, outputPath);
                }
            }
            this.emitLivesyncEvent(LiveSyncEvents.liveSyncExecuted, {
                projectDir: projectData.projectDir,
                applicationIdentifier,
                syncedFiles: liveSyncResultInfo.modifiedFilesData.map(m => m.getLocalPath()),
                deviceIdentifier: liveSyncResultInfo.deviceAppData.device.deviceInfo.identifier,
                isFullSync: liveSyncResultInfo.isFullSync
            });
            return result;
        });
    }
    refreshApplicationWithDebug(projectData, liveSyncResultInfo, debugOptions, outputPath) {
        return __awaiter(this, void 0, void 0, function* () {
            debugOptions = debugOptions || {};
            if (debugOptions.debugBrk) {
                liveSyncResultInfo.waitForDebugger = true;
            }
            const refreshInfo = yield this.refreshApplicationWithoutDebug(projectData, liveSyncResultInfo, debugOptions, outputPath, { shouldSkipEmitLiveSyncNotification: true, shouldCheckDeveloperDiscImage: true });
            debugOptions.start = !debugOptions.debugBrk;
            debugOptions.forceDebuggerAttachedEvent = !refreshInfo.didRefresh;
            const deviceOption = {
                deviceIdentifier: liveSyncResultInfo.deviceAppData.device.deviceInfo.identifier,
                debugOptions: debugOptions,
            };
            return this.enableDebuggingCoreWithoutWaitingCurrentAction(deviceOption, { projectDir: projectData.projectDir });
        });
    }
    handleDeveloperDiskImageError(err, liveSyncResultInfo, projectData, debugOpts, outputPath) {
        if ((err.message || err) === "Could not find developer disk image") {
            const deviceIdentifier = liveSyncResultInfo.deviceAppData.device.deviceInfo.identifier;
            const attachDebuggerOptions = {
                platform: liveSyncResultInfo.deviceAppData.device.deviceInfo.platform,
                isEmulator: liveSyncResultInfo.deviceAppData.device.isEmulator,
                projectDir: projectData.projectDir,
                deviceIdentifier,
                debugOptions: debugOpts,
                outputPath
            };
            this.emit(constants_1.USER_INTERACTION_NEEDED_EVENT_NAME, attachDebuggerOptions);
        }
    }
    attachDebugger(settings) {
        return __awaiter(this, void 0, void 0, function* () {
            if (settings.debugOptions) {
                settings.debugOptions.chrome = settings.debugOptions.chrome === undefined ? true : settings.debugOptions.chrome;
                settings.debugOptions.start = settings.debugOptions.start === undefined ? true : settings.debugOptions.start;
            }
            else {
                settings.debugOptions = {
                    chrome: true,
                    start: true
                };
            }
            const projectData = this.$projectDataService.getProjectData(settings.projectDir);
            const debugData = this.$debugDataService.createDebugData(projectData, { device: settings.deviceIdentifier });
            const buildConfig = {
                buildForDevice: !settings.isEmulator,
                release: false,
                device: settings.deviceIdentifier,
                provision: null,
                teamId: null,
                projectDir: settings.projectDir
            };
            debugData.pathToAppPackage = this.$platformService.lastOutputPath(settings.platform, buildConfig, projectData, settings.outputPath);
            const debugInfo = yield this.$debugService.debug(debugData, settings.debugOptions);
            const result = this.printDebugInformation(debugInfo, settings.debugOptions.forceDebuggerAttachedEvent);
            return result;
        });
    }
    printDebugInformation(debugInformation, fireDebuggerAttachedEvent = true) {
        if (!!debugInformation.url) {
            if (fireDebuggerAttachedEvent) {
                this.emit(constants_1.DEBUGGER_ATTACHED_EVENT_NAME, debugInformation);
            }
            this.$logger.info(`To start debugging, open the following URL in Chrome:${os_1.EOL}${debugInformation.url}${os_1.EOL}`.cyan);
        }
        return debugInformation;
    }
    enableDebugging(deviceOpts, debuggingAdditionalOptions) {
        return _.map(deviceOpts, d => this.enableDebuggingCore(d, debuggingAdditionalOptions));
    }
    getDeviceDescriptor(deviceIdentifier, projectDir) {
        const deviceDescriptors = this.getLiveSyncDeviceDescriptors(projectDir);
        return _.find(deviceDescriptors, d => d.identifier === deviceIdentifier);
    }
    enableDebuggingCoreWithoutWaitingCurrentAction(deviceOption, debuggingAdditionalOptions) {
        return __awaiter(this, void 0, void 0, function* () {
            const currentDeviceDescriptor = this.getDeviceDescriptor(deviceOption.deviceIdentifier, debuggingAdditionalOptions.projectDir);
            if (!currentDeviceDescriptor) {
                this.$errors.failWithoutHelp(`Couldn't enable debugging for ${deviceOption.deviceIdentifier}`);
            }
            currentDeviceDescriptor.debugggingEnabled = true;
            currentDeviceDescriptor.debugOptions = deviceOption.debugOptions;
            const currentDeviceInstance = this.$devicesService.getDeviceByIdentifier(deviceOption.deviceIdentifier);
            const attachDebuggerOptions = {
                deviceIdentifier: deviceOption.deviceIdentifier,
                isEmulator: currentDeviceInstance.isEmulator,
                outputPath: currentDeviceDescriptor.outputPath,
                platform: currentDeviceInstance.deviceInfo.platform,
                projectDir: debuggingAdditionalOptions.projectDir,
                debugOptions: deviceOption.debugOptions
            };
            let debugInformation;
            try {
                debugInformation = yield this.attachDebugger(attachDebuggerOptions);
            }
            catch (err) {
                this.$logger.trace("Couldn't attach debugger, will modify options and try again.", err);
                attachDebuggerOptions.debugOptions.start = false;
                try {
                    debugInformation = yield this.attachDebugger(attachDebuggerOptions);
                }
                catch (innerErr) {
                    this.$logger.trace("Couldn't attach debugger with modified options.", innerErr);
                    throw err;
                }
            }
            return debugInformation;
        });
    }
    enableDebuggingCore(deviceOption, debuggingAdditionalOptions) {
        return __awaiter(this, void 0, void 0, function* () {
            const liveSyncProcessInfo = this.liveSyncProcessesInfo[debuggingAdditionalOptions.projectDir];
            if (liveSyncProcessInfo && liveSyncProcessInfo.currentSyncAction) {
                yield liveSyncProcessInfo.currentSyncAction;
            }
            return this.enableDebuggingCoreWithoutWaitingCurrentAction(deviceOption, debuggingAdditionalOptions);
        });
    }
    disableDebugging(deviceOptions, debuggingAdditionalOptions) {
        return _.map(deviceOptions, d => this.disableDebuggingCore(d, debuggingAdditionalOptions));
    }
    getWatcherPatterns(liveSyncData, projectData, platforms) {
        return __awaiter(this, void 0, void 0, function* () {
            return [projectData.getAppDirectoryRelativePath(), projectData.getAppResourcesRelativeDirectoryPath()];
        });
    }
    disableDebuggingCore(deviceOption, debuggingAdditionalOptions) {
        return __awaiter(this, void 0, void 0, function* () {
            const liveSyncProcessInfo = this.liveSyncProcessesInfo[debuggingAdditionalOptions.projectDir];
            if (liveSyncProcessInfo.currentSyncAction) {
                yield liveSyncProcessInfo.currentSyncAction;
            }
            const currentDeviceDescriptor = this.getDeviceDescriptor(deviceOption.deviceIdentifier, debuggingAdditionalOptions.projectDir);
            if (currentDeviceDescriptor) {
                currentDeviceDescriptor.debugggingEnabled = false;
            }
            else {
                this.$errors.failWithoutHelp(`Couldn't disable debugging for ${deviceOption.deviceIdentifier}`);
            }
            const currentDevice = this.$devicesService.getDeviceByIdentifier(currentDeviceDescriptor.identifier);
            if (!currentDevice) {
                this.$errors.failWithoutHelp(`Couldn't disable debugging for ${deviceOption.deviceIdentifier}. Could not find device.`);
            }
            yield this.$debugService.debugStop(currentDevice.deviceInfo.identifier);
            this.emit(constants_1.DEBUGGER_DETACHED_EVENT_NAME, { deviceIdentifier: currentDeviceDescriptor.identifier });
        });
    }
    liveSyncOperation(deviceDescriptors, liveSyncData, projectData) {
        return __awaiter(this, void 0, void 0, function* () {
            let deviceDescriptorsForInitialSync = [];
            if (liveSyncData.syncToPreviewApp) {
                yield this.$previewAppLiveSyncService.initialize({
                    projectDir: projectData.projectDir,
                    bundle: liveSyncData.bundle,
                    useHotModuleReload: liveSyncData.useHotModuleReload,
                    env: liveSyncData.env
                });
            }
            else {
                const isAlreadyLiveSyncing = this.liveSyncProcessesInfo[projectData.projectDir] && !this.liveSyncProcessesInfo[projectData.projectDir].isStopped;
                const currentlyRunningDeviceDescriptors = this.getLiveSyncDeviceDescriptors(projectData.projectDir);
                deviceDescriptorsForInitialSync = isAlreadyLiveSyncing ? _.differenceBy(deviceDescriptors, currentlyRunningDeviceDescriptors, deviceDescriptorPrimaryKey) : deviceDescriptors;
            }
            this.setLiveSyncProcessInfo(liveSyncData, deviceDescriptors);
            const shouldStartWatcher = !liveSyncData.skipWatcher && (liveSyncData.syncToPreviewApp || this.liveSyncProcessesInfo[projectData.projectDir].deviceDescriptors.length);
            if (shouldStartWatcher) {
                this.$usbLiveSyncService.isInitialized = true;
                yield this.startWatcher(projectData, liveSyncData, deviceDescriptors);
            }
            yield this.initialSync(projectData, liveSyncData, deviceDescriptorsForInitialSync);
        });
    }
    setLiveSyncProcessInfo(liveSyncData, deviceDescriptors) {
        const { projectDir } = liveSyncData;
        this.liveSyncProcessesInfo[projectDir] = this.liveSyncProcessesInfo[projectDir] || Object.create(null);
        this.liveSyncProcessesInfo[projectDir].actionsChain = this.liveSyncProcessesInfo[projectDir].actionsChain || Promise.resolve();
        this.liveSyncProcessesInfo[projectDir].currentSyncAction = this.liveSyncProcessesInfo[projectDir].actionsChain;
        this.liveSyncProcessesInfo[projectDir].isStopped = false;
        this.liveSyncProcessesInfo[projectDir].syncToPreviewApp = liveSyncData.syncToPreviewApp;
        const currentDeviceDescriptors = this.getLiveSyncDeviceDescriptors(projectDir);
        this.liveSyncProcessesInfo[projectDir].deviceDescriptors = _.uniqBy(currentDeviceDescriptors.concat(deviceDescriptors), deviceDescriptorPrimaryKey);
    }
    getLiveSyncService(platform) {
        if (this.$mobileHelper.isiOSPlatform(platform)) {
            return this.$injector.resolve("iOSLiveSyncService");
        }
        else if (this.$mobileHelper.isAndroidPlatform(platform)) {
            return this.$injector.resolve("androidLiveSyncService");
        }
        this.$errors.failWithoutHelp(`Invalid platform ${platform}. Supported platforms are: ${this.$mobileHelper.platformNames.join(", ")}`);
    }
    ensureLatestAppPackageIsInstalledOnDevice(options, nativePrepare) {
        return __awaiter(this, void 0, void 0, function* () {
            const platform = options.device.deviceInfo.platform;
            const appInstalledOnDeviceResult = { appInstalled: false };
            if (options.preparedPlatforms.indexOf(platform) === -1) {
                options.preparedPlatforms.push(platform);
                const platformSpecificOptions = options.deviceBuildInfoDescriptor.platformSpecificOptions || {};
                const prepareInfo = {
                    platform,
                    appFilesUpdaterOptions: {
                        bundle: options.bundle,
                        release: options.release,
                        watchAllFiles: options.liveSyncData.watchAllFiles,
                        useHotModuleReload: options.liveSyncData.useHotModuleReload
                    },
                    projectData: options.projectData,
                    env: options.env,
                    nativePrepare: nativePrepare,
                    filesToSync: options.filesToSync,
                    filesToRemove: options.filesToRemove,
                    platformTemplate: null,
                    skipModulesNativeCheck: options.skipModulesNativeCheck,
                    config: platformSpecificOptions
                };
                yield this.$platformService.preparePlatform(prepareInfo);
            }
            const buildResult = yield this.installedCachedAppPackage(platform, options);
            if (buildResult) {
                appInstalledOnDeviceResult.appInstalled = true;
                return appInstalledOnDeviceResult;
            }
            const shouldBuild = yield this.$platformService.shouldBuild(platform, options.projectData, { buildForDevice: !options.device.isEmulator, clean: options.liveSyncData && options.liveSyncData.clean }, options.deviceBuildInfoDescriptor.outputPath);
            let pathToBuildItem = null;
            let action = constants_1.LiveSyncTrackActionNames.LIVESYNC_OPERATION;
            if (shouldBuild) {
                pathToBuildItem = yield options.deviceBuildInfoDescriptor.buildAction();
                options.rebuiltInformation.push({ isEmulator: options.device.isEmulator, platform, pathToBuildItem });
                action = constants_1.LiveSyncTrackActionNames.LIVESYNC_OPERATION_BUILD;
            }
            else {
                yield this.$analyticsService.trackEventActionInGoogleAnalytics({
                    action: "LiveSync",
                    device: options.device,
                    projectDir: options.projectData.projectDir
                });
            }
            yield this.$platformService.validateInstall(options.device, options.projectData, options, options.deviceBuildInfoDescriptor.outputPath);
            const shouldInstall = yield this.$platformService.shouldInstall(options.device, options.projectData, options, options.deviceBuildInfoDescriptor.outputPath);
            if (shouldInstall) {
                yield this.$platformService.installApplication(options.device, { release: false }, options.projectData, pathToBuildItem, options.deviceBuildInfoDescriptor.outputPath);
                appInstalledOnDeviceResult.appInstalled = true;
            }
            return appInstalledOnDeviceResult;
        });
    }
    installedCachedAppPackage(platform, options) {
        return __awaiter(this, void 0, void 0, function* () {
            const rebuildInfo = _.find(options.rebuiltInformation, info => info.platform === platform && (this.$mobileHelper.isAndroidPlatform(platform) || info.isEmulator === options.device.isEmulator));
            if (rebuildInfo) {
                yield this.$platformService.installApplication(options.device, { release: false }, options.projectData, rebuildInfo.pathToBuildItem, options.deviceBuildInfoDescriptor.outputPath);
                return rebuildInfo.pathToBuildItem;
            }
            return null;
        });
    }
    initialSync(projectData, liveSyncData, deviceDescriptors) {
        return __awaiter(this, void 0, void 0, function* () {
            if (!liveSyncData.syncToPreviewApp) {
                yield this.initialCableSync(projectData, liveSyncData, deviceDescriptors);
            }
        });
    }
    initialCableSync(projectData, liveSyncData, deviceDescriptors) {
        return __awaiter(this, void 0, void 0, function* () {
            const preparedPlatforms = [];
            const rebuiltInformation = [];
            const settings = this.getDefaultLatestAppPackageInstalledSettings();
            const deviceAction = (device) => __awaiter(this, void 0, void 0, function* () {
                const platform = device.deviceInfo.platform;
                try {
                    const platformLiveSyncService = this.getLiveSyncService(platform);
                    const deviceBuildInfoDescriptor = _.find(deviceDescriptors, dd => dd.identifier === device.deviceInfo.identifier);
                    yield this.ensureLatestAppPackageIsInstalledOnDevice({
                        device,
                        preparedPlatforms,
                        rebuiltInformation,
                        projectData,
                        deviceBuildInfoDescriptor,
                        liveSyncData,
                        settings,
                        bundle: liveSyncData.bundle,
                        release: liveSyncData.release,
                        env: liveSyncData.env
                    }, { skipNativePrepare: deviceBuildInfoDescriptor.skipNativePrepare });
                    const liveSyncResultInfo = yield platformLiveSyncService.fullSync({
                        projectData,
                        device,
                        syncAllFiles: liveSyncData.watchAllFiles,
                        useHotModuleReload: liveSyncData.useHotModuleReload,
                        watch: !liveSyncData.skipWatcher,
                        force: liveSyncData.force,
                        liveSyncDeviceInfo: deviceBuildInfoDescriptor
                    });
                    yield this.refreshApplication(projectData, liveSyncResultInfo, deviceBuildInfoDescriptor.debugOptions, deviceBuildInfoDescriptor.outputPath);
                    this.$logger.info(`Successfully synced application ${liveSyncResultInfo.deviceAppData.appIdentifier} on device ${liveSyncResultInfo.deviceAppData.device.deviceInfo.identifier}.`);
                    this.emitLivesyncEvent(LiveSyncEvents.liveSyncStarted, {
                        projectDir: projectData.projectDir,
                        deviceIdentifier: device.deviceInfo.identifier,
                        applicationIdentifier: projectData.projectIdentifiers[platform.toLowerCase()]
                    });
                }
                catch (err) {
                    this.$logger.warn(`Unable to apply changes on device: ${device.deviceInfo.identifier}. Error is: ${err.message}.`);
                    this.emitLivesyncEvent(LiveSyncEvents.liveSyncError, {
                        error: err,
                        deviceIdentifier: device.deviceInfo.identifier,
                        projectDir: projectData.projectDir,
                        applicationIdentifier: projectData.projectIdentifiers[platform.toLowerCase()]
                    });
                    yield this.stopLiveSync(projectData.projectDir, [device.deviceInfo.identifier], { shouldAwaitAllActions: false });
                }
            });
            yield this.addActionToChain(projectData.projectDir, () => this.$devicesService.execute(deviceAction, (device) => _.some(deviceDescriptors, deviceDescriptor => deviceDescriptor.identifier === device.deviceInfo.identifier)));
            this.attachDeviceLostHandler();
        });
    }
    getDefaultLatestAppPackageInstalledSettings() {
        return {
            [this.$devicePlatformsConstants.Android]: {
                [constants_2.DeviceTypes.Device]: false,
                [constants_2.DeviceTypes.Emulator]: false
            },
            [this.$devicePlatformsConstants.iOS]: {
                [constants_2.DeviceTypes.Device]: false,
                [constants_2.DeviceTypes.Emulator]: false
            }
        };
    }
    startWatcher(projectData, liveSyncData, deviceDescriptors) {
        return __awaiter(this, void 0, void 0, function* () {
            const devicesIds = deviceDescriptors.map(dd => dd.identifier);
            const devices = _.filter(this.$devicesService.getDeviceInstances(), device => _.includes(devicesIds, device.deviceInfo.identifier));
            const platforms = _(devices).map(device => device.deviceInfo.platform).uniq().value();
            const patterns = yield this.getWatcherPatterns(liveSyncData, projectData, platforms);
            if (liveSyncData.useHotModuleReload) {
                this.$hmrStatusService.attachToHmrStatusEvent();
            }
            if (liveSyncData.watchAllFiles) {
                const productionDependencies = this.$nodeModulesDependenciesBuilder.getProductionDependencies(projectData.projectDir);
                patterns.push(constants_1.PACKAGE_JSON_FILE_NAME);
                for (const index in productionDependencies) {
                    patterns.push(productionDependencies[index].directory);
                }
            }
            const currentWatcherInfo = this.liveSyncProcessesInfo[liveSyncData.projectDir].watcherInfo;
            const areWatcherPatternsDifferent = () => _.xor(currentWatcherInfo.patterns, patterns).length;
            if (!currentWatcherInfo || areWatcherPatternsDifferent()) {
                if (currentWatcherInfo) {
                    currentWatcherInfo.watcher.close();
                }
                let filesToSync = [];
                const hmrData = {};
                const filesToSyncMap = {};
                let filesToRemove = [];
                let timeoutTimer;
                const startSyncFilesTimeout = (platform, opts) => {
                    timeoutTimer = setTimeout(() => __awaiter(this, void 0, void 0, function* () {
                        if (platform && liveSyncData.bundle) {
                            filesToSync = filesToSyncMap[platform];
                        }
                        if (filesToSync.length || filesToRemove.length) {
                            const currentFilesToSync = _.cloneDeep(filesToSync);
                            filesToSync.splice(0, filesToSync.length);
                            const currentFilesToRemove = _.cloneDeep(filesToRemove);
                            filesToRemove = [];
                            if (liveSyncData.syncToPreviewApp) {
                                yield this.addActionToChain(projectData.projectDir, () => __awaiter(this, void 0, void 0, function* () {
                                    yield this.$previewAppLiveSyncService.syncFiles({
                                        projectDir: projectData.projectDir,
                                        bundle: liveSyncData.bundle,
                                        useHotModuleReload: liveSyncData.useHotModuleReload,
                                        env: liveSyncData.env
                                    }, currentFilesToSync, currentFilesToRemove);
                                }));
                            }
                            else {
                                yield this.addActionToChain(projectData.projectDir, () => __awaiter(this, void 0, void 0, function* () {
                                    try {
                                        const currentHmrData = _.cloneDeep(hmrData);
                                        const allModifiedFiles = [].concat(currentFilesToSync).concat(currentFilesToRemove);
                                        const preparedPlatforms = [];
                                        const rebuiltInformation = [];
                                        const latestAppPackageInstalledSettings = this.getDefaultLatestAppPackageInstalledSettings();
                                        yield this.$devicesService.execute((device) => __awaiter(this, void 0, void 0, function* () {
                                            const liveSyncProcessInfo = this.liveSyncProcessesInfo[projectData.projectDir];
                                            const deviceBuildInfoDescriptor = _.find(liveSyncProcessInfo.deviceDescriptors, dd => dd.identifier === device.deviceInfo.identifier);
                                            const platformHmrData = (currentHmrData && currentHmrData[device.deviceInfo.platform]) || {};
                                            const settings = {
                                                liveSyncDeviceInfo: deviceBuildInfoDescriptor,
                                                projectData,
                                                filesToRemove: currentFilesToRemove,
                                                filesToSync: currentFilesToSync,
                                                isReinstalled: false,
                                                syncAllFiles: liveSyncData.watchAllFiles,
                                                hmrData: platformHmrData,
                                                useHotModuleReload: liveSyncData.useHotModuleReload,
                                                force: liveSyncData.force,
                                                connectTimeout: 1000
                                            };
                                            const service = this.getLiveSyncService(device.deviceInfo.platform);
                                            const watchAction = (watchInfo) => __awaiter(this, void 0, void 0, function* () {
                                                let liveSyncResultInfo = yield service.liveSyncWatchAction(device, watchInfo);
                                                yield this.refreshApplication(projectData, liveSyncResultInfo, deviceBuildInfoDescriptor.debugOptions, deviceBuildInfoDescriptor.outputPath);
                                                if (!liveSyncResultInfo.didRecover && liveSyncData.useHotModuleReload && platformHmrData.hash) {
                                                    const status = yield this.$hmrStatusService.getHmrStatus(device.deviceInfo.identifier, platformHmrData.hash);
                                                    if (status === constants_2.HmrConstants.HMR_ERROR_STATUS) {
                                                        watchInfo.filesToSync = platformHmrData.fallbackFiles;
                                                        liveSyncResultInfo = yield service.liveSyncWatchAction(device, watchInfo);
                                                        liveSyncResultInfo.isFullSync = true;
                                                        yield this.refreshApplication(projectData, liveSyncResultInfo, deviceBuildInfoDescriptor.debugOptions, deviceBuildInfoDescriptor.outputPath);
                                                    }
                                                }
                                                this.$logger.info(`Successfully synced application ${liveSyncResultInfo.deviceAppData.appIdentifier} on device ${liveSyncResultInfo.deviceAppData.device.deviceInfo.identifier}.`);
                                            });
                                            if (liveSyncData.useHotModuleReload && opts && opts.calledFromHook) {
                                                try {
                                                    this.$logger.trace("Try executing watch action without any preparation of files.");
                                                    yield watchAction(settings);
                                                    this.$logger.trace("Successfully executed watch action without any preparation of files.");
                                                    return;
                                                }
                                                catch (err) {
                                                    this.$logger.trace(`Error while trying to execute fast sync. Now we'll check the state of the app and we'll try to resurrect from the error. The error is: ${err}`);
                                                }
                                            }
                                            const appInstalledOnDeviceResult = yield this.ensureLatestAppPackageIsInstalledOnDevice({
                                                device,
                                                preparedPlatforms,
                                                rebuiltInformation,
                                                projectData,
                                                deviceBuildInfoDescriptor,
                                                liveSyncData: _.assign({}, liveSyncData, { clean: false }),
                                                settings: latestAppPackageInstalledSettings,
                                                modifiedFiles: allModifiedFiles,
                                                filesToRemove: currentFilesToRemove,
                                                filesToSync: currentFilesToSync,
                                                bundle: liveSyncData.bundle,
                                                release: liveSyncData.release,
                                                env: liveSyncData.env,
                                                skipModulesNativeCheck: !liveSyncData.watchAllFiles
                                            }, { skipNativePrepare: deviceBuildInfoDescriptor.skipNativePrepare });
                                            settings.isReinstalled = appInstalledOnDeviceResult.appInstalled;
                                            settings.connectTimeout = null;
                                            if (liveSyncData.useHotModuleReload && appInstalledOnDeviceResult.appInstalled) {
                                                _.each(platformHmrData.fallbackFiles, fileToSync => currentFilesToSync.push(fileToSync));
                                            }
                                            yield watchAction(settings);
                                        }), (device) => {
                                            const liveSyncProcessInfo = this.liveSyncProcessesInfo[projectData.projectDir];
                                            return (!platform || platform.toLowerCase() === device.deviceInfo.platform.toLowerCase()) && liveSyncProcessInfo && _.some(liveSyncProcessInfo.deviceDescriptors, deviceDescriptor => deviceDescriptor.identifier === device.deviceInfo.identifier);
                                        });
                                    }
                                    catch (err) {
                                        const allErrors = err.allErrors;
                                        if (allErrors && _.isArray(allErrors)) {
                                            for (const deviceError of allErrors) {
                                                this.$logger.warn(`Unable to apply changes for device: ${deviceError.deviceIdentifier}. Error is: ${deviceError.message}.`);
                                                const device = this.$devicesService.getDeviceByIdentifier(deviceError.deviceIdentifier);
                                                this.emitLivesyncEvent(LiveSyncEvents.liveSyncError, {
                                                    error: deviceError,
                                                    deviceIdentifier: deviceError.deviceIdentifier,
                                                    projectDir: projectData.projectDir,
                                                    applicationIdentifier: projectData.projectIdentifiers[device.deviceInfo.platform.toLowerCase()]
                                                });
                                                yield this.stopLiveSync(projectData.projectDir, [deviceError.deviceIdentifier], { shouldAwaitAllActions: false });
                                            }
                                        }
                                    }
                                }));
                            }
                        }
                    }), liveSyncData.useHotModuleReload ? 0 : 250);
                    this.liveSyncProcessesInfo[liveSyncData.projectDir].timer = timeoutTimer;
                };
                yield this.$hooksService.executeBeforeHooks('watch', {
                    hookArgs: {
                        projectData,
                        config: {
                            env: liveSyncData.env,
                            appFilesUpdaterOptions: {
                                bundle: liveSyncData.bundle,
                                release: liveSyncData.release,
                                watchAllFiles: liveSyncData.watchAllFiles,
                                useHotModuleReload: liveSyncData.useHotModuleReload
                            },
                            platforms
                        },
                        filesToSync,
                        filesToSyncMap,
                        hmrData,
                        filesToRemove,
                        startSyncFilesTimeout: (platform) => __awaiter(this, void 0, void 0, function* () {
                            const opts = { calledFromHook: true };
                            if (platform) {
                                yield startSyncFilesTimeout(platform, opts);
                            }
                            else {
                                yield startSyncFilesTimeout(null, opts);
                            }
                        })
                    }
                });
                const watcherOptions = {
                    ignoreInitial: true,
                    cwd: liveSyncData.projectDir,
                    awaitWriteFinish: {
                        pollInterval: 100,
                        stabilityThreshold: 500
                    },
                    ignored: ["**/.*", ".*"]
                };
                const watcher = choki.watch(patterns, watcherOptions)
                    .on("all", (event, filePath) => __awaiter(this, void 0, void 0, function* () {
                    clearTimeout(timeoutTimer);
                    filePath = path.join(liveSyncData.projectDir, filePath);
                    this.$logger.trace(`Chokidar raised event ${event} for ${filePath}.`);
                    if (event === "add" || event === "addDir" || event === "change") {
                        filesToSync.push(filePath);
                    }
                    else if (event === "unlink" || event === "unlinkDir") {
                        filesToRemove.push(filePath);
                    }
                    startSyncFilesTimeout();
                }));
                this.liveSyncProcessesInfo[liveSyncData.projectDir].watcherInfo = { watcher, patterns };
                this.liveSyncProcessesInfo[liveSyncData.projectDir].timer = timeoutTimer;
                this.$processService.attachToProcessExitSignals(this, () => {
                    _.keys(this.liveSyncProcessesInfo).forEach(projectDir => {
                        this.stopLiveSync(projectDir);
                    });
                });
            }
        });
    }
    attachDeviceLostHandler() {
        this.$devicesService.on(constants_2.DeviceDiscoveryEventNames.DEVICE_LOST, (device) => __awaiter(this, void 0, void 0, function* () {
            this.$logger.trace(`Received ${constants_2.DeviceDiscoveryEventNames.DEVICE_LOST} event in LiveSync service for ${device.deviceInfo.identifier}. Will stop LiveSync operation for this device.`);
            for (const projectDir in this.liveSyncProcessesInfo) {
                try {
                    if (_.find(this.liveSyncProcessesInfo[projectDir].deviceDescriptors, d => d.identifier === device.deviceInfo.identifier)) {
                        yield this.stopLiveSync(projectDir, [device.deviceInfo.identifier]);
                    }
                }
                catch (err) {
                    this.$logger.warn(`Unable to stop LiveSync operation for ${device.deviceInfo.identifier}.`, err);
                }
            }
        }));
    }
    addActionToChain(projectDir, action) {
        return __awaiter(this, void 0, void 0, function* () {
            const liveSyncInfo = this.liveSyncProcessesInfo[projectDir];
            if (liveSyncInfo) {
                liveSyncInfo.actionsChain = liveSyncInfo.actionsChain.then(() => __awaiter(this, void 0, void 0, function* () {
                    if (!liveSyncInfo.isStopped) {
                        liveSyncInfo.currentSyncAction = action();
                        const res = yield liveSyncInfo.currentSyncAction;
                        return res;
                    }
                }));
                const result = yield liveSyncInfo.actionsChain;
                return result;
            }
        });
    }
    emitLivesyncEvent(event, livesyncData) {
        this.$logger.trace(`Will emit event ${event} with data`, livesyncData);
        return this.emit(event, livesyncData);
    }
}
__decorate([
    decorators_2.performanceLog()
], LiveSyncService.prototype, "refreshApplication", null);
__decorate([
    decorators_2.performanceLog()
], LiveSyncService.prototype, "refreshApplicationWithDebug", null);
__decorate([
    decorators_2.performanceLog()
], LiveSyncService.prototype, "enableDebuggingCoreWithoutWaitingCurrentAction", null);
__decorate([
    helpers_1.hook('watchPatterns')
], LiveSyncService.prototype, "getWatcherPatterns", null);
__decorate([
    helpers_1.hook("liveSync")
], LiveSyncService.prototype, "liveSyncOperation", null);
__decorate([
    decorators_1.cache()
], LiveSyncService.prototype, "attachDeviceLostHandler", null);
exports.LiveSyncService = LiveSyncService;
$injector.register("liveSyncService", LiveSyncService);
class DeprecatedUsbLiveSyncService {
    constructor() {
        this.isInitialized = false;
    }
}
exports.DeprecatedUsbLiveSyncService = DeprecatedUsbLiveSyncService;
$injector.register("usbLiveSyncService", DeprecatedUsbLiveSyncService);
