#!/usr/bin/ruby

require "builder/buildAnyStage.rb"
require "builder/buildFirstStage.rb"
require "builder/buildSecondStage.rb"
require "builder/buildThirdStage.rb"
require "builder/fileDownLoader.rb"
require "builder/singleSourceFile.rb"
require "builder/fileChecker.rb"
require "builder/initramfsAssembly.rb"
require "builder/bootdiskAssembly.rb"
require "builder/packageBuilder.rb"
require "builder/packageVersion.rb"
require "rexml/document"
require "digest/sha1"
require "mysql"
require 'optparse' 

# require 'rdoc/usage'

$debug_messages = Array.new

@run_tests = true
@skip_stages = Array.new
@pkg_list_file = nil
@config_file = "config/general.xml"
@brand_file = "config/branding.xml"
@kernel_file = "config/kernels.xml"
@unstable = false
@tstamp_now = Time.new.to_i
@breakpoint = nil
@ask_stage03 = false
@check_updates = false

opts = OptionParser.new 
opts.on('-n', '--no-test')    { @run_tests = false }
opts.on('-s', '--skip-stages', :REQUIRED )    { |i| @skip_stages = i.split(",") }
opts.on('-p', '--package-list', :REQUIRED ) { |i| @pkg_list_file = i.strip } 
opts.on('-c', '--config', :REQUIRED ) { |i| @config_file = i.chomp } 
opts.on('-b', '--branding', :REQUIRED ) { |i| @brand_file = i.chomp }
opts.on('-k', '--kernels', :REQUIRED ) { |i| @kernel_file = i.chomp }
opts.on('-u', '--unstable') { @unstable = true }
opts.on('-d', '--breakpoint', :REQUIRED ) { |i| @breakpoint = i.chomp }
opts.on('-a', '--ask') { @ask = true }
opts.on('-o', '--check-only') { @check_updates = true }
opts.parse!

# raise "DebugBreakPoint"

xcfg = File.new( @config_file )
cfg = REXML::Document.new xcfg
@cfgroot = cfg.root
@srcdir = @cfgroot.elements["sourcedir"].text
@builddir = @cfgroot.elements["builddir"].text
@unpriv = @cfgroot.elements["unpriv"].text
@overlays = @cfgroot.elements["overlays"].text
@dbuser = @cfgroot.elements["database/user"].text
@dbname = @cfgroot.elements["database/dbname"].text
@dbpass = @cfgroot.elements["database/pass"].text
@modmodel = "old"
begin
	@modmodel = @cfgroot.elements["build/modmodel"].text.downcase.strip
rescue
end
@workdir = Dir.pwd
@build_timestamp = Time.now.strftime("%Y%m%d-%H%M%S")

xbranding = File.new( @brand_file )
@branding = REXML::Document.new  xbranding

# Connect to database
@dbh = Mysql.real_connect("localhost",  @dbuser, @dbpass, @dbname)

# Check for existence of  builddir and sourcedir
unless File.exists?(@builddir) && File.ftype(@builddir) == "directory"
	puts "=> please create the directory " + @builddir + " first"
	raise "NoBuilddir"
end
unless File.exists?(@srcdir) && File.ftype(@srcdir) == "directory"
	puts "=> please create the directory " + @srcdir + " first"
	raise "NoBuilddir"
end

# objects for stage 02 are also needed by stage three
@stage_two_scripts = []
@stage_two_objs = []
Dir.foreach("scripts/stage02") { |f|
	# fcomps = File.split(f)
	if (f =~ /\d+.*\.xml$/ )
		if @unstable == true && File.exists?("scripts/stage02.unstable/" + f)
			@stage_two_scripts.push(f)
		else
			@stage_two_scripts.push(f)
		end
	end
}
@stage_two_scripts.sort.each { |i| 
	@stage_two_objs.push(SecondStage.new(i, @srcdir, @builddir, @unpriv, "stage02", @dbh, @unstable))
}

#=============================================================================
#
# Some common functions - You might fold them, they are not essential for
# understanding the flow of the program
#
#=============================================================================

