#!/usr/bin/env ruby

require 'rubygems'
require 'readline'
require 'logger'
require 'shellwords'
require 'tempfile'
require 'digest/sha1'
require 'abbrev'
require 'treequel'
require 'treequel/mixins'
require 'treequel/constants'


class Shell
	include Treequel::Loggable,
	        Treequel::Constants::Patterns


	### Create a new shell that will traverse the directory at the specified +uri+.
	def initialize( uri )
		Treequel.logger.level = Logger::WARN

		@uri        = uri
		@quit       = false
		@dir        = Treequel.directory( @uri )
		@currbranch = @dir

		@commands = self.find_commands
		@completions = @commands.abbrev
		@command_table = make_command_table( @commands )
	end


	### The command loop: run the shell until the user wants to quit
	def run
		$stderr.puts "Connected to %s" % [ @uri ]

		self.setup_completion

		until @quit
			input = Readline.readline( @currbranch.dn + '> ', true )
			self.log.debug "Input is: %p" % [ input ]

			# EOL makes the shell quit
			if input.nil?
				@quit = true

			elsif input == ''
				self.log.debug "No command. Re-displaying the prompt."

			# Parse everything else into command + everything else
			else
				command, *args = Shellwords.shellwords( input )

				begin
					if meth = @command_table[ command ]
						meth.call( *args )
					else
						self.handle_missing_command( command )
					end
				rescue => err
					$stderr.puts "Error: %s" % [ err.message ]
					err.backtrace.each do |frame|
						self.log.debug "  " + frame
					end
				end
			end
		end

		$stderr.puts "done."
	end


	#########
	protected
	#########

	### Set up Readline completion
	def setup_completion
		Readline.completion_proc = self.method( :completion_callback ).to_proc
		Readline.completer_word_break_characters = ''
	end


	### Handle completion requests from Readline.
	def completion_callback( input )
		if command = @completions[ input ]
			return []
		end
	end


	### Quit the shell.
	def quit_command( *args )
		$stderr.puts "Okay, exiting."
		@quit = true
	end


	LOG_LEVELS = {
		'debug' => Logger::DEBUG,
		'info'  => Logger::INFO,
		'warn'  => Logger::WARN,
		'error' => Logger::ERROR,
		'fatal' => Logger::FATAL,
	}.freeze
	LOG_LEVEL_NAMES = LOG_LEVELS.invert.freeze

	### Set the logging level (if invoked with an argument) or display the current
	### level (with no argument).
	def log_command( *args )
		newlevel = args.shift
		if newlevel
			if LOG_LEVELS.key?( newlevel )
				Treequel.logger.level = LOG_LEVELS[ newlevel ]
				$stderr.puts "Set log level to: %s" % [ newlevel ]
			else
				levelnames = LOG_LEVEL_NAMES.keys.sort.join(', ')
				raise "Invalid log level %p: valid values are:\n   %s" % [ newlevel, levelnames ]
			end
		else
			$stderr.puts "Log level is currently: %s" %
				[ LOG_LEVEL_NAMES[Treequel.logger.level] ]
		end
	end


	### Show the completions hash
	def show_completions_command
		$stderr.puts "Completions:",
			@completions.inspect
	end


	### Display LDIF for the specified RDNs.
	def cat_command( *args )
		args.each do |rdn|
			branch = rdn.split( /\s*,\s*/ ).inject( @currbranch ) do |branch, dnpair|
				attribute, value = dnpair.split( /\s*=\s*/, 2 )
				branch.send( attribute, value )
			end

			$stdout.puts( branch.to_ldif )
		end
	end


	### List the children of the current branch.
	def ls_command( *args )
		$stdout.puts *@currbranch.children.collect {|b| b.rdn }.sort
	end


	### Change the current working DN to +rdn+.
	def cdn_command( rdn, *args )
		raise "invalid RDN %p" % [ rdn ] unless RELATIVE_DISTINGUISHED_NAME.match( rdn )

		pairs = rdn.split( /\s*,\s*/ )
		pairs.each do |dnpair|
			self.log.debug "  cd to %p" % [ dnpair ]
			attribute, value = dnpair.split( /=/, 2 )
			self.log.debug "  changing to %s( %p )" % [ attribute, value ]
			@currbranch = @currbranch.send( attribute, value )
		end
	end


	### Change the current working DN to the current entry's parent.
	def parent_command( *args )
		parent = @currbranch.parent or raise "%s is the root DN" % [ @currbranch.dn ]

		self.log.debug "  changing to %s" % [ parent.dn ]
		@currbranch = parent
	end


	### Edit the entry specified by +rdn+.
	def edit_command( rdn, *args )
		branch = @currbranch.get_child( rdn )

		fn = Digest::SHA1.hexdigest( rdn )
		tf = Tempfile.new( fn )
		if branch.exists?
			tf.print(  )
	end


	### Handle a command from the user that doesn't exist.
	def handle_missing_command( *args )
		command = args.shift || '(testing?)'
		$stderr.puts "Unknown command %p" % [ command ]
		$stderr.puts "Known commands: ", '  ' + @commands.join(', ')
	end


	### Find methods that implement commands and return them in a sorted Array.
	def find_commands
		return self.methods.
			grep( /^(\w+)_command$/ ).
			collect {|mname| mname[/^(\w+)_command$/, 1] }.
			sort
	end


	#######
	private
	#######

	### Create a command table that maps command abbreviations to the Method object that
	### implements it.
	def make_command_table( commands )
		table = commands.abbrev
		table.keys.each do |abbrev|
			mname = table.delete( abbrev )
			table[ abbrev ] = self.method( mname + '_command' )
		end

		return table
	end

end


if __FILE__ == $0
	ldapuri = URI( ARGV.shift || 'ldap://localhost' )
	Shell.new( ldapuri ).run
end

