import systemfs from 'fs';
import pify from 'pify';
import path from 'path';

var fs = function () {
  return global.fs || systemfs;
};

async function rm(filepath) {
  try {
    await pify(fs().unlink)(filepath);
  } catch (err) {
    if (err.code !== 'ENOENT') throw err;
  }
}

// An async exists variant
async function exists(file, options) {
  return new Promise(function (resolve, reject) {
    fs().stat(file, (err, stats) => {
      if (err) return err.code === 'ENOENT' ? resolve(false) : reject(err);
      resolve(true);
    });
  });
}

// @flow
/*::
type Node = {
  type: string,
  fullpath: string,
  basename: string,
  metadata: Object, // mode, oid
  parent?: Node,
  children: Array<Node>
}
*/

function flatFileListToDirectoryStructure(files /*: Array<{path: string}> */
) /*: Node|void */{
  const inodes /*: Map<string, Node> */ = new Map();
  const mkdir = function (name) /*: Node|void */{
    if (!inodes.has(name)) {
      let dir /*: Node */ = {
        type: 'tree',
        fullpath: name,
        basename: path.basename(name),
        metadata: {},
        children: []
      };
      inodes.set(name, dir);
      // This recursively generates any missing parent folders.
      // We do it after we've added the inode to the set so that
      // we don't recurse infinitely trying to create the root '.' dirname.
      dir.parent = mkdir(path.dirname(name));
      if (dir.parent && dir.parent !== dir) dir.parent.children.push(dir);
    }
    return inodes.get(name);
  };

  const mkfile = function (name, metadata) /*: Node|void */{
    if (!inodes.has(name)) {
      let file /*: Node */ = {
        type: 'blob',
        fullpath: name,
        basename: path.basename(name),
        metadata: metadata,
        // This recursively generates any missing parent folders.
        parent: mkdir(path.dirname(name)),
        children: []
      };
      if (file.parent) file.parent.children.push(file);
      inodes.set(name, file);
    }
    return inodes.get(name);
  };

  for (let file of files) {
    mkfile(file.path, file);
  }
  return inodes.get('.');
}

const readdir = pify(fs().readdir);
const stat = pify(fs().stat);

// Based on an elegant concurrent recursive solution from SO
// https://stackoverflow.com/a/45130990/2168416
async function readDirAsFlatFileList(dir) {
  const subdirs = await readdir(dir);
  const files = await Promise.all(subdirs.map(async subdir => {
    const res = dir + '/' + subdir;
    return (await stat(res)).isDirectory() ? readDirAsFlatFileList(res) : res;
  }));
  return files.reduce((a, f) => a.concat(f), []);
}

async function sleep(ms) {
  return new Promise((resolve, reject) => setTimeout(resolve, ms));
}

// @flow
// This is modeled after the lockfile strategy used by the git source code.
const delayedReleases = new Map();

async function lock(filename /*: string */
, triesLeft /*: number */ = 3) {
  // check to see if we still have it
  if (delayedReleases.has(filename)) {
    clearTimeout(delayedReleases.get(filename));
    delayedReleases.delete(filename);
    return;
  }
  if (triesLeft === 0) {
    throw new Error(`Unable to acquire lockfile '${filename}'. Exhausted tries.`);
  }
  try {
    await pify(fs().mkdir)(`${filename}.lock`);
  } catch (err) {
    if (err.code === 'EEXIST') {
      await sleep(100);
      await lock(filename, triesLeft - 1);
    }
  }
}

async function unlock(filename /*: string */
, delayRelease /*: number */ = 50) {
  if (delayedReleases.has(filename)) {
    throw new Error('Cannot double-release lockfile');
  }
  // Basically, we lie and say it was deleted ASAP.
  // But really we wait a bit to see if you want to acquire it again.
  delayedReleases.set(filename, setTimeout(async () => {
    delayedReleases.delete(filename);
    await pify(fs().rmdir)(`${filename}.lock`);
  }));
}

// @flow
async function mkdir(dirpath /*: string */) {
  try {
    await pify(fs().mkdir)(dirpath);
    return;
  } catch (err) {
    // If err is null then operation succeeded!
    if (err === null) return;
    // If the directory already exists, that's OK!
    if (err.code === 'EEXIST') return;
    // If we got a "no such file or directory error" backup and try again.
    if (err.code === 'ENOENT') {
      let parent = path.dirname(dirpath);
      // Check to see if we've gone too far
      if (parent === '.' || parent === '/' || parent === dirpath) throw err;
      // Infinite recursion, what could go wrong?
      await mkdir(parent);
      await mkdir(dirpath);
    }
  }
}

async function mkdirs(dirlist /*: string[] */) {
  return Promise.all(dirlist.map(mkdir));
}

// An async readFile variant that returns null instead of throwing errors
async function read(file, options) {
  return new Promise(function (resolve, reject) {
    fs().readFile(file, options, (err, file) => err ? resolve(null) : resolve(file));
  });
}

