_ = require("lodash")
fse = require("fs-extra")
os = require("os")
del = require("del")
path = require("path")
cp = require("child_process")
gulp = require("gulp")
chalk = require("chalk")
Promise = require("bluebird")
gulpDebug = require("gulp-debug")
gulpCoffee = require("gulp-coffee")
gulpTypeScript = require("gulp-typescript")
pluralize = require("pluralize")
vinylPaths = require("vinyl-paths")
coffee = require("@packages/coffee")
execa = require("execa")
electron = require("@packages/electron")
debug = require("debug")("cypress:binary")
R = require("ramda")
la = require("lazy-ass")
check = require("check-more-types")
humanInterval = require("human-interval")

meta = require("./meta")
smoke = require("./smoke")
packages = require("./util/packages")
xvfb = require("../../cli/lib/exec/xvfb")
linkPackages = require('../link-packages')

rootPackage = require("@packages/root")

fs = Promise.promisifyAll(fse)

logger = (msg, platform) ->
  time = new Date()
  timeStamp = time.toLocaleTimeString()
  console.log(timeStamp, chalk.yellow(msg), chalk.blue(platform))

logBuiltAllPackages = () ->
  console.log("built all packages")

logBuiltAllJs = () ->
  console.log("built all JS")

# can pass options to better control the build
# for example
#   skipClean - do not delete "dist" folder before build
buildCypressApp = (platform, version, options = {}) ->
  la(check.unemptyString(version), "missing version to build", version)

  distDir = _.partial(meta.distDir, platform)
  buildDir = _.partial(meta.buildDir, platform)
  buildAppDir = _.partial(meta.buildAppDir, platform)

  log = _.partialRight(logger, platform)

  testVersion = (folderNameFn) -> () ->
    log("#testVersion")
    dir = folderNameFn()
    la(check.unemptyString(dir), "missing folder for platform", platform)
    console.log("testing dist package version")
    console.log("by calling: node index.js --version")
    console.log("in the folder %s", dir)

    execa("node", ["index.js", "--version"], {
      cwd: dir
    }).then (result) ->
      la(check.unemptyString(result.stdout),
        'missing output when getting built version', result)

      console.log('app in %s', dir)
      console.log('built app version', result.stdout)
      la(result.stdout == version, "different version reported",
        result.stdout, "from input version to build", version)

  canBuildInDocker = ->
    platform is "linux" and os.platform() is "darwin"

  badPlatformMismatch = ->
    console.error("⛔️  cannot build #{platform} from #{os.platform()}")
    console.error("⛔️  should use matching platform to build it")
    console.error("program arguments")
    console.error(process.argv)

  checkPlatform = ->
    log("#checkPlatform")
    if platform is os.platform()
      return

    console.log("trying to build #{platform} from #{os.platform()}")
    if platform is "linux" and os.platform() is "darwin"
      console.log("npm run binary-build-linux")
    Promise.reject(new Error("Build platform mismatch"))

  cleanupPlatform = ->
    log("#cleanupPlatform")

    if options.skipClean
      log("skipClean")
      return

    cleanup = ->
      dir = distDir()
      la(check.unemptyString(dir), "empty dist dir", dir, "for platform", platform)
      fs.removeAsync(distDir())

    cleanup()
    .catch(cleanup)

  buildPackages = ->
    log("#buildPackages")

    packages.runAllBuild()
    # Promise.resolve()
    .then(R.tap(logBuiltAllPackages))
    .then(packages.runAllBuildJs)
    .then(R.tap(logBuiltAllJs))

  copyPackages = ->
    log("#copyPackages")

    packages.copyAllToDist(distDir())

  npmInstallPackages = ->
    log("#npmInstallPackages")

    packages.npmInstallAll(distDir("packages", "*"), options)

  createRootPackage = ->
    log("#createRootPackage #{platform} #{version}")

    fs.outputJsonAsync(distDir("package.json"), {
      name: "cypress"
      productName: "Cypress",
      description: rootPackage.description
      version: version
      main: "index.js"
      scripts: {}
      env: "production"
    })
    .then =>
      str = """
      process.env.CYPRESS_ENV = process.env.CYPRESS_ENV || 'production'
      require('./packages/server')
      """

      fs.outputFileAsync(distDir("index.js"), str)

  copyPackageProxies = (destinationFolder) ->
    () ->
      log("#copyPackageProxies")
      la(check.fn(destinationFolder),
        "missing destination folder function", destinationFolder)
      dest = destinationFolder("node_modules", "@packages")
      la(check.unemptyString(dest), "missing destination folder", dest)
      source = path.join(process.cwd(), "node_modules", "@packages")
      fs.unlinkAsync(dest).catch(_.noop)
      .then(() ->
        console.log("Copying #{source} to #{dest}")
        fs.copyAsync(source, dest)
      )

  removeTypeScript = ->
    ## remove the .ts files in our packages
    log("#removeTypeScript")
    del([
      ## include ts files of packages
      distDir("**", "*.ts")

      ## except those in node_modules
      "!" + distDir("**", "node_modules", "**", "*.ts")
    ])
    .then (paths) ->
      console.log(
        "deleted %d TS %s",
        paths.length,
        pluralize("file", paths.length)
      )
      console.log(paths)

  cleanJs = ->
    log("#cleanJs")

    packages.runAllCleanJs()

  convertCoffeeToJs = ->
    log("#convertCoffeeToJs")

    ## grab everything in src
    ## convert to js
    new Promise (resolve, reject) =>
      gulp.src([
        ## include coffee files of packages
        distDir("**", "*.coffee")

        ## except those in node_modules
        "!" + distDir("**", "node_modules", "**", "*.coffee")
      ])
      .pipe vinylPaths(del)
      .pipe(gulpDebug())
      .pipe gulpCoffee({
        coffee: coffee
      })
      .pipe gulp.dest(distDir())
      .on("end", resolve)
      .on("error", reject)

  elBuilder = ->
    log("#elBuilder")
    dir = distDir()
    dist = buildDir()
    console.log("from #{dir}")
    console.log("into #{dist}")

    electron.install({
      dir
      dist
      platform
      appVersion: version
    })

  removeDevElectronApp = ->
    log("#removeDevElectronApp")
    # when we copy packages/electron, we get the "dist" folder with
    # empty Electron app, symlinked to our server folder
    # in production build, we do not need this link, and it
    # would not work anyway with code signing

    # hint: you can see all symlinks in the build folder
    # using "find build/darwin/Cypress.app/ -type l -ls"
    console.log("platform", platform)
    electronDistFolder = meta.buildAppDir(platform, "packages", "electron", "dist")
    la(check.unemptyString(electronDistFolder),
      "empty electron dist folder for platform", platform)

    console.log("Removing unnecessary folder '#{electronDistFolder}'")
    fs.removeAsync(electronDistFolder).catch(_.noop)

  runSmokeTests = ->
    log("#runSmokeTests")

    run = ->
      smoke.test(meta.buildAppExecutable(platform))

    if xvfb.isNeeded()
      xvfb.start()
      .then(run)
      .finally(xvfb.stop)
    else
      run()

  codeSign = Promise.method ->
    if platform isnt "darwin"
      # do we need to code sign on Windows?
      return

    appFolder = meta.zipDir(platform)
    fiveMinutes = humanInterval("5 minutes")

    execaBuild = Promise.method ->
      log("#codeSign #{appFolder}")

      execa('build', ["--publish", "never", "--prepackaged", appFolder], {
        stdio: "inherit"
      })
      .catch (err) ->
        ## ignore canceled errors
        if err.isCanceled
          return

        throw err

    ## try to build and if we timeout in 5 minutes
    ## then try again - which sometimes happens in
    ## circle CI
    b = execaBuild()

    b
    .timeout(fiveMinutes)
    .catch Promise.TimeoutError, (err) ->
      console.log(
        chalk.red("timed out signing binary after #{fiveMinutes}ms. retrying...")
      )

      b.cancel()

      execaBuild()


  verifyAppCanOpen = ->
    if (platform != "darwin") then return Promise.resolve()

    appFolder = meta.zipDir(platform)
    log("#verifyAppCanOpen #{appFolder}")
    new Promise (resolve, reject) =>
      args = ["-a", "-vvvv", appFolder]
      debug("cmd: spctl #{args.join(' ')}")
      sp = cp.spawn "spctl", args, {stdio: "inherit"}
      sp.on "exit", (code) ->
        if code is 0
          resolve()
        else
          reject new Error("Verifying App via GateKeeper failed")

  Promise.resolve()
  .then(checkPlatform)
  .then(cleanupPlatform)
  .then(buildPackages)
  .then(copyPackages)
  .then(npmInstallPackages)
  .then(createRootPackage)
  .then(copyPackageProxies(distDir))
  .then(convertCoffeeToJs)
  .then(removeTypeScript)
  .then(cleanJs)
  .then(testVersion(distDir))
  .then(elBuilder) # should we delete everything in the buildDir()?
  .then(removeDevElectronApp)
  .then(copyPackageProxies(buildAppDir))
  .then(testVersion(buildAppDir))
  .then(runSmokeTests)
  .then(codeSign) ## codesign after running smoke tests due to changing .cy
  .then(verifyAppCanOpen)
  .return({
    buildDir: buildDir()
  })

module.exports = buildCypressApp
