text: var fs = require('fs-extra'),
      path = require('path'),
      os = require('os'),
      glob = require('fast-glob'),
      targets = require('./targets'),
      console = require('./console_ex');
  
  
  function confirm(what) {
      return function (done) {
          console.confirm(what, function (yes) {
              done(null, yes);
          });
      };
  }
  
  /**
   * Counts character occurrences into given string
   */
  function countOccurrences(ch, str) {
      return str.split(ch).length - 1;
  }
  
  
  /**
   * Load content of the .npmignore file
   */
  function* loadIgnoreFile(ignoreFile) {
      // Create ignore file if it doesn't exists, then read it
      yield fs.ensureFile(ignoreFile.path);
      ignoreFile.content = (yield fs.readFile(ignoreFile.path)).toString();
  
  
      // Be a selfish bastard if we don't have content
      if (!ignoreFile.content.trim())
          ignoreFile.content = '# Generated by dmn (https://github.com/inikulin/dmn)';
  }
  
  
  /**
   * Extract .npmignore file patterns
   */
  function parseIgnoreFile(ignoreFile) {
      // At first we'll try to determine line endings for ignore file.
      // For this we will count \n and \r\n occurrences and choose the one
      // which dominates. If we don't have line endings at all we will keep
      // default OS line endings.
      var lfCount = countOccurrences('\n', ignoreFile.content),
          crlfCount = countOccurrences('\r\n', ignoreFile.content);
  
      if (lfCount > 0 || crlfCount > 0)
          ignoreFile.eol = lfCount > crlfCount ? '\n' : '\r\n';
  
  
      ignoreFile.patterns = ignoreFile.content
  
          // Normalize new lines
          .replace(/\r\n?/g, '\n')
  
          // Then split content by new line
          .split('\n')
  
          // Trim items
          .map(function (str) {
              return str.trim();
          })
  
          // Skip empty strings and comments
          .filter(function (str) {
              return str && str.indexOf('#') !== 0;
          });
  }
  
  
  /**
   *  Produce pattern variations for filtering.
   *  We skip patterns if they are already in .npmignore files or
   *  if they are restricted to be ignored (with leading '!').
   *  Also, we test for pattern without trailing '/'
   */
  function createPatternVariations(pattern) {
      var lastCharIdx = pattern.length - 1,
          patternVariations = [pattern, '!' + pattern];
  
      if (pattern[lastCharIdx] === '/') {
          var unslashed = pattern.substring(0, lastCharIdx);
  
          patternVariations.push(unslashed);
          patternVariations.push('!' + unslashed);
      }
  
      return patternVariations;
  }
  
  
  /**
   *  Create filter for matching patterns which can be passed to Array.filter().
   */
  function createPatternFilter(ignoreFile) {
      return function (pattern) {
          var patternVariations = createPatternVariations(pattern);
  
          // Loop through patterns in .npmignore and try to find duplicates
          var duplicates = patternVariations.filter(function (patternVar) {
              return ignoreFile.patterns.indexOf(patternVar) !== -1;
          });
  
          return !duplicates.length;
      };
  }
  
  
  /**
   * Test given pattern against project directory files.
   * If we have matches then return pattern, otherwise return null
   */
  function* testPattern(pattern, projectDir) {
      var matchingFiles = yield glob(pattern, {cwd: projectDir, onlyFiles: false});
      return matchingFiles.length ? pattern : null;
  }
  
  
  /**
   * Sorting function which produces case-insensitive sort
   * and can be passed to Array.sort()
   */
  function caseInsensitiveSorter(pattern1, pattern2) {
      pattern1 = pattern1.toLowerCase();
      pattern2 = pattern2.toLowerCase();
  
      if (pattern1 === pattern2)
          return 0;
  
      return pattern1 > pattern2 ? 1 : -1;
  }
  
  
  /**
   * Create sorted list of patterns which should be added
   * to .npmignore file
   */
  function* findPatternsToAdd(projectDir, ignoreFile) {
      // Test patterns against project directory files in parallel
      var tests = yield targets.map(function (pattern) {
          return testPattern(pattern, projectDir);
      });
  
  
      return tests
  
          // Leave only those patterns that passed the test
          .filter(function (pattern) {
              return pattern !== null;
          })
  
          // Filter patterns considering current
          // content of the .npmignore file
          .filter(createPatternFilter(ignoreFile))
  
          // In case-insensitive filesystems we will have duplicate patterns
          // which differ only by case (e.g. 'Gulpfile.js' and 'gulpfile.js').
          // We need to sort list but keep those patterns grouped. Therefore,
          // we sort twice. First sort is required to get the right order
          // (upper-case first) in the group. Second sort is case-insensitive
          // and produces groups.
          .sort()
          .sort(caseInsensitiveSorter);
  }
  
  
  /**
   * Save gathered patterns to the .npmignore file
   */
  function* save(ignoreFile, patterns) {
      // Separate new content with new lines
      ignoreFile.content += ignoreFile.eol +
                            ignoreFile.eol +
                            patterns.join(ignoreFile.eol);
  
  
      // Write new content
      yield fs.writeFile(ignoreFile.path, ignoreFile.content);
  
  
      // Report task status
      console.ok('.npmignore file was updated.');
      return 'OK: saved';
  }
  
  
  /**
   * Save gathered patterns to the .npmignore file, but consider
   * user confirmation.
   */
  function* saveWithConfirmation(ignoreFile, patterns) {
      var yes = yield confirm('Save?');
  
      if (yes)
          return yield save(ignoreFile, patterns);
  
      console.ok('.npmignore file update was canceled.');
      return 'OK: canceled';
  }
  
  
  /**
   * Gen module entry point
   */
  module.exports = function* (projectDir, options) {
      var ignoreFile = {
          path: path.join(projectDir, './.npmignore'),
          content: '',
          eol: os.EOL,
          patterns: []
      };
  
      console.info('Searching for items to add to .npmignore file...').spin();
  
  
      // Gather info from existing .npmignore file first
      yield loadIgnoreFile(ignoreFile);
      parseIgnoreFile(ignoreFile);
  
  
      // Find patterns to add
      var patterns = yield findPatternsToAdd(projectDir, ignoreFile);
  
      if (!patterns.length) {
          console.ok('Unignored patterns was not found. Your .npmignore file is already perfect.');
          return 'OK: already-perfect';
      }
  
  
      // Report found patterns
      console.info('Following patterns will be added to .npmignore file:');
      console.list(patterns);
  
  
      // Save patterns or ask for user confirmation
      // if we don't have 'force' option enabled
      return yield options.force ?
                   save(ignoreFile, patterns) :
                   saveWithConfirmation(ignoreFile, patterns);
  };
  
  