var fs = require("fs")
  , child_process = require("child_process")
  , path = require("path")
  , chain = require("slide").chain
  , asyncMap = require("slide").asyncMap
  , TapProducer = require("./tap-producer.js")
  , TapConsumer = require("./tap-consumer.js")
  , assert = require("./tap-assert.js")
  , inherits = require("inherits")
  , util = require("util")
  , CovHtml = require("./tap-cov-html.js")
  , glob = require("glob")

  // XXX Clean up the coverage options
  , doCoverage = process.env.TAP_COV
               || process.env.npm_package_config_coverage
               || process.env.npm_config_coverage

module.exports = Runner

inherits(Runner, TapProducer)

function Runner (options, cb) {
  this.options = options

  var diag = this.options.diag
  var dir = this.options.argv.remain
  TapProducer.call(this, diag)

  this.doCoverage = doCoverage
  // An array of full paths to files to obtain coverage
  this.coverageFiles = []
  // The source of these files
  this.coverageFilesSource = {}
  // Where to write coverage information
  this.coverageOutDir = this.options["coverage-dir"]
  // Temporary test files bunkerified we'll remove later
  this.f2delete = []
  // Raw coverage stats, as read from JSON files
  this.rawCovStats = []
  // Processed coverage information, per file to cover:
  this.covStats = {}

  if (dir) {
    var filesToCover = this.options.cover

    if (doCoverage) {
      var mkdirp = require("mkdirp")
      this.coverageOutDir = path.resolve(this.coverageOutDir)
      this.getFilesToCover(filesToCover)
      var self = this
      return mkdirp(this.coverageOutDir, '0755', function (er) {
        if (er) return self.emit("error", er)
        self.run(dir, cb)
      })
    }

    this.run(dir, cb)
  }
}


Runner.prototype.run = function() {
  var self = this
    , args = Array.prototype.slice.call(arguments)
    , cb = args.pop() || finish

  function finish (er) {
    if (er) {
      self.emit("error", er)
    }

    if (!doCoverage) return self.end()

    // Cleanup temporary test files with coverage:
    self.f2delete.forEach(function(f) {
      fs.unlinkSync(f)
    })
    self.getFilesToCoverSource(function(err, data) {
      if (err) {
        self.emit("error", err)
      }
      self.getPerFileCovInfo(function(err, data) {
        if (err) {
          self.emit("error", err)
        }
        self.mergeCovStats(function(err, data) {
          if (err) {
            self.emit("error", err)
          }
          CovHtml(self.covStats, self.coverageOutDir, function() {
            self.end()
          })
        })
      })
    })
  }

  if (Array.isArray(args[0])) {
    args = args[0]
  }
  self.runFiles(args, "", cb)
}

Runner.prototype.runDir = function (dir, cb) {
  var self = this
  fs.readdir(dir, function (er, files) {
    if (er) {
      self.write(assert.fail("failed to readdir " + dir, { error: er }))
      self.end()
      return
    }
    files = files.sort(function(a, b) {
      return a > b ? 1 : -1
    })
    files = files.filter(function(f) {
      return !f.match(/^\./)
    })
    files = files.map(function(file) {
      return path.resolve(dir, file)
    })

    self.runFiles(files, path.resolve(dir), cb)
  })
}


// glob the filenames so that test/*.js works on windows
Runner.prototype.runFiles = function (files, dir, cb) {
  var self = this
  var globRes = []
  chain(files.map(function (f) {
    return function (cb) {
      glob(f, function (er, files) {
        if (er)
          return cb(er)
        globRes.push.apply(globRes, files)
        cb()
      })
    }
  }), function (er) {
    if (er)
      return cb(er)
    runFiles(self, globRes, dir, cb)
  })
}

// set some default options for node debugging tests
function setOptionsForDebug(self) {
  // Note: we automatically increase the default timeout here. Yes
  // the user can specify --timeout to increase, but by default,
  // 30 seconds is not a long time to debug your test.
  self.options.timeout = 1000000;

  // Note: we automatically turn on stderr so user can see the 'debugger listening on port' message.
  // Without this it looks like tap has hung..
  self.options.stderr = true;
}

function runFiles(self, files, dir, cb) {
  chain(files.map(function(f) {
    return function (cb) {
      if (self._bailedOut) return
      var relDir = dir || path.dirname(f)
        , fileName = relDir === "." ? f : f.substr(relDir.length + 1)

      self.write(fileName)
      fs.lstat(f, function(er, st) {
        if (er) {
          self.write(assert.fail("failed to stat " + f, {error: er}))
          return cb()
        }

        var cmd = f, args = [], env = {}

        if (path.extname(f) === ".js") {
          cmd = process.execPath
          if (self.options.gc) {
            args.push("--expose-gc")
          }
          if (self.options.debug) {
            args.push("--debug")
            setOptionsForDebug(self)
          }
          if (self.options["debug-brk"]) {
            args.push("--debug-brk")
            setOptionsForDebug(self)
          }
          if (self.options.strict) {
            args.push("--use-strict")
          }
          if (self.options.harmony) {
            args.push("--harmony")
          }
          args.push(fileName)
        } else if (path.extname(f) === ".coffee") {
          cmd = "coffee"
          args.push(fileName)
        } else {
          // Check if file is executable
          if ((st.mode & parseInt('0100', 8)) && process.getuid) {
            if (process.getuid() != st.uid) {
              return cb()
            }
          } else if ((st.mode & parseInt('0010', 8)) && process.get