# vim: foldmethod=marker
#from methadone (error.rb, exit_now.rb, process_status.rb, run.rb; last
#import v1.3.1-2-g9be3b5a)
require_relative 'logger'
require_relative 'run'
require 'simplecolor'

module SH
	# ExitNow {{{
	# Standard exception you can throw to exit with a given status code.
	# Generally, you should prefer DR::ExitNow.exit_now! over using this
	# directly, however you may wish to create a rich hierarchy of exceptions
	# that extend from this in your app, so this is provided if you wish to
	# do so.
	class ExitError < StandardError
		attr_reader :exit_code
		# Create an Error with the given status code and message
		def initialize(exit_code,message=nil)
			super(message)
			@exit_code = exit_code
		end
	end

	# Provides #exit_now! You might mix this into your business logic classes
	# if they will need to exit the program with a human-readable error
	# message.
	module ExitNow
		# Call this to exit the program immediately
		# with the given error code and message.
		# +exit_code+:: exit status you'd like to exit with
		# +message+:: message to display to the user explaining the problem
		# If +exit_code+ is a String and +message+ is omitted, +exit_code+ will
		# === Examples
		#			exit_now!(4,"Oh noes!")
		#				# => exit app with status 4 and show the user "Oh noes!" on stderr
		#			exit_now!("Oh noes!")
		#				# => exit app with status 1 and show the user "Oh noes!" on stderr
		#			exit_now!(4)
		#				# => exit app with status 4 and dont' give the user a message (how rude of you)
		def exit_now!(exit_code,message=nil)
			if exit_code.kind_of?(String) && message.nil?
				raise ExitError.new(1,exit_code)
			else
				raise ExitError.new(exit_code,message)
			end
		end
	end
	# }}}
	# Sh {{{
	# Module with various helper methods for executing external commands.
	# In most cases, you can use #sh to run commands and have decent logging
	# done.
	# == Examples
	#
	#		 extend SH::Sh
	#
	#		 sh 'cp foo.txt /tmp'
	#		 # => logs the command to DEBUG, executes the command, logs its output to DEBUG and its error output to WARN, returns 0
	#
	#		 sh 'cp non_existent_file.txt /nowhere_good'
	#		 # => logs the command to DEBUG, executes the command, logs its output to INFO and its error output to WARN, returns the nonzero exit status of the underlying command
	#
	#		 sh! 'cp non_existent_file.txt /nowhere_good'
	#		 # => same as above, EXCEPT, raises a Methadone::FailedCommandError
	#
	#		 sh 'cp foo.txt /tmp' do
	#			 # Behaves exactly as before, but this block is called after
	#		 end
	#
	#		 sh 'cp non_existent_file.txt /nowhere_good' do
	#			 # This block isn't called, since the command failed
	#		 end
	#
	#		 sh 'ls -l /tmp/' do |stdout|
	#			 # stdout contains the output of the command
	#		 end
	#		 sh 'ls -l /tmp/ /non_existent_dir' do |stdout,stderr|
	#			 # stdout contains the output of the command,
	#			 # stderr contains the standard error output.
	#			end

	# FailedCommandError {{{
	# Thrown by certain methods when an externally-called command exits nonzero
	class FailedCommandError < StandardError
		# The command that caused the failure
		attr_reader :command
		# exit_code:: exit code of the command that caused this
		# command:: the entire command-line that caused this
		# custom_error_message:: an error message to show the user instead of
		# the boilerplate one.	Useful for allowing this exception to bubble up
		# and exit the program, but to give the user something actionable.
		def initialize(exit_code,command,failure_msg: nil)
			error_message = String(failure_msg).empty? ?	"Command '#{command}' exited #{exit_code}" : failure_msg
			super(error_message)
			@command = command
		end
	end
	# }}}

	module Sh
		include CLILogging
		extend self
		attr_writer :default_sh_options
		def default_sh_options
			@default_sh_options||={log: false, capture: false, on_success: nil, on_failure: nil, expected:0, dryrun: false, escape: false,
			log_level_execute: :debug, log_level_error: :error,
			log_level_stderr: :error, log_level_stdout_success: :info,
			log_level_stdout_fail: :warn}
		end

		# Run a shell command, capturing and logging its output.
		# keywords:: log+capture
		# If the command completed successfully, it's output is logged at DEBUG.
		# If not, its output is logged at INFO.  In either case, its
		# error output is logged at WARN.
		#						+:expected+:: an Int or Array of Int representing error codes, <b>in addition to 0</b>, that are expected and therefore constitute success.  Useful for commands that don't use exit codes the way you'd like
		#			name: pretty name of command
		#			on_success,on_failure: blocks to call on success/failure
		# block:: if provided, will be called if the command exited nonzero.	The block may take 0, 1, 2, or 3 arguments.
		#					The arguments provided are the standard output as a string, standard error as a string, and the processstatus as DR::ProcessStatus
		#					You should be safe to pass in a lambda instead of a block, as long as your lambda doesn't take more than three arguments
		#
		# Example
		#			sh "cp foo /tmp"
		#			sh "ls /tmp" do |stdout|
		#				# stdout contains the output of ls /tmp
		#			end
		#			sh "ls -l /tmp foobar" do |stdout,stderr|
		#				# ...
		#			end
		#
		# Returns the exit status of the command.  Note that if the command doesn't exist, this returns 127.
		def sh(*command, **opts, &block)
			defaults=default_sh_options
			curopts=defaults.dup
			defaults.keys.each do |k|
				v=opts.delete(k)
				curopts[k]=v if v
			end
			log=curopts[:log]
			command=command.first if command.length==1 and command.first.kind_of?(Array)
			command_name = curopts[:name] || command_name(command)
			command=command.shelljoin if curopts[:escape]
			sh_logger.send(curopts[:log_level_execute], SimpleColor.color("Executing '#{command_name}'",:bold)) if log

			if !curopts[:dryrun]
				if curopts[:capture]
					case command
					when Array
						stdout,stderr,status = DR::Run.run_command(*command,**opts)
					else
						stdout,stderr,status = DR::Run.run_command(command.to_s,**opts)
					end
				else
					case command
					when Array
						system(*command,**opts)
					else
						system(command.to_s,**opts)
					end
					status=$?
					stdout=nil; stderr=nil
				end
			else
				puts command.to_s
				status=0
				stdout=nil; stderr=nil
			end
			process_status = ProcessStatus.new(status,curopts[:expected])

			sh_logger.send(curopts[:log_level_stderr], SimpleColor.color("stderr output of '#{command_name}':\n",:bold,:red)+stderr) unless stderr.nil? or stderr.strip.length == 0 or !log
			if process_status.success?
				sh_logger.send(curopts[:log_level_stdout_success], SimpleColor.color("stdout output of '#{command_name}':\n",:bold,:green)+stdout) unless stdout.nil? or stdout.strip.length == 0 or !log
				curopts[:on_success].call(stdout,stderr,process_status) unless curopts[:on_success].nil?
				block.call(stdout,stderr,process_status) unless block.nil?
			else
				sh_logger.send(curopts[:log_level_stdout_fail], SimpleColor.color("stdout output of '#{command_name}':\n",:bold,:yellow)+stdout) unless stdout.nil? or stdout.strip.length == 0 or !log
				sh_logger.send(curopts[:log_level_error], SimpleColor.color("Error running '#{command_name}': #{process_status.status}",:red,:bold)) if log
				curopts[:on_failure].call(stdout,stderr,process_status) unless curopts[:on_failure].nil?
			end
			return process_status.success?,stdout,stderr,process_status

		rescue SystemCallError => ex
			sh_logger.send(curopts[:log_level_error], SimpleColor.color("Error running '#{command_name}': #{ex.message}",:red,:bold)) if log
			return 127
		end

		# Run a command, throwing an exception if the command exited nonzero.
		# Otherwise, behaves exactly like #sh.
		# Raises DR::FailedCommandError if the command exited nonzero.
		# Examples:
		#
		#			sh!("rsync foo bar")
		#			# => if command fails, app exits and user sees: "error: Command 'rsync foo bar' exited 12"
		#			sh!("rsync foo bar", :failure_msg => "Couldn't rsync, check log for details")
		#			# => if command fails, app exits and user sees: "error: Couldn't rsync, check log for details
		def sh!(*args,failure_msg: nil,**opts, &block)
			on_failure=Proc.new do |*blockargs|
				process_status=blockargs.last
				raise DR::FailedCommandError.new(process_status.exitstatus,command_name(args),failure_msg: failure_msg)
			end
			sh(*args,**opts,on_failure: on_failure,&block)
		end

		# Override the default logger (which is the one provided by CLILogging).
		# You would do this if you want a custom logger or you aren't mixing-in
		# CLILogging.
		#
		# Note that this method is *not* called <tt>sh_logger=</tt> to avoid annoying situations
		# where Ruby thinks you are setting a local variable
		def change_sh_logger(logger)
			@sh_logger = logger
		end

	private
		def command_name(command)
			if command.size == 1
				return command.first.to_s
			else
				return command.to_s
			end
		end

		def sh_logger
			@sh_logger ||= begin
				raise StandardError, "No logger set! Please include DR::CLILogging
ng or provide your own via #change_sh_logger." unless self.respond_to?(:logger)
				self.logger
			end
		end

	end

	#SH::ShLog.sh is like SH::Sh.sh but with login enabled even when
	#command succeed
	module ShLog
		include Sh
		extend self
		@default_sh_options=default_sh_options
		@default_sh_options[:log]=true
		@default_sh_options[:log_level_execute]=:info
	end
	# }}}
end