#!/usr/bin/ruby

require 'fileutils.rb'
require 'find'
require 'rbconfig'
require 'optparse'

# Install directory - this contains a space to check
# for correct escaping of paths when passing comamnd
# line arguments under Windows
INSTALL_DIR = File.expand_path("install dir/")
PACKAGE_DIR = File.expand_path("package-dir/")
PACKAGE_SRC_DIR = File.expand_path("package-src-dir/")
IS_WINDOWS = RbConfig::CONFIG['host_os'] =~ /mswin|mingw/

if IS_WINDOWS
	OLDAPP_NAME = "oldapp.exe"
	NEWAPP_NAME = "newapp.exe"
	APP_NAME = "app.exe"
	UPDATER_NAME = "updater.exe"
	ZIP_TOOL = File.expand_path("../zip-tool.exe")
else
	OLDAPP_NAME = "oldapp"
	NEWAPP_NAME = "newapp"
	APP_NAME = "app"
	UPDATER_NAME = "updater"
	ZIP_TOOL = File.expand_path("../zip-tool")
end

file_list_vars = {
  "APP_FILENAME" => APP_NAME,
  "UPDATER_FILENAME" => UPDATER_NAME
}

def replace_vars(src_file,dest_file,vars)
	content = File.read(src_file)
	vars.each do |key,value|
		content.gsub! "$#{key}",value
	end
	File.open(dest_file,'w') do |file|
		file.print content
	end
end

# Returns true if |src_file| and |dest_file| have the same contents, type
# and permissions or false otherwise
def compare_files(src_file, dest_file)
	if File.ftype(src_file) != File.ftype(dest_file)
		$stderr.puts "Type of file #{src_file} and #{dest_file} differ"
		return false
	end

	if File.file?(src_file) && !FileUtils.identical?(src_file, dest_file)
		$stderr.puts "Contents of file #{src_file} and #{dest_file} differ"
		return false
	end

	src_stat = File.stat(src_file)
	dest_stat = File.stat(dest_file)

	if src_stat.mode != dest_stat.mode
		$stderr.puts "Permissions of #{src_file} and #{dest_file} differ"
		return false
	end

	return true
end

# Compares the contents of two directories and returns a map of (file path => change type)
# for files and directories which differ between the two
def compare_dirs(src_dir, dest_dir)
	src_dir += '/' if !src_dir.end_with?('/')
	dest_dir += '/' if !dest_dir.end_with?('/')

	src_file_map = {}
	Find.find(src_dir) do |src_file|
		src_file = src_file[src_dir.length..-1]
		src_file_map[src_file] = nil
	end

	change_map = {}
	Find.find(dest_dir) do |dest_file|
		dest_file = dest_file[dest_dir.length..-1]

		if !src_file_map.include?(dest_file)
			change_map[dest_file] = :deleted
		elsif !compare_files("#{src_dir}/#{dest_file}", "#{dest_dir}/#{dest_file}")
			change_map[dest_file] = :updated
		end

		src_file_map.delete(dest_file)
	end

	src_file_map.each do |file|
		change_map[file] = :added
	end

	return change_map
end

def create_test_file(name, content)
	File.open(name, 'w') do |file|
		file.puts content
	end
	return name
end

force_elevation = false
run_in_debugger = false

OptionParser.new do |parser|
	parser.on("-f","--force-elevated","Force the updater to elevate itself") do
		force_elevation = true
	end
	parser.on("-d","--debug","Run the updater under GDB") do
		run_in_debugger = true
	end
end.parse!

# copy 'src' to 'dest', preserving the attributes
# of 'src'
def copy_file(src, dest)
	FileUtils.cp src, dest, :preserve => true
end

# Remove the install and package dirs if they
# already exist
FileUtils.rm_rf(INSTALL_DIR)
FileUtils.rm_rf(PACKAGE_DIR)
FileUtils.rm_rf(PACKAGE_SRC_DIR)

# Create the install directory with the old app
Dir.mkdir(INSTALL_DIR)
copy_file OLDAPP_NAME, "#{INSTALL_DIR}/#{APP_NAME}"

