var fs = require('fs')
var path = require('path')
var crypto = require('crypto')
var mkdirp = require('mkdirp')
var glob = require('glob')

var isWindows = process.platform === 'win32'
var pathSep   = path.sep

var keysForTreeWarningPrinted = false

exports.hashTree = hashTree
// This function is used by the watcher. It makes the following guarantees:
//
// (1) It never throws an exception.
//
// (2) It does not miss changes. In other words, if after this function returns,
// any part of the directory hierarchy changes, a subsequent call must
// return a different hash.
//
// (1) and (2) hold even in the face of a constantly-changing file system.
function hashTree (fullPath) {
  return hashStrings(keysForTree(fullPath))
}

exports.keysForTree = keysForTree
function keysForTree (fullPath, initialRelativePath) {
  var relativePath   = initialRelativePath || '.'
  var stats
  var statKeys

  try {
    stats = fs.statSync(fullPath)
  } catch (err) {
    if (!keysForTreeWarningPrinted) {
      console.warn('Warning: failed to stat ' + fullPath)
      keysForTreeWarningPrinted = true
    }
    // fullPath has probably ceased to exist. Leave `stats` undefined and
    // proceed hashing.
  }
  var childKeys = []
  if (stats) {
    statKeys = ['stats', stats.mode]
  } else {
    statKeys = ['stat failed']
  }
  if (stats && stats.isDirectory()) {
    var fileIdentity = stats.dev + '\x00' + stats.ino
    var entries
    try {
      entries = fs.readdirSync(fullPath).sort()
    } catch (err) {
      console.warn('Warning: Failed to read directory ' + fullPath)
      console.warn(err.stack)
      childKeys = ['readdir failed']
      // That's all there is to say about this directory.
    }
    if (entries != null) {
      for (var i = 0; i < entries.length; i++) {
        var keys = keysForTree(
          path.join(fullPath, entries[i]),
          path.join(relativePath, entries[i])
        )
        childKeys = childKeys.concat(keys)
      }
    }
  } else if (stats && stats.isFile()) {
    statKeys.push(stats.mtime.getTime())
    statKeys.push(stats.size)
  }

  return ['path', relativePath]
    .concat(statKeys)
    .concat(childKeys)
}


exports.hashStats = hashStats
function hashStats (stats, path) {
  // Both stats and path can be null
  var keys = []
  if (stats != null) {
    keys.push(stats.mode, stats.size, stats.mtime.getTime())
  }
  if (path != null) {
    keys.push(path)
  }
  return hashStrings(keys)
}


exports.hashStrings = hashStrings
function hashStrings (strings) {
  var joinedStrings = strings.join('\x00')
  return crypto.createHash('md5').update(joinedStrings).digest('hex')
}


// If src is a file, dest is a file name. If src is a directory, dest is the
// directory that the contents of src will be copied into.
//
// This function refuses to overwrite files, but accepts if directories exist
// already.
//
// This function dereferences symlinks.
//
// Note that unlike cp(1), we do not special-case if dest is an existing
// directory, because relying on things to exist when we're in the middle of
// assembling a new tree is too brittle.
//
// This function is deprecated in favor of
// https://github.com/broccolijs/node-copy-dereference
//
// copy-dereference differs from copyRecursivelySync in that it won't call
// mkdirp to create the target directory (or the parent directory of the
// target file), which makes it stricter: (1) It's not OK for the target
// directory to exist already, and (2) missing parent directories will not
// automatically be created.
exports.copyRecursivelySync = copyRecursivelySync
function copyRecursivelySync (src, dest, _mkdirp) {
  if (_mkdirp == null) _mkdirp = true
  // Note: We could try readdir'ing and catching ENOTDIR exceptions, but that
  // is 3x slower than stat'ing in the common case that we have a file.
  var srcStats = fs.statSync(src)
  if (srcStats.isDirectory()) {
    mkdirp.sync(dest)
    var entries = fs.readdirSync(src).sort()
    for (var i = 0; i < entries.length; i++) {
      // Set _mkdirp to false when recursing to avoid extra mkdirp calls.
      copyRecursivelySync(src + '/' + entries[i], dest + '/' + entries[i], false)
    }
  } else {
    if (_mkdirp) {
      mkdirp.sync(path.dirname(dest))
    }
    copyPreserveSync(src, dest, srcStats)
  }
}

// This function is deprecated in favor of
// https://github.com/broccolijs/node-copy-dereference
//
// srcStats is optional; use it as an optimization to avoid double stats
// This function refuses to overwrite files.
exports.copyPreserveSync = copyPreserveSync
function copyPreserveSync (src, dest, srcStats) {
  if (srcStats == null) srcStats = fs.statSync(src)
  if (srcStats.isFile()) {
    var content = fs.readFileSync(src)
    fs.writeFileSync(dest, content, { flag: 'wx' })
    fs.utimesSync(dest, srcStats.atime, srcStats.mtime)
  } else {
    throw new Error('Unexpected file type for ' + src)
  }
}

exports.linkRecursivelySync = linkRecursivelySync
function linkRecursivelySync () {
  throw new Error('linkRecursivelySync has been removed; use copyRecursivelySync instead (note: it does not overwrite)')
}

exports.linkAndOverwrite = linkAndOverwrite
function linkAndOverwrite () {
  throw new Error('linkAndOverwrite has been removed; use copyPreserveSync instead (note: it does not overwrite)')
}


exports.assertAbsolutePaths = assertAbsolutePaths
function assertAbsolutePaths (paths) {
  for (var i = 0; i < paths.length; i++) {
    if (paths[i][0] !== '/') {
      throw new Error('Path must be absolute: "' + paths[i] + '"')
    }
  }
}


// Multi-glob with reasonable defaults, so APIs all behave the same
exports.multiGlob = multiGlob
function multiGlob (globs, globOptions) {
  if (!Array.isArray(globs)) {
    throw new TypeError("multiGlob's first argument must be an array");
  }
  var options = {
    follow: true,
    nomount: true,
    strict: true
  }
  for (var key in globOptions) {
    if (globOptions.hasOwnProperty(key)) {
      options[key] = globOptions[key]
    }
  }

  var pathSet = {}
  var paths = []
  for (var i = 0; i < globs.length; i++) {
    if (options.nomount && globs[i][0] === '/') {
      throw new Error('Absolute paths not allowed (`nomount` is enabled): ' + globs[i])
    }
    var matches = glob.sync(globs[i], options)
    if (matches.length === 0) {
      throw new Error('Path or pattern "' + globs[i] + '" did not match any files')
    }
    for (var j = 0; j < matches.length; j++) {
      if (!pathSet[matches[j]]) {
        pathSet[matches[j]] = true
        paths.push(matches[j])
      }
    }
  }
  return paths
}


// This function is deprecated in favor of
// https://github.com/broccolijs/node-symlink-or-copy
exports.symlinkOrCopyPreserveSync = symlinkOrCopyPreserveSync
function symlinkOrCopyPreserveSync (sourcePath, destPath) {
  if (isWindows) {
    copyRecursivelySync(sourcePath, destPath)
  } else {
    if (fs.lstatSync(sourcePath).isSymbolicLink()) {
      // When we encounter symlinks, follow them. This prevents indirection
      // from growing out of control. Note: At the moment `realpath` on Node
      // is 70x slower than native: https://github.com/joyent/node/issues/7902
      sourcePath = fs.realpathSync(sourcePath)
    } else if (sourcePath[0] !== pathSep) {
      sourcePath = process.cwd() + pathSep + sourcePath
    }

    fs.symlinkSync(sourcePath, destPath)
  }
}
