#  Phusion Passenger - https://www.phusionpassenger.com/
#  Copyright (c) 2012 Phusion
#
#  "Phusion Passenger" is a trademark of Hongli Lai & Ninh Bui.
#
#  Permission is hereby granted, free of charge, to any person obtaining a copy
#  of this software and associated documentation files (the "Software"), to deal
#  in the Software without restriction, including without limitation the rights
#  to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
#  copies of the Software, and to permit persons to whom the Software is
#  furnished to do so, subject to the following conditions:
#
#  The above copyright notice and this permission notice shall be included in
#  all copies or substantial portions of the Software.
#
#  THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
#  IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
#  FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
#  AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
#  LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
#  OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
#  THE SOFTWARE.

# This file lists all the Phusion Passenger C++ library files and contains
# code for calculating how to compile and how to link them into executables.
# It's used by the build system (build/*.rb) and
# lib/phusion_passenger/standalone/runtime_installer.rb.

class CommonLibraryBuilder
	include Rake::DSL if defined?(Rake::DSL)

	attr_reader :all_components, :selected_components, :output_dir

	def initialize(&block)
		@all_components = {}
		@all_ordered_components = []
		@selected_components = {}
		@namespace = "common"
		if defined?(COMMON_OUTPUT_DIR)
			@output_dir = COMMON_OUTPUT_DIR + "libpassenger_common"
		else
			@output_dir = "."
		end
		instance_eval(&block) if block
	end

	def initialize_copy(other)
		[:all_components, :all_ordered_components, :selected_components, :namespace, :output_dir].each do |name|
			var_name = "@#{name}"
			instance_variable_set(var_name, other.instance_variable_get(var_name).dup)
		end
	end

	def define_component(object_name, options)
		options[:deps] ||= []
		@all_components[object_name] = options
		@all_ordered_components << object_name
		@selected_components[object_name] = options
	end

	def only(*selector)
		return dup.send(:only!, *selector)
	end

	def exclude(*selector)
		return dup.send(:exclude!, *selector)
	end

	def set_namespace(namespace)
		return dup.send(:set_namespace!, namespace)
	end

	def set_output_dir(dir)
		return dup.send(:set_output_dir!, dir)
	end

	def link_objects
		result = []

		selected_categories.each do |category|
			if category_complete?(category) && false
				# Feature disabled: we don't want to waste too much space when
				# packaging the runtime ('passenger package-runtime') so we
				# never generate static libraries.
				if aggregate_sources?
					result << "#{@output_dir}/#{category}.o"
				else
					result << "#{@output_dir}/#{category}.a"
				end
			else
				object_names = selected_objects_beloging_to_category(category)
				result.concat(object_filenames_for(object_names))
			end
		end

		return result
	end

	def link_objects_as_string
		return link_objects.join(' ')
	end

	def define_tasks(extra_compiler_flags = nil)
		flags =  "-Iext -Iext/common #{LIBEV_CFLAGS} #{extra_compiler_flags} "
		flags << "#{PlatformInfo.portability_cflags} #{EXTRA_CXXFLAGS}"
		flags.strip!

		group_all_components_by_category.each_pair do |category, object_names|
			define_category_tasks(category, object_names, flags)
		end

		task("#{@namespace}:clean") do
			sh "rm -rf #{@output_dir}"
		end

		return self
	end

