#!/usr/local/bin/ruby
#
# Copyright (C) 2002 Eivind Eklund.
# All rights reserved.
#

#
# Find common prefix for all arguments, relative to the present working
# directory.  Will strip off the prefix from the arguments.
#
# Returns prefix.
#
def vvs_prefix(args)	# Command line arguments
	#throw "No args to vvs_prefix" if args.length == 0

	pwd = Dir.getwd
	#
	# Basic file/directory name normalization
	#
	args.each { |arg|
		# Make everything use absolute paths
		arg = "#{pwd}/#{arg}" if arg.index(%r{^/})
		# Collapse multiple slashes to one
		arg.gsub!(%r{//+}, "/")
		# Strip directory/.. parts
		true while arg.gsub!(%r{[^/]+/\.\.}, "")
		# Strip /./ parts
		true while arg.gsub!(%r{/\./}, "")
		# Strip trailing / if present
		arg.gsub!(%r{/$}, "")
		# Strip trailing /. if present
		arg.gsub!(%r{/\.$}, "")
	}

	#
	# Find common prefix between the files passed in
	#
	prefix = args.length > 0 ? args[0].dup : "."
	# Loop once too much - args[0] already processed
	args.each { |arg|
		arg = arg.dup
		(arg.length > prefix.length ? arg : prefix).gsub!(%r{(^|/)[^/]+$}, "") while arg != prefix
	}
	# Make sure to get a prefix, and not the actual name passed in
	prefix.gsub!(%r{(^|/)[^/]*$}, "") if FileTest.file?(prefix)
	throw "vvs prefix: \"#{prefix}\" does not contain CVS metadata" unless
		FileTest.file?("#{prefix != "" ? prefix : "."}/CVS/Entries") 
	# Strip off prefix from all arguments
	args.collect! { |arg|
		arg.gsub!(%r{^#{prefix}/?}, "")		# XXX Should escape interpolation
		arg != "" ? arg : "."
	}
	prefix = "." if prefix == ""
	Dir.chdir(prefix);
	newpwd = Dir.getwd
	if (pwd == newpwd)
		prefix = "";
	else
		prefix = newpwd.gsub(%r{^#{pwd}/}, "")		# XXX Should escape interpolation
		prefix += "/";
	end
	print "Calculated prefix: \"#{prefix}\"\n" if $verbose > 0
	return prefix
end

#
# Like pushd/popd in the shell
#
class DirStack
	def initialize
		@directory_stack = []
	end
	def push(newdir)
		@directory_stack.push(Dir.getwd)
		Dir.chdir(newdir)
	end
	def pop
		Dir.chdir(@directory_stack.pop)
	end
end

#
# Helper for list_tree; recursive
#
def lf_helper(prefix, internalprefix, filelist, filehash, dirhash, recurse)
	#my $callback = shift;		# Callback for actually doing something
	#my $prefix = shift;		# Display prefix
	#my $internalprefix = shift;	# Internal prefix
	#my $filelist = shift;		# List of files
	#my $filehash = shift;		# Hash indicating files/dirs to display
	#my $dirhash = shift;		# Hash indicating dirs to scan
	#my $recurse = shift;		# Should we recurse into directories
					# NOT in filehash?
	throw "nil internalprefix" unless internalprefix
	print "Prefix:          \"#{prefix}\"\n" if $verbose > 0
	print "Internal Prefix: \"#{internalprefix}\"\n" if $verbose > 0
	print "Recurse:         #{recurse}\n" if $verbose > 0
	print "Dirs:            \"#{dirhash.keys.sort.join('", "')}\"\n" if $verbose > 0
	print "Files:           \"#{filehash.keys.sort.join('", "')}\"\n" if $verbose > 0
	dirstack = DirStack.new
	# XXX Convert to a different exception on error?
	entries = File.open("CVS/Entries").each { |line|
		if (line.index(%r{^D/([^/]+)}))
			name = $1;
			print "Checking directory \"#{internalprefix}#{name}\"\n" if $verbose > 0
			if (recurse || filehash.has_key?("#{internalprefix}#{name}"))
				print "Found #{internalprefix}#{name}\n" if $verbose > 0
				# Scan directory, printing out every file
				# inside
				yield(false, prefix, name);
				dirstack.push(name);
				lf_helper("#{prefix}#{name}/", "#{internalprefix}#{name}/", filelist, filehash, dirhash,
					true) { |a, b, c| yield(a,b,c) }
				filehash.delete "#{internalprefix}#{name}"
				dirstack.pop
			elsif (dirhash.has_key?("#{internalprefix}#{name}"))
				print "Scanning #{name}\n" if $verbose > 0
				# Scan directory, as we have things to print
				# below
				dirstack.push(name);
				lf_helper("#{prefix}#{name}/", "#{internalprefix}#{name}/", filelist, filehash, dirhash,
					false) { |a,b,c| yield(a,b,c) }
				dirhash.delete "#{internalprefix}#{name}"
				dirstack.pop
			else
				print "Skipped directory #{name}\n" if $verbose > 0
			end
		elsif (line.index(%r{/([^/]+)/}))
			name = $1;
			if (recurse || filehash.has_key?("#{internalprefix}#{name}"))
				yield(true, prefix, name)
				filehash.delete "#{internalprefix}#{name}"
			else
				print "Skipped file #{name} in #{internalprefix}\n" if $verbose > 0
			end
		else
			# Strange line
			next;
		end
	}
	return 0;
end

#
# Helper for listing files/directories; normalizes pathnames and builds
# indices, then calls lf_helper
#
def list_tree(prefix, args, &block)
	#my $callback = shift;	# Callback for handling each file/dir
	#my $args = shift;	# Command line arguments - arrayref

	#my $i;			# Iterator
	#my $prefix;		# Common prefix of files
	#my %files;		# File/dirname as key => 1
	#my %dirhash;		# Extra directories to scan without printing

	#
	# Remove duplicate args
	#
	args.sort!
	lastarg = nil
	args.collect! { |arg|
		ret = (arg != lastarg ? arg : nil)
		lastarg = arg
		ret
	}
	args.compact!
	#
	# Record directories we need
	# to scan to find files
	#
	dirhash = {}
	args.each { |arg|
		components = arg.split(/\//);
		components.pop if FileTest.file?(arg);
		tmppath = components.shift
		next if tmppath == "";
		dirhash[tmppath] = true;
		components.each { |tmpi|
			dirhash[tmppath] = true;
			tmppath += "/#{tmpi}";
		}
	}
	dirhash.delete "";
	# Sort the arguments alphabetically
	args.sort!
	# Create a hash of relative filenames
	files = {}
	args.each { |arg| files[arg] = true }
	#
	# We now have a sorted file/directory list relative to prefix,
	# prefix contains CVS metadata, we have a hash of files to display,
	# and we are ready to roll
	#

	#print "Parameters:\n  " . join("\n  ", @{$args}) . "\nEnd of parameters\n";
	#print "Files:\n  " . join("\n  ", keys %files) . "\nEnd of keys\n";
	#print "Dirs:\n  " . join("\n  ", keys %dirhash) . "\nEnd of keys\n";
	#print "prefix: \"$prefix\"\n";
	#
	# Recurse if we got no parameters or only a directory as parameter
	#
	if (args[0] == ".") 
		yield(false, prefix, "");
	end
	recurse = args.length == 0 || (args.length == 1 && FileTest.directory?(args[0])) ||
		args.length > 0 && args[0] == "."
	return lf_helper(prefix, "", args, files, dirhash, recurse) { |a, b, c|
		   	yield(a, b, c)
		};
end

#
# Iterate through a set of files
#
def list_files(prefix, args, &block)
	list_tree(prefix, args) { |isfile, prefix, name|
		yield(prefix, name) if isfile
	}
end

#
# Iterate through a set of directories
#
def list_dirs(prefix, args, &block)
	list_tree(prefix, args) { |isfile, prefix, name|
		yield(prefix, name) if !isfile
	}
end

#
# Retrieve the last revision available for a set of files,
# doing callbacks to get it.
#
def vvs_lastrev(prefix, files)
	#my $prefix = shift;
	#my $files = shift;

	#
	# Print all available revisions, filename followed by
	# revision.
	#
	IO.popen("cvs status #{files.join(" ")} < /dev/null").each { |line|
		if (line =~ /^   Repository revision:\s+([^\s]+)/)
			yield(prefix, files.shift, $1)
		end
	}
	# XXX Better error return checking?  Or assume throw?
	return 0;
end

#
# List what branches exists in the args.
#
def vvs_branches(files)
	state = 0
	workfile = nil
	IO.popen("cvs log #{files.join(" ")} < /dev/null").each { |line|
		if (state == 0)
			# Looking for "Working file", which is the start of
			# a header parse
			if (line =~ /^Working file: (.*)/)
				workfile = $1
				state = 1
			end
		elsif (state == 1)
			# Looking for 'symbolic names:', which is the start of
			# the tag list
			if (line =~ /^symbolic names:$/)
				state = 2
			end
		elsif (state == 2)
			# Looking for actual tags
			if (line =~ /^\t([^:]+): ((\d+\.)+\d+)$/)
				branch = $1; revision = $2
				revisionparts = revision.split(/\./)
				if (revisionparts.length % 2) == 1 || revisionparts[-2].to_i == 0
					yield(branch, workfile)
				end
			else
				# No more tags - scan for end of this file
				state = 3
			end
		elsif (state == 3)
			# Termination indicator
			if (line =~ /^=============================================================================$/)
				state = 0
			end
		end
	}
	return 0
end

class GenericArg
	attr_reader :name, :types, :longname, :args;
	attr_writer :longname, :args;
	def initialize(name, types)
		@name = name
		@types = types
		types.each { |type|
			throw "Bad type #{type}" unless respond_to?("parse_#{type}")
		}
		@longname = nil
		if (types)
			@args = []
		else
			@args = 0
		end
	end
	def tryparse(arg)
		types.each { |type|
			attempt = send("parse_#{type}", arg)
			return attempt if attempt
		}
		nil
	end
end

class GenericArgs
	# XXX The template format need to be able to specifiy if this is a
	# counting switch, a toggle, an enable, accumulating, or replacing
	# switch.
	# Presently, everything is implemented with all switches without
	# parameters being counting switches, and all switches with parameters
	# being accumulating switches.
	def initialize(template, args)
		template = [template] unless template.kind_of?(Array)
		shortswitches = {}
		longswitches = {}
		template.each { |argtype|
			if (argtype =~ /^([a-z_]+):/)
				name = $1
				argtype.gsub!(/^([a-z_]+):/, "")
			else
				throw "Bad argtype \"#{argtype}\"" 	
			end
			# XXX This verficiation is not complete.
			throw "Bad argtype \"#{argtype}\"" unless
				argtype =~ /^[a-z-]+(,[a-z-]+)*(:[a-z]+(,[a-z]+)*)?$/
			if (argtype =~ /:/)
				(switches, types) = argtype.split(/:/)
			else
				switches = argtype
				types = nil
			end
			switches = switches.split(/,/)
			types = types.split(/,/) if types
			vvsarg = newarg(name, types)
			# switches is now an array of alternative switch names
			# for this switch, types is either empty or an array
			# of types that can be a parameter for this switch.
			#
			# Each switch need to start with either -- (in which
			# case it has to stand alone, and if it takes a
			# parameter, takes the next argument) or - (in which
			# case it can be concatenated with an argument or take
			# the next one, but can only be a single letter)
			switches.each { |switch|
				if (switch =~ /^--/)
					longswitches[switch] = vvsarg
					vvsarg.longname = switch
				else
					shortswitches[switch] = vvsarg
				end
			}
		}
		# We have now parsed out the various switches from the
		# template.  Now, use this to parse the input.
		while (args[0] =~ /^-/)
			switch = args.shift;
			if (switch =~ /^--/)
				# We have a long switch.  Let's see what we
				# need to do.
				throw "Unknown switch #{switch}" unless
					longswitches.has_key?(switch)
				vvsarg = longswitches[switch]
				if (vvsarg.types)
					# Parse out date to correct type, and
					# push this on the stack of data.
					throw "Missing parameter to switch #{switch}" unless args.length > 0
					arg = args.shift
					parsed = vvsarg.tryparse(arg)
					if (parsed)
						vvsarg.types.push(parsed)
					else
						# XXX Should have a more useful type
						throw "Unable to parse argument to #{switch}"
					end
				else
					# Just register that this switch
					# occured
					vvsarg.args += 1
				end
			else
				# Parse single-letter options
				switch[0..0] = ""		# Remove the -
				while (switch != "")
					letter = switch[0..0]
					switch[0..0] = ""
					throw "Unknown switch -#{letter}" unless
						shortswitches.has_key?("-#{letter}")
					vvsarg = shortswitches["-#{letter}"]
					if (vvsarg.types)
						arg = nil
						if (switch != "")
							arg = switch
							switch = ""
						else
							throw "Missing parameter to switch -#{letter}" unless
								args.length > 0
							arg = args.shift
						end
						parsed = vvsarg.tryparse(arg)
						if (parsed)
							vvsarg.types.push(parsed)
						else
							# XXX Should have a more useful type
							throw "Unable to parse argument to -#{letter}"
						end
					else
						# Just register that this switch
						# occured
						vvsarg.args += 1
					end
				end
			end
		end
	end
	def newarg(name, types)
		return GenericArg.new(name, types)
	end
end

class VVSArgVersion
	attr_reader :id;
	def initialize(id)
		@id = id
	end
end

class VVSArgTag
	attr_reader :tag;
	def initialize(tag)
		@tag = tag
	end
end

class VVSArgTagDate
	attr_reader :tag, :date;
	def initialize(tag, date)
		@tag = tag
		# XXX Should verify date format
		@date = date
	end
end

class VVSArg < GenericArg
	def parse_tag(arg)
		return VVSArgTag.new($1) if arg =~ /^[a-z_][a-z_0-9]*$/i
		return nil
	end
	def parse_version(arg)
		return VVSArgVersion.new($1) if arg =~ /^(1\.(\d+\.)*\d+)$/
		return nil
	end
	def parse_tagdate(arg)
		return VVSArgTagDate.new($1, $2) if arg =~ /^[a-z_][a-z_0-9]*:(.*)$/i
		return nil
	end
end

class VVSArgs < GenericArgs
	def newarg(name, types)
		return VVSArg.new(name, types)
	end
end

module CLIDispatcherClass
	def shorthelp(text)
		@clishorthelp_text = text
	end
	def method_added(symbol)
		return if symbol.id2name =~ /_shorthelp$/
		module_eval <<-end_eval
			def #{symbol}_shorthelp
				"#{@clishorthelp_text}"
			end
		end_eval
	end
end

module CLIDispatcher
	extend CLIDispatcherClass
	shorthelp "Display help"
	def help(args)
		if (args.length == 0)
			self.methods.sort.each { |arg|
				if (arg.gsub!(/_shorthelp$/, ""))
					printf("\t%-9s - %s\n", arg, self.send("#{arg}_shorthelp"))
				end
			}
		else
			if (args.length > 1)
				help_help
			else
				arg = args[0]
				if (self.respond_to?("#{arg}_help"))
					send("#{arg}_help")
				else
					print "No help for #{arg}\n"
				end
			end
		end
		return 0
	end
	def help_help
		print "This would be the help for help\n"
	end
end

class VVSCommand
	extend CLIDispatcherClass
	include CLIDispatcher
	shorthelp "List branches available"
	def branches(args)
		prefix = vvs_prefix(args)
		have = {}
		vvs_branches(args) { |branch, file|
			have[branch] = true
		}
		have.sort { |a,b| a[0] <=> b[0] }.each { |branch, nothing|
			print "#{branch}\n"
		}
		return 0
	end

	def checkout(args)
		switches = VVSArgs.new("revision:-r,--revision:tag,version,tagdate", args)
		#prefix = vvs_prefix(args)
		#vvs_checkout(prefix, args) { |state, file|
		#}
		return 0
	end

	#
	# XXX Good idea for return codes, or fix this up?
	#
	shorthelp "Find last revision of file(s)"
	def lastrev(args)
		prefix = vvs_prefix(args);
		files = []
		# XXX I am not at all sure that pushing the name is the right
		# thing here, but it seemed to fix a bug...
		list_files(prefix, args) { |prefix, name| files.push(name) }
		return 0 if files.length == 0;
		return vvs_lastrev(prefix, files) { |prefix, name, revision|
			print "#{prefix}#{name} #{revision}\n";
		}
	end

	shorthelp "List directories"
	def listdirs(args)
		prefix = vvs_prefix(args);
		return list_dirs(prefix, args) { |prefix, name| print "#{prefix}#{name}\n" }
	end

	shorthelp "List files"
	def listfiles(args)
		prefix = vvs_prefix(args);
		return list_files(prefix, args) { |prefix, name| print "#{prefix}#{name}\n" }
	end

	shorthelp "Determine common prefix for files"
	def prefix(args)
		prefix = vvs_prefix(args);
		if (prefix)
			print "#{prefix}\n";
			return 0;
		else
			return 1;
		end
	end

	shorthelp "Find previous revision of parameter"
	def prevrev(args)
		version = args[0];
		if (!version)
			$stderr.print "vvs prevrev: No version\n";
			print "1.1\n";
			return 1;
		elsif (version !~ /^\d+\.\d+(\.\d+)*$/ || version =~ /^\d+.1$/)
			$stderr.print "vvs prevref: Malformed version \"#{version}\"\n";
			print "1.1\n";
			return 1;
		end
		versionparts = version.split('.');
		if (versionparts.length % 2 != 0)
			# This is a branch
			version.gsub!(/\.\d+$/, "");
			print "#{version}\n";
			return 0;
		else
			# This is a normal revision of a file; handle as such
			version.gsub!(/\.(\d+)$/, "");
			if ($1 == "1")
				version.gsub!(/\.\d+$/, "");
				print "#{version}\n";
			else
				print "#{version}.#{$1.to_i - 1}\n";
			end
			return 0;
		end
	end
end

$verbose = 0
while ARGV[0] =~ /^-/
	switch = ARGV.shift
	if (switch == "-v")
		$verbose += 1
	else
		$stderr.print "Bad command line switch #{switch}\n"
		exit 1
	end
end
vvscommand = VVSCommand.new
command = ARGV.shift
if (!command)
	vvscommand.help([])
	exit 1
end
if vvscommand.respond_to?(command)
	exit vvscommand.send(command, ARGV)
else
	$stderr.print "Unknown command \"#{command}\"\n"
	exit 1
end
