# vim: foldmethod=marker
#from methadone (error.rb, exit_now.rb, process_status.rb, run.rb;
#last import 4626a2bca9b6e54077a06a0f8e11a04fadc6e7ae, 2017-01-19
require 'shell_helpers/logger'
require 'shell_helpers/run'
require 'forwardable'

begin
	require 'simplecolor'
rescue LoadError
	#fallback, don't colorize
	module SimpleColor
		def self.color(s,**opts)
			puts s
		end
	end
end

module ShellHelpers
	# ExitNow {{{
	# Standard exception you can throw to exit with a given status code.
	# Generally, you should prefer SH::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
	# }}}
	
	# SudoLoop {{{
	# extend SudoLoop.configure to run a sudo loop
	# SH.sh("ls /", sudo: "sudo".extend(SH::SudoLoop.configure(**opts))
	module SudoLoop
		extend self
		def configure(**opts)
			return Module.new do |m|
				m.define_method(:sudo_loop) do
					SudoLoop.instance_method(:run_sudo_loop).bind(self).call(**opts)
				end
				m.define_method(:stop_sudo_loop) do
					SudoLoop.instance_method(:stop_sudo_loop).bind(self).call
				end
			end
		end

		def run_sudo_loop(command: "sudo -v", interval: 30, init_command: command)
			if @sudo_loop_thread.nil? or !@sudo_loop_thread.alive?
				Sh.sh_or_proc(init_command, log: false)
				#require 'pry'; binding.pry
				@sudo_loop_thread = Thread.new do
					loop do
						Sh.sh_or_proc(command, log: false)
						sleep(interval)
					end
				end
				@sudo_loop_thread.run
			end
		end

		def stop_sudo_loop
			@sudo_loop_thread&.kill
		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/', capture: true do |status, stdout|
	#			 # stdout contains the output of the command
	#		 end
	#		 sh 'ls -l /tmp/ /non_existent_dir', capture: true do |status, 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
	# }}}

	ShError=Class.new(StandardError)
	module Sh
		include CLILogging
		extend self
		attr_writer :default_sh_options
		def default_sh_options
			@default_sh_options||={log: true, capture: false, on_success: nil, on_error: nil, expected:0, dryrun: false, escape: false,
			log_level_execute_debug: :debug,
			log_level_execute: :info, log_level_error: :error,
			log_level_stderr: :error, log_level_stdout_success: :info,
			log_level_stdout_fail: :warn, detach: false,
			mode: :system}
		end

		attr_writer :spawned
		def spawned
			@spawned||=[]
		end
		def wait_spawned
			spawned.each {|c| Process.waitpid(c)}
		end

		# callback called by sh to select the exec mode
		# mode: :system,:spawn,:exec,:capture
		# opts: sudo, env
		def shrun(*args,mode: :system, block: nil, **opts)
			env, args, spawn_opts=Run.process_command(*args, **opts)
			# p env, args, spawn_opts
			case mode
			when :system
				system(env,*args,spawn_opts)
			when :spawn, :detach
				pid=spawn(env,*args,spawn_opts, &block)
				if mode==:detach
					Process.detach(pid) 
				else
					spawned << pid
					if block_given?
						yield pid
						Process.wait(pid)
					else
						pid
					end
				end
			when :exec
				exec(env,*args,spawn_opts, &block)
			when :capture
				Run.run_command(env,*args,spawn_opts, &block)
			when :run
				Run.run(env,*args,spawn_opts, &block)
			else
				raise ShError.new("In shrun, mode #{mode} not understood")
			end
		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_error: 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 SH::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.), stdout, stderr and the full status of the command

		# callbacks: on_success, on_error
		# yield process_status.success?,stdout,stderr,process_status if block_given?
		# returns success; except in capture mode where it returns success,
		# stdout, stderr, process_status
		def sh(*command, argv0: nil, **opts)
			defaults=default_sh_options
			curopts=defaults.dup
			defaults.keys.each do |k|
				v=opts.delete(k)
				curopts[k]=v unless v.nil?
			end

			log=curopts[:log]
			command=[[command.first, argv0], *command[1..-1]] if argv0 and command.length > 1 and !curopts[:escape]

			if command.length==1 and command.first.kind_of?(Array) #so that sh(["ls", "-a"]) works
				command=command.first
				command=[[command.first, argv0], *command[1..-1]] if argv0 and !curopts[:escape]
			end
			command_name = curopts[:name] || command_name(command) #this keep the options
			# this should not be needed
			command=command.shelljoin if curopts[:escape]
			if log
				sh_logger.send(curopts[:log_level_execute], SimpleColor.color("Executing '#{command_name}'",:bold))
				p_env, p_args, p_opts= Run.process_command(*command, **opts)
				sh_logger.send(curopts[:log_level_execute_debug], SimpleColor.color("Debug execute: #{[p_env, *p_args, p_opts]}", :bold))
			end

			if !curopts[:dryrun]
				if curopts[:capture] || curopts[:mode]==:capture
					stdout,stderr,status = shrun(*command,**opts,mode: :capture)
				elsif curopts[:detach] || curopts[:mode]==:spawn || curopts[:mode]==:detach
					mode = curopts[:detach] ? :detach : curops[:mode]
					_pid = shrun(*command,**opts, mode: mode)
					status=0; stdout=nil; stderr=nil
				elsif curopts[:mode]==:run
					status, stdout, stderr=shrun(*command,mode: curopts[:mode], **opts)
				else
					mode = curopts[:mode] || :system
					shrun(*command,mode: mode, **opts)
					status=$?; stdout=nil; stderr=nil
				end
			else
				sh_logger.info 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_error].call(stdout,stderr,process_status) unless curopts[:on_error].nil?
			end
			yield process_status.success?,stdout,stderr,process_status if block_given?
			if curopts[:capture] || curopts[:mode]==:capture || curopts[:mode]==:run
				return process_status.success?,stdout,stderr,process_status
			else
				return process_status.success?
			end

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

		# Run a command, throwing an exception if the command exited nonzero.
		# Otherwise, behaves exactly like #sh.
		# Raises SH::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_error=Proc.new do |*blockargs|
				process_status=blockargs.last
				raise FailedCommandError.new(process_status.exitstatus,command_name(args),failure_msg: failure_msg)
			end
			sh(*args,**opts,on_error: on_error,&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

		#split commands on newlines and run sh on each line
		def sh_commands(com, **opts)
			com.each_line do |line|
				sh(line.chomp,**opts)
			end
		end

		# returns only the success or failure
		def sh_or_proc(cmd, *args, **opts, &b)
			case cmd
			when Proc
				cmd.call(*args, **opts, &b)
			when Array
				suc, _r=SH.sh(*cmd, *args, **opts, &b)
				suc
			when String
				suc, _r=SH.sh(cmd + " #{args.shelljoin}", **opts, &b)
				suc
			end
		end

	private
		def command_name(command)
			if command.size == 1
				command.first.to_s
			else
				#command.to_s
				#command.map {|i| i.to_s}.to_s
				command.shelljoin
			end
		end

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

	end

	#SH::ShLog.sh is by default like SH::Sh.sh.
	# It is easy to change it to be more verbose though
	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

	# Do not log execution
	module ShQuiet
		include Sh
		extend self
		@default_sh_options=default_sh_options
		@default_sh_options[:log]=true
		@default_sh_options[:log_level_execute]=:debug
	end

	# Completely silent
	module ShSilent
		include Sh
		extend self
		@default_sh_options=default_sh_options
		@default_sh_options[:log]=false
	end

	module ShDryRun
		include Sh
		extend self
		@default_sh_options=default_sh_options
		@default_sh_options[:log]=true
		@default_sh_options[:log_level_execute]=:info
		@default_sh_options[:dryrun]=true
	end

	# this modules deal with default options, paths and arg handling for
	# different programs, while letting the user control
	# Exemples:
	# ShConfig.launch(:ls, "/", config: {ls: {default_opts: ["-l"]}})
	# => ["ls", "-l", "/", {}]
	# ShConfig.launch(:ls, "/", config: {ls: {default_opts: ["-l"]}}, method: :sh)
	# ShConfig.launch(:ls, "/", config: {ls: {default_opts: ["-l"]}}) { |*args| SH.sh(*args) }
	# ShConfig.launch(:ls, "/", config: {ls: {wrap: ->(cmd,*args, &b) { b.call(cmd, '-l', *args) } }}, method: :sh)

	module ShConfig
		extend self
		def launch(*args, opts: [], cmd_prepend: [], cmd_postpone: [], config: self.sh_config, default_opts: true, method: nil, **keywords, &b)
			if args.length == 1 and (arg=args.first).is_a?(String)
				args=arg.shellsplit
				args[0]=args[0][1..-1].to_sym if args[0][0]==':'
			end
			opts=opts.shellsplit if opts.is_a?(String)
			default_opts=default_opts.shellsplit if default_opts.is_a?(String)
			cmd_prepend=cmd_prepend.shellsplit if cmd_prepend.is_a?(String)
			cmd_postpone=cmd_postpone.shellsplit if cmd_postpone.is_a?(String)
			dopts = default_opts.is_a?(Array) ? default_opts : []
			cmd, *args=args
			if cmd.is_a?(Symbol)
				if config.key?(cmd)
					c=config[cmd]
					cmd=c[:bin] || cmd.to_s
					dopts += (Array(c[:default_opts])||[]) if default_opts
					wrap=c[:wrap]
				else
					cmd=cmd.to_s
				end
			end
			cargs=Array(cmd_prepend) + [cmd] + dopts + Array(opts) + args + Array(cmd_postpone)
			if !b
				if method
					b=lambda do |*args, **kw|
						SH.public_send(method, *args, **kw)
					end
				else
					b=lambda do |*args|
						return *args
					end
				end
			end
			if wrap
				wrap.call(*cargs, **keywords, &b)
			else
				b.call(*cargs, **keywords)
			end
		end

		def sh_config
			{}
		end
	end
	# }}}
end