// @flow
// An async writeFile variant that automatically creates missing directories,
// and returns null instead of throwing errors.
async function write(filepath /*: string */
, contents /*: string|Buffer */
, options /*: Object */ = {}) {
  try {
    await pify(fs().writeFile)(filepath, contents, options);
    return;
  } catch (err) {
    // Hmm. Let's try mkdirp and try again.
    await mkdir(path.dirname(filepath));
    await pify(fs().writeFile)(filepath, contents, options);
  }
}

var name = "isomorphic-git";
var version = "0.0.23";
var description = "A pure JavaScript implementation of git for node and browsers!";
var main = "dist/for-node/";
var browser = "dist/for-browserify/";
var module$1 = "dist/for-future/";
var unpkg = "dist/bundle.umd.min.js";
var bin = { "isogit": "./cli.js" };
var engines = { "node": ">=7.6.0" };
var scripts = { "start": "nps", "test": "nps test", "precommit": "nps format toc" };
var repository = { "type": "git", "url": "git+https://github.com/wmhilton/isomorphic-git.git" };
var keywords = ["git"];
var author = "William Hilton <wmhilton@gmail.com>";
var license = "Unlicense";
var bugs = { "url": "https://github.com/wmhilton/isomorphic-git/issues" };
var homepage = "https://github.com/wmhilton/isomorphic-git#readme";
var files = ["dist", "cli.js"];
var dependencies = { "async-lock": "^1.0.0", "await-stream-ready": "^1.0.1", "babel-runtime": "^6.26.0", "buffer": "^5.0.7", "buffer-peek-stream": "^1.0.1", "buffercursor": "0.0.12", "gartal": "^1.1.2", "git-apply-delta": "0.0.7", "git-list-pack": "0.0.10", "ignore": "^3.3.6", "ini": "^1.3.4", "marky": "^1.2.0", "minimisted": "^2.0.0", "openpgp": "^2.5.10", "pad": "^2.0.1", "pako": "^1.0.5", "pify": "^3.0.0", "shasum": "^1.0.2", "simple-concat": "^1.0.0", "simple-get": "^2.7.0", "thru": "git+https://github.com/wmhilton-contrib/thru.git#master" };
var devDependencies = { "babel-plugin-external-helpers": "^6.22.0", "babel-plugin-transform-es2015-modules-commonjs": "^6.24.1", "babel-plugin-transform-object-rest-spread": "^6.23.0", "babel-plugin-transform-runtime": "^6.23.0", "babel-preset-env": "^1.6.0", "babel-preset-flow": "^6.23.0", "ban-sensitive-files": "^1.9.0", "browserfs": "^1.4.3", "browserify": "^14.4.0", "browserify-shim": "^3.8.14", "codecov": "^3.0.0", "doctoc": "^1.3.0", "husky": "^0.14.3", "jest": "^21.2.1", "jest-fixtures": "^0.6.0", "jsonfile": "^4.0.0", "karma": "^1.7.1", "karma-browserify": "^5.1.1", "karma-chrome-launcher": "^2.2.0", "karma-firefox-launcher": "^1.0.1", "karma-sauce-launcher": "^1.2.0", "karma-tap": "^3.1.1", "lodash": "^4.17.4", "nock": "^9.0.17", "npm-run-all": "^4.1.1", "nps": "^5.7.1", "nps-utils": "^1.4.0", "parse-header-stream": "^1.1.1", "prettier-standard": "^7.0.1", "rollup": "^0.50.0", "rollup-plugin-babel": "^3.0.2", "rollup-plugin-json": "^2.3.0", "standard": "^10.0.3", "stream-equal": "^1.0.1", "tape": "^4.8.0", "uglify-es": "^3.1.2", "watchify": "^3.9.0" };
var ava = { "source": ["dist/for-node/*"] };
var browserify = { "transform": ["browserify-shim"] };
var testling = { "files": "testling/basic-test.js", "browsers": ["chrome/latest", "firefox/latest", "ie/latest"] };
var jest = { "testPathIgnorePatterns": ["__helpers__"], "testEnvironment": "node" };
var _package = {
	name: name,
	version: version,
	description: description,
	main: main,
	browser: browser,
	module: module$1,
	unpkg: unpkg,
	bin: bin,
	engines: engines,
	scripts: scripts,
	repository: repository,
	keywords: keywords,
	author: author,
	license: license,
	bugs: bugs,
	homepage: homepage,
	files: files,
	dependencies: dependencies,
	devDependencies: devDependencies,
	ava: ava,
	browserify: browserify,
	testling: testling,
	jest: jest,
	"browserify-shim": { "fs": "global:fs" }
};

var _package$1 = Object.freeze({
	name: name,
	version: version,
	description: description,
	main: main,
	browser: browser,
	module: module$1,
	unpkg: unpkg,
	bin: bin,
	engines: engines,
	scripts: scripts,
	repository: repository,
	keywords: keywords,
	author: author,
	license: license,
	bugs: bugs,
	homepage: homepage,
	files: files,
	dependencies: dependencies,
	devDependencies: devDependencies,
	ava: ava,
	browserify: browserify,
	testling: testling,
	jest: jest,
	default: _package
});

export { rm, exists, flatFileListToDirectoryStructure, readDirAsFlatFileList, fs, lock, unlock, mkdirs, read, sleep, write, _package$1 as pkg };
