# 7 April 2015
# https://github.com/bevry/base


# =====================================
# Imports

fsUtil = require('fs')
pathUtil = require('path')


# =====================================
# Variables

WINDOWS          = process.platform.indexOf('win') is 0
NODE             = process.execPath
NPM              = (if WINDOWS then process.execPath.replace('node.exe', 'npm.cmd') else 'npm')
EXT              = (if WINDOWS then '.cmd' else '')
GIT              = "git"

APP_PATH         = process.cwd()
PACKAGE_PATH     = pathUtil.join(APP_PATH, "package.json")
PACKAGE_DATA     = require(PACKAGE_PATH)

MODULES_PATH     = pathUtil.join(APP_PATH, "node_modules")
DOCPAD_PATH      = pathUtil.join(MODULES_PATH, "docpad")
CAKE             = pathUtil.join(MODULES_PATH, ".bin", "cake")
COFFEE           = pathUtil.join(MODULES_PATH, ".bin", "coffee")
PROJECTZ         = pathUtil.join(MODULES_PATH, ".bin", "projectz")
DOCCO            = pathUtil.join(MODULES_PATH, ".bin", "docco")
DOCPAD           = pathUtil.join(MODULES_PATH, ".bin", "docpad")
BISCOTTO         = pathUtil.join(MODULES_PATH, ".bin", "biscotto")
YUIDOC           = pathUtil.join(MODULES_PATH, ".bin", "yuidoc")
BABEL            = pathUtil.join(MODULES_PATH, ".bin", "babel")
ESLINT           = pathUtil.join(MODULES_PATH, ".bin", "eslint")
COFFEELINT       = pathUtil.join(MODULES_PATH, ".bin", "coffeelint")

config = {}
config.TEST_PATH           = "test"
config.DOCCO_SRC_PATH      = null
config.DOCCO_OUT_PATH      = "docs"
config.BISCOTTO_SRC_PATH   = null
config.BISCOTTO_OUT_PATH   = "docs"
config.YUIDOC_SRC_PATH     = null
config.YUIDOC_OUT_PATH     = "docs"
config.COFFEE_SRC_PATH     = null
config.COFFEE_OUT_PATH     = "out"
config.DOCPAD_SRC_PATH     = null
config.DOCPAD_OUT_PATH     = "out"
config.BABEL_SRC_PATH      = null
config.BABEL_OUT_PATH      = "es5"
config.ESLINT_SRC_PATH     = null
config.COFFEELINT_SRC_PATH = null

for own key,value of (PACKAGE_DATA.cakeConfiguration or {})
	config[key] = value

#for own key,value of config
#	config[key] = pathUtil.resolve(APP_PATH, value)  if value
# ^ causes issues with biscotto, as it just wants relative paths


# =====================================
# Generic

child_process = require('child_process')

spawn = (command, args, opts, next) ->
	commandString = command+' '+args.join(' ')
	if opts.output is true
		console.log(commandString)
		opts.stdio = 'inherit'
	pid = child_process.spawn(command, args, opts)
	pid.on 'close', (args...) ->
		if args[0] is 1
			error = new Error("Process [#{commandString}] exited with error status code.")
		else
			next?(args...)
	return pid

exec = (command, opts, next) ->
	if opts.output is true
		console.log(command)
		return child_process.exec command, opts, (err, stdout, stderr) ->
			console.log(stdout)
			console.log(stderr)
			next?()
	else
		return child_process.exec(command, opts, next)

finish = (error) ->
	if error
		process.stderr.write( (error.stack ? error) + '\n' )
		throw error
	else
		process.stdout.write('OK\n')

steps = (next, steps) ->
	step = 0

	complete = (error) ->
		# success status code
		if error is 0
			error = null

		# error status code
		else if error is 1
			error = new Error('Process exited with error status code')

		# Error
		if error
			next(error)
		else
			++step
			if step is steps.length
				next()
			else
				steps[step](complete)

	steps[step](complete)


# =====================================
# Actions

