"use strict";

Object.defineProperty(exports, "__esModule", {
  value: true
});
exports.sync = sync;

var _execa = _interopRequireDefault(require("execa"));

var _ora = _interopRequireDefault(require("ora"));

var _flatten = _interopRequireDefault(require("lodash/flatten"));

var _fromPairs = _interopRequireDefault(require("lodash/fromPairs"));

var _mergeFiles = require("./merge-files");

var _mergeScripts = require("./merge-scripts");

var _mergeDependencies = require("./merge-dependencies");

var _coatManifestFile = require("../types/coat-manifest-file");

var _constants = require("../constants");

var _polishFiles = require("./polish-files");

var _isEqual = _interopRequireDefault(require("lodash/isEqual"));

var _generateLockfileFiles = require("../lockfiles/generate-lockfile-files");

var _getUnmanagedFiles = require("./get-unmanaged-files");

var _getAllTemplates = require("../util/get-all-templates");

var _updateLockfiles = require("../lockfiles/update-lockfiles");

var _setup = require("../setup");

var _immer = _interopRequireDefault(require("immer"));

var _groupFiles = require("./group-files");

var _getDefaultFiles = require("./get-default-files");

var _getCurrentFiles = require("./get-current-files");

var _updateFilesOnDisk = require("./update-files-on-disk");

var _removeUnmanagedDependencies = require("./remove-unmanaged-dependencies");

var _getNormalizedFilePath = require("../util/get-normalized-file-path");

function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; }

/**
 * Generates all files from the current coat project.
 *
 * The sync function gathers all extended templates of the current project
 * and merges all files that should be written to the project directory.
 *
 * Setup tasks that have not been run before will be run at the beginning
 * of the sync process, to ensure that the templates have access to the
 * latest configuration and task results that are required to generate
 * the project files.
 *
 * @param cwd The working directory of the current coat project
 */