# Create a dummy file to uninstall
uninstall_test_file = create_test_file("#{INSTALL_DIR}/file-to-uninstall.txt", "this file should be removed after the update")
uninstall_test_symlink = if not IS_WINDOWS
	FileUtils.ln_s("#{INSTALL_DIR}/file-to-uninstall.txt", "#{INSTALL_DIR}/symlink-to-file-to-uninstall.txt")
else
	create_test_file("#{INSTALL_DIR}/symlink-to-file-to-uninstall.txt", "dummy file.  this is a symlink on Unix")
end

# Create a dummy file to uninstall in a directory
# which becomes empty after the update
empty_dir_path = "#{INSTALL_DIR}/will-become-empty-after-update/nested"
FileUtils.mkdir_p(empty_dir_path)
create_test_file("#{empty_dir_path}/file-to-uninstall.txt", "this file and its containing dir should be removed after the update")

# Populate package source dir with files to install
Dir.mkdir(PACKAGE_SRC_DIR)
nested_dir_path = "#{PACKAGE_SRC_DIR}/new-dir/new-dir2"
FileUtils.mkdir_p(nested_dir_path)
FileUtils::chmod 0755, "#{PACKAGE_SRC_DIR}/new-dir"
FileUtils::chmod 0755, "#{PACKAGE_SRC_DIR}/new-dir/new-dir2"
nested_dir_test_file = "#{nested_dir_path}/new-file.txt"
File.open(nested_dir_test_file,'w') do |file|
	file.puts "this is a new file in a new nested dir"
end
FileUtils::chmod 0644, nested_dir_test_file
copy_file NEWAPP_NAME, "#{PACKAGE_SRC_DIR}/#{APP_NAME}"
FileUtils::chmod 0755, "#{PACKAGE_SRC_DIR}/#{APP_NAME}"

# Create .zip packages from source files
Dir.mkdir(PACKAGE_DIR)
Dir.chdir(PACKAGE_SRC_DIR) do
	if !system("#{ZIP_TOOL} #{PACKAGE_DIR}/app-pkg.zip .")
		raise "Unable to create update package"
	end
end

# Copy the install script and updater to the target
# directory
replace_vars("file_list.xml","#{PACKAGE_DIR}/file_list.xml",file_list_vars)
copy_file "../#{UPDATER_NAME}", "#{PACKAGE_DIR}/#{UPDATER_NAME}"

# Run the updater using the new syntax
#
# Run the application from the install directory to
# make sure that it looks in the correct directory for
# the file_list.xml file and packages
#
install_path = File.expand_path(INSTALL_DIR)
Dir.chdir(INSTALL_DIR) do
	flags = "--force-elevated" if force_elevation
	debug_flags = "gdb --args" if run_in_debugger
	cmd = "#{debug_flags} #{PACKAGE_DIR}/#{UPDATER_NAME} #{flags} --install-dir \"#{install_path}\" --package-dir \"#{PACKAGE_DIR}\" --script file_list.xml --auto-close"
	puts "Running '#{cmd}'"
	system(cmd)
end

# TODO - Correctly wait until updater has finished
sleep(1)

# Check that the app was updated
app_path = "#{INSTALL_DIR}/#{APP_NAME}"
output = `"#{app_path}"`
if (output.strip != "new app starting")
	throw "Updated app produced unexpected output: #{output}"
end

# Check that the packaged dir and install dir match
dir_diff = compare_dirs(PACKAGE_SRC_DIR, INSTALL_DIR)
ignored_files = ["test-dir", "test-dir/app-symlink", UPDATER_NAME]
have_unexpected_change = false
dir_diff.each do |path, change_type|
	if !ignored_files.include?(path)
		case change_type
		when :added
			$stderr.puts "File #{path} was not installed"
		when :changed
			$stderr.puts "File #{path} differs between install and package dir"
		when :deleted
			$stderr.puts "File #{path} was not uninstalled"
		end
		have_unexpected_change = true
	end
end

if have_unexpected_change
	throw "Unexpected differences between packaging and update dir"
end

puts "Test passed"