actions =
	clean: (opts,next) ->
		# Steps
		steps(next, [
			(complete) ->
				console.log('\nclean:')

				# Prepare rm args
				args = ['-Rf']

				# Add compilation paths to args
				for own key, value of config
					if key.indexOf('OUT_PATH') isnt -1
						args.push(value)

				# Add common ignore paths to args
				for path in [APP_PATH, config.TEST_PATH]
					args.push(
						pathUtil.join(path,  'build')
						pathUtil.join(path,  'components')
						pathUtil.join(path,  'bower_components')
						pathUtil.join(path,  'node_modules')
						pathUtil.join(path,  '*out')
						pathUtil.join(path,  '*log')
						pathUtil.join(path,  '*heapsnaphot')
						pathUtil.join(path,  '*cpuprofile')
					)

				# rm
				spawn('rm', args, {output:true, cwd:APP_PATH}, complete)
		])

	setup: (opts,next) ->
		# Steps
		steps(next, [
			(complete) ->
				console.log('\nnpm install (for app):')
				spawn(NPM, ['install'], {output:true, cwd:APP_PATH}, complete)
			(complete) ->
				return complete()  if !config.TEST_PATH or !fsUtil.existsSync(config.TEST_PATH)
				console.log('\nnpm install (for test):')
				spawn(NPM, ['install'], {output:true, cwd:config.TEST_PATH}, complete)
			(complete) ->
				return complete()  if !fsUtil.existsSync(DOCPAD_PATH)
				console.log('\nnpm install (for docpad tests):')
				spawn(NPM, ['install'], {output:true, cwd:DOCPAD_PATH}, complete)
		])

	compile: (opts,next) ->
		# Steps
		steps(next, [
			(complete) ->
				console.log('\ncake setup')
				actions.setup(opts, complete)
			(complete) ->
				return complete()  if !config.COFFEE_SRC_PATH or !fsUtil.existsSync(COFFEE)
				console.log('\ncoffee compile:')
				spawn(NODE, [COFFEE, '-co', config.COFFEE_OUT_PATH, config.COFFEE_SRC_PATH], {output:true, cwd:APP_PATH}, complete)
			(complete) ->
				return complete()  if !config.BABEL_SRC_PATH or !fsUtil.existsSync(BABEL)
				console.log('\nbabel compile:')
				spawn(NODE, [BABEL, config.BABEL_SRC_PATH, '--out-dir', config.BABEL_OUT_PATH], {output:true, cwd:APP_PATH}, complete)
			(complete) ->
				return complete()  if !config.DOCPAD_SRC_PATH or !fsUtil.existsSync(DOCPAD)
				console.log('\ndocpad generate:')
				spawn(NODE, [DOCPAD, 'generate'], {output:true, cwd:APP_PATH}, complete)
		])

	watch: (opts,next) ->
		# Steps
		steps(next, [
			(complete) ->
				console.log('\ncake setup')
				actions.setup(opts, complete)
			(complete) ->
				return complete()  if !config.BABEL_SRC_PATH or !fsUtil.existsSync(BABEL)
				console.log('\nbabel compile:')
				spawn(NODE, [BABEL, '-w', config.BABEL_SRC_PATH, '--out-dir', config.BABEL_OUT_PATH], {output:true, cwd:APP_PATH}, complete)
			(complete) ->
				return complete()  if !config.COFFEE_SRC_PATH or !fsUtil.existsSync(COFFEE)
				console.log('\ncoffee watch:')
				spawn(NODE, [COFFEE, '-wco', config.COFFEE_OUT_PATH, config.COFFEE_SRC_PATH], {output:true, cwd:APP_PATH})  # background
				complete()  # continue while coffee runs in background
			(complete) ->
				return complete()  if !config.DOCPAD_SRC_PATH or !fsUtil.existsSync(DOCPAD)
				console.log('\ndocpad run:')
				spawn(NODE, [DOCPAD, 'run'], {output:true, cwd:APP_PATH})  # background
				complete()  # continue while docpad runs in background
		])

	verify: (opts,next) ->
		# Steps
		steps(next, [
			(complete) ->
				console.log('\ncake compile')
				actions.compile(opts, complete)
			(complete) ->
				console.log('\ncoffeelint:')
				return complete()  if !config.COFFEELINT_SRC_PATH or !fsUtil.existsSync(COFFEELINT)
				spawn(COFFEELINT, [config.COFFEELINT_SRC_PATH], {output:true, cwd:APP_PATH}, complete)
			(complete) ->
				console.log('\neslint:')
				return complete()  if !config.ESLINT_SRC_PATH or !fsUtil.existsSync(ESLINT)
				spawn(ESLINT, [config.ESLINT_SRC_PATH], {output:true, cwd:APP_PATH}, complete)
			(complete) ->
				console.log('\nnpm test:')
				spawn(NPM, ['test'], {output:true, cwd:APP_PATH}, complete)
		])

	meta: (opts, next) ->
		# Steps
		steps(next, [
			(complete) ->
				return complete()  if !config.DOCCO_SRC_PATH or !fsUtil.existsSync(DOCCO)
				console.log('\ndocco compile:')
				command = "#{NODE} #{DOCCO} -o #{config.DOCCO_OUT_PATH} #{config.DOCCO_SRC_PATH}"
				exec(command, {output:true, cwd:APP_PATH}, complete)
			(complete) ->
				return complete()  if !config.BISCOTTO_SRC_PATH or !fsUtil.existsSync(BISCOTTO)
				console.log('\nbiscotto compile:')
				command = """#{BISCOTTO} -n #{PACKAGE_DATA.title or PACKAGE_DATA.name} --title "#{PACKAGE_DATA.title or PACKAGE_DATA.name} API Documentation" -r README.md -o #{config.BISCOTTO_OUT_PATH} #{config.BISCOTTO_SRC_PATH} - LICENSE.md HISTORY.md"""
				exec(command, {output:true, cwd:APP_PATH}, complete)
			(complete) ->
				return complete()  if !fsUtil.existsSync(YUIDOC)
				console.log('\nyuidoc compile:')
				command = [YUIDOC]
				command.push('-o', config.YUIDOC_OUT_PATH)  if config.YUIDOC_OUT_PATH
				command.push(config.YUIDOC_SRC_PATH)  if config.YUIDOC_SRC_PATH
				spawn(NODE, command, {output:true, cwd:APP_PATH}, complete)
			(complete) ->
				return complete()  if !fsUtil.existsSync(PROJECTZ)
				console.log('\nprojectz compile')
				spawn(NODE, [PROJECTZ, 'compile'], {output:true, cwd:APP_PATH}, complete)
		])

	prerelease: (opts,next) ->
		# Steps
		steps(next, [
			(complete) ->
				console.log('\ncake verify')
				actions.verify(opts, complete)
			(complete) ->
				console.log('\ncake meta')
				actions.meta(opts, complete)
		])

	release: (opts,next) ->
		# Steps
		steps(next, [
			(complete) ->
				console.log('\ncake prerelease')
				actions.prerelease(opts, complete)
			(complete) ->
				console.log('\nnpm publish:')
				spawn(NPM, ['publish'], {output:true, cwd:APP_PATH}, complete)
				# ^ npm will run prepublish and postpublish for us
			(complete) ->
				console.log('\ncake postrelease')
				actions.postrelease(opts, complete)
		])

	postrelease: (opts,next) ->
		# Steps
		steps(next, [
			(complete) ->
				console.log('\ngit tag:')
				spawn(GIT, ['tag', 'v'+PACKAGE_DATA.version, '-a'], {output:true, cwd:APP_PATH}, complete)
			(complete) ->
				console.log('\ngit push origin master:')
				spawn(GIT, ['push', 'origin', 'master'], {output:true, cwd:APP_PATH}, complete)
			(complete) ->
				console.log('\ngit push tags:')
				spawn(GIT, ['push', 'origin', '--tags'], {output:true, cwd:APP_PATH}, complete)
		])


# =====================================
# Commands

commands =
	clean:       'clean up instance'
	setup:       'setup our project for development'
	compile:     'compile our files (includes setup)'
	watch:       'compile our files initially, and again for each change (includes setup)'
	verify:      'verify our project works (includes compile)'
	meta:        'compile our meta files'
	prerelease:  'prepare our project for publishing (includes verify and compile)'
	release:     'publish our project using npm (includes prerelease and postrelease)'
	postrelease: 'prepare our project after publishing'
aliases =
	install:     'setup'
	test:        'verify'
	docs:        'meta'
	prepare:     'prerelease'
	prepublish:  'prerelease'
	publish:     'release'
	postpublish: 'postpublish'

Object.keys(commands).forEach (key) ->
	description = commands[key]
	actualMethod = actions[key]
	task key, description, (opts) ->  actualMethod(opts, finish)

Object.keys(aliases).forEach (desiredMethodName) ->
	actualMethodName = aliases[desiredMethodName]
	actualMethod = actions[actualMethodName]
	description = "alias for #{actualMethodName}"
	task desiredMethodName, description, (opts) ->  actualMethod(opts, finish)