//
// used by archiver
// Source: https://github.com/Yqnn/node-readdir-glob
//

module.exports = readdirGlob;

const fs = require('fs');
const { EventEmitter } = require('events');
const { Minimatch } = require('../archiverUtils/minimatch');
const { resolve } = require('path');

function readdir(dir, strict) {
  return new Promise((resolve, reject) => {
    fs.readdir(dir, { withFileTypes: true }, (err, files) => {
      if (err) {
        switch (err.code) {
          case 'ENOTDIR':      // Not a directory
            if (strict) {
              reject(err);
            } else {
              resolve([]);
            }
            break;
          case 'ENOTSUP':      // Operation not supported
          case 'ENOENT':       // No such file or directory
          case 'ENAMETOOLONG': // Filename too long
          case 'UNKNOWN':
            resolve([]);
            break;
          case 'ELOOP':        // Too many levels of symbolic links
          default:
            reject(err);
            break;
        }
      } else {
        resolve(files);
      }
    });
  });
}
function stat(file, followSymlinks) {
  return new Promise((resolve, reject) => {
    const statFunc = followSymlinks ? fs.stat : fs.lstat;
    statFunc(file, (err, stats) => {
      if (err) {
        switch (err.code) {
          case 'ENOENT':
            if (followSymlinks) {
              // Fallback to lstat to handle broken links as files
              resolve(stat(file, false));
            } else {
              resolve(null);
            }
            break;
          default:
            resolve(null);
            break;
        }
      } else {
        resolve(stats);
      }
    });
  });
}

async function* exploreWalkAsync(dir, path, followSymlinks, useStat, shouldSkip, strict) {
  let files = await readdir(path + dir, strict);
  for (const file of files) {
    let name = file.name;
    if (name === undefined) {
      // undefined file.name means the `withFileTypes` options is not supported by node
      // we have to call the stat function to know if file is directory or not.
      name = file;
      useStat = true;
    }
    const filename = dir + '/' + name;
    const relative = filename.slice(1); // Remove the leading /
    const absolute = path + '/' + relative;
    let stats = null;
    if (useStat || followSymlinks) {
      stats = await stat(absolute, followSymlinks);
    }
    if (!stats && file.name !== undefined) {
      stats = file;
    }
    if (stats === null) {
      stats = { isDirectory: () => false };
    }

    if (stats.isDirectory()) {
      if (!shouldSkip(relative)) {
        yield { relative, absolute, stats };
        yield* exploreWalkAsync(filename, path, followSymlinks, useStat, shouldSkip, false);
      }
    } else {
      yield { relative, absolute, stats };
    }
  }
}
async function* explore(path, followSymlinks, useStat, shouldSkip) {
  yield* exploreWalkAsync('', path, followSymlinks, useStat, shouldSkip, true);
}


function readOptions(options) {
  return {
    pattern: options.pattern,
    dot: !!options.dot,
    noglobstar: !!options.noglobstar,
    matchBase: !!options.matchBase,
    nocase: !!options.nocase,
    ignore: options.ignore,
    skip: options.skip,

    follow: !!options.follow,
    stat: !!options.stat,
    nodir: !!options.nodir,
    mark: !!options.mark,
    silent: !!options.silent,
    absolute: !!options.absolute
  };
}

class ReaddirGlob extends EventEmitter {
  constructor(cwd, options, cb) {
    super();
    if (typeof options === 'function') {
      cb = options;
      options = null;
    }

    this.options = readOptions(options || {});

    this.matchers = [];
    if (this.options.pattern) {
      const matchers = Array.isArray(this.options.pattern) ? this.options.pattern : [this.options.pattern];
      this.matchers = matchers.map(m =>
        new Minimatch(m, {
          dot: this.options.dot,
          noglobstar: this.options.noglobstar,
          matchBase: this.options.matchBase,
          nocase: this.options.nocase
        })
      );
    }

    this.ignoreMatchers = [];
    if (this.options.ignore) {
      const ignorePatterns = Array.isArray(this.options.ignore) ? this.options.ignore : [this.options.ignore];
      this.ignoreMatchers = ignorePatterns.map(ignore =>
        new Minimatch(ignore, { dot: true })
      );
    }

    this.skipMatchers = [];
    if (this.options.skip) {
      const skipPatterns = Array.isArray(this.options.skip) ? this.options.skip : [this.options.skip];
      this.skipMatchers = skipPatterns.map(skip =>
        new Minimatch(skip, { dot: true })
      );
    }

    this.iterator = explore(resolve(cwd || '.'), this.options.follow, this.options.stat, this._shouldSkipDirectory.bind(this));
    this.paused = false;
    this.inactive = false;
    this.aborted = false;

    if (cb) {
      this._matches = [];
      this.on('match', match => this._matches.push(this.options.absolute ? match.absolute : match.relative));
      this.on('error', err => cb(err));
      this.on('end', () => cb(null, this._matches));
    }

    setTimeout(() => this._next(), 0);
  }

  _shouldSkipDirectory(relative) {
    //console.log(relative, this.skipMatchers.some(m => m.match(relative)));
    return this.skipMatchers.some(m => m.match(relative));
  }

  _fileMatches(relative, isDirectory) {
    const file = relative + (isDirectory ? '/' : '');
    return (this.matchers.length === 0 || this.matchers.some(m => m.match(file)))
      && !this.ignoreMatchers.some(m => m.match(file))
      && (!this.options.nodir || !isDirectory);
  }

  _next() {
    if (!this.paused && !this.aborted) {
      this.iterator.next()
        .then((obj) => {
          if (!obj.done) {
            const isDirectory = obj.value.stats.isDirectory();
            if (this._fileMatches(obj.value.relative, isDirectory)) {
              let relative = obj.value.relative;
              let absolute = obj.value.absolute;
              if (this.options.mark && isDirectory) {
                relative += '/';
                absolute += '/';
              }
              if (this.options.stat) {
                this.emit('match', { relative, absolute, stat: obj.value.stats });
              } else {
                this.emit('match', { relative, absolute });
              }
            }
            this._next(this.iterator);
          } else {
            this.emit('end');
          }
        })
        .catch((err) => {
          this.abort();
          this.emit('error', err);
          if (!err.code && !this.options.silent) {
            console.error(err);
          }
        });
    } else {
      this.inactive = true;
    }
  }

  abort() {
    this.aborted = true;
  }

  pause() {
    this.paused = true;
  }

  resume() {
    this.paused = false;
    if (this.inactive) {
      this.inactive = false;
      this._next();
    }
  }
}


function readdirGlob(pattern, options, cb) {
  return new ReaddirGlob(pattern, options, cb);
}
readdirGlob.ReaddirGlob = ReaddirGlob;