private
	def define_category_tasks(category, object_names, flags)
		object_filenames = object_filenames_for(object_names)

		object_names.each do |object_name|
			options     = @all_components[object_name]
			source_file = "ext/common/#{options[:source]}"
			object_file = "#{@output_dir}/#{object_name}"

			file(object_file => dependencies_for(options)) do
				ensure_directory_exists(File.dirname(object_file))
				if source_file =~ /\.c$/
					compile_c(source_file, "#{flags} -o #{object_file}")
				else
					compile_cxx(source_file, "#{flags} -o #{object_file}")
				end
			end
		end

		task "#{@namespace}:clean" do
			sh "rm -f #{object_filenames.join(' ')}"
		end

		if aggregate_sources?
			aggregate_source = "#{@output_dir}/#{category}.cpp"
			aggregate_object = "#{@output_dir}/#{category}.o"

			file(aggregate_object => dependencies_for(object_names)) do
				ensure_directory_exists(File.dirname(aggregate_source))
				ensure_directory_exists(File.dirname(aggregate_object))

				File.open(aggregate_source, "w") do |f|
					f.puts %q{
						#ifndef _GNU_SOURCE
							#define _GNU_SOURCE
						#endif
					}
					object_names.each do |object_name|
						options = @all_components[object_name]
						source_file = options[:source].sub(%r(^ext/common), '')
						f.puts "#include \"#{source_file}\""
					end
				end

				compile_cxx(aggregate_source, "#{flags} -o #{aggregate_object}")
			end

			task "#{@namespace}:clean" do
				sh "rm -f #{aggregate_source} #{aggregate_object}"
			end
		elsif false
			# Feature disabled: we don't want to waste too much space when
			# packaging the runtime ('passenger package-runtime') so we
			# never generate static libraries.
			library = "#{@output_dir}/#{category}.a"
			
			file(library => object_filenames) do
				create_static_library(library, object_filenames.join(' '))
			end

			task "#{@namespace}:clean" do
				sh "rm -f #{library}"
			end
		end
	end

	def set_namespace!(namespace)
		@namespace = namespace
		return self
	end

	def set_output_dir!(dir)
		@output_dir = dir
		return self
	end

	def only!(*selector)
		new_components = apply_selector(*selector)
		@selected_components = new_components
		return self
	end

	def exclude!(*selector)
		apply_selector(*selector).each_key do |object_name|
			@selected_components.delete(object_name)
		end
		return self
	end

	def apply_selector(*selector)
		result = {}
		selector = [selector].flatten
		selector.each do |condition|
			@selected_components.each do |object_name, options|
				if component_satisfies_condition?(object_name, options, condition)
					result[object_name] = options
				end
			end
		end
		return result
	end

	def component_satisfies_condition?(object_name, options, condition)
		case condition
		when Symbol
			return condition == :all || options[:category] == condition
		when String
			return object_name == condition
		else
			raise ArgumentError, "Invalid condition #{condition.inspect}"
		end
	end

	def ensure_directory_exists(dir)
		sh("mkdir -p #{dir}") if !File.directory?(dir)
	end

	def selected_categories
		categories = {}
		@selected_components.each_value do |options|
			categories[options[:category]] = true
		end
		return categories.keys
	end

	def category_complete?(category)
		expected = 0
		actual   = 0
		@all_components.each_value do |options|
			if options[:category] == category
				expected += 1
			end
		end
		@selected_components.each_value do |options|
			if options[:category] == category
				actual += 1
			end
		end
		return expected == actual
	end

	def selected_objects_beloging_to_category(category)
		result = []
		@selected_components.each_pair do |object_name, options|
			if options[:category] == category
				result << object_name
			end
		end
		return result
	end

	def dependencies_for(component_options_or_object_names)
		result = nil
		case component_options_or_object_names
		when Hash
			component_options = component_options_or_object_names
			result = ["ext/common/#{component_options[:source]}"]
			component_options[:deps].each do |dependency|
				result << "ext/common/#{dependency}"
			end
		when Array
			result = []
			object_names = component_options_or_object_names
			object_names.each do |object_name|
				options = @all_components[object_name]
				result.concat(dependencies_for(options))
			end
			result.uniq!
		end
		return result
	end

	def object_filenames_for(object_names)
		return object_names.map { |name| "#{@output_dir}/#{name}" }
	end

	def group_all_components_by_category
		categories = {}
		@all_ordered_components.each do |object_name|
			options  = @all_components[object_name]
			category = options[:category]
			categories[category] ||= []
			categories[category] << object_name
		end
		return categories
	end

	def aggregate_sources?
		# Feature disabled: it's too hard to make it work because
		# lots of executables have to be linked to individual objects
		# anyway.
		return false
	end
end