async function sync(cwd) {
  var _context$packageJson, _context$packageJson$, _context$packageJson2, _context$packageJson$2, _context$packageJson3, _context$packageJson$3, _context$packageJson4, _context$packageJson$4, _context$packageJson5, _mergedFiles$find;

  // Run setup tasks that have not been run before
  // TODO: See #36
  // Let user skip setup tasks if they should be run later
  let context = await (0, _setup.setup)(cwd, false); // Gather all extended templates

  const allTemplates = (0, _getAllTemplates.getAllTemplates)(context); // Merge scripts

  const {
    scripts: mergedScripts,
    parallelScriptPrefixes
  } = (0, _mergeScripts.mergeScripts)(allTemplates.map(template => template.scripts)); // Get all current scripts from the project's package.json file
  //

  const previouslyManagedScripts = new Set(context.coatGlobalLockfile.scripts); // Node.js 10 compatibility
  // Use Object.fromEntries once Node 10 is no longer supported

  const currentScripts = (0, _fromPairs.default)(Object.entries(((_context$packageJson = context.packageJson) === null || _context$packageJson === void 0 ? void 0 : _context$packageJson.scripts) || {}).filter(([scriptName]) => // Filter out scripts that have been added / managed by coat.
  // They will be re-added from mergedScripts or will be removed
  // in case the coat manifest or its templates no longer supply
  // a particular script
  !previouslyManagedScripts.has(scriptName) && // Also filter out scripts that start with a script that is managed by
  // coat and will be run in parallel. This is done in order to ensure
  // that the scripts from coat are running as expected
  parallelScriptPrefixes.every(prefix => !scriptName.startsWith(prefix))));
  const scripts = { ...currentScripts,
    ...mergedScripts
  }; // Merge dependencies
  //
  // Add current dependencies from package.json, to satisfy the
  // correct minimum required versions for potentially existing dependencies

  const currentDependencies = {
    dependencies: (_context$packageJson$ = (_context$packageJson2 = context.packageJson) === null || _context$packageJson2 === void 0 ? void 0 : _context$packageJson2.dependencies) !== null && _context$packageJson$ !== void 0 ? _context$packageJson$ : {},
    devDependencies: (_context$packageJson$2 = (_context$packageJson3 = context.packageJson) === null || _context$packageJson3 === void 0 ? void 0 : _context$packageJson3.devDependencies) !== null && _context$packageJson$2 !== void 0 ? _context$packageJson$2 : {},
    optionalDependencies: (_context$packageJson$3 = (_context$packageJson4 = context.packageJson) === null || _context$packageJson4 === void 0 ? void 0 : _context$packageJson4.optionalDependencies) !== null && _context$packageJson$3 !== void 0 ? _context$packageJson$3 : {},
    peerDependencies: (_context$packageJson$4 = (_context$packageJson5 = context.packageJson) === null || _context$packageJson5 === void 0 ? void 0 : _context$packageJson5.peerDependencies) !== null && _context$packageJson$4 !== void 0 ? _context$packageJson$4 : {}
  };
  const templateDependencies = (0, _mergeDependencies.mergeDependencies)(allTemplates.map(template => template.dependencies)); // Remove dependencies that have been previously added
  // by coat, but are no longer a part of any template

  const strippedCurrentDependencies = (0, _removeUnmanagedDependencies.removeUnmanagedDependencies)(currentDependencies, templateDependencies, context);
  const mergedDependencies = (0, _mergeDependencies.mergeDependencies)([strippedCurrentDependencies, templateDependencies]);
  const allFiles = []; // Add package.json file entry that can be merged and customized

  const packageJsonFileContent = { ...context.packageJson,
    ...mergedDependencies,
    scripts
  }; // Remove empty dependency and scripts properties

  const keysToRemove = ["dependencies", "devDependencies", "peerDependencies", "optionalDependencies", "scripts"];
  keysToRemove.forEach(key => {
    if (!Object.keys(packageJsonFileContent[key]).length) {
      delete packageJsonFileContent[key];
    }
  }); // Only add package.json to the allFiles array if it currently exists
  // or if the contents of the file have been updated by coat

  if (context.packageJson || !(0, _isEqual.default)(packageJsonFileContent, {})) {
    allFiles.push({
      type: _coatManifestFile.CoatManifestFileType.Json,
      file: _constants.PACKAGE_JSON_FILENAME,
      content: packageJsonFileContent
    }); // Update package.json in context
    // to let files access the newest version

    context = (0, _immer.default)(context, draft => {
      draft.packageJson = packageJsonFileContent;
    });
  } // Add default files that are generated by coat sync


  const defaultFiles = (0, _getDefaultFiles.getDefaultFiles)();
  allFiles.push(...defaultFiles); // Add files from all templates

  allFiles.push( // Node.js 10 compatibility
  // Use Array.flatMap once Node 10 is no longer supported
  ...(0, _flatten.default)(allTemplates.map(template => template.files))); // Group files by file path

  const groupedFiles = (0, _groupFiles.groupFiles)(allFiles, context); // Gather previously placed files to exclude one-time files
  // that have already been generated

  const previouslyPlacedFiles = [...context.coatGlobalLockfile.files, ...context.coatLocalLockfile.files].map(file => file.path); // Gather the files of which the current disk content
  // should be retrieved to determine they need to be updated

  const filesToRetrieve = [...Object.keys(groupedFiles), ...previouslyPlacedFiles.map(relativePath => (0, _getNormalizedFilePath.getNormalizedFilePath)(relativePath, context))]; // Filter out duplicate file paths, since filesToRetrieve
  // contains both the current lockfile entries and new groupedFiles
  // keys that overlap for consecutive sync runs

  const filesToRetrieveUnique = [...new Set(filesToRetrieve)]; // Retrieve the contents of the files

  const currentFiles = await (0, _getCurrentFiles.getCurrentFiles)(filesToRetrieveUnique); // Create a Set to easily access the generated file paths
  // when grouping files

  const previouslyPlacedFileSet = new Set(previouslyPlacedFiles); // Group by files that should only be placed once
  // and have already been placed in a previous run of coat sync

  const {
    onceAlreadyPlaced,
    filesToMerge
  } = Object.values(groupedFiles).reduce((accumulator, file) => {
    let targetProperty;

    if (file.once && ( // Check if the once file has already been placed via coat
    // and is tracked in a lockfile
    previouslyPlacedFileSet.has(file.relativePath) || // Even if the file has not yet been tracked in a lockfile
    // it should also not be placed if it already exists on the disk
    typeof currentFiles[file.file] !== "undefined")) {
      targetProperty = accumulator.onceAlreadyPlaced;
    } else {
      targetProperty = accumulator.filesToMerge;
    }

    targetProperty[file.file] = file;
    return accumulator;
  }, {
    onceAlreadyPlaced: {},
    filesToMerge: {}
  }); // Merge files

  const mergedFiles = await (0, _mergeFiles.mergeFiles)(filesToMerge, context); // Polish files and convert their content into strings to
  // be able to place them on the disk

  const polishedFiles = (0, _polishFiles.polishFiles)(mergedFiles, context); // Generate new coat lockfile files property from merged files

  const lockFileCanditates = [...Object.values(onceAlreadyPlaced), ...polishedFiles]; // Split lockfile candidates by local and global file entries

  const {
    localLockFileEntries,
    globalLockFileEntries
  } = lockFileCanditates.reduce((accumulator, file) => {
    if (file.local) {
      accumulator.localLockFileEntries.push(file);
    } else {
      accumulator.globalLockFileEntries.push(file);
    }

    return accumulator;
  }, {
    localLockFileEntries: [],
    globalLockFileEntries: []
  });
  const newLocalLockFiles = (0, _generateLockfileFiles.generateLockfileFiles)(localLockFileEntries);
  const newGlobalLockFiles = (0, _generateLockfileFiles.generateLockfileFiles)(globalLockFileEntries);
  const filesToRemove = [...(0, _getUnmanagedFiles.getUnmanagedFiles)(newLocalLockFiles, context.coatLocalLockfile), ...(0, _getUnmanagedFiles.getUnmanagedFiles)(newGlobalLockFiles, context.coatGlobalLockfile)]; // Place or update new polished files on the disk and remove unmanaged files

  await (0, _updateFilesOnDisk.updateFilesOnDisk)(polishedFiles, filesToRemove, currentFiles, context); // Node.js 10 compatibility
  // Use Object.fromEntries once Node 10 is no longer supported

  const newLockfileDependencies = (0, _fromPairs.default)(Object.entries(templateDependencies).map(([dependencyKey, dependencyEntries]) => [dependencyKey, Object.keys(dependencyEntries).sort()]));
  const newLockfileScripts = Object.keys(mergedScripts).sort(); // Update the lockfiles with the new file entries

  context = await (0, _updateLockfiles.updateLockfiles)({
    updatedGlobalLockfile: {
      files: newGlobalLockFiles,
      dependencies: newLockfileDependencies,
      scripts: newLockfileScripts
    },
    updatedLocalLockfile: {
      files: newLocalLockFiles
    },
    context
  }); // Retrieve dependencies after merging to run npm install if they have changed
  //
  // If the package.json file still exists, it has to be a JsonObject,
  // since an altered file type would throw an error during merging

  const mergedPackageJson = (_mergedFiles$find = mergedFiles.find(file => file.relativePath === _constants.PACKAGE_JSON_FILENAME)) === null || _mergedFiles$find === void 0 ? void 0 : _mergedFiles$find.content;

  if (mergedPackageJson) {
    var _mergedPackageJson$de, _mergedPackageJson$de2, _mergedPackageJson$op, _mergedPackageJson$pe;

    const finalDependencies = {
      dependencies: (_mergedPackageJson$de = mergedPackageJson.dependencies) !== null && _mergedPackageJson$de !== void 0 ? _mergedPackageJson$de : {},
      devDependencies: (_mergedPackageJson$de2 = mergedPackageJson.devDependencies) !== null && _mergedPackageJson$de2 !== void 0 ? _mergedPackageJson$de2 : {},
      optionalDependencies: (_mergedPackageJson$op = mergedPackageJson.optionalDependencies) !== null && _mergedPackageJson$op !== void 0 ? _mergedPackageJson$op : {},
      peerDependencies: (_mergedPackageJson$pe = mergedPackageJson.peerDependencies) !== null && _mergedPackageJson$pe !== void 0 ? _mergedPackageJson$pe : {}
    };

    if (!(0, _isEqual.default)(finalDependencies, currentDependencies)) {
      const installSpinner = (0, _ora.default)("Installing dependencies\n").start();

      try {
        await (0, _execa.default)("npm", ["install"], {
          cwd: context.cwd
        });
        installSpinner.succeed();
      } catch (error) {
        installSpinner.fail();
        throw error;
      }
    }
  }
}