require 'shellwords'
require 'shell_helpers/pathname'
require 'dr/base/uri'

module ShellHelpers

	module ExtendSSHKit
		def backend(&b)
			local? ? SSHKit::Backend::Local.new(&b) : SSHKit.config.backend.new(self, &b)
		end
		def connect(&b)
			backend(&b).run
		end
	end

	module Utils
		extend self

		def eval_shell(r, shell: :puts)
			return r if r.nil? or r.empty?
			case (shell||"").to_sym
			when :puts
				puts r
			when :eval
				r+=";" if r && !r.end_with?(';')
				print r
			when :exec
				require 'shell_helpers/sh'
				return ShLog.sh(r)
			when :exec_quiet
				require 'shell_helpers/sh'
				return Sh.sh(*r)
			end
			return r
		end

		class << self
			attr_accessor :orig_stdin, :orig_stdout, :orig_stderr
		end
		@orig_stdin=$stdin
		@orig_stdout=$stdout
		@orig_stderr=$stderr

		#An improved find from Find::find that takes in the block the absolute and relative name of the files (+the directory where the relative file is from), and has filter options
		#Returns ::Pathname, except when the value is a SH::Pathname where it
		#returns a SH::Pathname
		def find(*bases, filter: nil, prune: nil, follow_symlink: false, depth: false, max_depth: nil, chdir: false)
			block_given? or return enum_for(__method__, *bases, filter: filter, follow_symlink: follow_symlink, depth: depth, max_depth: max_depth, chdir: chdir)
			bases.collect!{|d| raise Errno::ENOENT unless File.exist?(d); d.dup}.each do |base|
				klass=base.is_a?(::Pathname) ? base.class : ::Pathname
				base=klass.new(base)

				test_filter=lambda do |filter,*files|
					case filter
					when Proc
						filter.call(*files)
					when Array
						file=files.first
						filter.any? do |test|
							case test
							when :directory? #special case
								file.directory? && !file.symlink?
							else
								file.send(test)
							end
						end
					end
				end

				yield_files=lambda do |*files|
					unless test_filter.(filter,*files)
						files.map! {|f| f.dup.taint}
						if chdir
							Dir.chdir(base) do
								yield(*files, base)
							end
						else
							yield(*files, base)
						end
					end
				end

				do_find=lambda do |*files|
					file,filerel=*files
					catch(:prune) do #use throw(:prune) to skip a path (recursively)
						unless test_filter.(prune,*files)
							yield_files.(*files) unless depth
							if file.directory? and (max_depth.nil? or (filerel.to_s=="." and max_depth>0) or filerel.each_filename.to_a.size < max_depth)
								next if !follow_symlink && file.symlink?
								file.children(false).sort.reverse_each do |f|
									fj = file + f
									f = filerel + f
									do_find.(fj.untaint,f.untaint)
								end
								yield_files.(*files) if depth
							end
						end
					end
				end
				do_find.call(base, klass.new('.'))
			end
		end

		#all output is sent to the pager
		def run_pager(*args, launch: :tty, default_less_env: "-FRX")
			return unless $stdout.tty? and launch != :never
			read, write = IO.pipe

			unless Kernel.fork # Child process
				$stdout.reopen(write)
				$stderr.reopen(write) if $stderr.tty?
				read.close
				write.close
				return
			end

			# Parent process, become pager
			$stdin.reopen(read)
			read.close
			write.close

			#ENV['LESS'] = 'FSRX' # Don't page if the input is short enough
			less_env=ENV['LESS']
			less_env=default_less_env if less_env.empty?
			less_env+="F" unless less_env.match(/F/) or launch == :always
			less_env+="R" unless less_env.match(/R/)
			less_env+="X" unless less_env.match(/X/)
			ENV['LESS']=less_env

			Kernel.select [$stdin] # Wait until we have input before we start the pager
			pager = ENV['PAGER'] || 'less'
			run=args.unshift(pager)
			exec(*run) rescue exec "/bin/sh", "-c", *run
		end

		#inside run_pager, escape from the pager
		#does not work :-(
		def escape_pager(mode=nil)
			case mode
			when :orig
				stdout=ShellUtils.orig_stdout
				stderr=ShellUtils.orig_stderr
				stdin=ShellUtils.orig_stdin
			else
				stdout=STDOUT
				stderr=STDERR
				stdin=STDIN
			end
			$stdout.reopen(stdout)
			$stderr.reopen(stderr)
			$stdin.reopen(stdin)
		end

		def output_list(s, split: "\n")
			s=s.shelljoin if s.kind_of?(Array)
			return open("| #{s}").read.split(split)
		end

		#Stolen from mkmf:
		# Searches for the executable +bin+ on +path+.	The default path is your
		# +PATH+ environment variable. If that isn't defined, it will resort to
		# searching /usr/local/bin, /usr/ucb, /usr/bin and /bin.
		# If found, it will return the full path, including the executable name, of
		# where it was found.
		# exts: an array of extensions to add
		def find_executable(bin, path = nil, exts: nil)
			executable_file = lambda do |name|
				name=Pathname.new(name)
				return name if name.file? and name.executable?
			end
			#we use Proc so that 'return' escapes the block
			try_executable = Proc.new do |file|
				return file if executable_file.call(file)
				exts && exts.each {|ext| executable_file.call(ext = file.append_name(ext)) and return ext}
				nil
			end

			bin=Pathname.new(bin)
			if bin.absolute?
				try_executable.call(bin)
			else
				path ||= ENV['PATH'] || %w[/usr/local/bin /usr/bin /bin]
				path = path.split(File::PATH_SEPARATOR) unless path.kind_of?(Array)
				path.each do |dir|
					dir=Pathname.new(dir)
					try_executable.call(dir+bin)
				end
			end
			nil
		end

		def find_file(file,path)
			path.each do |dir|
				dir=Pathname.new(dir)
				path=dir+file
				return path if path.file?
			end
			return nil
		end

		def find_files(pattern,path)
			path.map { |dir| Pathname.glob(dir+pattern) }.flatten
		end

		def rsync(*files, out, default_opts: "-vczz", preserve: true, partial: true, keep_dirlinks: false, backup: false, relative: false, delete: false, clean_out: false, clobber: true, expected: 23, chown: nil, sshcommand: nil, exclude: [], **opts, &b)
			require 'shell_helpers/sh'
			rsync_opts=[*opts.delete(:rsync_opts)] || []
			rsync_opts << default_opts
			rsync_opts << "-a" if preserve
			rsync_opts << "-P" if partial #--partial --progress
			rsync_opts+=%w(--no-owner --no-group) if preserve==:nochown
			rsync_opts+=["--chown", chown] if chown
			rsync_opts << "--ignore-existing" unless clobber
			#on dest: do not replace a symlink to a directory with the real directory
			#use --copy-dirlinks for the same usage on source
			rsync_opts << "--keep-dirlinks" if keep_dirlinks
			exclude.each do |ex|
				rsync_opts += ["--exclude", ex.shellescape]
			end
			if relative
				rsync_opts << "--relative"
				rsync_opts << "--no-implied-dirs"
			end
			rsync_opts << "--delete" if delete
			if clean_out
				out=Pathname.new(out)
				out.rmtree
				out.mkpath
			end
			opts[:log]||=true
			opts[:log_level_execute]||=:info
			if backup
				rsync_opts << "--backup"
				rsync_opts << (backup.to_s[-1]=="/" ? "--backup-dir=#{backup}" : "--suffix=#{backup}") unless backup==true
			end
			if sshcommand
				rsync_opts << "-e"
				rsync_opts << sshcommand.shellescape
			end
			rsync_opts+=opts.delete(:rsync_late_opts)||[]
			cmd=["rsync"]+rsync_opts+files.map(&:to_s)+[out.to_s]
			Sh.sh(*cmd, expected: expected, **opts, &b)
			#expected: rsync error code 23 is some files/attrs were not transferred
		end

		# host can be of the form user@host:port
		# warning this is different from standard ssh syntax of user@host:path
		def ssh(host, *commands, mode: :system, ssh_command: 'ssh',
			ssh_options: [], ssh_Ooptions: [],
			port: nil, forward: nil, x11: nil, user: nil, path: nil, parse: true,
			pty: nil, ssh_env:nil,
			**opts, &b)

			#sshkit has a special setting for :local
			host=host.to_s unless mode==:sshkit and host.is_a?(Symbol)
			parse and host.is_a?(String) and host.match(/^(?:(.*)@)?(.*?)(?::(\d*))?$/) do |m|
				user||=m[1]
				host=m[2]
				port||=m[3]
			end
			unless mode==:net_ssh or mode==:sshkit
				ssh_command, *command_options= ssh_command.shellsplit
				ssh_options=command_options+ssh_options
				ssh_options += ["-p", port.to_s] if port
				ssh_options += ["-W", forward] if forward
				if x11 == :trusted
					ssh_options << "-Y"
				elsif x11
					ssh_options << "-X"
				end
				ssh_options << "-T" if pty==false
				ssh_options << "-t" if pty==true
				ssh_options += ssh_Ooptions.map {|o| ["-o", o]}.flatten
			else #net_ssh options needs to be a hash
				ssh_options={} if ssh_options.is_a?(Array)
				ssh_options[:port]=port if port
			end
			case mode
			when :system,:spawn,:capture,:exec
				host="#{user}@#{host}" if user
				env_commands= ssh_env ? [Export.export_variables(ssh_env, inline: true)]+commands : commands
				Sh.sh(* [ssh_command]+ssh_options+[host]+env_commands, mode: mode, **opts)
			when :net_ssh
				require 'net/ssh'
				user=nil;
				Net::SSH.start(host, user, ssh_options, &b)
			when :sshkit
				require 'sshkit'
				host=SSHKit::Host.new(host)
				host.extend(ExtendSSHKit)
				host.port=port if port
				host.user=user if user
				host.ssh_options=ssh_options
				host
			when :uri
				URI::Ssh::Generic.build(scheme: 'ssh', userinfo: user, host: host, path: path, port: port) #, query: ssh_options.join('&'))
			else
				# return options
				{ ssh_command: ssh_command,
				  ssh_options: ssh_options,
				  ssh_command_options: ([ssh_command]+ssh_options).shelljoin,
				  user: user,
				  host: host,
				  path: path,
				  hostssh: user ? "#{user}@#{host}" : host,
				  command: commands }
			end
		end

		def capture_stdout
			old_stdout = $stdout
			$stdout = StringIO.new('','w')
			if block_given?
				begin
					yield
					output=$stdout.string
				ensure
					$stdout = old_stdout
				end
				return output
			else
				return old_stdout
			end
		end

	end
end