"use strict";
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());
    });
};
const syncBatchLib = require("../../common/services/livesync/sync-batch");
const path = require("path");
const minimatch = require("minimatch");
const util = require("util");
const helpers = require("../../common/helpers");
const livesyncInfoFileName = ".nslivesyncinfo";
class PlatformLiveSyncServiceBase {
    constructor(_liveSyncData, $devicesService, $mobileHelper, $logger, $options, $deviceAppDataFactory, $injector, $projectFilesManager, $projectFilesProvider, $platformService, $projectChangesService, $liveSyncProvider, $fs) {
        this.$devicesService = $devicesService;
        this.$mobileHelper = $mobileHelper;
        this.$logger = $logger;
        this.$options = $options;
        this.$deviceAppDataFactory = $deviceAppDataFactory;
        this.$injector = $injector;
        this.$projectFilesManager = $projectFilesManager;
        this.$projectFilesProvider = $projectFilesProvider;
        this.$platformService = $platformService;
        this.$projectChangesService = $projectChangesService;
        this.$liveSyncProvider = $liveSyncProvider;
        this.$fs = $fs;
        this.batch = Object.create(null);
        this.livesyncData = Object.create(null);
        this.liveSyncData = _liveSyncData;
    }
    fullSync(postAction) {
        return __awaiter(this, void 0, void 0, function* () {
            let appIdentifier = this.liveSyncData.appIdentifier;
            let platform = this.liveSyncData.platform;
            let projectFilesPath = this.liveSyncData.projectFilesPath;
            let canExecute = this.getCanExecuteAction(platform, appIdentifier);
            let action = (device) => __awaiter(this, void 0, void 0, function* () {
                let deviceAppData = this.$deviceAppDataFactory.create(appIdentifier, this.$mobileHelper.normalizePlatformName(platform), device);
                let localToDevicePaths = null;
                if (yield this.shouldTransferAllFiles(platform, deviceAppData)) {
                    localToDevicePaths = yield this.$projectFilesManager.createLocalToDevicePaths(deviceAppData, projectFilesPath, null, this.liveSyncData.excludedProjectDirsAndFiles);
                    yield this.transferFiles(deviceAppData, localToDevicePaths, this.liveSyncData.projectFilesPath, true);
                    yield device.fileSystem.putFile(this.$projectChangesService.getPrepareInfoFilePath(platform), yield this.getLiveSyncInfoFilePath(deviceAppData), appIdentifier);
                }
                if (postAction) {
                    yield this.finishLivesync(deviceAppData);
                    yield postAction(deviceAppData);
                    return;
                }
                yield this.refreshApplication(deviceAppData, localToDevicePaths, true);
                yield this.finishLivesync(deviceAppData);
            });
            yield this.$devicesService.execute(action, canExecute);
        });
    }
    partialSync(event, filePath, dispatcher, afterFileSyncAction) {
        return __awaiter(this, void 0, void 0, function* () {
            if (this.isFileExcluded(filePath, this.liveSyncData.excludedProjectDirsAndFiles)) {
                this.$logger.trace(`Skipping livesync for changed file ${filePath} as it is excluded in the patterns: ${this.liveSyncData.excludedProjectDirsAndFiles.join(", ")}`);
                return;
            }
            if (event === "add" || event === "addDir" || event === "change") {
                this.batchSync(filePath, dispatcher, afterFileSyncAction);
            }
            else if (event === "unlink" || event === "unlinkDir") {
                yield this.syncRemovedFile(filePath, afterFileSyncAction);
            }
        });
    }
    getCanExecuteAction(platform, appIdentifier) {
        let isTheSamePlatformAction = ((device) => device.deviceInfo.platform.toLowerCase() === platform.toLowerCase());
        if (this.$options.device) {
            return (device) => isTheSamePlatformAction(device) && device.deviceInfo.identifier === this.$devicesService.getDeviceByDeviceOption().deviceInfo.identifier;
        }
        return isTheSamePlatformAction;
    }
    refreshApplication(deviceAppData, localToDevicePaths, isFullSync) {
        return __awaiter(this, void 0, void 0, function* () {
            let deviceLiveSyncService = this.resolveDeviceSpecificLiveSyncService(deviceAppData.device.deviceInfo.platform, deviceAppData.device);
            this.$logger.info("Refreshing application...");
            yield deviceLiveSyncService.refreshApplication(deviceAppData, localToDevicePaths, isFullSync);
        });
    }
    finishLivesync(deviceAppData) {
        return __awaiter(this, void 0, void 0, function* () {
            this.$logger.info(`Successfully synced application ${deviceAppData.appIdentifier} on device ${deviceAppData.device.deviceInfo.identifier}.\n`);
        });
    }
    transferFiles(deviceAppData, localToDevicePaths, projectFilesPath, isFullSync) {
        return __awaiter(this, void 0, void 0, function* () {
            this.$logger.info("Transferring project files...");
            let canTransferDirectory = isFullSync && (this.$devicesService.isAndroidDevice(deviceAppData.device) || this.$devicesService.isiOSSimulator(deviceAppData.device));
            if (canTransferDirectory) {
                yield deviceAppData.device.fileSystem.transferDirectory(deviceAppData, localToDevicePaths, projectFilesPath);
            }
            else {
                yield this.$liveSyncProvider.transferFiles(deviceAppData, localToDevicePaths, projectFilesPath, isFullSync);
            }
            this.logFilesSyncInformation(localToDevicePaths, "Successfully transferred %s.", this.$logger.info);
        });
    }
    resolveDeviceSpecificLiveSyncService(platform, device) {
        return this.$injector.resolve(this.$liveSyncProvider.deviceSpecificLiveSyncServices[platform.toLowerCase()], { _device: device });
    }
    isFileExcluded(filePath, excludedPatterns) {
        let isFileExcluded = false;
        _.each(excludedPatterns, pattern => {
            if (minimatch(filePath, pattern, { nocase: true })) {
                isFileExcluded = true;
                return false;
            }
        });
        return isFileExcluded;
    }
    batchSync(filePath, dispatcher, afterFileSyncAction) {
        let platformBatch = this.batch[this.liveSyncData.platform];
        if (!platformBatch || !platformBatch.syncPending) {
            let done = () => __awaiter(this, void 0, void 0, function* () {
                dispatcher.dispatch(() => __awaiter(this, void 0, void 0, function* () {
                    try {
                        for (let platform in this.batch) {
                            let batch = this.batch[platform];
                            yield batch.syncFiles((filesToSync) => __awaiter(this, void 0, void 0, function* () {
                                yield this.$platformService.preparePlatform(this.liveSyncData.platform);
                                let canExecute = this.getCanExecuteAction(this.liveSyncData.platform, this.liveSyncData.appIdentifier);
                                let deviceFileAction = (deviceAppData, localToDevicePaths) => this.transferFiles(deviceAppData, localToDevicePaths, this.liveSyncData.projectFilesPath, !filePath);
                                let action = this.getSyncAction(filesToSync, deviceFileAction, afterFileSyncAction);
                                yield this.$devicesService.execute(action, canExecute);
                            }));
                        }
                    }
                    catch (err) {
                        this.$logger.warn(`Unable to sync files. Error is:`, err.message);
                    }
                }));
            });
            this.batch[this.liveSyncData.platform] = this.$injector.resolve(syncBatchLib.SyncBatch, { done: done });
            this.livesyncData[this.liveSyncData.platform] = this.liveSyncData;
        }
        this.batch[this.liveSyncData.platform].addFile(filePath);
    }
    syncRemovedFile(filePath, afterFileSyncAction) {
        return __awaiter(this, void 0, void 0, function* () {
            let deviceFilesAction = (deviceAppData, localToDevicePaths) => {
                let deviceLiveSyncService = this.resolveDeviceSpecificLiveSyncService(this.liveSyncData.platform, deviceAppData.device);
                return deviceLiveSyncService.removeFiles(this.liveSyncData.appIdentifier, localToDevicePaths);
            };
            let canExecute = this.getCanExecuteAction(this.liveSyncData.platform, this.liveSyncData.appIdentifier);
            let action = this.getSyncAction([filePath], deviceFilesAction, afterFileSyncAction);
            yield this.$devicesService.execute(action, canExecute);
        });
    }
    getSyncAction(filesToSync, fileSyncAction, afterFileSyncAction) {
        let action = (device) => __awaiter(this, void 0, void 0, function* () {
            let deviceAppData = null;
            let localToDevicePaths = null;
            let isFullSync = false;
            if (this.$options.clean || this.$projectChangesService.currentChanges.changesRequireBuild) {
                let buildConfig = { buildForDevice: !device.isEmulator };
                let platform = device.deviceInfo.platform;
                if (this.$platformService.shouldBuild(platform, buildConfig)) {
                    yield this.$platformService.buildPlatform(platform, buildConfig);
                }
                yield this.$platformService.installApplication(device);
                deviceAppData = this.$deviceAppDataFactory.create(this.liveSyncData.appIdentifier, this.$mobileHelper.normalizePlatformName(this.liveSyncData.platform), device);
                isFullSync = true;
            }
            else {
                deviceAppData = this.$deviceAppDataFactory.create(this.liveSyncData.appIdentifier, this.$mobileHelper.normalizePlatformName(this.liveSyncData.platform), device);
                const mappedFiles = filesToSync.map((file) => this.$projectFilesProvider.mapFilePath(file, device.deviceInfo.platform));
                const existingFiles = mappedFiles.filter(m => this.$fs.exists(m));
                this.$logger.trace("Will execute livesync for files: ", existingFiles);
                const skippedFiles = _.difference(mappedFiles, existingFiles);
                if (skippedFiles.length) {
                    this.$logger.trace("The following files will not be synced as they do not exist:", skippedFiles);
                }
                localToDevicePaths = yield this.$projectFilesManager.createLocalToDevicePaths(deviceAppData, this.liveSyncData.projectFilesPath, mappedFiles, this.liveSyncData.excludedProjectDirsAndFiles);
                yield fileSyncAction(deviceAppData, localToDevicePaths);
            }
            if (!afterFileSyncAction) {
                yield this.refreshApplication(deviceAppData, localToDevicePaths, isFullSync);
            }
            yield device.fileSystem.putFile(this.$projectChangesService.getPrepareInfoFilePath(device.deviceInfo.platform), yield this.getLiveSyncInfoFilePath(deviceAppData), this.liveSyncData.appIdentifier);
            yield this.finishLivesync(deviceAppData);
            if (afterFileSyncAction) {
                yield afterFileSyncAction(deviceAppData, localToDevicePaths);
            }
        });
        return action;
    }
    shouldTransferAllFiles(platform, deviceAppData) {
        return __awaiter(this, void 0, void 0, function* () {
            try {
                if (this.$options.clean) {
                    return false;
                }
                let fileText = yield this.$platformService.readFile(deviceAppData.device, yield this.getLiveSyncInfoFilePath(deviceAppData));
                let remoteLivesyncInfo = JSON.parse(fileText);
                let localPrepareInfo = this.$projectChangesService.getPrepareInfo(platform);
                return remoteLivesyncInfo.time !== localPrepareInfo.time;
            }
            catch (e) {
                return true;
            }
        });
    }
    getLiveSyncInfoFilePath(deviceAppData) {
        return __awaiter(this, void 0, void 0, function* () {
            let deviceRootPath = path.dirname(yield deviceAppData.getDeviceProjectRootPath());
            let deviceFilePath = helpers.fromWindowsRelativePathToUnix(path.join(deviceRootPath, livesyncInfoFileName));
            return deviceFilePath;
        });
    }
    logFilesSyncInformation(localToDevicePaths, message, action) {
        if (localToDevicePaths && localToDevicePaths.length < 10) {
            _.each(localToDevicePaths, (file) => {
                action.call(this.$logger, util.format(message, path.basename(file.getLocalPath()).yellow));
            });
        }
        else {
            action.call(this.$logger, util.format(message, "all files"));
        }
    }
}
exports.PlatformLiveSyncServiceBase = PlatformLiveSyncServiceBase;
$injector.register("platformLiveSyncService", PlatformLiveSyncServiceBase);
