require 'shellwords'
require 'dr/ruby_ext/core_ext'
require_relative 'pathname'
require_relative 'parser'

module SH
	module ShellExport
		extend self

		#export a value for SHELL consumption
		def export_value(v)
			case v
				when String
					return v.shellescape
				when Array
					return "(#{v.map {|i| i.to_s.shellescape}.join(' ')})"
				when Hash
					return "(#{v.map {|k,v| k.to_s.shellescape+" "+v.to_s.shellescape}.join(' ')})"
				when nil
					return ""
				else
					return v.to_s.shellescape
			end
		end

		#from {ploum: plim} return something like
		#ploum=plim
		#that can be evaluated by the shell
		def export_hash(hash, local: false, export: false, prefix:"")
			r=""
			r+="local #{hash.keys.map {|s| s.to_s.upcase}.join(" ")}\n" if local
			hash.each do |k,v|
				name=prefix+k.to_s.upcase
				r+="typeset -A #{name};\n" if Hash === v
				r+=name+"="+export_value(v)+";\n"
			end
			r+="export #{hash.keys.map {|k| prefix+k.to_s.upcase}.join(" ")}\n" if export
			return r
		end

		#export_variable("ploum","plam") yields ploum="plam"
		def export_variable(name, value, local: false, export: false)
			r=""
			#name=name.upcase
			r+="local #{name}\n" if local
			r+="typeset -A #{name};\n" if Hash === value
			r+=name+"="+export_value(value)+";\n"
			r+="export #{name}\n" if export
			return r
		end

		#export_parse(hash,"name:value")
		#will output name=$(hash[value])
		#special cases: when value = '/' we return the full hash
		#		when value ends by /, we return the splitted hash (and name serves
		#		as a prefix)
		#Ex: Numenor ~ $ ./mine/00COMPUTERS.rb --export=//
		#		 HOSTNAME=Numenor;
		#		 HOSTTYPE=perso;
		#		 HOMEPATH=/home/dams;...
		#		 Numenor ~ $ ./mine/00COMPUTERS.rb --export=syst/
		#		 LAPTOP=true;
		#		 ARCH=i686;...
		#Remark: in name:value, we don't put name in uppercase
		#But in split hash mode, we put the keys in uppercase (to prevent collisions)
		def export_parse(hash,s)
			r=""
			args=Parser.parse_string(s)
			args[:values].each do |k,v|
				name=k
				if !v
					v=name
					#in split mode, don't use prefix if name gave the value
					name= name.size==1? "all" : "" if name[name.size-1]=="/"
				end
				if v.size>1 && v[v.size-1]=='/'
					all=true
					v=v[0...v.size-1]
				end
				value=hash.keyed_value(v)
				if all
					r+=export_hash(value, local: args[:opts][k]["local"], export: args[:opts][k]["export"], prefix: name)
				else
					r+=export_variable(name,value, local: args[:opts][k]["local"], export: args[:opts][k]["export"])
				end
			end
			return r
		end
	end

	module ShellUtils
		extend self

		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
		def find(*paths, filter: nil, follow_symlink: false, depth: nil)
			block_given? or return enum_for(__method__, *paths, filter: filter, follow_symlink: follow_symlink, depth: depth)

			paths.collect!{|d| raise Errno::ENOENT unless File.exist?(d); Pathname.new(d.dup)}
			paths.collect!{|d| [ d, Pathname.new('.'), d ]}
			while filedata = paths.shift
				file, filerel, fileabs = *filedata
				catch(:prune) do #use throw(:prune) to skip a path
					Dir.chdir(fileabs) do
						#if the filter is true, we don't yield the file
						case filter
						when Proc
							filter.call(file,filerel,fileabs)
						when Array
							filter.all? do |test|
								case test
								when :directory? #special case
									file.directory? && !file.symlink?
								else
									file.send(test)
								end
							end
						end or yield file.dup.taint, filerel.dup.taint, fileabs.dup.taint
						if file.directory? and (depth.nil? or filerel.each_filename.size <= depth) then
							next if !follow_symlink && file.symlink?
							file.children(false).sort.reverse_each do |f|
								fj = file + f
								f = filerel + f
								paths.unshift [fj.untaint,f.untaint,fileabs]
							end
						end
					end
				end
			end
		end

		#all output is sent to the pager
		def run_pager(opt=nil)
			return unless $stdout.tty? and opt != :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
			lessenv=ENV['LESS']
			lessenv="-FRX" if lessenv.empty?
			lessenv+="F" unless lessenv.match(/F/) or opt == :always
			lessenv+="R" unless lessenv.match(/R/)
			lessenv+="X" unless lessenv.match(/X/)
			ENV['LESS']=lessenv

			Kernel.select [$stdin] # Wait until we have input before we start the pager
			pager = ENV['PAGER'] || 'less'
			exec pager rescue exec "/bin/sh", "-c", pager
		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

	end
end