var fs = require('fs')

var wx = 'wx'
if (process.version.match(/^v0.[456]/)) {
  var c = require('constants')
  wx = c.O_TRUNC | c.O_CREAT | c.O_WRONLY | c.O_EXCL
}

var locks = {}

process.on('exit', function () {
  // cleanup
  Object.keys(locks).forEach(exports.unlockSync)
})

// XXX https://github.com/joyent/node/issues/3555
// Remove when node 0.8 is deprecated.
process.on('uncaughtException', function H (er) {
  var l = process.listeners('uncaughtException').filter(function (h) {
    return h !== H
  })
  if (!l.length) {
    // cleanup
    Object.keys(locks).forEach(exports.unlockSync)
    process.removeListener('uncaughtException', H)
    throw er
  }
})

exports.unlock = function (path, cb) {
  // best-effort.  unlocking an already-unlocked lock is a noop
  fs.unlink(path, function (unlinkEr) {
    if (!locks.hasOwnProperty(path)) return cb()
    fs.close(locks[path], function (closeEr) {
      delete locks[path]
      cb()
    })
  })
}

exports.unlockSync = function (path) {
  try { fs.unlinkSync(path) } catch (er) {}
  if (!locks.hasOwnProperty(path)) return
  // best-effort.  unlocking an already-unlocked lock is a noop
  try { fs.close(locks[path]) } catch (er) {}
  delete locks[path]
}


// if the file can be opened in readonly mode, then it's there.
// if the error is something other than ENOENT, then it's not.
exports.check = function (path, opts, cb) {
  if (typeof opts === 'function') cb = opts, opts = {}
  fs.open(path, 'r', function (er, fd) {
    if (er) {
      if (er.code !== 'ENOENT') return cb(er)
      return cb(null, false)
    }

    if (!opts.stale) {
      return fs.close(fd, function (er) {
        return cb(er, true)
      })
    }

    fs.fstat(fd, function (er, st) {
      if (er) return fs.close(fd, function (er2) {
        return cb(er)
      })

      fs.close(fd, function (er) {
        var age = Date.now() - st.ctime.getTime()
        return cb(er, age <= opts.stale)
      })
    })
  })
}

exports.checkSync = function (path, opts) {
  opts = opts || {}
  if (opts.wait) {
    throw new Error('opts.wait not supported sync for obvious reasons')
  }

  try {
    var fd = fs.openSync(path, 'r')
  } catch (er) {
    if (er.code !== 'ENOENT') throw er
    return false
  }

  if (!opts.stale) {
    fs.closeSync(fd)
    return true
  }

  // file exists.  however, might be stale
  if (opts.stale) {
    try {
      var st = fs.fstatSync(fd)
    } finally {
      fs.closeSync(fd)
    }
    var age = Date.now() - st.ctime.getTime()
    return (age <= opts.stale)
  }
}



exports.lock = function (path, opts, cb) {
  if (typeof opts === 'function') cb = opts, opts = {}

  if (typeof opts.retries === 'number' && opts.retries > 0) {
    cb = (function (orig) { return function (er, fd) {
      if (!er) return orig(er, fd)
      var newRT = opts.retries - 1
      opts_ = Object.create(opts, { retries: { value: newRT }})
      if (opts.retryWait) setTimeout(function() {
        exports.lock(path, opts_, orig)
      }, opts.retryWait)
      else exports.lock(path, opts_, orig)
    }})(cb)
  }

  // try to engage the lock.
  // if this succeeds, then we're in business.
  fs.open(path, wx, function (er, fd) {
    if (!er) {
      locks[path] = fd
      return cb(null, fd)
    }

    // something other than "currently locked"
    // maybe eperm or something.
    if (er.code !== 'EEXIST') return cb(er)

    // someone's got this one.  see if it's valid.
    if (opts.stale) fs.stat(path, function (er, st) {
      if (er) {
        if (er.code === 'ENOENT') {
          // expired already!
          var opts_ = Object.create(opts, { stale: { value: false }})
          exports.lock(path, opts_, cb)
          return
        }
        return cb(er)
      }

      var age = Date.now() - st.ctime.getTime()
      if (age > opts.stale) {
        exports.unlock(path, function (er) {
          if (er) return cb(er)
          var opts_ = Object.create(opts, { stale: { value: false }})
          exports.lock(path, opts_, cb)
        })
      } else notStale(er, path, opts, cb)
    })
    else notStale(er, path, opts, cb)
  })
}

function notStale (er, path, opts, cb) {
  if (typeof opts.wait === 'number' && opts.wait > 0) {
    // wait for some ms for the lock to clear
    var start = Date.now()

    var retried = false
    function retry () {
      if (retried) return
      retried = true
      // maybe already closed.
      try { watcher.close() } catch (e) {}
      clearTimeout(timer)
      var newWait = Date.now() - start
      var opts_ = Object.create(opts, { wait: { value: newWait }})
      exports.lock(path, opts_, cb)
    }

    try {
      var watcher = fs.watch(path, function (change) {
        if (change === 'rename') {
          // ok, try and get it now.
          // if this fails, then continue waiting, maybe.
          retry()
        }
      })
      watcher.on('error', function (er) {
        // usually means it expired before the watcher spotted it
        retry()
      })
    } catch (er) {
      retry()
    }

    var timer = setTimeout(function () {
      try { watcher.close() } catch (e) {}
      cb(er)
    }, opts.wait)
  } else {
    // failed to lock!
    return cb(er)
  }
}

exports.lockSync = function (path, opts) {
  opts = opts || {}
  if (opts.wait || opts.retryWait) {
    throw new Error('opts.wait not supported sync for obvious reasons')
  }

  try {
    var fd = fs.openSync(path, wx)
    locks[path] = fd
    return fd
  } catch (er) {
    if (er.code !== 'EEXIST') return retryThrow(path, opts, er)

    if (opts.stale) {
      var st = fs.statSync(path)
      var age = Date.now() - st.ctime.getTime()
      if (age > opts.stale) {
        exports.unlockSync(path)
        return exports.lockSync(path, opts)
      }
    }

    // failed to lock!
    return retryThrow(path, opts, er)
  }
}

function retryThrow (path, opts, er) {
  if (typeof opts.retries === 'number' && opts.retries > 0) {
    var newRT = opts.retries - 1
    var opts_ = Object.create(opts, { retries: { value: newRT }})
    return exports.lockSync(path, opts_)
  }
  throw er
}

