"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 constants_1 = require("../common/constants");
const constants_2 = require("../constants");
const decorators_1 = require("../common/decorators");
const events_1 = require("events");
class RunController extends events_1.EventEmitter {
    constructor($analyticsService, $buildController, $debugController, $deviceInstallAppService, $devicesService, $errors, $injector, $hmrStatusService, $hooksService, $liveSyncServiceResolver, $liveSyncProcessDataService, $logger, $mobileHelper, $platformsDataService, $pluginsService, $prepareController, $prepareDataService, $prepareNativePlatformService, $projectDataService) {
        super();
        this.$analyticsService = $analyticsService;
        this.$buildController = $buildController;
        this.$debugController = $debugController;
        this.$deviceInstallAppService = $deviceInstallAppService;
        this.$devicesService = $devicesService;
        this.$errors = $errors;
        this.$injector = $injector;
        this.$hmrStatusService = $hmrStatusService;
        this.$hooksService = $hooksService;
        this.$liveSyncServiceResolver = $liveSyncServiceResolver;
        this.$liveSyncProcessDataService = $liveSyncProcessDataService;
        this.$logger = $logger;
        this.$mobileHelper = $mobileHelper;
        this.$platformsDataService = $platformsDataService;
        this.$pluginsService = $pluginsService;
        this.$prepareController = $prepareController;
        this.$prepareDataService = $prepareDataService;
        this.$prepareNativePlatformService = $prepareNativePlatformService;
        this.$projectDataService = $projectDataService;
        this.rebuiltInformation = {};
    }
    run(runData) {
        return __awaiter(this, void 0, void 0, function* () {
            const { liveSyncInfo, deviceDescriptors } = runData;
            const { projectDir } = liveSyncInfo;
            const projectData = this.$projectDataService.getProjectData(projectDir);
            yield this.initializeSetup(projectData);
            const platforms = this.$devicesService.getPlatformsFromDeviceDescriptors(deviceDescriptors);
            const deviceDescriptorsForInitialSync = this.getDeviceDescriptorsForInitialSync(projectDir, deviceDescriptors);
            this.$liveSyncProcessDataService.persistData(projectDir, deviceDescriptors, platforms);
            const shouldStartWatcher = !liveSyncInfo.skipWatcher && this.$liveSyncProcessDataService.hasDeviceDescriptors(projectDir);
            if (shouldStartWatcher && liveSyncInfo.useHotModuleReload) {
                this.$hmrStatusService.attachToHmrStatusEvent();
            }
            this.$prepareController.on(constants_2.PREPARE_READY_EVENT_NAME, (data) => __awaiter(this, void 0, void 0, function* () {
                yield this.syncChangedDataOnDevices(data, projectData, liveSyncInfo, deviceDescriptors);
            }));
            yield this.syncInitialDataOnDevices(projectData, liveSyncInfo, deviceDescriptorsForInitialSync);
            this.attachDeviceLostHandler();
        });
    }
    stop(data) {
        return __awaiter(this, void 0, void 0, function* () {
            const { projectDir, deviceIdentifiers, stopOptions } = data;
            const liveSyncProcessInfo = this.$liveSyncProcessDataService.getPersistedData(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);
                _.each(liveSyncProcessInfo.platforms, platform => {
                    const devices = this.$devicesService.getDevicesForPlatform(platform);
                    if (!devices || !devices.length) {
                        this.$prepareController.stopWatchers(projectDir, platform);
                    }
                });
                if (!deviceIdentifiers || !deviceIdentifiers.length || !liveSyncProcessInfo.deviceDescriptors || !liveSyncProcessInfo.deviceDescriptors.length) {
                    if (liveSyncProcessInfo.timer) {
                        clearTimeout(liveSyncProcessInfo.timer);
                    }
                    _.each(liveSyncProcessInfo.platforms, platform => {
                        this.$prepareController.stopWatchers(projectDir, platform);
                    });
                    liveSyncProcessInfo.isStopped = true;
                    if (liveSyncProcessInfo.actionsChain && shouldAwaitPendingOperation) {
                        yield liveSyncProcessInfo.actionsChain;
                    }
                    liveSyncProcessInfo.deviceDescriptors = [];
                    const projectData = this.$projectDataService.getProjectData(projectDir);
                    yield this.$hooksService.executeAfterHooks('watch', {
                        hookArgs: {
                            projectData
                        }
                    });
                }
                else if (liveSyncProcessInfo.currentSyncAction && shouldAwaitPendingOperation) {
                    yield liveSyncProcessInfo.currentSyncAction;
                }
                _.each(removedDeviceIdentifiers, deviceIdentifier => {
                    this.emitCore(constants_2.RunOnDeviceEvents.runOnDeviceStopped, {
                        projectDir,
                        deviceIdentifier
                    });
                });
            }
        });
    }
    getDeviceDescriptors(data) {
        return this.$liveSyncProcessDataService.getDeviceDescriptors(data.projectDir);
    }
    refreshApplication(projectData, liveSyncResultInfo, filesChangeEventData, deviceDescriptor, settings) {
        return __awaiter(this, void 0, void 0, function* () {
            const result = deviceDescriptor.debuggingEnabled ?
                yield this.refreshApplicationWithDebug(projectData, liveSyncResultInfo, filesChangeEventData, deviceDescriptor, settings) :
                yield this.refreshApplicationWithoutDebug(projectData, liveSyncResultInfo, filesChangeEventData, deviceDescriptor, settings);
            return result;
        });
    }
    refreshApplicationWithDebug(projectData, liveSyncResultInfo, filesChangeEventData, deviceDescriptor, settings) {
        return __awaiter(this, void 0, void 0, function* () {
            const debugOptions = deviceDescriptor.debugOptions || {};
            liveSyncResultInfo.waitForDebugger = !!debugOptions.debugBrk;
            const refreshInfo = yield this.refreshApplicationWithoutDebug(projectData, liveSyncResultInfo, filesChangeEventData, deviceDescriptor, settings);
            debugOptions.start = !debugOptions.debugBrk;
            debugOptions.forceDebuggerAttachedEvent = refreshInfo.didRestart;
            yield this.$debugController.enableDebuggingCoreWithoutWaitingCurrentAction(projectData.projectDir, deviceDescriptor.identifier, debugOptions);
            return refreshInfo;
        });
    }
    refreshApplicationWithoutDebug(projectData, liveSyncResultInfo, filesChangeEventData, deviceDescriptor, settings) {
        return __awaiter(this, void 0, void 0, function* () {
            const result = { didRestart: false };
            const platform = liveSyncResultInfo.deviceAppData.platform;
            const applicationIdentifier = projectData.projectIdentifiers[platform.toLowerCase()];
            const platformLiveSyncService = this.$liveSyncServiceResolver.resolveLiveSyncService(platform);
            try {
                let shouldRestart = filesChangeEventData && (filesChangeEventData.hasNativeChanges || !filesChangeEventData.hasOnlyHotUpdateFiles);
                if (!shouldRestart) {
                    shouldRestart = yield platformLiveSyncService.shouldRestart(projectData, liveSyncResultInfo);
                }
                if (!shouldRestart) {
                    shouldRestart = !(yield platformLiveSyncService.tryRefreshApplication(projectData, liveSyncResultInfo));
                }
                if (shouldRestart) {
                    this.emit(constants_2.DEBUGGER_DETACHED_EVENT_NAME, { deviceIdentifier: liveSyncResultInfo.deviceAppData.device.deviceInfo.identifier });
                    yield platformLiveSyncService.restartApplication(projectData, liveSyncResultInfo);
                    result.didRestart = true;
                }
            }
            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);
                const device = liveSyncResultInfo.deviceAppData.device;
                const deviceIdentifier = device.deviceInfo.identifier;
                if (!settings || !settings.shouldSkipEmitLiveSyncNotification) {
                    this.emitCore(constants_2.RunOnDeviceEvents.runOnDeviceNotification, {
                        projectDir: projectData.projectDir,
                        deviceIdentifier: device.deviceInfo.identifier,
                        applicationIdentifier: projectData.projectIdentifiers[device.deviceInfo.platform.toLowerCase()],
                        notification: msg
                    });
                }
                if (settings && settings.shouldCheckDeveloperDiscImage && (err.message || err) === "Could not find developer disk image") {
                    const attachDebuggerOptions = {
                        platform: device.deviceInfo.platform,
                        isEmulator: device.isEmulator,
                        projectDir: projectData.projectDir,
                        deviceIdentifier,
                        debugOptions: deviceDescriptor.debugOptions,
                        outputPath: deviceDescriptor.buildData.outputPath
                    };
                    this.emit(constants_2.USER_INTERACTION_NEEDED_EVENT_NAME, attachDebuggerOptions);
                }
            }
            return result;
        });
    }
    getDeviceDescriptorsForInitialSync(projectDir, deviceDescriptors) {
        const currentRunData = this.$liveSyncProcessDataService.getPersistedData(projectDir);
        const isAlreadyLiveSyncing = currentRunData && !currentRunData.isStopped;
        const deviceDescriptorsForInitialSync = isAlreadyLiveSyncing ? _.differenceBy(deviceDescriptors, currentRunData.deviceDescriptors, "identifier") : deviceDescriptors;
        return deviceDescriptorsForInitialSync;
    }
    initializeSetup(projectData) {
        return __awaiter(this, void 0, void 0, function* () {
            try {
                yield this.$pluginsService.ensureAllDependenciesAreInstalled(projectData);
            }
            catch (err) {
                this.$logger.trace(err);
                this.$errors.failWithoutHelp(`Unable to install dependencies. Make sure your package.json is valid and all dependencies are correct. Error is: ${err.message}`);
            }
        });
    }
    attachDeviceLostHandler() {
        this.$devicesService.on(constants_1.DeviceDiscoveryEventNames.DEVICE_LOST, (device) => __awaiter(this, void 0, void 0, function* () {
            this.$logger.trace(`Received ${constants_1.DeviceDiscoveryEventNames.DEVICE_LOST} event in LiveSync service for ${device.deviceInfo.identifier}. Will stop LiveSync operation for this device.`);
            for (const projectDir in this.$liveSyncProcessDataService.getAllPersistedData()) {
                try {
                    const deviceDescriptors = this.getDeviceDescriptors({ projectDir });
                    if (_.find(deviceDescriptors, d => d.identifier === device.deviceInfo.identifier)) {
                        yield this.stop({ projectDir, deviceIdentifiers: [device.deviceInfo.identifier] });
                    }
                }
                catch (err) {
                    this.$logger.warn(`Unable to stop LiveSync operation for ${device.deviceInfo.identifier}.`, err);
                }
            }
        }));
    }
    syncInitialDataOnDevices(projectData, liveSyncInfo, deviceDescriptors) {
        return __awaiter(this, void 0, void 0, function* () {
            this.rebuiltInformation = {};
            const deviceAction = (device) => __awaiter(this, void 0, void 0, function* () {
                const deviceDescriptor = _.find(deviceDescriptors, dd => dd.identifier === device.deviceInfo.identifier);
                const platformData = this.$platformsDataService.getPlatformData(device.deviceInfo.platform, projectData);
                const prepareData = this.$prepareDataService.getPrepareData(liveSyncInfo.projectDir, device.deviceInfo.platform, Object.assign({}, liveSyncInfo, deviceDescriptor.buildData, { nativePrepare: { skipNativePrepare: !!deviceDescriptor.skipNativePrepare }, watch: !liveSyncInfo.skipWatcher }));
                const prepareResultData = yield this.$prepareController.prepare(prepareData);
                const buildData = Object.assign({}, deviceDescriptor.buildData, { buildForDevice: !device.isEmulator });
                try {
                    let packageFilePath = null;
                    if (this.rebuiltInformation[platformData.platformNameLowerCase] && (this.$mobileHelper.isAndroidPlatform(platformData.platformNameLowerCase) || this.rebuiltInformation[platformData.platformNameLowerCase].isEmulator === device.isEmulator)) {
                        packageFilePath = this.rebuiltInformation[platformData.platformNameLowerCase].packageFilePath;
                        yield this.$deviceInstallAppService.installOnDevice(device, buildData, packageFilePath);
                    }
                    else {
                        const shouldBuild = prepareResultData.hasNativeChanges || (yield this.$buildController.shouldBuild(buildData));
                        if (shouldBuild) {
                            packageFilePath = yield deviceDescriptor.buildAction();
                            this.rebuiltInformation[platformData.platformNameLowerCase] = { isEmulator: device.isEmulator, platform: platformData.platformNameLowerCase, packageFilePath };
                        }
                        else {
                            yield this.$analyticsService.trackEventActionInGoogleAnalytics({
                                action: "LiveSync",
                                device,
                                projectDir: projectData.projectDir
                            });
                        }
                        yield this.$deviceInstallAppService.installOnDeviceIfNeeded(device, buildData, packageFilePath);
                    }
                    const platformLiveSyncService = this.$liveSyncServiceResolver.resolveLiveSyncService(platformData.platformNameLowerCase);
                    const { force, useHotModuleReload, skipWatcher } = liveSyncInfo;
                    const liveSyncResultInfo = yield platformLiveSyncService.fullSync({ force, useHotModuleReload, projectData, device, watch: !skipWatcher, liveSyncDeviceData: deviceDescriptor });
                    yield this.refreshApplication(projectData, liveSyncResultInfo, null, deviceDescriptor);
                    this.emitCore(constants_2.RunOnDeviceEvents.runOnDeviceExecuted, {
                        projectDir: projectData.projectDir,
                        deviceIdentifier: device.deviceInfo.identifier,
                        applicationIdentifier: projectData.projectIdentifiers[device.deviceInfo.platform.toLowerCase()],
                        syncedFiles: liveSyncResultInfo.modifiedFilesData.map(m => m.getLocalPath()),
                        isFullSync: liveSyncResultInfo.isFullSync
                    });
                    this.$logger.info(`Successfully synced application ${liveSyncResultInfo.deviceAppData.appIdentifier} on device ${liveSyncResultInfo.deviceAppData.device.deviceInfo.identifier}.`);
                    this.emitCore(constants_2.RunOnDeviceEvents.runOnDeviceStarted, {
                        projectDir: projectData.projectDir,
                        deviceIdentifier: device.deviceInfo.identifier,
                        applicationIdentifier: projectData.projectIdentifiers[device.deviceInfo.platform.toLowerCase()]
                    });
                }
                catch (err) {
                    this.$logger.warn(`Unable to apply changes on device: ${device.deviceInfo.identifier}. Error is: ${err.message}.`);
                    this.emitCore(constants_2.RunOnDeviceEvents.runOnDeviceError, {
                        projectDir: projectData.projectDir,
                        deviceIdentifier: device.deviceInfo.identifier,
                        applicationIdentifier: projectData.projectIdentifiers[device.deviceInfo.platform.toLowerCase()],
                        error: err,
                    });
                }
            });
            yield this.addActionToChain(projectData.projectDir, () => this.$devicesService.execute(deviceAction, (device) => _.some(deviceDescriptors, deviceDescriptor => deviceDescriptor.identifier === device.deviceInfo.identifier)));
        });
    }
    syncChangedDataOnDevices(data, projectData, liveSyncInfo, deviceDescriptors) {
        return __awaiter(this, void 0, void 0, function* () {
            this.rebuiltInformation = {};
            const deviceAction = (device) => __awaiter(this, void 0, void 0, function* () {
                const deviceDescriptor = _.find(deviceDescriptors, dd => dd.identifier === device.deviceInfo.identifier);
                const platformData = this.$platformsDataService.getPlatformData(data.platform, projectData);
                const prepareData = this.$prepareDataService.getPrepareData(liveSyncInfo.projectDir, device.deviceInfo.platform, Object.assign({}, liveSyncInfo, deviceDescriptor.buildData, { nativePrepare: { skipNativePrepare: !!deviceDescriptor.skipNativePrepare }, watch: !liveSyncInfo.skipWatcher }));
                try {
                    if (data.hasNativeChanges) {
                        const rebuiltInfo = this.rebuiltInformation[platformData.platformNameLowerCase] && (this.$mobileHelper.isAndroidPlatform(platformData.platformNameLowerCase) || this.rebuiltInformation[platformData.platformNameLowerCase].isEmulator === device.isEmulator);
                        if (!rebuiltInfo) {
                            yield this.$prepareNativePlatformService.prepareNativePlatform(platformData, projectData, prepareData);
                            yield deviceDescriptor.buildAction();
                            this.rebuiltInformation[platformData.platformNameLowerCase] = { isEmulator: device.isEmulator, platform: platformData.platformNameLowerCase, packageFilePath: null };
                        }
                        yield this.$deviceInstallAppService.installOnDevice(device, deviceDescriptor.buildData, this.rebuiltInformation[platformData.platformNameLowerCase].packageFilePath);
                    }
                    const isInHMRMode = liveSyncInfo.useHotModuleReload && data.hmrData && data.hmrData.hash;
                    if (isInHMRMode) {
                        this.$hmrStatusService.watchHmrStatus(device.deviceInfo.identifier, data.hmrData.hash);
                    }
                    const platformLiveSyncService = this.$liveSyncServiceResolver.resolveLiveSyncService(device.deviceInfo.platform);
                    const watchInfo = {
                        liveSyncDeviceData: deviceDescriptor,
                        projectData,
                        filesToRemove: [],
                        filesToSync: data.files,
                        isReinstalled: false,
                        hmrData: data.hmrData,
                        useHotModuleReload: liveSyncInfo.useHotModuleReload,
                        force: liveSyncInfo.force,
                        connectTimeout: 1000
                    };
                    let liveSyncResultInfo = yield platformLiveSyncService.liveSyncWatchAction(device, watchInfo);
                    yield this.refreshApplication(projectData, liveSyncResultInfo, data, deviceDescriptor);
                    this.emitCore(constants_2.RunOnDeviceEvents.runOnDeviceExecuted, {
                        projectDir: projectData.projectDir,
                        deviceIdentifier: device.deviceInfo.identifier,
                        applicationIdentifier: projectData.projectIdentifiers[device.deviceInfo.platform.toLowerCase()],
                        syncedFiles: liveSyncResultInfo.modifiedFilesData.map(m => m.getLocalPath()),
                        isFullSync: liveSyncResultInfo.isFullSync
                    });
                    if (!liveSyncResultInfo.didRecover && isInHMRMode) {
                        const status = yield this.$hmrStatusService.getHmrStatus(device.deviceInfo.identifier, data.hmrData.hash);
                        if (status === constants_1.HmrConstants.HMR_ERROR_STATUS) {
                            watchInfo.filesToSync = data.hmrData.fallbackFiles;
                            liveSyncResultInfo = yield platformLiveSyncService.liveSyncWatchAction(device, watchInfo);
                            liveSyncResultInfo.isFullSync = true;
                            yield this.refreshApplication(projectData, liveSyncResultInfo, data, deviceDescriptor);
                            this.emitCore(constants_2.RunOnDeviceEvents.runOnDeviceExecuted, {
                                projectDir: projectData.projectDir,
                                deviceIdentifier: device.deviceInfo.identifier,
                                applicationIdentifier: projectData.projectIdentifiers[device.deviceInfo.platform.toLowerCase()],
                                syncedFiles: liveSyncResultInfo.modifiedFilesData.map(m => m.getLocalPath()),
                                isFullSync: liveSyncResultInfo.isFullSync
                            });
                        }
                    }
                    this.$logger.info(`Successfully synced application ${liveSyncResultInfo.deviceAppData.appIdentifier} on device ${liveSyncResultInfo.deviceAppData.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}.`);
                            this.emitCore(constants_2.RunOnDeviceEvents.runOnDeviceError, {
                                projectDir: projectData.projectDir,
                                deviceIdentifier: device.deviceInfo.identifier,
                                applicationIdentifier: projectData.projectIdentifiers[device.deviceInfo.platform.toLowerCase()],
                                error: err,
                            });
                        }
                    }
                }
            });
            yield this.addActionToChain(projectData.projectDir, () => this.$devicesService.execute(deviceAction, (device) => {
                const liveSyncProcessInfo = this.$liveSyncProcessDataService.getPersistedData(projectData.projectDir);
                return (data.platform.toLowerCase() === device.deviceInfo.platform.toLowerCase()) && liveSyncProcessInfo && _.some(liveSyncProcessInfo.deviceDescriptors, deviceDescriptor => deviceDescriptor.identifier === device.deviceInfo.identifier);
            }));
        });
    }
    addActionToChain(projectDir, action) {
        return __awaiter(this, void 0, void 0, function* () {
            const liveSyncInfo = this.$liveSyncProcessDataService.getPersistedData(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;
            }
        });
    }
    emitCore(event, data) {
        this.$logger.trace(`Will emit event ${event} with data`, data);
        this.emit(event, data);
    }
}
__decorate([
    decorators_1.performanceLog()
], RunController.prototype, "refreshApplicationWithoutDebug", null);
__decorate([
    decorators_1.cache()
], RunController.prototype, "attachDeviceLostHandler", null);
exports.RunController = RunController;
$injector.register("runController", RunController);