def read_pkglist(filename)
	pkg_list = Array.new
	f = File.new(filename)
	begin
		while (line = f.readline)
			# line.chomp
			# $stdout.print line if line =~ /blue/
			rawline = line.strip
			unless rawline == ""
				rawtoks = rawline.split
				pkg_list.push(rawtoks[0]) unless rawtoks[0] =~ /^#/
			end
		end
	rescue EOFError
		f.close
	end
	return pkg_list
end

def get_stage_one_objs
	stage_one_scripts = []
	Dir.foreach("scripts/stage01") { |f|
		# fcomps = File.split(f)
		if (f =~ /\d+.*\.xml$/ )
			if @unstable == true && File.exists?("scripts/stage01.unstable/" + f)
				stage_one_scripts.push(f)
			else
				stage_one_scripts.push(f)
			end
		end
	}
	stage_one_objs = []
	stage_one_scripts.sort.each { |i| stage_one_objs.push(FirstStage.new(i, @srcdir, @builddir, @unpriv, "stage01", @dbh, @unstable)) }
	return stage_one_objs
end


#=============================================================================
#
# The first stage is used to prepare a preliminary toolchain that will be
# stored in the folder /tools. It is partially cross built and should just
# be used for bootstrapping the "real" toolchain.
#
# You might have downloaded a copy of this toolchain that allows you to skip
# this step and save an hour or two.
#
#==============================================================================

def run_stage_one 
	# directory tools is needed by stage on, be sure it does not exist yet
	if File.exists?("/tools")
		puts "=> directory/softlink tools already exists -- exiting!"
		raise "SoftlinkAlreadyThere"
	end
	# File.symlink(@builddir + "/stage01/chroot/tools", "/tools")
	system("mkdir /tools")
	system("mkdir -p " + @builddir + "/stage01/chroot/tools")
	system("mount -o bind " + @builddir + "/stage01/chroot/tools /tools")
	[ "/stage01", "/stage01/build","/stage01/chroot","/stage01/chroot/tools" ].each { |d|
		unless File.exists?(@builddir + d)
			Dir.mkdir(@builddir + d)
		end
	}
	# Stage 01 abfrühstücken
	# Alle Scripte in stage01 suchen
	stage_one_objs = get_stage_one_objs
	# Download first
	stage_one_objs.each { |i| i.download }
	# Unpack
	stage_one_objs.each { |i|
		i.unpack
		Dir.chdir(@workdir)
		i.patch
		Dir.chdir(@workdir)
		i.build
		Dir.chdir(@workdir)
		i.install
		Dir.chdir(@workdir)
		i.filecheck
		Dir.chdir(@workdir)
	}
	system("umount /tools")
end

#=============================================================================
#
# After successfully building the first stage, we might want to backup the
# complete chroot for later builds. Since stage 01 is less prone to changes,
# this might save a lot of time later.
#
#=============================================================================

def save_stage_one
	unless File.exists? @builddir + "/stage01/chroot.tbz"
		Dir.chdir(@builddir + "/stage01")
		system "tar cvjf chroot.tbz chroot"
		system "mysqldump --add-drop-table --password='" + @dbpass + "' " + 
			" -u '" + @dbuser + "' '" + @dbname + "' > chroot.sql "   
	end
end

#=============================================================================
#
# In the second stage the buildscripts chroot and use the toolchain to build 
# a linux distribution that uses normal paths. The result is a regular,
# complete linux that can be used as a chroot environment.
# 
# During the build installed and changed files are written to a database 
# which allows for easy package creation.
#
#=============================================================================

def run_stage_two
	[ "/stage02","/stage02/build"  ].each { |d|
		unless File.exists?(@builddir + d)
			Dir.mkdir(@builddir + d)
		end
	}
	# Stage 02 abfrühstücken
	# Alle Scripte in stage02 suchen

	
	# Download first
	@stage_two_objs.each { |i| i.download }
	@stage_two_objs.each { |i|
		i.unpack
		Dir.chdir(@workdir)
		i.patch
		Dir.chdir(@workdir)
		i.build
		Dir.chdir(@workdir)
		if (@run_tests == true)  
			i.test_func
			Dir.chdir(@workdir)
		end
		i.install
		Dir.chdir(@workdir)
		i.filecheck
		pb = PackageBuilder.new(@builddir, i.pkg_name, i.pkg_version, @dbh)
		pb.create_pkgdesc
		Dir.chdir(@workdir)
		raise "BreakpointReached" if i.pkg_name == @breakpoint
	}
	