COMMON_LIBRARY = CommonLibraryBuilder.new do
	define_component 'Logging.o',
		:source   => 'Logging.cpp',
		:category => :base,
		:deps     => %w(
			Logging.cpp
			Logging.h
		)
	define_component 'Utils/SystemTime.o',
		:source   => 'Utils/SystemTime.cpp',
		:category => :base,
		:deps     => %w(
			Utils/SystemTime.h
		)
	define_component 'Utils/StrIntUtils.o',
		:source   => 'Utils/StrIntUtils.cpp',
		:category => :base,
		:deps     => %w(
			Utils/StrIntUtils.h
		)
	define_component 'Utils/IOUtils.o',
		:source   => 'Utils/IOUtils.cpp',
		:category => :base,
		:deps     => %w(
			Utils/IOUtils.h
		)
	define_component 'Utils.o',
		:source   => 'Utils.cpp',
		:category => :base,
		:deps     => %w(
			Utils.h
			Utils/Base64.h
			Utils/StrIntUtils.h
			ResourceLocator.h
		)

	define_component 'Utils/Base64.o',
		:source   => 'Utils/Base64.cpp',
		:category => :other,
		:deps     => %w(
			Utils/Base64.h
		)
	define_component 'Utils/CachedFileStat.o',
		:source   => 'Utils/CachedFileStat.cpp',
		:category => :other,
		:deps     => %w(
			Utils/CachedFileStat.h
			Utils/CachedFileStat.hpp
		)
	define_component 'Utils/LargeFiles.o',
		:source   => 'Utils/LargeFiles.cpp',
		:category => :other,
		:deps     => %w(
			Utils/LargeFiles.h
		)
	define_component 'ApplicationPool2/Implementation.o',
		:source   => 'ApplicationPool2/Implementation.cpp',
		:category => :other,
		:deps     => %w(
			ApplicationPool2/Spawner.h
			ApplicationPool2/Common.h
			ApplicationPool2/Pool.h
			ApplicationPool2/SuperGroup.h
			ApplicationPool2/Group.h
			ApplicationPool2/Session.h
			ApplicationPool2/Options.h
			ApplicationPool2/PipeWatcher.h
			ApplicationPool2/AppTypes.h
			ApplicationPool2/Spawner.h
			ApplicationPool2/SpawnerFactory.h
			ApplicationPool2/SmartSpawner.h
			ApplicationPool2/DirectSpawner.h
			ApplicationPool2/DummySpawner.h
		)
	define_component 'ApplicationPool2/AppTypes.o',
		:source   => 'ApplicationPool2/AppTypes.cpp',
		:category => :other,
		:deps     => %w(
			ApplicationPool2/AppTypes.h
			Utils/StrIntUtils.h
			Utils/CachedFileStat.h
		)
	define_component 'AgentsStarter.o',
		:source   => 'AgentsStarter.cpp',
		:category => :other,
		:deps     => %w(
			AgentsStarter.h
			ResourceLocator.h
			MessageClient.h
			ServerInstanceDir.h
			Utils/IniFile.h
			Utils/VariantMap.h
		)
	define_component 'AgentsBase.o',
		:source   => 'agents/Base.cpp',
		:category => :other,
		:deps     => %w(
			agents/Base.h
			Utils/VariantMap.h
		)
	define_component 'agents/LoggingAgent/FilterSupport.o',
		:source   => 'agents/LoggingAgent/FilterSupport.cpp',
		:category => :logging_agent,
		:deps     => %w(
			agents/LoggingAgent/FilterSupport.h
		)
	define_component 'Utils/MD5.o',
		:source   => 'Utils/MD5.cpp',
		:category => :other,
		:deps     => %w(
			Utils/MD5.h
		)
	define_component 'Utils/fib.o',
		:source   => 'Utils/fib.c',
		:category => :other,
		:deps     => %w(
			Utils/fib.h
			Utils/fibpriv.h
		)
	define_component 'Utils/jsoncpp.o',
		:source   => 'Utils/jsoncpp.cpp',
		:category => :other,
		:deps     => %w(
			Utils/json.h
			Utils/json-forwards.h
		)

	#'BCrypt.o' => %w(
	#	BCrypt.cpp
	#	BCrypt.h
	#	Blowfish.h
	#	Blowfish.c)
end

# Objects that must be linked into the Nginx binary.
NGINX_LIBS_SELECTOR = [:base, 'AgentsStarter.o', 'ApplicationPool2/AppTypes.o',
	'Utils/CachedFileStat.o', 'Utils/Base64.o', 'agents/LoggingAgent/FilterSupport.o']