end

#=============================================================================
#
# In stage three files from the second stage are transfered to the "assemble 
# directory". This directory is later converted to SquashFS files that make up
# your final distribution. Since just relevant subpackages are used, the 
# result is much smaller than the content of the build directory.
#
#=============================================================================

def run_stage_three
	stage_three_dirs = [ "/stage03", "/stage03/initramfs", "/stage03/build", "/stage03/squash", "/stage03/cdmaster" ]
	stage_three_dirs.each { |d|
		if File.exists?(@builddir + d)
			puts "=> please remove the directories " + stage_three_dirs.join(" ") + " in " + @builddir + " first!"
			raise "SomeDirectoriesTooMuch"
		end
	}
	stage_three_dirs.each { |d|
		unless File.exists?(@builddir + d)
			Dir.mkdir(@builddir + d)
		end
	}
	# Stage 03 (Bau des Initramfs und der Container) abfrühstücken
	# Alle Scripte in Stage03 suchen

	stage_three_scripts = []
	stage_three_objs = []
	Dir.foreach("scripts/stage03") { |f|
		# fcomps = File.split(f)
		if (f =~ /\d+.*\.xml$/ )
			if @unstable == true && File.exists?("scripts/stage03.unstable/" + f)
				stage_three_scripts.push(f)
			else
				stage_three_scripts.push(f)
			end
		end
	}
	pkg_list = nil
	pkg_list = read_pkglist(@pkg_list_file) unless @pkg_list_file.nil?
	debug_pkglist = File.new("/tmp/lesslinux_" + @build_timestamp + "_all_packages.txt", 'w') 
	debug_skipped = File.new("/tmp/lesslinux_" + @build_timestamp + "_skipped_packages.txt" , 'w')
	stage_three_scripts.sort.each { |i| 
		this_stage_three_obj = ThirdStage.new(i, @srcdir, @builddir, @unpriv, "stage03", @dbh, @unstable)
		debug_pkglist.write(this_stage_three_obj.pkg_name + "\n")
		if pkg_list.nil? || pkg_list.include?(this_stage_three_obj.pkg_name)   
			stage_three_objs.push(this_stage_three_obj)
		else
			debug_skipped.write(this_stage_three_obj.pkg_name + "\n")
		end
	}
	debug_pkglist.close
	debug_skipped.close
	# Download first
	stage_three_objs.each { |i| i.download }
	# Calculate dependencies
	additional_dependencies = Array.new
	provided_libs = Array.new
	provided_links = Array.new
	stage_three_objs.each { |i|
		deps = i.calculate_deps
		libs = i.show_libs
		links = i.show_links
		additional_dependencies = additional_dependencies  + deps
		provided_libs = provided_libs + libs
		provided_links = provided_links + links
		#puts "foreign deps for " + i.pkg_name + " " + deps.join(", ")
	}
	
	#puts "=== Dumping deps"
	#additional_dependencies.each  {  |a|
	#	puts a
	#}
	#puts "=== Found links"
	#provided_links.each  {  |a|
	#	puts a
	#}
	
	# raise "DebugBreakPoint"
	additional_dependencies.uniq!
	still_required_libs = additional_dependencies.clone
	additional_dependencies.each { |i|
		provided_libs.each { |l|
			unless l.index(i).nil?
				still_required_libs.delete(i)
			end
		}
		provided_links.each { |l|
			unless l.index(i).nil?
				still_required_libs.delete(i)
			end
		}
	}

	libs_stage_two = Array.new
	links_stage_two = Array.new
	really_unmet_deps = still_required_libs.clone
	additional_deps = Array.new
	@stage_two_objs.each { |i|
		still_required_libs.each { |j|
			i.show_libs.each { |k|
				provided_file = File.split(k)[1]
				# puts "GCC contains " + provided_file if i.pkg_name == "gcc"
				#if provided_file.index(j) == 0 || j.index(provided_file) == 0
				#	really_unmet_deps.delete(j)
				#	additional_deps.push(i)
				#	puts "Found required lib: " + j + " as " + provided_file + " in stage two package " + i.pkg_name
				#end
				unless k.index(j).nil?
					really_unmet_deps.delete(j)
					additional_deps.push(i)
					puts "Found required lib: " + j + " in stage two package " + i.pkg_name
				end
			}
		}
		links = i.show_links
	}

	additional_names = additional_deps.collect { |x| x.pkg_name + "-" + x.pkg_version }
	puts "=> adding required packages (minimal): " + additional_names.uniq.join(", ")
	puts "=> still unmet dependencies: " + really_unmet_deps.uniq.join(", ")
	
	# Now check for stage03 objects that do not have corresponding - i.e. name and 
	# version matches - stage02 objects
	
	stage2missing = Array.new
	
	stage_three_objs.each { |i|
		vers_match = false
		@stage_two_objs.each { |j| vers_match = true if i.pkg_name == j.pkg_name && i.pkg_version == j.pkg_version }
		vers_match = true if i.allowfail == true
		if vers_match == false 
			stage2missing.push(i.pkg_name + " " + i.pkg_version)
		end
		# raise "CorrespondingStage2Missing" if vers_match == false 
	}
	
	if stage2missing.size > 0
		stage2missing.each { |i|
			$stderr.puts i + " matching stage2 package missing"
		}
		raise "CorrespondingStage2Missing"
	end

	# raise "DebugBreakPoint"
	# Install all regularily selected stage 03 packages

	stage_three_objs.each { |i|
		i.makedirs
		i.touchfiles
		i.scriptgen
		i.assemble_modules(@kernel_file)
		i.unpack
		Dir.chdir(@workdir)
		i.install
		Dir.chdir(@workdir)
		i.pkg_install( ["minimal","bin","config","localization","tzdata","uncategorized","skel"], ["de", "en"])
		Dir.chdir(@workdir)
	}

	# Create temporary buildscripts for stage02 packages not yet included

	additional_deps.uniq.each { |a|
		a.dump_stage03_script
		dep = ThirdStage.new("added-dependency-" + a.pkg_name + ".xml", @srcdir, @builddir, @unpriv, "stage03", @dbh, @unstable)
		dep.unpack
		Dir.chdir(@workdir)
		dep.install
		Dir.chdir(@workdir)
		dep.pkg_install( ["minimal"], ["de", "en"])
		Dir.chdir(@workdir)
	}

	# raise "DebugBreakPoint"
	ThirdStage.create_softlinks(@builddir, @dbh)
	ThirdStage.copy_firmware(@builddir)
	# raise "DebugBreakPoint"
	ThirdStage.create_squashfs(@builddir, @kernel_file)
	
	# include branding
	ThirdStage.write_branding(@builddir, @branding, @brand_file, @build_timestamp)
	ThirdStage.sync_overlay(@builddir, @overlays + "/initramfs")
	#raise "DebugBreakPoint"
	
	# Build the initramfs' for kernel version independent stuff
	initramfs_assembly = InitramfsAssembly.new(@srcdir, @builddir, @dbh, "config/initramfs.xml")
	initrd_location = initramfs_assembly.build

	# Build the initramfs for kernel modules
	mod_locations = initramfs_assembly.assemble_modules(@kernel_file, @modmodel)

	# Synchronize Kernel and initramfs for modules to the CD build directory 
	# Build the ISO-Image
	bootable = BootdiskAssembly.new(@srcdir, @builddir, @dbh, "config/initramfs.xml", @build_timestamp, @branding)
	BootdiskAssembly.sync_overlay(@builddir, @overlays + "/bootdisk")
	BootdiskAssembly.copy_version(@builddir)
	#raise "DebugBreakPoint"
	unless @skip_stages.include?("bootconf")
		bootable.create_isoimage(@kernel_file, true)
	else
		bootable.create_isoimage(@kernel_file, false)
	end
end

#=============================================================================
#
# And now run it...
#
#=============================================================================

if (@check_updates == true)
	get_stage_one_objs.each { |o| o.check_updates }
	@stage_two_objs.each { |o| o.check_updates }
else
	unless @skip_stages.include?("1")
		run_stage_one
		save_stage_one
	end
	run_stage_two unless @skip_stages.include?("2")
	run_stage_three unless @skip_stages.include?("3")
end

@dbh.close
puts $debug_messages.join("\n")
